import { Op, type Includeable, type InferAttributes, type InferCreationAttributes, type WhereAttributeHash, } from 'sequelize'; import db from '@/db/models'; import { removeRecord, deleteRecordsByIds, autocompleteByField, } from '@/db/api/shared/repository'; import { BULK_IMPORT_TIMESTAMP_STEP_MS } from '@/shared/constants/database'; import { resolvePagination } from '@/shared/constants/pagination'; import Utils from '@/db/utils'; import FileDBApi from '@/db/api/file'; import type { Staff } from '@/db/models/staff'; import type { CurrentUser, DbApiOptions, FileInput } from '@/db/api/types'; type StaffData = Partial> & { organization?: string | null; campus?: string | null; user?: string | null; photo?: FileInput | FileInput[] | null; }; interface StaffFilter { limit?: number | string; page?: number | string; id?: string; employee_number?: string; job_title?: string; hire_dateRange?: Array; active?: boolean | string; staff_type?: string; status?: string; campus?: string; user?: string; organization?: string; createdAtRange?: Array; field?: string; sort?: string; } const NO_USER: CurrentUser = { id: null }; function staffTableName(): string { const name = db.staff.getTableName(); return typeof name === 'string' ? name : name.tableName; } class StaffDBApi { static async create( data: StaffData, options?: DbApiOptions, ): Promise { const currentUser = options?.currentUser ?? NO_USER; const transaction = options?.transaction; const staff = await db.staff.create( { id: data.id || undefined, employee_number: data.employee_number || null, job_title: data.job_title || null, staff_type: data.staff_type || null, hire_date: data.hire_date || null, status: data.status || null, importHash: data.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, }, { transaction }, ); await staff.setOrganization(currentUser.organizationId ?? undefined, { transaction, }); await staff.setCampus(data.campus ?? undefined, { transaction }); await staff.setUser(data.user ?? undefined, { transaction }); await FileDBApi.replaceRelationFiles( { belongsTo: staffTableName(), belongsToColumn: 'photo', belongsToId: staff.id, }, data.photo, options, ); return staff; } static async bulkImport( data: StaffData[], options?: DbApiOptions, ): Promise { const currentUser = options?.currentUser ?? NO_USER; const transaction = options?.transaction; const staffData = data.map((item, index) => ({ id: item.id || undefined, employee_number: item.employee_number || null, job_title: item.job_title || null, staff_type: item.staff_type || null, hire_date: item.hire_date || null, status: item.status || null, importHash: item.importHash || null, createdById: currentUser.id, updatedById: currentUser.id, createdAt: new Date(Date.now() + index * BULK_IMPORT_TIMESTAMP_STEP_MS), })); const staff = await db.staff.bulkCreate(staffData, { transaction }); for (let i = 0; i < staff.length; i++) { await FileDBApi.replaceRelationFiles( { belongsTo: staffTableName(), belongsToColumn: 'photo', belongsToId: staff[i].id, }, data[i].photo, options, ); } return staff; } static async update( id: string, data: StaffData, options?: DbApiOptions, ): Promise { const currentUser = options?.currentUser ?? NO_USER; const transaction = options?.transaction; const globalAccess = currentUser.app_role?.globalAccess; const staff = await db.staff.findByPk(id, { transaction }); if (!staff) { return null; } const updatePayload: Partial> = {}; if (data.employee_number !== undefined) updatePayload.employee_number = data.employee_number; if (data.job_title !== undefined) updatePayload.job_title = data.job_title; if (data.staff_type !== undefined) updatePayload.staff_type = data.staff_type; if (data.hire_date !== undefined) updatePayload.hire_date = data.hire_date; if (data.status !== undefined) updatePayload.status = data.status; updatePayload.updatedById = currentUser.id; await staff.update(updatePayload, { transaction }); if (data.organization !== undefined) { const orgId = globalAccess ? data.organization : currentUser.organizationId; await staff.setOrganization(orgId ?? undefined, { transaction }); } if (data.campus !== undefined) { await staff.setCampus(data.campus ?? undefined, { transaction }); } if (data.user !== undefined) { await staff.setUser(data.user ?? undefined, { transaction }); } await FileDBApi.replaceRelationFiles( { belongsTo: staffTableName(), belongsToColumn: 'photo', belongsToId: staff.id, }, data.photo, options, ); return staff; } static async deleteByIds( ids: string[], options?: DbApiOptions, ): Promise { return deleteRecordsByIds(db.staff, ids, options); } static async remove( id: string, options?: DbApiOptions, ): Promise { return removeRecord(db.staff, id, options); } static async findBy( where: WhereAttributeHash, options?: DbApiOptions, ): Promise | null> { const transaction = options?.transaction; const staff = await db.staff.findOne({ where, transaction }); if (!staff) { return null; } const output: Record = staff.get({ plain: true }); const [ classes_homeroom_teacher, class_subjects_teacher, attendance_sessions_taken_by, payments_received_by, organization, campus, user, photo, ] = await Promise.all([ staff.getClasses_homeroom_teacher({ transaction }), staff.getClass_subjects_teacher({ transaction }), staff.getAttendance_sessions_taken_by({ transaction }), staff.getPayments_received_by({ transaction }), staff.getOrganization({ transaction }), staff.getCampus({ transaction }), staff.getUser({ transaction }), staff.getPhoto({ transaction }), ]); output.classes_homeroom_teacher = classes_homeroom_teacher; output.class_subjects_teacher = class_subjects_teacher; output.attendance_sessions_taken_by = attendance_sessions_taken_by; output.payments_received_by = payments_received_by; output.organization = organization; output.campus = campus; output.user = user; output.photo = photo; return output; } static async findAll( filter: StaffFilter, globalAccess: boolean, options?: DbApiOptions, ): Promise<{ rows: Staff[]; count: number }> { const { limit, offset } = resolvePagination(filter.limit, filter.page); let where: WhereAttributeHash = {}; const userOrganizations = options?.currentUser?.organizations?.id ?? null; if (userOrganizations && options?.currentUser?.organizationId) { where.organizationId = options.currentUser.organizationId; } const include: Includeable[] = [ { model: db.organizations, as: 'organization' }, { model: db.campuses, as: 'campus', where: filter.campus ? { [Op.or]: [ { id: { [Op.in]: filter.campus.split('|').map((t) => Utils.uuid(t)), }, }, { name: { [Op.or]: filter.campus .split('|') .map((t) => ({ [Op.iLike]: `%${t}%` })), }, }, ], } : {}, }, { model: db.users, as: 'user', where: filter.user ? { [Op.or]: [ { id: { [Op.in]: filter.user.split('|').map((t) => Utils.uuid(t)), }, }, { firstName: { [Op.or]: filter.user .split('|') .map((t) => ({ [Op.iLike]: `%${t}%` })), }, }, ], } : {}, }, { model: db.file, as: 'photo' }, ]; if (filter.id) { where = { ...where, id: Utils.uuid(filter.id) }; } if (filter.employee_number) { where = { ...where, [Op.and]: Utils.ilike('staff', 'employee_number', filter.employee_number), }; } if (filter.job_title) { where = { ...where, [Op.and]: Utils.ilike('staff', 'job_title', filter.job_title), }; } if (filter.hire_dateRange) { const [start, end] = filter.hire_dateRange; if (start !== undefined && start !== null && start !== '') { where = { ...where, hire_date: { [Op.gte]: start } }; } if (end !== undefined && end !== null && end !== '') { where = { ...where, hire_date: { ...(typeof where.hire_date === 'object' ? where.hire_date : {}), [Op.lte]: end, }, }; } } if (filter.active !== undefined) { where = { ...where, active: filter.active === true || filter.active === 'true', }; } if (filter.staff_type) { where = { ...where, staff_type: filter.staff_type }; } if (filter.status) { where = { ...where, status: filter.status }; } if (filter.organization) { const listItems = filter.organization .split('|') .map((item) => Utils.uuid(item)); where = { ...where, organizationId: { [Op.or]: listItems } }; } if (filter.createdAtRange) { const [start, end] = filter.createdAtRange; if (start !== undefined && start !== null && start !== '') { where = { ...where, createdAt: { [Op.gte]: start } }; } if (end !== undefined && end !== null && end !== '') { where = { ...where, createdAt: { ...(typeof where.createdAt === 'object' ? where.createdAt : {}), [Op.lte]: end, }, }; } } if (globalAccess) { delete where.organizationId; } const order: [string, string][] = filter.field && filter.sort ? [[filter.field, filter.sort]] : [['createdAt', 'desc']]; const { rows, count } = await db.staff.findAndCountAll({ where, include, distinct: true, order, transaction: options?.transaction, limit: !options?.countOnly && limit ? limit : undefined, offset: !options?.countOnly && offset ? offset : undefined, }); return { rows: options?.countOnly ? [] : rows, count }; } static async findAllAutocomplete( query: string | undefined, limit: number | undefined, offset: number | undefined, globalAccess: boolean, organizationId: string | undefined, ): Promise> { return autocompleteByField( db.staff, 'employee_number', query, limit, offset, globalAccess, organizationId, ); } } export default StaffDBApi;