diff --git a/app/[locale]/@modals/(.)profile/delete/page.tsx b/app/[locale]/@modals/(.)profile/delete/page.tsx new file mode 100644 index 00000000..a2ee4b52 --- /dev/null +++ b/app/[locale]/@modals/(.)profile/delete/page.tsx @@ -0,0 +1,54 @@ +'use client'; +import { Button } from '~/components/buttons'; +import { Dialog } from '~/components/dialog'; +import { Card, CardFooter, CardHeader } from '~/components/ui'; +import { toast } from '~/lib/hooks'; +import { cn } from '~/lib/utils'; +import { deleteFacultySelectionElement } from '~/server/actions'; + +export default function Page({ + params: { locale }, + searchParams: { topic, id }, +}: { + params: { locale: string }; + searchParams: { topic?: string; id?: string }; +}) { + return ( + + + +

Do you really want to delete this information?

+
+ + + + +
+
+ ); +} diff --git a/app/[locale]/@modals/(.)profile/edit/client-utils.tsx b/app/[locale]/@modals/(.)profile/edit/client-utils.tsx new file mode 100644 index 00000000..cae92258 --- /dev/null +++ b/app/[locale]/@modals/(.)profile/edit/client-utils.tsx @@ -0,0 +1,274 @@ +'use client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { type Control, type FieldPath, useForm } from 'react-hook-form'; +import type z from 'zod'; +import { ZodFirstPartyTypeKind, type ZodSchema } from 'zod'; + +import { Button } from '~/components/buttons'; +import { Input } from '~/components/inputs'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '~/components/inputs/select'; +import { CardContent, CardFooter } from '~/components/ui'; +import { + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, + FormProvider, +} from '~/components/ui/form'; +import { toast } from '~/lib/hooks'; +import { + facultyPersonalDetailsSchema, + facultyProfileSchemas, +} from '~/lib/schemas/faculty-profile'; +import { + editFacultyProfilePersonalDetails, + upsertFacultySection, +} from '~/server/actions/faculty-profile'; + +export function FacultyForm({ + locale, + topic, + id, + existingDetails, +}: { + locale: string; + topic: string; + id?: number; + existingDetails?: z.infer< + (typeof facultyProfileSchemas)[keyof typeof facultyProfileSchemas] + > | null; +}) { + const schema = + facultyProfileSchemas[topic as keyof typeof facultyProfileSchemas]; + + const form = useForm>({ + resolver: zodResolver(schema), + defaultValues: + existingDetails ?? + (Object.keys(schema.shape).reduce( + (acc, key) => { + acc[key as keyof typeof schema.shape] = undefined; + return acc; + }, + {} as Record + ) as z.infer), + }); + + const onSubmit = async (values: z.infer) => { + const result = await upsertFacultySection(topic, id ?? null, values); + toast({ + title: result.success ? 'Success' : 'Error', + description: result.success + ? `Successfully ${id ? 'updated' : 'created'} ${topic} details.` + : result.message, + variant: result.success ? 'success' : 'error', + }); + window.history.go(-1); + }; + + return ( + +
+ + {renderFields(form.control, schema.shape)} + + + + + +
+
+ ); +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const renderFields = >( + formControl: Control, + // eslint-disable-next-line @typescript-eslint/no-redundant-type-constituents + schemaShape: Record | never> +) => { + const renderField = (fieldName: string) => { + const fieldSchema = schemaShape[fieldName] as + | z.ZodOptional + | z.ZodString + | z.ZodNumber + | z.ZodDate + | z.ZodEnum<[string, ...string[]]> + | z.ZodEffects, Date, string | Date> + | z.ZodEffects< + z.ZodUnion<[z.ZodOptional, z.ZodOptional]>, + Date | undefined, + string | Date | undefined + > + | z.ZodUnion<[z.ZodDate, z.ZodString]>; + const isOptional = fieldSchema.isOptional(); + + // Generate appropriate label from field name + const label = fieldName + .replace(/([A-Z])/g, ' $1') + .replace(/^./, (str: string) => str.toUpperCase()) + .trim(); + + // Check if it's an enum field + if (fieldSchema._def.typeName === ZodFirstPartyTypeKind.ZodEnum) { + const enumValues = fieldSchema._def.values; + return ( + } + render={({ field }) => ( + + {label} + + + + + + )} + /> + ); + } + + const isDateField = + fieldSchema._def.typeName === ZodFirstPartyTypeKind.ZodDate || + (fieldSchema._def.typeName === ZodFirstPartyTypeKind.ZodUnion && + Array.isArray(fieldSchema._def.options) && + fieldSchema._def.options.some( + (option) => + option._def?.typeName === ZodFirstPartyTypeKind.ZodDate || + option._def?.typeName === ZodFirstPartyTypeKind.ZodString + )) || + (fieldSchema._def.typeName == ZodFirstPartyTypeKind.ZodEffects && + fieldSchema._def.schema._def.typeName === + ZodFirstPartyTypeKind.ZodUnion && + Array.isArray(fieldSchema._def.schema._def.options) && + fieldSchema._def.schema._def.options.some( + (option) => + (option._def?.typeName === ZodFirstPartyTypeKind.ZodOptional && + option._def?.innerType._def.typeName === + ZodFirstPartyTypeKind.ZodDate) || + option._def?.typeName === ZodFirstPartyTypeKind.ZodDate + )) || + fieldName.toLowerCase().includes('date'); + const isNumberField = + fieldSchema._def.typeName === ZodFirstPartyTypeKind.ZodNumber; + + // Default to text input + return ( + } + render={({ field }) => ( + + + + + + + )} + /> + ); + }; + + return Object.keys(schemaShape).map((fieldName) => { + // Make certain fields span full width + const isFullWidth = ['description', 'about', 'title', 'people'].includes( + fieldName.toLowerCase() + ); + + return ( +
+ {renderField(fieldName)} +
+ ); + }); +}; + +export function FacultyPersonalDetailsForm({ + locale, + existingDetails, +}: { + locale: string; + existingDetails: z.infer; +}) { + const form = useForm>({ + resolver: zodResolver(facultyPersonalDetailsSchema), + defaultValues: existingDetails, + }); + + const onSubmit = async ( + values: z.infer + ) => { + const result = await editFacultyProfilePersonalDetails(values); + toast({ + title: result.success ? 'Success' : 'Error', + description: result.success + ? `Successfully updated personal details.` + : result.message, + variant: result.success ? 'success' : 'error', + }); + window.history.go(-1); + }; + + return ( + +
+ + {renderFields(form.control, facultyPersonalDetailsSchema.shape)} + + + + + +
+
+ ); +} diff --git a/app/[locale]/@modals/(.)profile/edit/page.tsx b/app/[locale]/@modals/(.)profile/edit/page.tsx new file mode 100644 index 00000000..11eae658 --- /dev/null +++ b/app/[locale]/@modals/(.)profile/edit/page.tsx @@ -0,0 +1,149 @@ +import { and, eq, getTableColumns } from 'drizzle-orm'; +import { notFound } from 'next/navigation'; + +import { Dialog } from '~/components/dialog'; +import { Card, CardHeader } from '~/components/ui'; +import { facultyProfileSchemas } from '~/lib/schemas/faculty-profile'; +import { cn } from '~/lib/utils'; +import { getServerAuthSession } from '~/server/auth'; +import { + awardsAndHonors, + continuingEducation, + db, + experience, + faculty, + publications, + qualifications, + researchProjects, +} from '~/server/db'; + +import { FacultyForm, FacultyPersonalDetailsForm } from './client-utils'; + +const facultyTables = { + qualifications, + experience, + publications, + researchProjects, + continuingEducation, + awardsAndHonors, +} as const; + +export default async function Page({ + params: { locale }, + searchParams: { topic, id, personal }, +}: { + params: { locale: string }; + searchParams: { topic?: string; id?: string; personal?: boolean }; +}) { + const { + person: { id: userId }, + } = (await getServerAuthSession())!; + if (personal) { + const personalDetails = await db.query.faculty + .findFirst({ + where: (faculty, { eq }) => eq(faculty.id, userId), + columns: { + officeAddress: true, + scopusId: true, + linkedInId: true, + googleScholarId: true, + researchGateId: true, + }, + with: { + person: { + columns: { + countryCode: true, + telephone: true, + alternateCountryCode: true, + alternateTelephone: true, + }, + }, + }, + }) + .then((result) => { + if (!result) return null; + return { + officeAddress: result.officeAddress, + scopusId: result.scopusId ?? undefined, + linkedInId: result.linkedInId ?? undefined, + googleScholarId: result.googleScholarId ?? undefined, + researchGateId: result.researchGateId ?? undefined, + countryCode: result.person.countryCode ?? undefined, + telephone: result.person.telephone, + alternateCountryCode: result.person.alternateCountryCode ?? undefined, + alternateTelephone: result.person.alternateTelephone ?? undefined, + }; + }); + if (!personalDetails) { + return notFound(); + } + return ( + + + + + + + ); + } + if ( + !topic || + !(topic in facultyProfileSchemas) || + (id && isNaN(parseInt(id, 10))) + ) { + return notFound(); + } + + const table = facultyTables[topic as keyof typeof facultyTables]; + + const existingDetails = + id && table + ? await db + .select({ + ...(({ facultyId: _, id: __, ...rest }) => rest)( + getTableColumns(table) + ), + }) + .from(table) + .innerJoin(faculty, eq(table.facultyId, faculty.employeeId)) + .where(and(eq(faculty.id, userId), eq(table.id, parseInt(id, 10)))) + .limit(1) + .then((results) => results[0] || null) + : null; + + return ( + + + +

+ {id ? 'Edit' : 'Add'}{' '} + {topic.charAt(0).toUpperCase() + topic.slice(1)} +

+

+ {id ? 'Update the existing record' : 'Create a new record'} +

+
+ +
+
+ ); +} diff --git a/app/[locale]/faculty-and-staff/[employee_id]/[faculty_section]/page.tsx b/app/[locale]/faculty-and-staff/[employee_id]/[faculty_section]/page.tsx new file mode 100644 index 00000000..65dcf950 --- /dev/null +++ b/app/[locale]/faculty-and-staff/[employee_id]/[faculty_section]/page.tsx @@ -0,0 +1,21 @@ +import { type Translations } from '~/i18n/translations'; + +import { FacultySectionComponent } from '../../utils'; + +export default async function FacultySection({ + params: { locale, faculty_section, employee_id: employeeId }, +}: { + params: { + locale: string; + faculty_section: keyof Translations['FacultyAndStaff']['tabs']; + employee_id: string; + }; +}) { + return ( + + ); +} diff --git a/app/[locale]/faculty-and-staff/[employee_id]/layout.tsx b/app/[locale]/faculty-and-staff/[employee_id]/layout.tsx new file mode 100644 index 00000000..5145b317 --- /dev/null +++ b/app/[locale]/faculty-and-staff/[employee_id]/layout.tsx @@ -0,0 +1,28 @@ +import { union } from 'drizzle-orm/pg-core'; +import { type ReactNode } from 'react'; + +import { db, faculty, staff } from '~/server/db'; + +import { FacultyOrStaffComponent } from '../utils'; + +export async function generateStaticParams() { + const facultyIds = db + .select({ employee_id: faculty.employeeId }) + .from(faculty); + const staffIds = db.select({ employee_id: staff.employeeId }).from(staff); + return await union(facultyIds, staffIds); +} + +export default async function FacultyOrStaffLayout({ + children, + params: { locale, employee_id }, +}: { + children?: ReactNode; + params: { locale: string; employee_id: string }; +}) { + return ( + + {children} + + ); +} diff --git a/app/[locale]/faculty-and-staff/[employee_id]/page.tsx b/app/[locale]/faculty-and-staff/[employee_id]/page.tsx index 6d41226e..b9f25cb1 100644 --- a/app/[locale]/faculty-and-staff/[employee_id]/page.tsx +++ b/app/[locale]/faculty-and-staff/[employee_id]/page.tsx @@ -1,20 +1,17 @@ -import { union } from 'drizzle-orm/pg-core'; +import FacultySection from './[faculty_section]/page'; -import { WorkInProgressStatus } from '~/components/status'; -import { db, faculty, staff } from '~/server/db'; - -export async function generateStaticParams() { - const facultyIds = db - .select({ employee_id: faculty.employeeId }) - .from(faculty); - const staffIds = db.select({ employee_id: staff.employeeId }).from(staff); - return await union(facultyIds, staffIds); -} - -export default function FacultyOrStaff({ - params: { locale, employee_id: employeeId }, +export default function FacultyAndStaff({ + params: { locale, employee_id }, }: { params: { locale: string; employee_id: string }; }) { - return ; + return ( + + ); } diff --git a/app/[locale]/faculty-and-staff/utils.tsx b/app/[locale]/faculty-and-staff/utils.tsx new file mode 100644 index 00000000..c2d7713e --- /dev/null +++ b/app/[locale]/faculty-and-staff/utils.tsx @@ -0,0 +1,604 @@ +'use server'; + +import { eq, sql } from 'drizzle-orm'; +import Image from 'next/image'; +import Link from 'next/link'; +import { notFound } from 'next/navigation'; +import { type ReactNode } from 'react'; +import { + MdCall, + MdLocationOn, + MdMail, + MdOutlineAdd, + MdOutlineDelete, + MdOutlineEdit, +} from 'react-icons/md'; +import 'server-only'; + +import { PathnameAwareSuspense, Tabs } from '~/app/profile/client-utils'; +import { Button } from '~/components/buttons'; +import { WorkInProgressStatus } from '~/components/status'; +import { ScrollArea } from '~/components/ui'; +import { getTranslations, type Translations } from '~/i18n/translations'; +import { cn } from '~/lib/utils'; +import { + awardsAndHonors, + continuingEducation, + db, + doctorates, + experience, + faculty, + majors, + persons, + publications, + qualifications, + researchProjects, + studentAcademicDetails, + students, +} from '~/server/db'; + +async function FacultyOrStaffComponent({ + children, + employeeId, + id, + locale, +}: { locale: string; children?: ReactNode } & ( + | { id: number; employeeId?: never } + | { id?: never; employeeId: string } +)) { + const text = (await getTranslations(locale)).FacultyAndStaff; + + const tabs = [ + { + label: text.tabs.qualifications, + href: 'qualifications', + }, + { + label: text.tabs.experience, + href: 'experience', + }, + { + label: text.tabs.projects, + href: 'projects', + }, + { + label: text.tabs.continuingEducation, + href: 'continuingEducation', + }, + { + label: text.tabs.publications, + href: 'publications', + }, + { + label: text.tabs.researchScholars, + href: 'researchScholars', + }, + { + label: text.tabs.awardsAndHonors, + href: 'awardsAndHonors', + }, + ]; + const facultyDescriptionTmp = await db.query.faculty.findFirst({ + where: (faculty, { eq }) => + !employeeId ? eq(faculty.id, id!) : eq(faculty.employeeId, employeeId), + columns: { + id: true, + employeeId: true, + officeAddress: true, + designation: true, + googleScholarId: true, + linkedInId: true, + researchGateId: true, + scopusId: true, + }, + with: { + person: { + columns: { + name: true, + email: true, + telephone: true, + countryCode: true, + alternateTelephone: true, + alternateCountryCode: true, + }, + }, + }, + extras: { + publications: db + .$count( + publications, + sql`publications.faculty_id = faculty.employee_id` // drzzle orm bug https://github.com/drizzle-team/drizzle-orm/issues/3546 + ) + .as('publications'), + continuingEducation: db + .$count( + continuingEducation, + sql`continuing_education.faculty_id = faculty.employee_id` + ) + .as('continuingEducation'), + }, + }); + + if (!facultyDescriptionTmp) { + return notFound(); + } + + const facultyDescription = { doctoralStudents: 0, ...facultyDescriptionTmp }; // Placeholder for doctoral students count + return ( + <> +
+
+

+ {facultyDescription.person.name} +

+ +
+ {facultyDescription.designation} +
+ {id && ( + + )} +
+ {id && ( + + )} + 0 +
    +
  • +

    + + {facultyDescription.person.email} +

    +
  • +
  • +

    + + {`+${facultyDescription.person.countryCode ?? 91} ${facultyDescription.person.telephone} (off-Direct no) `} + {facultyDescription.person.alternateTelephone && + `+${facultyDescription.person.alternateCountryCode ?? 91} ${facultyDescription.person.alternateTelephone} (Mob)`} +

    +
  • +
  • +

    + + {facultyDescription.officeAddress === '' + ? 'Not Available' + : facultyDescription.officeAddress} +

    +
  • +
+
+
+ 0 +
+
+
    + {Object.entries(text.intellectualContributions).map( + ([key, value]) => ( +
  • +

    + {facultyDescription[ + key as keyof typeof text.intellectualContributions + ] ?? 0} +

    +

    + {value} +

    +
  • + ) + )} +
+
+
+
+ {( + Object.entries(text.externalLinks) as [ + keyof typeof text.externalLinks, + string, + ][] + ).map(([key, value]) => { + if (key in facultyDescription) { + return ( + + {key} +
{value}
+ + ); + } + })} +
+ +
+ +
    + +
+
+ + {children} + +
+
+ + ); +} +const facultyTables = { + qualifications: qualifications, + experience: experience, + projects: researchProjects, + publications: publications, + continuingEducation: continuingEducation, + awardsAndHonors: awardsAndHonors, +} as const; + +async function FacultySectionComponent({ + locale, + facultySection, + id, + employeeId, +}: { + locale: string; + facultySection: keyof Translations['FacultyAndStaff']['tabs']; +} & ({ id: number; employeeId?: never } | { id?: never; employeeId: string })) { + const text = (await getTranslations(locale)).FacultyAndStaff; + // Get the appropriate table for the faculty section + const table = + facultySection in facultyTables + ? facultyTables[facultySection as keyof typeof facultyTables] + : undefined; + + const result = (await (async () => { + if (facultySection === 'researchScholars') { + return await fetchResearchScholars(id, employeeId); + } else if (id) { + return await fetchSectionByFacultyId( + id, + facultySection === 'projects' ? 'researchProjects' : facultySection, + !table + ); + } + // Using employee ID + else if (employeeId) { + // Standard faculty tables + if (table !== undefined) { + return await db + .select() + .from(table) + .where(eq(table.facultyId, employeeId)); + } else { + return ( + await db.query.customTopics.findFirst({ + where: (customTopics, { eq, and }) => + and( + eq(customTopics.facultyId, employeeId), + eq(customTopics.name, facultySection) + ), + columns: {}, + with: { + customInformation: true, + }, + }) + )?.customInformation; + } + } + return []; + })()) as { + title: string; + details?: string; + field?: string; + type?: string; + people?: string; + location?: string; + role?: string; + date?: string; + startDate?: string; + createdOn?: string; + endedOn?: Date; + status?: string; + fundingAgency?: string; + endDate?: string; + tag?: string; + caption?: string; + id: number; + degree?: string; + description?: string; + }[]; //typescript cannot infer the type of result, so we have to specify it explicitly + if (!result || facultySection === 'researchScholars') { + return ( +
+ +
+ ); + } + + const uniqueTags = Array.from( + new Set(result.filter((item) => item.tag).map((item) => item.tag)) + ) as string[]; + + const tagStyle = + ` + .tag-filter:has(#filter-all:checked) ~ .rounded-2xl ul li { + display: flex; + } + + .tag-filter:has(.filter-input:checked:not(#filter-all)) + ~ .rounded-2xl + ul + li { + display: none; + } + + ` + + uniqueTags + .map( + (tag) => ` + .tag-filter:has(#filter-${tag}:checked) ~ .rounded-2xl ul li[data-tag=${tag}] { + display: flex; + }` + ) + .join('\n'); + return ( + <> +

{text.tabs[facultySection]}

+ + {uniqueTags.length > 0 && ( + <> + +
+ {['all', ...uniqueTags].map((tag) => ( +
+ + +
+ ))} +
+ + )} + {id && ( + + )} +
+ +
    + {result.map((item, index) => ( +
  • + +
    {item.title}
    + + {id ? ( + <> + + + + + + + + ) : null} +
    +

    + {item.details ?? + item.field ?? + item.type ?? + item.description ?? + item.degree} +

    +

    + {item.people ?? item.location ?? item.role ?? item.caption} +

    +

    + {item.date ?? item.startDate} + {item.startDate && item.endDate && ' - '} + {item.endDate ?? + (item.endedOn ? item.endedOn.toDateString() : item.status)} + {item.tag && ( + + {item.tag} + + )} +

    +
  • + ))} +
+
+ + ); +} + +async function fetchResearchScholars(id?: number, employeeId?: string) { + const baseQuery = () => + db.select({ + title: doctorates.title, + details: doctorates.type, + createdOn: doctorates.createdOn, + endedOn: doctorates.endedOn, + degree: majors.degree, + people: persons.name, + }); + const query = ( + employeeId + ? baseQuery().from(doctorates) + : baseQuery() + .from(faculty) + .innerJoin( + doctorates, + eq(doctorates.supervisorId, faculty.employeeId) + ) + ) + .innerJoin(students, eq(students.id, doctorates.studentId)) + .innerJoin(persons, eq(persons.id, students.id)) + .innerJoin( + studentAcademicDetails, + eq(studentAcademicDetails.id, students.id) + ) + .innerJoin(majors, eq(majors.id, studentAcademicDetails.majorId)); + + return employeeId + ? await query.where(eq(doctorates.supervisorId, employeeId)) + : await query.where(eq(faculty.id, id!)); +} + +type SectionKey = keyof typeof facultyTables | 'researchProjects'; + +async function fetchSectionByFacultyId( + id: number, + section: SectionKey, + customColumn: boolean +) { + const facultyData = await db.query.faculty.findFirst({ + where: (faculty, { eq }) => eq(faculty.id, id), + columns: {}, + with: { + ...(customColumn + ? { + customTopics: { + where: (customTopics, { eq }) => eq(customTopics.name, section), + columns: {}, + with: { customInformation: true }, + }, + } + : { + [section]: true, + }), + }, + }); + + return customColumn // @ts-expect-error - broken type inference + ? (facultyData?.customTopics[0]?.customInformation as { + id: number; + description: string | null; + title: string; + startDate: string | null; + endDate: string | null; + topicId: number; + caption: string | null; + }[]) + : ( + facultyData as unknown as { + [K in SectionKey]?: { title: string }[]; + } + )?.[section]; +} + +export { FacultyOrStaffComponent, FacultySectionComponent }; diff --git a/app/[locale]/profile/[faculty_section]/page.tsx b/app/[locale]/profile/[faculty_section]/page.tsx new file mode 100644 index 00000000..43f5241a --- /dev/null +++ b/app/[locale]/profile/[faculty_section]/page.tsx @@ -0,0 +1,24 @@ +import { FacultySectionComponent } from '~/app/faculty-and-staff/utils'; +import { type Translations } from '~/i18n/translations'; +import { getServerAuthSession } from '~/server/auth'; + +export default async function FacultyProfileSection({ + params: { locale, faculty_section }, +}: { + params: { + locale: string; + faculty_section: keyof Translations['FacultyAndStaff']['tabs']; + }; +}) { + const { + person: { id }, + } = (await getServerAuthSession())!; + + return ( + + ); +} diff --git a/app/[locale]/profile/client-utils.tsx b/app/[locale]/profile/client-utils.tsx index ea7b80d6..2a50e31a 100644 --- a/app/[locale]/profile/client-utils.tsx +++ b/app/[locale]/profile/client-utils.tsx @@ -5,9 +5,15 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import { Suspense } from 'react'; import { BsBellFill, BsPeopleFill, BsPersonFill } from 'react-icons/bs'; -import { FaBookmark, FaNewspaper } from 'react-icons/fa'; +import { FaBook, FaBookmark, FaFlask, FaNewspaper } from 'react-icons/fa'; import { IoMdSend } from 'react-icons/io'; -import { MdSchool } from 'react-icons/md'; +import { + MdApproval, + MdEmojiEvents, + MdGroups, + MdSchool, + MdWork, +} from 'react-icons/md'; import { Button } from '~/components/buttons'; import { @@ -38,11 +44,13 @@ export const LogOut = ({ export const PathnameAwareSuspense = ({ children, + defaultPathname, }: { children: React.ReactNode; + defaultPathname: string; }) => { const path = usePathname().split('/').splice(3); // ['', 'en|hi', 'profile', ?] - const tab = path.length === 0 ? 'personal' : path[0]; + const tab = path.length === 0 ? defaultPathname : path[0]; return ( } key={tab}> {children} @@ -53,92 +61,81 @@ export const PathnameAwareSuspense = ({ export const Tabs = ({ locale, select = false, - text, + tabs, + defaultPath, + basePath, + pathLength = 3, }: { locale: string; select?: boolean; - text: { - bookmarks: string; - clubs: string; - courses: string; - notifications: string; - personal: string; - quickSend: string; - results: string; - }; + tabs: { + label: string; + href: string; + }[]; + defaultPath: string; + basePath: string; + pathLength?: number; }) => { - const path = usePathname().split('/').slice(3); // ['', 'en|hi', 'profile', ?] - const tab = path.length === 0 ? 'personal' : path[0]; + const pathname = usePathname(); + const path = pathname.split('/').slice(pathLength); // ['', 'en|hi', 'profile', ?] + const tab = path.length === 0 ? defaultPath : path[0]; - const tabs = [ - { - label: text.personal, - href: 'personal', - icon: BsPersonFill, - }, - { - label: text.notifications, - href: 'notifications', - icon: BsBellFill, - }, - { - label: text.courses, - href: 'courses', - icon: MdSchool, - }, - { - label: text.clubs, - href: 'clubs', - icon: BsPeopleFill, - }, - { - label: text.results, - href: 'results', - icon: FaNewspaper, - }, - { - label: text.bookmarks, - href: 'bookmarks', - icon: FaBookmark, - }, - { - label: text.quickSend, - href: 'quick-send', - icon: IoMdSend, - }, - ]; + const icons = { + personal: BsPersonFill, + notifications: BsBellFill, + courses: MdSchool, + clubs: BsPeopleFill, + results: FaNewspaper, + bookmarks: FaBookmark, + 'quick-send': IoMdSend, + qualifications: MdSchool, + experience: MdWork, + projects: FaFlask, + continuingEducation: FaBook, + publications: MdApproval, + researchScholars: MdGroups, + awardsAndHonors: MdEmojiEvents, + }; return select ? ( - {tabs.map(({ label, href }, index) => ( - + {label} ))} ) : ( - tabs.map(({ label, href, icon: Icon }, index) => ( -
  • - -
  • - )) + tabs.map(({ label, href }, index) => { + const Icon = icons[href as keyof typeof icons]; + return ( +
  • + +
  • + ); + }) ); }; diff --git a/app/[locale]/profile/layout.tsx b/app/[locale]/profile/layout.tsx index 5745f2d2..7459310b 100644 --- a/app/[locale]/profile/layout.tsx +++ b/app/[locale]/profile/layout.tsx @@ -6,6 +6,7 @@ import { cn } from '~/lib/utils'; import { getServerAuthSession } from '~/server/auth'; import { db } from '~/server/db'; +import { FacultyOrStaffComponent } from '../faculty-and-staff/utils'; import { LogOut, PathnameAwareSuspense, Tabs } from './client-utils'; export default async function ProfileLayout({ @@ -17,7 +18,12 @@ export default async function ProfileLayout({ }) { const session = await getServerAuthSession(); if (!session) return ; - + else if (session.person.type == 'faculty') + return ( + + {children} + + ); const text = (await getTranslations(locale)).Profile; const student = (await db.query.students.findFirst({ @@ -25,6 +31,37 @@ export default async function ProfileLayout({ where: (student, { eq }) => eq(student.id, session.person.id), }))!; + const tabs = [ + { + label: text.tabs.personal.title, + href: 'personal', + }, + { + label: text.tabs.notifications.title, + href: 'notifications', + }, + { + label: text.tabs.courses.title, + href: 'courses', + }, + { + label: text.tabs.clubs.title, + href: 'clubs', + }, + { + label: text.tabs.results.title, + href: 'results', + }, + { + label: text.tabs.bookmarks.title, + href: 'bookmarks', + }, + { + label: text.tabs.quickSend.title, + href: 'quick-send', + }, + ]; + return (
    @@ -84,19 +115,15 @@ export default async function ProfileLayout({ -
    - {children} +
    + + {children} +
    ); diff --git a/app/[locale]/profile/page.tsx b/app/[locale]/profile/page.tsx index f9065d2b..fb4067c5 100644 --- a/app/[locale]/profile/page.tsx +++ b/app/[locale]/profile/page.tsx @@ -1,9 +1,22 @@ -import Personal from './personal/page'; +import { getServerAuthSession } from '~/server/auth'; + +import { FacultySectionComponent } from '../faculty-and-staff/utils'; +import { Personal } from './personal/utils'; export default async function Profile({ - params, + params: { locale }, }: { params: { locale: string }; }) { - return ; + const session = (await getServerAuthSession())!; + if (session.person.type == 'faculty') + return ( + + ); + + return ; } diff --git a/app/[locale]/profile/personal/page.tsx b/app/[locale]/profile/personal/page.tsx index 3732ea31..e8e1e1d5 100644 --- a/app/[locale]/profile/personal/page.tsx +++ b/app/[locale]/profile/personal/page.tsx @@ -1,166 +1,12 @@ -import { getTranslations } from '~/i18n/translations'; import { getServerAuthSession } from '~/server/auth'; -import { db } from '~/server/db'; -export default async function Personal({ +import { Personal } from './utils'; + +export default async function PersonalPage({ params: { locale }, }: { params: { locale: string }; }) { - const text = (await getTranslations(locale)).Profile.tabs.personal; - - const session = (await getServerAuthSession())!; - const person = (await db.query.persons.findFirst({ - columns: { - alternateTelephone: true, - createdOn: true, - dateOfBirth: true, - email: true, - id: true, - name: true, - sex: true, - telephone: true, - }, - where: (person, { eq }) => eq(person.id, session.person.id), - }))!; - const student = (await db.query.students.findFirst({ - where: (student, { eq }) => eq(student.id, person.id), - }))!; - const studentAcademicDetails = - (await db.query.studentAcademicDetails.findFirst({ - where: (studentAcademic, { eq }) => eq(studentAcademic.id, student.id), - with: { major: { columns: { degree: true, name: true } } }, - }))!; - - return ( - <> -
    -

    {text.title}

    -
    -
    - -
    -
    - -
    - -
    - -
    - -
    -

    {text.guardians.father}

    -
    -

    - {text.guardians.name}: {student.fathersName} -

    -

    - {text.guardians.telephone}: {student.fathersTelephone} -

    -

    - {text.guardians.email}: {student.fathersEmail ?? '-'} -

    -
    -

    {text.guardians.mother}

    -
    -

    - {text.guardians.name}: {student.mothersName ?? '-'} -

    -

    - {text.guardians.telephone}: {student.mothersTelephone ?? '-'} -

    -
    -

    {text.guardians.local}

    -
    -

    - {text.guardians.name}: {student.localGuardiansName ?? '-'} -

    -

    - {text.guardians.telephone}:{' '} - {student.localGuardiansTelephone ?? '-'} -

    -
    -
    - -
    -
    - - ); + const personId = (await getServerAuthSession())!.person.id; + return ; } - -const Section = ({ - children, - items = [], - title, -}: { - children?: React.ReactNode; - items?: string[]; - title: string; -}) => ( - <> -
    -
    {title}
    -
    -
    - {items.map((item, index) => ( -

    {item}

    - ))} - {children} -
    -
    - -); diff --git a/app/[locale]/profile/personal/utils.tsx b/app/[locale]/profile/personal/utils.tsx new file mode 100644 index 00000000..a87ef129 --- /dev/null +++ b/app/[locale]/profile/personal/utils.tsx @@ -0,0 +1,160 @@ +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; + +export async function Personal({ locale, id }: { locale: string; id: number }) { + const text = (await getTranslations(locale)).Profile.tabs.personal; + + const person = (await db.query.persons.findFirst({ + columns: { + alternateTelephone: true, + createdOn: true, + dateOfBirth: true, + email: true, + id: true, + name: true, + sex: true, + telephone: true, + }, + where: (person, { eq }) => eq(person.id, id), + }))!; + const student = (await db.query.students.findFirst({ + where: (student, { eq }) => eq(student.id, person.id), + }))!; + const studentAcademicDetails = + (await db.query.studentAcademicDetails.findFirst({ + where: (studentAcademic, { eq }) => eq(studentAcademic.id, student.id), + with: { major: { columns: { degree: true, name: true } } }, + }))!; + + return ( + <> +
    +

    {text.title}

    +
    +
    + +
    +
    + +
    + +
    + +
    + +
    +

    {text.guardians.father}

    +
    +

    + {text.guardians.name}: {student.fathersName} +

    +

    + {text.guardians.telephone}: {student.fathersTelephone} +

    +

    + {text.guardians.email}: {student.fathersEmail ?? '-'} +

    +
    +

    {text.guardians.mother}

    +
    +

    + {text.guardians.name}: {student.mothersName ?? '-'} +

    +

    + {text.guardians.telephone}: {student.mothersTelephone ?? '-'} +

    +
    +

    {text.guardians.local}

    +
    +

    + {text.guardians.name}: {student.localGuardiansName ?? '-'} +

    +

    + {text.guardians.telephone}:{' '} + {student.localGuardiansTelephone ?? '-'} +

    +
    +
    + +
    +
    + + ); +} + +const Section = ({ + children, + items = [], + title, +}: { + children?: React.ReactNode; + items?: string[]; + title: string; +}) => ( + <> +
    +
    {title}
    +
    +
    + {items.map((item, index) => ( +

    {item}

    + ))} + {children} +
    +
    + +); diff --git a/components/inputs/select.tsx b/components/inputs/select.tsx index 0cfcae33..bc0f93da 100644 --- a/components/inputs/select.tsx +++ b/components/inputs/select.tsx @@ -164,7 +164,7 @@ const SelectContent = React.forwardRef< className={cn( selectContentVariants({ variant }), position === 'popper' && - 'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', + 'z-modal data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1', className )} position={position} diff --git a/i18n/en.ts b/i18n/en.ts index 4e0179a6..cb71cde7 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -147,6 +147,32 @@ const text: Translations = { FacultyAndStaff: { placeholder: 'Search by name or email', departmentHead: 'Head of Department', + externalLinks: { + googleScholarId: 'Google Scholar', + linkedInId: 'LinkedIn', + researchGateId: 'Research Gate', + scopusId: 'Scopus', + }, + intellectualContributions: { + publications: 'PUBLICATIONS', + continuingEducation: 'CONTINUING EDUCATION', + doctoralStudents: 'DOCTORAL STUDENTS', + }, + tags: { + book: 'Book', + chapter: 'Chapter', + journal: 'Journal', + conference: 'Conference', + }, + tabs: { + qualifications: 'Education Qualifications', + experience: 'Experience', + projects: 'Projects', + continuingEducation: 'Continuing Education', + publications: 'Publications', + researchScholars: 'Research Scholars', + awardsAndHonors: 'Awards and Honors', + }, }, FAQ: { title: 'Frequently Asked Questions' }, Footer: { diff --git a/i18n/hi.ts b/i18n/hi.ts index 78c2a811..46c743f1 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -151,10 +151,35 @@ const text: Translations = { }, viewAll: 'सभी देखें', }, - FacultyAndStaff: { placeholder: 'नाम या ईमेल से खोजें', departmentHead: 'विभागाध्यक्ष', + externalLinks: { + googleScholarId: 'गूगल स्कॉलर', + linkedInId: 'लिंक्डइन', + researchGateId: 'रिसर्च गेट', + scopusId: 'स्कोपस', + }, + intellectualContributions: { + publications: 'प्रकाशन', + continuingEducation: 'निरंतर शिक्षा', + doctoralStudents: 'डॉक्टरेट छात्र', + }, + tags: { + book: 'पुस्तक', + journal: 'जर्नल', + chapter: 'अध्याय', + conference: 'सम्मेलन', + }, + tabs: { + qualifications: 'शैक्षिक योग्यता', + experience: 'अनुभव', + projects: 'प्रोजेक्ट्स', + continuingEducation: 'निरंतर शिक्षा', + publications: 'प्रकाशन', + researchScholars: 'अनुसंधान विद्वान', + awardsAndHonors: 'पुरस्कार और सम्मान', + }, }, FAQ: { title: 'अक्सर पूछे जाने वाले प्रश्न' }, Footer: { diff --git a/i18n/translations.ts b/i18n/translations.ts index 4d5b5ac4..c73b3be4 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -100,6 +100,32 @@ export interface Translations { FacultyAndStaff: { placeholder: string; departmentHead: string; + externalLinks: { + googleScholarId: string; + linkedInId: string; + researchGateId: string; + scopusId: string; + }; + intellectualContributions: { + publications: string; + continuingEducation: string; + doctoralStudents: string; + }; + tags: { + book: string; + chapter: string; + journal: string; + conference: string; + }; + tabs: { + qualifications: string; + experience: string; + projects: string; + continuingEducation: string; + publications: string; + researchScholars: string; + awardsAndHonors: string; + }; }; FAQ: { title: string }; Footer: { diff --git a/lib/schemas/faculty-profile.ts b/lib/schemas/faculty-profile.ts new file mode 100644 index 00000000..70409a2c --- /dev/null +++ b/lib/schemas/faculty-profile.ts @@ -0,0 +1,115 @@ +import { z } from 'zod'; + +// Helper function to handle both string and Date inputs +const dateInput = () => + z + .union([z.string().min(1), z.date()]) + .transform((val) => (typeof val === 'string' ? new Date(val) : val)); + +const optionalDateInput = () => + z.union([z.string().optional(), z.date().optional()]).transform((val) => { + if (!val) return undefined; + return typeof val === 'string' ? new Date(val) : val; + }); + +// Shared schemas for faculty profile sections that work in both frontend and backend +export const facultyProfileSchemas = { + qualifications: z.object({ + title: z.string().min(1, 'Title is required'), + field: z.string().min(1, 'Field is required'), + location: z.string().min(1, 'Location is required'), + startDate: dateInput(), + endDate: optionalDateInput(), + }), + + experience: z.object({ + title: z.string().min(1, 'Title is required'), + field: z.string().min(1, 'Field is required'), + location: z.string().min(1, 'Location is required'), + startDate: dateInput(), + endDate: dateInput(), + }), + + publications: z.object({ + title: z.string().min(1, 'Title is required'), + details: z.string().min(1, 'Details are required'), + people: z.string().min(1, 'People are required'), + date: dateInput(), + tag: z.enum(['book', 'journal', 'conference']), + }), + + continuingEducation: z.object({ + title: z.string().min(1, 'Title is required'), + type: z.string().min(1, 'Type is required'), + role: z.string().min(1, 'Role is required'), + startDate: dateInput(), + endDate: dateInput(), + }), + + projects: z.object({ + title: z.string().min(1, 'Title is required'), + fundingAgency: z.string().min(1, 'Funding agency is required'), + amount: z + .union([z.string(), z.number()]) + .transform((val) => (typeof val === 'string' ? parseFloat(val) : val)) + .refine((val) => val > 0, 'Amount must be greater than 0'), + role: z.string().min(1, 'Role is required'), + status: z.string().default('on-going'), + durationPeriod: z.string().min(1, 'Duration period is required'), + durationPeriodType: z.string().min(1, 'Duration period type is required'), + endedOn: optionalDateInput(), + }), + + awardsAndHonors: z.object({ + title: z.string().min(1, 'Title is required'), + field: z.string().min(1, 'Field is required'), + location: z.string().min(1, 'Location is required'), + date: dateInput(), + }), + + customTopics: z.object({ + name: z.string().min(1, 'Name is required'), + }), + + customInformation: z.object({ + title: z.string().min(1, 'Title is required'), + description: z.string().optional(), + caption: z.string().optional(), + startDate: optionalDateInput(), + endDate: optionalDateInput(), + }), +}; +export const facultyPersonalDetailsSchema = z.object({ + scopusId: z + .string() + .regex(/^\d+-\d+$/, 'Invalid Scopus ID format') + .optional(), + linkedInId: z + .string() + .regex(/^[a-zA-Z0-9-]+$/, 'Invalid LinkedIn ID format') + .optional(), + googleScholarId: z + .string() + .regex(/^[a-zA-Z0-9_-]+$/, 'Invalid Google Scholar ID format') + .optional(), + researchGateId: z + .string() + .regex(/^[a-zA-Z0-9_-]+$/, 'Invalid ResearchGate ID format') + .optional(), + countryCode: z + .string() + .regex(/^\d{1,3}$/, 'Enter valid country code (e.g. +91)') + .optional(), + telephone: z.string().regex(/^\d{5,15}$/, 'Enter a valid phone number'), + alternateCountryCode: z + .string() + .regex(/^\d{1,3}$/, 'Enter valid country code') + .optional(), + alternateTelephone: z + .string() + .regex(/^\d{5,15}$/, 'Enter a valid phone number') + .optional(), + officeAddress: z.string().max(200), +}); + +export type FacultyProfileTopic = keyof typeof facultyProfileSchemas; diff --git a/server/actions/faculty-profile.ts b/server/actions/faculty-profile.ts new file mode 100644 index 00000000..76999ffe --- /dev/null +++ b/server/actions/faculty-profile.ts @@ -0,0 +1,170 @@ +'use server'; +import { eq, sql } from 'drizzle-orm'; +import { revalidatePath } from 'next/cache'; +import type z from 'zod'; + +import { + facultyPersonalDetailsSchema, + facultyProfileSchemas, +} from '~/lib/schemas/faculty-profile'; +import { getServerAuthSession } from '~/server/auth'; +import { db } from '~/server/db'; +import { + awardsAndHonors, + continuingEducation, + experience, + faculty, + persons, + publications, + qualifications, + researchProjects, +} from '~/server/db/schema'; + +// Configuration for each section type +const sectionConfig = { + qualifications: { + table: qualifications, + schema: facultyProfileSchemas.qualifications, + }, + experience: { + table: experience, + schema: facultyProfileSchemas.experience, + }, + publications: { + table: publications, + schema: facultyProfileSchemas.publications, + }, + projects: { + table: researchProjects, + schema: facultyProfileSchemas.projects, + }, + continuingEducation: { + table: continuingEducation, + schema: facultyProfileSchemas.continuingEducation, + }, + awardsAndHonors: { + table: awardsAndHonors, + schema: facultyProfileSchemas.awardsAndHonors, + }, +} as const; + +export async function upsertFacultySection( + topic: string, + id: number | null, + formData: z.infer< + (typeof facultyProfileSchemas)[keyof typeof facultyProfileSchemas] + > +) { + const session = await getServerAuthSession(); + if (!session || session.person.type !== 'faculty') { + return { success: false, message: 'Not authorized' }; + } + + const faculty = await db.query.faculty.findFirst({ + where: (faculty, { eq }) => eq(faculty.id, session.person.id), + columns: { employeeId: true }, + }); + + if (!faculty) { + return { success: false, message: 'Faculty not found' }; + } + + const config = sectionConfig[topic as keyof typeof sectionConfig]; + if (!config) { + return { success: false, message: 'Invalid topic' }; + } + + try { + const validated = config.schema.parse(formData); + + const insertData = { + ...validated, + facultyId: faculty.employeeId, + ...(id && { id }), + }; + + await db.insert(config.table).values(insertData).onConflictDoUpdate({ + target: config.table.id, + set: validated, + }); + + revalidatePath('/profile/' + topic); + return { success: true, message: 'Updated successfully' }; + } catch (error) { + console.error('Error updating faculty section:', error); + return { success: false, message: 'Failed to update' }; + } +} + +export async function deleteFacultySelectionElement( + topic?: string, + id?: string +) { + const session = await getServerAuthSession(); + if (!session || session.person.type !== 'faculty' || !session.person.id) { + return { success: false, message: 'Not authorized' }; + } + + if (!faculty) { + return { success: false, message: 'Faculty not found' }; + } + + const { table } = sectionConfig[topic as keyof typeof sectionConfig]; + if (!table || !topic || !id || isNaN(parseInt(id, 10))) { + return { success: false, message: 'Unknown Error' }; + } + + try { + // No equivalent for the complex delete logic in Drizzle ORM + await db.execute( + sql`DELETE FROM ${table} WHERE id = ${parseInt(id, 10)} AND faculty_id = (SELECT employee_id FROM faculty WHERE id = ${session.person.id})` + ); + + revalidatePath('/profile/' + topic); + return { success: true, message: 'Deleted successfully' }; + } catch (error) { + console.error('Error deleting faculty section:', error); + return { success: false, message: 'Failed to delete' }; + } +} + +export async function editFacultyProfilePersonalDetails( + personalDetails: z.infer +) { + const session = await getServerAuthSession(); + if (!session || session.person.type !== 'faculty') { + return { success: false, message: 'Not authorized' }; + } + + try { + const validated = facultyPersonalDetailsSchema.parse(personalDetails); + + await db.transaction(async (tx) => { + await tx + .update(faculty) + .set({ + officeAddress: validated.officeAddress, + scopusId: validated.scopusId, + linkedInId: validated.linkedInId, + googleScholarId: validated.googleScholarId, + researchGateId: validated.researchGateId, + }) + .where(eq(faculty.id, session.person.id)); + + await tx + .update(persons) + .set({ + countryCode: validated.countryCode, + telephone: validated.telephone, + alternateCountryCode: validated.alternateCountryCode, + alternateTelephone: validated.alternateTelephone, + }) + .where(eq(persons.id, session.person.id)); + }); + revalidatePath('/profile/personal-details'); + return { success: true, message: 'Personal details updated successfully' }; + } catch (error) { + console.error('Error updating personal details:', error); + return { success: false, message: 'Failed to update personal details' }; + } +} diff --git a/server/actions/index.ts b/server/actions/index.ts new file mode 100644 index 00000000..7c6eb3ce --- /dev/null +++ b/server/actions/index.ts @@ -0,0 +1 @@ +export * from './faculty-profile'; diff --git a/server/db/schema/doctorates.schema.ts b/server/db/schema/doctorates.schema.ts index f7a0be44..7d70edae 100644 --- a/server/db/schema/doctorates.schema.ts +++ b/server/db/schema/doctorates.schema.ts @@ -15,8 +15,8 @@ export const doctorates = pgTable('doctorates', (t) => ({ .references(() => students.id) .notNull(), supervisorId: t - .integer() - .references(() => faculty.id) + .varchar() + .references(() => faculty.employeeId) .notNull(), type: t.varchar({ enum: ['part-time', 'full-time'] }).notNull(), title: t.varchar({ length: 256 }).notNull(), @@ -35,6 +35,6 @@ export const doctoratesRelations = relations(doctorates, ({ one }) => ({ }), supervisor: one(faculty, { fields: [doctorates.supervisorId], - references: [faculty.id], + references: [faculty.employeeId], }), })); diff --git a/server/db/schema/faculty.schema.ts b/server/db/schema/faculty.schema.ts index 98f81790..af07d82d 100644 --- a/server/db/schema/faculty.schema.ts +++ b/server/db/schema/faculty.schema.ts @@ -1,4 +1,4 @@ -import { relations, sql } from 'drizzle-orm'; +import { relations } from 'drizzle-orm'; import { pgTable, uniqueIndex } from 'drizzle-orm/pg-core'; import { @@ -7,8 +7,8 @@ import { departments, doctorates, persons, + researchProjects, sectionHeads, - sponsoredResearchProjects, } from '.'; export const faculty = pgTable( @@ -39,95 +39,124 @@ export const faculty = pgTable( // Socials googleScholarId: t.text(), - orcidId: t.text(), + linkedInId: t.text(), researchGateId: t.text(), scopusId: t.text(), - - // Miscellaneous - qualifications: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - areasOfInterest: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - teachingInterests: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - researchInterests: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - patents: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - copyrights: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - publications: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - journals: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - conferences: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - books: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - workshops: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - expertLectures: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - awards: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - outreach: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - eContent: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), - researchProjects: t - .text() - .array() - .default(sql`'{}'`) - .notNull(), }), (table) => [uniqueIndex('faculty_employee_id_idx').on(table.employeeId)] ); +// Qualifications Table +export const qualifications = pgTable('qualifications', (t) => ({ + id: t.serial().primaryKey(), + facultyId: t + .varchar() + .references(() => faculty.employeeId) + .notNull(), + title: t.text().notNull(), + field: t.text().notNull(), + location: t.text().notNull(), + startDate: t.date().notNull(), + endDate: t.date(), +})); + +// Experience Table +export const experience = pgTable('experience', (t) => ({ + id: t.serial().primaryKey(), + facultyId: t + .varchar() + .references(() => faculty.employeeId) + .notNull(), + title: t.text().notNull(), + field: t.text().notNull(), + location: t.text().notNull(), + startDate: t.date().notNull(), + endDate: t.date().notNull(), +})); + +// Continuing Education Table +export const continuingEducation = pgTable('continuing_education', (t) => ({ + id: t.serial().primaryKey(), + facultyId: t + .varchar() + .references(() => faculty.employeeId) + .notNull(), + title: t.text().notNull(), + type: t.text().notNull(), + role: t.text().notNull(), + startDate: t.date().notNull(), + endDate: t.date().notNull(), +})); + +// Publications Table +export const publications = pgTable('publications', (t) => ({ + id: t.serial().primaryKey(), + facultyId: t + .varchar() + .references(() => faculty.employeeId) + .notNull(), + title: t.text().notNull(), + details: t.text().notNull(), + people: t.text().notNull(), + date: t.date().notNull(), + tag: t + .varchar({ + enum: ['book', 'journal', 'conference'], + }) + .notNull(), +})); + +// // Research Scholars Table +// export const researchScholars = pgTable('research_scholars', (t) => ({ +// id: t.serial().primaryKey(), +// facultyId: t +// .varchar() +// .references(() => faculty.employeeId) +// .notNull(), +// title: t.text().notNull(), +// role: t.text().notNull(), +// person: t.text().notNull(), +// date: t.date().notNull(), +// tag: t.text(), +// })); + +// Awards and Honors Table +export const awardsAndHonors = pgTable('awards_and_honors', (t) => ({ + id: t.serial().primaryKey(), + facultyId: t + .varchar() + .references(() => faculty.employeeId) + .notNull(), + title: t.text().notNull(), + field: t.text().notNull(), + date: t.date().notNull(), + location: t.text().notNull(), +})); + +// Custom Topics Table +export const customTopics = pgTable('custom_topics', (t) => ({ + id: t.serial().primaryKey(), + facultyId: t + .varchar() + .references(() => faculty.employeeId) + .notNull(), + name: t.text().notNull(), +})); + +// Custom Information Table +export const customInformation = pgTable('custom_information', (t) => ({ + id: t.serial().primaryKey(), + topicId: t + .integer() + .references(() => customTopics.id, { onDelete: 'cascade' }) + .notNull(), + title: t.text().notNull(), + description: t.text(), + caption: t.text(), + startDate: t.date(), + endDate: t.date(), +})); + export const facultyRelations = relations(faculty, ({ many, one }) => ({ courseLogs: many(courseLogs), courses: many(courses), @@ -137,9 +166,77 @@ export const facultyRelations = relations(faculty, ({ many, one }) => ({ }), doctorates: many(doctorates), sectionHead: many(sectionHeads), - sponsoredResearchProjects: many(sponsoredResearchProjects), + researchProjects: many(researchProjects), person: one(persons, { fields: [faculty.id], references: [persons.id], }), + qualifications: many(qualifications), + experience: many(experience), + continuingEducation: many(continuingEducation), + publications: many(publications), + awardsAndHonors: many(awardsAndHonors), + customTopics: many(customTopics), +})); + +export const qualificationsRelations = relations(qualifications, ({ one }) => ({ + faculty: one(faculty, { + fields: [qualifications.facultyId], + references: [faculty.employeeId], + }), +})); + +export const experienceRelations = relations(experience, ({ one }) => ({ + faculty: one(faculty, { + fields: [experience.facultyId], + references: [faculty.employeeId], + }), +})); + +export const continuingEducationRelations = relations( + continuingEducation, + ({ one }) => ({ + faculty: one(faculty, { + fields: [continuingEducation.facultyId], + references: [faculty.employeeId], + }), + }) +); + +export const publicationsRelations = relations(publications, ({ one }) => ({ + faculty: one(faculty, { + fields: [publications.facultyId], + references: [faculty.employeeId], + }), })); + +export const awardsAndHonorsRelations = relations( + awardsAndHonors, + ({ one }) => ({ + faculty: one(faculty, { + fields: [awardsAndHonors.facultyId], + references: [faculty.employeeId], + }), + }) +); + +export const customTopicsRelations = relations( + customTopics, + ({ one, many }) => ({ + faculty: one(faculty, { + fields: [customTopics.facultyId], + references: [faculty.employeeId], + }), + customInformation: many(customInformation), + }) +); + +export const customInformationRelations = relations( + customInformation, + ({ one }) => ({ + customTopic: one(customTopics, { + fields: [customInformation.topicId], + references: [customTopics.id], + }), + }) +); diff --git a/server/db/schema/index.ts b/server/db/schema/index.ts index c131d732..20cf91d0 100644 --- a/server/db/schema/index.ts +++ b/server/db/schema/index.ts @@ -17,10 +17,10 @@ export * from './library.schema'; export * from './majors.schema'; export * from './notifications.schema'; export * from './persons.schema'; +export * from './research-projects.schema'; export * from './roles.schema'; -export * from './sections.schema'; export * from './section-heads.schema'; -export * from './sponsored-research-projects.schema'; +export * from './sections.schema'; export * from './staff.schema'; export * from './student-academic-details.schema'; export * from './students.schema'; diff --git a/server/db/schema/research-projects.schema.ts b/server/db/schema/research-projects.schema.ts new file mode 100644 index 00000000..c940f2ce --- /dev/null +++ b/server/db/schema/research-projects.schema.ts @@ -0,0 +1,32 @@ +import { relations } from 'drizzle-orm'; +import { pgTable } from 'drizzle-orm/pg-core'; + +import { faculty } from '.'; + +export const researchProjects = pgTable('research_projects', (t) => ({ + id: t.serial().primaryKey(), + title: t.varchar().notNull(), + fundingAgency: t.varchar().notNull(), + facultyId: t + .varchar() + .references(() => faculty.employeeId) + .notNull(), + // You can use { mode: "bigint" } if numbers are exceeding js number limitations + amount: t.bigint({ mode: 'number' }).notNull(), + role: t.varchar().notNull(), + status: t.varchar().default('on-going').notNull(), + durationPeriod: t.varchar().notNull(), + durationPeriodType: t.varchar().notNull(), + createdOn: t.date({ mode: 'date' }).defaultNow().notNull(), + endedOn: t.date({ mode: 'date' }), +})); + +export const researchProjectsRelations = relations( + researchProjects, + ({ one }) => ({ + faculty: one(faculty, { + fields: [researchProjects.facultyId], + references: [faculty.employeeId], + }), + }) +); diff --git a/server/db/schema/sponsored-research-projects.schema.ts b/server/db/schema/sponsored-research-projects.schema.ts deleted file mode 100644 index 314064e3..00000000 --- a/server/db/schema/sponsored-research-projects.schema.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { relations } from 'drizzle-orm'; -import { pgTable } from 'drizzle-orm/pg-core'; - -import { faculty } from '.'; - -export const sponsoredResearchProjects = pgTable( - 'sponsored_research_projects', - (t) => ({ - id: t.serial().primaryKey(), - title: t.varchar().notNull(), - fundingAgency: t.varchar().notNull(), - facultyId: t - .integer() - .references(() => faculty.id) - .notNull(), - // You can use { mode: "bigint" } if numbers are exceeding js number limitations - amount: t.bigint({ mode: 'number' }).notNull(), - status: t.varchar().default('on-going').notNull(), - durationPeriod: t.varchar().notNull(), - durationPeriodType: t.varchar().notNull(), - createdOn: t.date({ mode: 'date' }).defaultNow().notNull(), - endedOn: t.date({ mode: 'date' }), - }) -); - -export const sponsoredResearchProjectsRelations = relations( - sponsoredResearchProjects, - ({ one }) => ({ - faculty: one(faculty, { - fields: [sponsoredResearchProjects.facultyId], - references: [faculty.id], - }), - }) -);