import {arrayUnion, where} from "firebase/firestore"; import FirebaseQuery from "./firebase/firebaseQuery"; import {CohortData, ExternalEvent, ExternalEventAttendee, InstituteData, OrganisationAddress, ProviderData, SchoolData, StudentPlacementData, UserData} from "./typeDefinitions"; import {capitaliseWords, getAccess} from "./firebase/util"; import { convertDate } from "./firebase/util"; import {getIndividualPlacementForTaskList} from "./hooks"; const firebaseQuery = new FirebaseQuery; type InstituteTipNames = "createCohort"|"addSchools"|"uploadStaff"|"assignStaffRoles"|"allowExternalPlacementUpload"|"uploadStaffGuidance"|"uploadStudentGuidance" type ProviderTipNames = string type StudentTipNames = string export type InstituteTaskNames = "missingParentEmail"|"employerEventRequests"|"schoolEventRequests"|"eventsGetAvailableProviders"|"awaitingInsurance"|"expiredInsurance"|"approveAlumniConversation"|"outstandingReminders"|"invalidStaffEmails"|"invalidStudentEmails"|"invalidParentEmails"|"invalidProviderEmails"|"verifyInsurance"|"verifyRiskAssessment"|"verifyDbsCheck"|"inactiveStudents"|"uploadStudents"|"inactiveStaff"|"requiredStage"|"approveProvider"|"overdueStage"|"latePlacementStage"|"stagnantPlacementStage" export type StudentTaskNames = "completeOnboarding" export type ProviderTaskNames = "applicationRequireReview"|"activateStaff"|"requestedVisiblePlacementListings"|"requestedVisibleAddresses"|"completeStudentDocs"|"uploadOnboarding"|"reviewOnboarding"|"completeListing"|"completeAddress"|"registrationRequests"|"placementStarting"|"completeFeedback"|"setUpFeedback" // IF UPDATING LOGIC WITHIN THIS FILE, PLACEMENTT-BACKEND LOGIC MUST ALSO BE CHANGED ACCORDINGLY export type TaskItemObject = { title?: string, message?: string, link?: string, buttonTitle?: string, dismissible?: boolean, severity?: "error"|"warning"|"success"|"primary"|"info" }; export type TaskItem = TaskItemObject|{[key: string]: StudentPlacementData&{ eliData?: string, riskAssessmentURL?: string, dbsCheckURL?: string, }}; export type TaskQueryReturnObject = { [itemName in InstituteTaskNames|InstituteTipNames|StudentTipNames|ProviderTipNames]: TaskItem|TaskItem[]}|undefined; export type TipQueryReturnObject = { itemName?: InstituteTaskNames|InstituteTipNames|StudentTipNames|ProviderTipNames, title?: string, message?: string, link?: string, buttonTitle?: string, dismissible?: boolean, severity?: "error"|"warning"|"success"|"primary"|"info" }|undefined; /* type TasksObject = { [key in TaskNames]: { callback: (user: UserData) => Promise, }; }; */ type InstituteTipsObject = { [key in InstituteTipNames]: { callback: (user: UserData, organisation?:InstituteData&ProviderData, additional?:{[key: string]: OrganisationAddress}) => Promise, }; }; type ProviderTipsObject = { [key in ProviderTipNames]: { callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise, }; }; type StudentTipsObject = { [key in StudentTipNames]: { callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise, }; }; export type InstituteTaskObject = { [key in InstituteTaskNames]: { callback: ({user, organisation, cohorts, schools, eventId, type}:{user: UserData, organisation:InstituteData&ProviderData, cohorts: CohortData|[string, CohortData][], schools?: InstituteData|[string, InstituteData][], eventId?: string, type: "event" | "cohort" | "home" | "employerDatabase"}) => Promise, }; }; export type StudentTaskObject = { [key in StudentTaskNames]: { callback: (user: UserData) => Promise, }; }; export type ProviderTaskObject = { [key in ProviderTaskNames]: { callback: (user: UserData) => Promise, }; }; const providerTips:ProviderTipsObject = {} const studentTips:StudentTipsObject = {} const instituteTips:InstituteTipsObject = { addSchools: { callback: async (user, institute, schools) => { if (!getAccess(user, "addSchools") || institute?.package !== "institutes-two") return; if (Object.keys(schools as {[key: string]: OrganisationAddress}).length === 0) { return { title: "Add your schools", message: "Add your schools. These will show up in the 'Cohorts' tab where you can assign cohorts of students to them.", link: "/institutes/organisation/overview", buttonTitle: "Add schools", dismissible: true }; } return; }, }, createCohort: { callback: async (user, institute) => { if (!getAccess(user, "createCohorts") || institute?.package === "careersHub" || !institute?.workExperience) return; const cohorts = await firebaseQuery.getCount("cohorts", [where("product", "==", user.product), where("oId", "==", user.oId)]) if (cohorts === 0 && institute?.package === "institutes-one") { return { title: "Create a cohort", message: "Create a cohort to manage your students, process their placements and track their progress", link: "/institutes/cohorts/new", buttonTitle: "Create cohort", }; } return; }, }, uploadStaff: { callback: async (user) => { const returnObj: TipQueryReturnObject = { link: "", dismissible: true, }; if ((await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff")])) == 1) { return { ...returnObj, title: "Upload staff", message: "Upload staff to help manage your students", link: "/institutes/organisation/staff/all", buttonTitle: "Upload staff", }; } return; }, }, assignStaffRoles: { callback: async () => { return undefined; }, }, allowExternalPlacementUpload: { callback: async (user, organisation) => { if (user.product !== "institutes") return; if ((organisation as InstituteData).acceptingEmployers) return; return { dismissible: true, title: "Allow external uploads", message: "Allow employers to join your database and share their opportunities with you and your students.", link: "/institutes/network/overview#employerSettings", buttonTitle: "View Network", }; }, }, uploadStaffGuidance: { callback: async (user, organisation) => { if (user.product !== "institutes") return; const returnObj: TipQueryReturnObject = { link: "", dismissible: true, }; const guidanceTips:TipQueryReturnObject[] = []; if (!Object.keys((organisation as InstituteData).staffGuidance || {}).length) { guidanceTips.push({ ...returnObj, title: "Upload staff guidance", message: "Upload guidance documents to support your staff in coordinating work experience.", buttonTitle: "Staff guidance", link: `/${user.product}/organisation/guidance`, }); } return guidanceTips; }, }, uploadStudentGuidance: { callback: async (user, organisation) => { if (user.product !== "institutes" || organisation?.package === "careersHub") return; const returnObj: TipQueryReturnObject = { link: "", dismissible: true, }; const guidanceTips:TipQueryReturnObject[] = []; if (!Object.keys((organisation as InstituteData).studentsGuidance || {}).length) { guidanceTips.push({ ...returnObj, title: "Upload student guidance", message: "Upload guidance documents to support your staff with key information.", buttonTitle: "Student guidance", link: `/${user.product}/organisation/guidance`, }); } return guidanceTips; }, }, }; // Accept a cohort to any task const instituteTasks:InstituteTaskObject = { invalidStaffEmails: { callback: async ({user, cohorts, type}) => { if (type !== "home") return; if (!getAccess(user, "viewStaff") || !Array.isArray(cohorts)) return; const staffCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff"), where("flags", "array-contains", "userEmailFailed")])); if (staffCount > 0) { return {invalidStaffEmails: { dismissible: false, severity: "error", title: `${staffCount} staff have invalid emails.`, message: `${staffCount} staff accounts have invalid email addresses. Delete and reupload these users.`, link: "/institutes/organisation/staff/all", }}; } return; }, }, latePlacementStage: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!Array.isArray(cohorts)) { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohorts.oId), where("cohort", "==", cohorts.id), where("flags", "array-contains", "latePlacementStage")])); if (placementCount > 0) { return {latePlacementStage: { dismissible: false, severity: "warning", title: `${placementCount} placements are late.`, message: `${placementCount} placement${placementCount === 1 ? "" : "s"} ha${placementCount === 1 ? "s" : "ve"} been awaiting sign-off from users for one week. We've contacted all required users, including parents and students, to make them aware.`, link: `/institutes/cohorts/${cohorts.id}/placements`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohort.oId), where("cohort", "==", id), where("flags", "array-contains", "latePlacementStage")])); if (placementCount > 0) { return { dismissible: false, severity: "warning", title: `${placementCount} placements are late.`, message: `Your cohort, ${cohort.name} has ${placementCount} placement${placementCount === 1 ? "" : "s"} ha${placementCount === 1 ? "s" : "ve"} been awaiting sign-off from users for one week. We've contacted all required users, including parents and students, to make them aware.`, link: `/institutes/cohorts/${id}/placements`, } as TaskItem; } return; })); return {latePlacementStage: items.filter((v) => v) as TaskItem[]}; }, }, stagnantPlacementStage: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!Array.isArray(cohorts)) { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohorts.oId), where("cohort", "==", cohorts.id), where("flags", "array-contains", "stagnantPlacementStage")])); if (placementCount > 0) { return {stagnantPlacementStage: { dismissible: false, severity: "warning", title: `${placementCount} placements are late.`, message: `${placementCount} placement${placementCount === 1 ? "" : "s"} ha${placementCount === 1 ? "s" : "ve"} been awaiting sign-off from users for more than two weeks. We've contacted all required users, including parents and students, to make them aware.`, link: `/institutes/cohorts/${cohorts.id}/placements`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohort.oId), where("cohort", "==", id), where("flags", "array-contains", "stagnantPlacementStage")])); if (placementCount > 0) { return { dismissible: false, severity: "warning", title: `${placementCount} placements are late.`, message: `Your cohort, ${cohort.name} has ${placementCount} placement${placementCount === 1 ? "" : "s"} ha${placementCount === 1 ? "s" : "ve"} been awaiting sign-off from users for more than two weeks. We've contacted all required users, including parents and students, to make them aware.`, link: `/institutes/cohorts/${id}/placements`, } as TaskItem; } return; })); return {stagnantPlacementStage: items.filter((v) => v) as TaskItem[]}; }, }, approveAlumniConversation: { callback: async ({user, organisation, schools, type}) => { if (type !== "home") return; if (!schools) { if (!organisation.alumniConversations) return; const convos = (await firebaseQuery.getCount(["alumniConversations"], [where("oId", "==", user.oId), where("open", "==", true), where("delivered", "==", "pending")])); if (convos > 0) { return {approveAlumniConversation: { dismissible: false, severity: "info", title: `${convos} alumni conversation message${convos > 1 ? "s" : ""} needs approval.`, message: `There are ${convos} message${convos > 1 ? "s" : ""} to alumni for you to review and release.`, link: "/institutes/network/alumni/conversations", } as TaskItem}; } return; } if (!Array.isArray(schools)) { // One school const convos = (await firebaseQuery.getCount(["alumniConversations"], [where("oId", "==", user.oId), where("schoolId", "==", schools.id), where("open", "==", true), where("delivered", "==", "pending")])); if (convos > 0) { return {approveAlumniConversation: { dismissible: false, severity: "info", title: `${convos} alumni conversation message${convos > 1 ? "s" : ""} needs approval.`, message: `There are ${convos} message${convos > 1 ? "s" : ""} to alumni for you to review and release.`, link: `/institutes/network/alumni/conversations/${schools.id}`, } as TaskItem}; } return; } const items = await Promise.all(schools.map(async ([id, school]) => { if (!school.alumniConversations) return; const convos = (await firebaseQuery.getCount(["alumniConversations"], [where("oId", "==", user.oId), where("schoolId", "==", id), where("open", "==", true), where("delivered", "==", "pending")])); if (convos > 0) { return { dismissible: false, severity: "info", title: `${convos} alumni conversation message${convos > 1 ? "s" : ""} needs approval.`, message: `${school.name}, has ${convos} outstanding alumni message${convos > 1 ? "s" : ""} to approve. Click to view.`, link: `/institutes/network/alumni/conversations/${school.id}`, } as TaskItem; } return; })); return ({approveAlumniConversation: items.filter((v) => v) as TaskItem[]}); }, }, invalidStudentEmails: { callback: async ({user, cohorts, type}) => { if (type !== "home") return; if (!getAccess(user, "viewStudents") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; if (!Array.isArray(cohorts)) { const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", cohorts.oId), where("userType", "==", "Students"), where("cohort", "==", cohorts.id), where("flags", "array-contains", "userEmailFailed")])); if (studentCount > 0) { return {invalidStudentEmails: { dismissible: false, severity: "error", title: `${studentCount} students have invalid emails.`, message: `${studentCount} student accounts have invalid email addresses. Delete and reupload these users.`, link: `/institutes/cohorts/${cohorts.id}/students`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", cohort.oId), where("userType", "==", "Students"), where("cohort", "==", id), where("flags", "array-contains", "userEmailFailed")])); if (studentCount > 0) { return { dismissible: false, severity: "error", title: `${studentCount} students have invalid emails.`, message: `Your cohort, ${cohort.name}, has ${studentCount} student accounts with invalid email addresses. Delete and reupload these users.`, link: `/institutes/cohorts/${id}/students`, } as TaskItem; } return; })); return {invalidStudentEmails: items.filter((v) => v) as TaskItem[]}; }, }, outstandingReminders: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!Array.isArray(cohorts)) { const reminderCount = (await firebaseQuery.getCount(["reminders"], [where("oId", "==", cohorts.oId), where("uid", "==", user.id), where("dueDate", "<=", convertDate(new Date(), "dbstring") as string), where("cohort", "==", cohorts.id), where("status", "==", "upcoming")])); if (reminderCount > 0) { return {outstandingReminders: { dismissible: false, severity: "primary", title: `You have ${reminderCount} reminder${reminderCount > 1 ? "s" : ""}.`, message: `You have ${reminderCount} outstanding placement reminder${reminderCount > 1 ? "s" : ""}. Click to view.`, link: `/institutes/cohorts/${cohorts.id}/placements`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const reminderCount = (await firebaseQuery.getCount(["reminders"], [where("oId", "==", cohort.oId), where("uid", "==", user.id), where("dueDate", "<=", convertDate(new Date(), "dbstring") as string), where("cohort", "==", id), where("status", "==", "upcoming")])); if (reminderCount > 0) { return { dismissible: false, severity: "primary", title: `You have ${reminderCount} reminder${reminderCount > 1 ? "s" : ""}.`, message: `Your cohort, ${cohort.name}, has ${reminderCount} outstanding placement reminder${reminderCount > 1 ? "s" : ""}. Click to view.`, link: `/institutes/cohorts/${id}/placements`, } as TaskItem; } return; })); return {outstandingReminders: items.filter((v) => v) as TaskItem[]}; }, }, invalidParentEmails: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!getAccess(user, "signOffPlacements") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; const today = convertDate(new Date(), "dbstring") as string; if (!Array.isArray(cohorts)) { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("flags", "array-contains", "parentEmailFailed")])); if (placementCount > 0) { return {invalidParentEmails: { dismissible: false, severity: "error", title: `${placementCount} placements have invalid parent emails.`, message: `Your cohort '${cohorts.name}' has placements with invalid parent emails. Click to view these placements.`, link: `/institutes/cohorts/${cohorts.schoolId ? `${cohorts.schoolId}/${cohorts.id}` : cohorts.id}/placements?selectedView=allData&id=inProgress`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("flags", "array-contains", "parentEmailFailed")])); if (placementCount > 0) { return { dismissible: false, severity: "error", title: `${placementCount} placements in '${cohort.name}' have invalid parent emails.`, message: `Your cohort '${cohort.name}' has placements with invalid parent emails. Click to view these placements.`, link: `/institutes/cohorts/${cohort.schoolId ? `${cohort.schoolId}/${id}` : id}/placements?selectedView=allData&id=inProgress`, buttonTitle: "Review placements", } as TaskItem; } return; })); return {invalidParentEmails: items.filter((v) => v) as TaskItem[]}; }, }, expiredInsurance: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; const today = convertDate(new Date(), "dbstring") as string; if (!getAccess(user, "signOffPlacements") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; if (!Array.isArray(cohorts)) { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("endDate", ">", convertDate(new Date(), "dbstring")), where("flags", "array-contains", "insuranceExpired")])); if (placementCount > 0) { return {expiredInsurance: { dismissible: false, severity: "error", title: `${placementCount} placements have expired insurance.`, message: `Your cohort '${cohorts.name}' has placements with expired insurance Click to view these placements.`, link: `/institutes/cohorts/${cohorts.schoolId ? `${cohorts.schoolId}/${cohorts.id}` : cohorts.id}/placements?selectedView=allData&id=inProgress`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("endDate", ">", convertDate(new Date(), "dbstring")), where("flags", "array-contains", "insuranceExpired")])); if (placementCount > 0) { return { dismissible: false, severity: "error", title: `${placementCount} placements in '${cohort.name}' have expired insurance.`, message: `Your cohort '${cohort.name}' has placements with expired insurance. Click to view these placements.`, link: `/institutes/cohorts/${cohort.schoolId ? `${cohort.schoolId}/${id}` : id}/placements?selectedView=allData&id=inProgress`, buttonTitle: "Review placements", } as TaskItem; } return; })); return {expiredInsurance: items.filter((v) => v) as TaskItem[]}; }, }, invalidProviderEmails: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!getAccess(user, "signOffPlacements") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; const today = convertDate(new Date(), "dbstring") as string; if (!Array.isArray(cohorts)) { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("flags", "array-contains", "providerEmailFailed")])); if (placementCount > 0) { return {invalidProviderEmails: { dismissible: false, severity: "error", title: `${placementCount} placements have invalid provider emails.`, message: `Your cohort '${cohorts.name}' has placements with invalid provider emails. Click to view these placements.`, link: `/institutes/cohorts/${cohorts.schoolId ? `${cohorts.schoolId}/${cohorts.id}` : cohorts.id}/placements?selectedView=allData&id=inProgress`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("flags", "array-contains", "providerEmailFailed")])); if (placementCount > 0) { return { dismissible: false, severity: "error", title: `${placementCount} placements in '${cohort.name}' have invalid provider emails.`, message: `Your cohort '${cohort.name}' has placements with invalid provider emails. Click to view these placements.`, link: `/institutes/cohorts/${cohort.schoolId ? `${cohort.schoolId}/${id}` : id}/placements?selectedView=allData&id=inProgress`, buttonTitle: "Review placements", } as TaskItem; } return; })); return {invalidProviderEmails: items.filter((v) => v) as TaskItem[]}; }, }, requiredStage: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!getAccess(user, "signOffPlacements") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; const today = convertDate(new Date(), "dbstring") as string; if (!Array.isArray(cohorts)) { const placements = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("reqUserType", "==", user.userType)])) as {[key: string]: StudentPlacementData}; if (Object.keys(placements).length > 0) { return {requiredStage: placements}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placements = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("reqUserType", "==", user.userType)])) as {[key: string]: StudentPlacementData}; if (Object.keys(placements).length > 0) { return placements; } return; })); return {requiredStage: items.filter((v) => v) as TaskItem[]}; }, }, verifyInsurance: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!getAccess(user, "verifyInsurance") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; const today = convertDate(new Date(), "dbstring") as string; console.log("FETChING INS", cohorts); if (!Array.isArray(cohorts)) { const placements = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("insurance", "==", "awaitingReview")])) as {[key: string]: StudentPlacementData}; console.log("VERIFY", placements); if (Object.keys(placements).length > 0) { const placementsWithHooks = Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([k, v]) => { const placementWithUrl = await getIndividualPlacementForTaskList({placement: v, type: "insurance"}); return [k, placementWithUrl]; }))); return {verifyInsurance: placementsWithHooks}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placements = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("insurance", "==", "awaitingReview")])) as {[key: string]: StudentPlacementData}; console.log("VERIFY OBJ", placements); if (Object.keys(placements).length > 0) { return Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([k, v]) => { const placementWithUrl = await getIndividualPlacementForTaskList({placement: v, type: "insurance"}); return [k, placementWithUrl]; }))); } return; })); return {verifyInsurance: items.filter((v) => v) as TaskItem[]}; }, }, awaitingInsurance: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; const today = convertDate(new Date(), "dbstring") as string; if (!Array.isArray(cohorts)) { const placementsViewedByEmployer = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("providerCompleted", "array-contains", "details"), where("insurance", "==", false)])) as {[key: string]: StudentPlacementData}; const placementsRequiringProvider = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("reqUserType", "==", "Provider"), where("insurance", "==", false)])) as {[key: string]: StudentPlacementData}; const placements = {...placementsViewedByEmployer, ...placementsRequiringProvider}; if (Object.keys(placements).length > 0) { const placementsWithHooks = Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([k, v]) => { const placementWithUrl = await getIndividualPlacementForTaskList({placement: v, type: "insurance"}); return [k, placementWithUrl]; }))); return {awaitingInsurance: placementsWithHooks}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placementsViewedByEmployer = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("providerCompleted", "array-contains", "details"), where("insurance", "==", false)])) as {[key: string]: StudentPlacementData}; const placementsRequiringProvider = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("reqUserType", "==", "Provider"), where("insurance", "==", false)])) as {[key: string]: StudentPlacementData}; const placements = {...placementsViewedByEmployer, ...placementsRequiringProvider}; if (Object.keys(placements).length > 0) { return Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([k, v]) => { const placementWithUrl = await getIndividualPlacementForTaskList({placement: v, type: "insurance"}); return [k, placementWithUrl]; }))); } return; })); return {awaitingInsurance: items.filter((v) => v) as TaskItem[]}; }, }, verifyDbsCheck: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!getAccess(user, "verifyDbsChecks") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; const today = convertDate(new Date(), "dbstring") as string; if (!Array.isArray(cohorts)) { const placements = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("dbsCheck", "==", "awaitingReview")])) as {[key: string]: StudentPlacementData}; if (Object.keys(placements).length > 0) { const placementsWithHooks = Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([k, v]) => { const placementWithUrl = await getIndividualPlacementForTaskList({placement: v, type: "dbsCheck"}); return [k, placementWithUrl]; }))); return {verifyDbsCheck: placementsWithHooks}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placements = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("dbsCheck", "==", "awaitingReview")])) as {[key: string]: StudentPlacementData}; if (Object.keys(placements).length > 0) { return Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([k, v]) => { const placementWithUrl = await getIndividualPlacementForTaskList({placement: v, type: "dbsCheck"}); return [k, placementWithUrl]; }))); } return; })); return {verifyDbsCheck: items.filter((v) => v) as TaskItem[]}; }, }, employerEventRequests: { callback: async ({user, type}) => { if (type !== "home") return; const openRequests = await firebaseQuery.getCount(["externalEventRequests"], [where("oId", "==", user.oId), where("requestedBy", "==", "employer"), where("status", "==", "pendingInstitute")]); if (openRequests > 0) { return {employerEventRequests: { dismissible: false, severity: "primary", title: `${openRequests} employer${openRequests === 1 ? " has" : "s have"} requested ${openRequests === 1 ? "an event" : "events"}.`, message: "Click to view and respond to pending event requests.", link: "/institutes/events/employerRequests", }}; } return; }, }, schoolEventRequests: { callback: async ({user, type}) => { if (type !== "home") return; const openRequests = await firebaseQuery.getCount(["externalEventRequests"], [where("oId", "==", user.oId), where("requestedBy", "==", "school"), where("status", "==", "pendingInstitute")]); if (openRequests > 0) { return {schoolEventRequests: { dismissible: false, severity: "primary", title: `${openRequests} school${openRequests === 1 ? " has" : "s have"} requested ${openRequests === 1 ? "an event" : "events"}.`, message: "Click to view and respond to pending event requests.", link: "/institutes/events/schoolRequests", }}; } return; }, }, eventsGetAvailableProviders: { callback: async ({user, eventId, type}) => { if (!["home", "event"].includes(type)) return; if (type === "event" && eventId) { const availableAttendees = await firebaseQuery.getCount(["externalEventAttendees"], [where("oId", "==", user.oId), where("eventId", "==", eventId), where("status", "==", "providerAvailable")]); if (availableAttendees > 0) { return {eventsGetAvailableProviders: { dismissible: false, severity: "success", title: `${availableAttendees} employer${availableAttendees === 1 ? " is" : "s are"} available.`, message: "Click to view employers and confirm their attendance. Employers can't view additional details without your sign-off", link: `/institutes/event/${eventId}/employers`, }}; } return; } if (type === "home") { const availableAttendees = await firebaseQuery.getDocsWhere(["externalEventAttendees"], [where("oId", "==", user.oId), where("status", "==", "providerAvailable")]) as {[key: string]: ExternalEventAttendee}; const eventIds = Object.values(availableAttendees).reduce((acc, ev) => { acc[ev.eventId] = (acc[ev.eventId] || 0) + 1; return acc; }, {} as {[key: string]: number}); const allItems = await Promise.all(Object.entries(eventIds).map(async ([eventId, number]) => { const event = await firebaseQuery.getDocData(["externalEvents", eventId]).catch(() => false) as ExternalEvent|false; if (!event) return; return { dismissible: false, severity: "success", title: `${number} employer${number === 1 ? " is" : "s are"} available for your event: ${event.name}.`, message: "Click to view employers and confirm their attendance. Employers can't view additional details without your sign-off", link: `/institutes/event/${eventId}/employers`, } as TaskItem; })); return {eventsGetAvailableProviders: allItems.filter((v) => v) as TaskItem[]}; } return; }, }, verifyRiskAssessment: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!getAccess(user, "verifyRiskAssessments") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; const today = convertDate(new Date(), "dbstring") as string; if (!Array.isArray(cohorts)) { const placements = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohorts.oId), where("endDate", ">=", today), where("cohort", "==", cohorts.id), where("riskAssessment", "==", "awaitingReview")])) as {[key: string]: StudentPlacementData}; if (Object.keys(placements).length > 0) { const placementsWithHooks = Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([k, v]) => { const placementWithUrl = await getIndividualPlacementForTaskList({placement: v, type: "riskAssessment"}); return [k, placementWithUrl]; }))); return {verifyRiskAssessment: placementsWithHooks}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const placements = (await firebaseQuery.getDocsWhere(["placements"], [where("oId", "==", cohort.oId), where("endDate", ">=", today), where("cohort", "==", id), where("riskAssessment", "==", "awaitingReview")])) as {[key: string]: StudentPlacementData}; if (Object.keys(placements).length > 0) { return Object.fromEntries(await Promise.all(Object.entries(placements).map(async ([k, v]) => { const placementWithUrl = await getIndividualPlacementForTaskList({placement: v, type: "riskAssessment"}); return [k, placementWithUrl]; }))); } return; })); return {verifyRiskAssessment: items.filter((v) => v) as TaskItem[]}; }, }, missingParentEmail: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!getAccess(user, "editStudents") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; if (!Array.isArray(cohorts)) { const requiresParents = Boolean(cohorts.workflow.find((node) => node.userType === "Parent")) if (!requiresParents) return; const constraints = [where("oId", "==", cohorts.oId), where("cohort", "==", cohorts.id), where("details.parentEmail", "==", "null")]; if (user.groupData?.viewStudents === "filter") { constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || "")); } const studentCount = (await firebaseQuery.getCount(["users"], constraints)); if (studentCount > 0) { return {missingParentEmail: { dismissible: false, severity: "info", title: `${studentCount} students do not have a parent email.`, message: `Your cohort '${cohorts.name}' has students without a parent email. To allow proper processing of their placements, add these emails.`, link: `/institutes/cohorts/${cohorts.id}/students?parentEmail=false`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const requiresParents = Boolean(cohort.workflow.find((node) => node.userType === "Parent")) if (!requiresParents) return; const constraints = [where("oId", "==", cohort.oId), where("cohort", "==", id), where("details.parentEmail", "==", "null")]; if (user.groupData?.viewStudents === "filter") { constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || "")); } const studentCount = (await firebaseQuery.getCount(["users"], constraints)); if (studentCount > 0) { return { dismissible: false, severity: "info", title: `${studentCount} students in '${cohort.name}' do not have a parent email.`, message: `Your cohort '${cohort.name}' has students without a parent email. To allow proper processing of their placements, add these emails.`, link: `/institutes/cohorts/${cohort.id}/students?parentEmail=false`, buttonTitle: "View students", } as TaskItem; } return; })); return {missingParentEmail: items.filter((v) => v) as TaskItem[]}; }, }, inactiveStudents: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!getAccess(user, "activateStudents") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; if (!Array.isArray(cohorts)) { const constraints = [where("oId", "==", cohorts.oId), where("cohort", "==", cohorts.id), where("status", "==", "inactive")]; if (user.groupData?.viewStudents === "filter") { constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || "")); } const studentCount = (await firebaseQuery.getCount(["users"], constraints)); if (studentCount > 0) { return {inactiveStudents: { dismissible: false, severity: "info", title: `${studentCount} students in are inactive.`, message: `Your cohort '${cohorts.name}' has inactive students. Activate them to enable them to use the platform.`, link: `/institutes/cohorts/${cohorts.id}/students?status=inactive`, }}; } return; } const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const constraints = [where("oId", "==", cohort.oId), where("cohort", "==", id), where("status", "==", "inactive")]; if (user.groupData?.viewStudents === "filter") { constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || "")); } const studentCount = (await firebaseQuery.getCount(["users"], constraints)); if (studentCount > 0) { return { dismissible: false, severity: "info", title: `${studentCount} students in '${cohort.name}' are inactive.`, message: `Your cohort '${cohort.name}' has inactive students. Activate them to enable them to use the platform.`, link: `/institutes/cohorts/${cohort.id}/students?status=inactive`, buttonTitle: "Review students", } as TaskItem; } return; })); return {inactiveStudents: items.filter((v) => v) as TaskItem[]}; }, }, uploadStudents: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; if (!Array.isArray(cohorts)) return; if (!getAccess(user, "addStudents") || (user.userGroup !== "admin" && user.viewStudents === "none")) return; if (user.product !== "institutes") return; const returnObj: TaskItem = { link: "", dismissible: true, severity: "warning", }; const items = await Promise.all(cohorts.map(async ([id, cohort]) => { const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", cohort.oId), where("userType", "==", "Students"), where("cohort", "==", id)])); if (studentCount === 0) { return { ...returnObj, title: "Upload students", message: `Your cohort '${cohort.name}' has no students. Add them in the cohort 'Students' tab`, link: `/institutes/cohorts/${id}/students`, buttonTitle: "Upload students", }; } return; })); return {uploadStudents: items.filter((v) => v) as TaskItem[]}; }, }, inactiveStaff: { callback: async ({user, cohorts, type}) => { if (!["home"].includes(type)) return; if (!getAccess(user, "addStaff") || !getAccess(user, "viewStaff")) return; if (!Array.isArray(cohorts) || user.product === "students") return; const returnObj: TaskItem = { link: `/${user.product}/cohorts/staff/all?status=inactive`, dismissible: false, severity: "info", }; const inactiveStaff = await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff"), where("status", "==", "inactive")]); if (inactiveStaff > 0) { return {inactiveStaff: { ...returnObj, title: "Inactive staff", message: `You have ${inactiveStaff} inactive staff members. You can activate them in the 'Staff' cohorts tab.`, }}; } return; }, }, overdueStage: { callback: async ({user, cohorts, type}) => { if (!["home", "cohort"].includes(type)) return; console.log(user, cohorts); return undefined; }, }, approveProvider: { callback: async ({user, cohorts, type}) => { if (!["home", "employerDatabase"].includes(type)) return; const availableAttendees = await firebaseQuery.getCount(["providerContacts"], [where(`savedBy.${user.oId}.exists`, "==", true), where(`savedBy.${user.oId}.status`, "==", "providerReviewed")]); if (availableAttendees > 0) { return {eventsGetAvailableProviders: { dismissible: false, severity: "success", title: `${availableAttendees} employer${availableAttendees === 1 ? " is" : "s are"} awaiting your approval to join your database.`, message: "Click to view employers and confirm their details. Employers can't be invited to events until they are accepted.", link: `/institutes/network/employers`, }}; } return; }, }, }; const studentTasks:StudentTaskObject = { completeOnboarding: { callback: async (user) => { const placementsWithoutOnboarding = await firebaseQuery.getDocsWhere("placements", [where("uid", "==", user.id), where("onboarding.deadline", "<=", convertDate(new Date(), "dbstring")), where("onboarding.completed.submitted", "==", false)]) as {[key: string]: StudentPlacementData}; if (Object.keys(placementsWithoutOnboarding).length === 0) return; const items = Object.entries(placementsWithoutOnboarding).map(([k, placement]) => ({ dismissible: false, severity: "primary", title: `Complete onboarding for ${placement.name}`, message: `Review onboarding for your placement starting on ${convertDate(placement.startDate, "visual")}`, link: `/${user.product}/placements/${k}`, buttonTitle: "View onboarding", } as TaskItem)) return {completeOnboarding: items}; }, } } // const providerTasks:ProviderTaskObject = { // requestedVisibleAddresses: { // callback: async (user) => { // if (!getAccess(user, "addStaff")) return; // const accessRequests = await firebaseQuery.getCount("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleAddresses")]) - ((Array.isArray(user?.requestedVisibleAddresses) && user?.requestedVisibleAddresses.length > 0) ? 1 : 0); // if (accessRequests === 0) return; // if (accessRequests === 1) { // const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", "providers"), where("oId", "==", user.oId), orderBy("requestedVisibleAddresses")]) || {})[0] as [string, UserData]; // return { // dismissible: false, // severity: "primary", // title: `${userRequestingAccess[1].details.forename} ${userRequestingAccess[1].details.surname} has requested access to view addresses.`, // message: `Click to review the addresses and grant access to the user.`, // link: `/${user.product}/users/${userRequestingAccess[0]}`, // buttonTitle: "View request", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "warning", // title: `Multiple users have requested access to view addresses.`, // message: `Click to review the addresses and grant access to the user.`, // link: `/${user.product}/organisation/staff/all`, // buttonTitle: "View request", // } as TaskQueryReturnObject; // }, // }, // requestedVisiblePlacementListings: { // callback: async (user) => { // if (!getAccess(user, "addStaff")) return; // const accessRequests = await firebaseQuery.getCount("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleListings")])- ((Array.isArray(user?.requestedVisibleListings) && user?.requestedVisibleListings.length > 0) ? 1 : 0);; // if (accessRequests === 0) return; // if (accessRequests === 1) { // const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleListings")]) || {})[0] as [string, UserData]; // return { // dismissible: false, // severity: "primary", // title: `${userRequestingAccess[1].details.forename} ${userRequestingAccess[1].details.surname} has requested access to view placement listings.`, // message: `Click to review the placement listings and grant access to the user.`, // link: `/${user.product}/users/${userRequestingAccess[0]}`, // buttonTitle: "View request", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "warning", // title: `Multiple users have requested access to view placement listings.`, // message: `Click to review the placement listings and grant access to the user.`, // link: `/${user.product}/organisation/staff/all`, // buttonTitle: "View request", // } as TaskQueryReturnObject; // }, // }, // applicationRequireReview: { // callback: async (user) => { // const constraints = [where("providerId", "==", user.oId), where("reqUserType", "==", "Staff"), where("status", "==", "submitted")]; // if (user.userGroup !== "admin") { // if (!user.viewPlacementListings || user.viewPlacementListings === "none") return; // if (!user.viewAddresses || user.viewAddresses === "none") return; // if (user.viewPlacementListings === "request") { // if (!user.visibleListings || user.visibleListings?.length === 0) return; // constraints.push(where("listingId", 'in', user.visibleListings)); // } else { // // viewPlacementListings must be 'all' // if (user.viewAddresses === "request") { // if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return; // constraints.push(where("addressId", 'in', user.visibleAddresses)); // } // } // } // const applicationCount = await firebaseQuery.getCount("applications", constraints); // if (applicationCount === 0) return; // return { // dismissible: false, // severity: "primary", // title: `${applicationCount} applications require your review.`, // message: `Click to view ${applicationCount} that require your attention.`, // link: `/${user.product}/placementListings/applicants`, // buttonTitle: "View applications", // } as TaskQueryReturnObject; // }, // }, // completeStudentDocs: { // callback: async (user) => { // return; // return {} as TaskQueryReturnObject; // }, // }, // reviewOnboarding: { // callback: async (user) => { // const constraints = [where("providerId", "==", user.oId), where("onboarding.completed.accepted", "==", false), where("onboarding.completed.submitted", "==", true), where("endDate", ">=", dateToString(new Date()))] // if (user.userGroup !== "admin") { // if (!user.viewPlacementListings || user.viewPlacementListings === "none") return; // if (!user.viewAddresses || user.viewAddresses === "none") return; // if (user.viewPlacementListings === "request") { // if (!user.visibleListings || user.visibleListings?.length === 0) return; // constraints.push(where("placementId", 'in', user.visibleListings)); // } else { // // viewPlacementListings must be 'all' // if (user.viewAddresses === "request") { // if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return; // constraints.push(where("addressId", 'in', user.visibleAddresses)); // } // } // } // const toReview = await firebaseQuery.getCount("placements", constraints); // if (toReview === 0) return; // if (toReview === 1) { // const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData]; // const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData; // return { // dismissible: false, // severity: "primary", // title: `${student.details.forename} ${student.details.surname} has completed their onboarding for their placement from ${convertDate(placement[1].startDate, "visual")} to ${convertDate(placement[1].endDate, "visual")}`, // message: `Click to view the placement and review the onboarding.`, // link: `/${user.product}/placements/${placement[0]}`, // buttonTitle: "View", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "primary", // title: `${toReview} student have completed their onboarding.`, // message: `Click to view your placements and approve completed onboarding`, // link: `/${user.product}/placementListings/placements`, // buttonTitle: "View placements", // } as TaskQueryReturnObject; // }, // }, // uploadOnboarding: { // callback: async (user) => { // const constraints = [where("providerId", "==", user.oId), where("onboarding", "==", null), where("endDate", ">=", dateToString(new Date()))]; // if (user.userGroup !== "admin") { // if (!user.viewPlacementListings || user.viewPlacementListings === "none") return; // if (!user.viewAddresses || user.viewAddresses === "none") return; // if (user.viewPlacementListings === "request") { // if (!user.visibleListings || user.visibleListings?.length === 0) return; // constraints.push(where("placementId", 'in', user.visibleListings)); // } else { // // viewPlacementListings must be 'all' // if (user.viewAddresses === "request") { // if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return; // constraints.push(where("addressId", 'in', user.visibleAddresses)); // } // } // } // const withoutOnboarding = await firebaseQuery.getCount("placements", constraints); // if (withoutOnboarding === 0) return; // if (withoutOnboarding === 1) { // const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData]; // const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData; // return { // dismissible: false, // severity: "primary", // title: `Send onboarding documents to ${student.details.forename} ${student.details.surname}'s placement from ${convertDate(placement[1].startDate, "visual")} to ${convertDate(placement[1].endDate, "visual")}`, // message: `Click to view the placement and add or dismiss onboarding reminders.`, // link: `/${user.product}/placements/${placement[0]}`, // buttonTitle: "View", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "primary", // title: `Set up onboarding for ${withoutOnboarding} placements to prepare yourself and your students.`, // message: `Click to view your placements and add or dismiss onboarding reminders.`, // link: `/${user.product}/placementListings/placements`, // buttonTitle: "View placements", // } as TaskQueryReturnObject; // }, // }, // completeListing: { // callback: async (user) => { // const constraints = [where("providerId", "==", user.oId), where("status", "==", "draft")]; // if (user.userGroup !== "admin") { // if (!user.viewPlacementListings || user.viewPlacementListings === "none") return; // if (!user.viewAddresses || user.viewAddresses === "none") return; // if (user.viewPlacementListings === "request") { // if (!user.visibleListings || user.visibleListings?.length === 0) return; // constraints.push(where(documentId(), 'in', user.visibleListings)); // } else { // // viewPlacementListings must be 'all' // if (user.viewAddresses === "request") { // if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return; // constraints.push(where("addressId", 'in', user.visibleAddresses)); // } // } // } // const incompleteListings = await firebaseQuery.getCount("placementListings", constraints); // if (incompleteListings === 0) return; // if (incompleteListings === 1) { // const incompleteListing = Object.entries(await firebaseQuery.getDocsWhere("placementListings", constraints) || {})[0] as [string, PlacementListing]; // const address = incompleteListing[1].addressId ? await firebaseQuery.getDocData(["addresses", incompleteListing[1].addressId]) as OrganisationAddress : undefined; // return { // dismissible: false, // severity: "info", // title: `Your listing '${incompleteListing[1].title || "unnamed"}' at ${address ? `${address["address-line1"]}, ${address.postal_code.toUpperCase()}, ${capitaliseWords(camelCaseToNormal(address.country))}` : "unknown address"} requires more information before publishing.`, // message: `Click to complete and publish the placement listing.`, // link: `/${user.product}/addListing/${incompleteListing[0]}`, // buttonTitle: "View listing", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "info", // title: `You have ${incompleteListings} draft listings waiting to be published.`, // message: `Click to review and publish the placement listings.`, // link: `/${user.product}/placementListings/listings`, // buttonTitle: "View listings", // } as TaskQueryReturnObject; // }, // }, // completeAddress: { // callback: async (user) => { // const constraints = [where("product", "==", "providers"), where("oId", "==", user.oId), where("stage", "!=", "complete")]; // if (user.userGroup !== "admin") { // if (!user.viewAddresses || user.viewAddresses === "none") return; // if (user.viewAddresses === "request") { // if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return; // constraints.push(where("addressId", 'in', user.visibleAddresses)); // } // } // const incompleteAddresses = await firebaseQuery.getCount("addresses", constraints); // if (incompleteAddresses === 0) return; // if (incompleteAddresses === 1) { // const address = Object.entries(await firebaseQuery.getDocsWhere("addresses", constraints) || {})[0] as [string, OrganisationAddress]; // return { // dismissible: false, // severity: "info", // title: `Your address: ${address[1]["address-line1"]}, ${address[1].postal_code.toUpperCase()}, ${capitaliseWords(camelCaseToNormal(address[1].country))} is currently incomplete.`, // message: `Click to complete the addresses.`, // link: `/${user.product}/addAddress/${address[0]}`, // buttonTitle: "View address", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "info", // title: `You have ${incompleteAddresses} draft addresses waiting to be published.`, // message: `Click to review the addresses.`, // link: `/${user.product}/organisation/addresses`, // buttonTitle: "View addresses", // } as TaskQueryReturnObject; // }, // }, // registrationRequests: { // callback: async (user) => { // if (!getAccess(user, "addStaff")) return; // const regRequests = await firebaseQuery.getCount("requests", [where("product", "==", user.product), where("oId", "==", user.oId)]); // if (regRequests === 0) return; // if (regRequests === 1) { // const request = Object.entries(await firebaseQuery.getDocsWhere("requests", [where("product", "==", user.product), where("oId", "==", user.oId)]) || {})[0] as [string, RegistrationRequest]; // return { // dismissible: false, // severity: "primary", // title: `${request[1].forename} ${request[1].surname} has requested to access your organisation.`, // message: `Click to review these request.`, // link: `/${user.product}/organisation/staff/requests`, // buttonTitle: "View requests", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "primary", // title: `${regRequests} people have requested to register with your organisation.`, // message: `Click to review these requests.`, // link: `/${user.product}/organisation/staff/requests`, // buttonTitle: "View requests", // } as TaskQueryReturnObject; // }, // }, // activateStaff: { // callback: async (user) => { // if (!getAccess(user, "addStaff")) return; // const inactiveAccounts = await firebaseQuery.getCount("users", [where("product", "==", user.product), where("oId", "==", user.oId), where("status", "==", "inactive")]); // if (inactiveAccounts === 0) return; // if (inactiveAccounts === 1) { // const account = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", user.product), where("oId", "==", user.oId), where("status", "==", "inactive")]) || {})[0] as [string, UserData]; // return { // dismissible: false, // severity: "info", // title: `Activate ${account[1].details.forename} ${account[1].details.surname}'s staff account.`, // message: "Activate this account to give the user access to CareerThread.", // link: `/${user.product}/organisation/staff/all`, // buttonTitle: "View accounts", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "info", // title: `${inactiveAccounts} staff have inactive active accounts.`, // message: "Activate these accounts to give the user access to CareerThread.", // link: `/${user.product}/organisation/staff/all`, // buttonTitle: "View accounts", // } as TaskQueryReturnObject; // }, // }, // placementStarting: { // callback: async (user) => { // const sevenDaysInFuture = new Date(); // sevenDaysInFuture.setDate(sevenDaysInFuture.getDate() + 7) // const constraints = [where("providerId", "==", user.oId), where("startDate", "<=", convertDate(sevenDaysInFuture, "dbstring")), where("startDate", ">", dateToString(new Date()))]; // if (user.userGroup !== "admin") { // if (!user.viewPlacementListings || user.viewPlacementListings === "none") return; // if (!user.viewAddresses || user.viewAddresses === "none") return; // if (user.viewPlacementListings === "request") { // if (!user.visibleListings || user.visibleListings?.length === 0) return; // constraints.push(where(documentId(), 'in', user.visibleListings)); // } else { // // viewPlacementListings must be 'all' // if (user.viewAddresses === "request") { // if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return; // constraints.push(where("addressId", 'in', user.visibleAddresses)); // } // } // } // const placementsStartingSoon = await firebaseQuery.getCount("placements", constraints); // if (placementsStartingSoon === 0) return; // if (placementsStartingSoon === 1) { // const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData]; // const student = placement[1].uid ? await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData : { // details: { // forename: placement[1].studentForename, // surname: placement[1].studentSurname, // } // }; // return { // dismissible: false, // severity: "success", // title: `${student.details.forename} ${student.details.surname}'s placement from ${convertDate(placement[1].startDate, "visual")} to ${convertDate(placement[1].endDate, "visual")} is starting in less than a week.`, // message: `Click to view the placement and acquaint yourself with the student.`, // link: `/${user.product}/placements/${placement[0]}`, // buttonTitle: "View", // } as TaskQueryReturnObject; // } // return { // dismissible: false, // severity: "success", // title: `${placementsStartingSoon} placements starting soon.`, // message: `Click to view scheduled placements.`, // link: `/${user.product}/placementListings/placements`, // buttonTitle: "View placements", // } as TaskQueryReturnObject; // }, // }, // completeFeedback: { // callback: async (user) => { // return; // return {} as TaskQueryReturnObject; // }, // }, // setUpFeedback: { // callback: async (user) => { // return; // return {} as TaskQueryReturnObject; // }, // }, // } export const getTips = async (user: UserData, organisation: InstituteData|ProviderData, addresses?: {[key: string]: OrganisationAddress}):Promise<(TipQueryReturnObject)[]> => { const tipsObject = { providers: providerTips, institutes: instituteTips, studentTips: studentTips, } const includedItems = Object.entries(tipsObject[user.product]).filter(([k]) => !user.dismissedTips?.includes(k)); const processedTips = await includedItems.reduce(async (acc, [itemName, item]) => { const callbackParams:[UserData, InstituteData|ProviderData] = [user, organisation]; if ((itemName === "addAddresses" || itemName === "addSchools") && addresses) { callbackParams.push(addresses as any); } const queryResult = await (item as { callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise, }).callback(...callbackParams); if (!queryResult) return await acc; const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult]; const results = queryResultArray.map((r) => ({itemName: itemName as InstituteTipNames|ProviderTipNames|StudentTipNames|InstituteTaskNames|ProviderTaskNames|StudentTaskNames, ...r})); (await acc).push(...results); return await acc; }, Promise.resolve([])); return processedTips; } export const getTasks = async ({user, organisation, cohort, school, eventId, type, schools, cohorts}:{user: UserData, organisation?: InstituteData&ProviderData, cohort?: CohortData, school?: InstituteData, eventId?: string, type?: "home"|"cohort"|"employerDatabase"|"event"|"employerEvents", schools?: {[key: string]: InstituteData}, cohorts?: {[key: string]: CohortData}}):Promise => { // Cohort is either a specific one or all. if (user.product === "institutes" && user.userType === "Staff" && organisation && type !== "employerEvents") { return await getInstituteTasks({user, organisation, cohort, school, eventId, type: type || "home", schools, cohorts}); } if (user.product === "students" || user.userType === "Students") { return await getStudentTasks(user, organisation, cohort); } if (user.product === "providers" && organisation) { // return await getProviderTasks(user, organisation); } return {}; }; const getStudentTasks = async (user: UserData, organisation?: InstituteData|ProviderData, cohort?: CohortData):Promise => { const processedTasks = await Object.entries(studentTasks).reduce(async (accPromise, [itemName, item]) => { const acc = await accPromise; // Wait for the previous accumulator value const queryResult = await item.callback(user); if (!queryResult) return acc; // If no result, return the accumulator as is // Merge the query result with the current accumulator return { ...acc, ...queryResult}; // Merge the query result into the accumulator object }, Promise.resolve({})); return processedTasks; }; // const getProviderTasks = async (user: UserData, organisation: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => { // const processedTasks = await Object.entries(providerTasks).reduce(async (acc, [itemName, item]) => { // const queryResult = await item.callback(user); // if (!queryResult) return await acc; // const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult]; // const results = queryResultArray.map((r) => ({itemName: itemName as ProviderTaskNames, ...r})); // (await acc).push(...results); // return await acc; // }, Promise.resolve([])); // return processedTasks;}; const getInstituteTasks = async ({user, organisation, cohort, school, type, eventId, schools, cohorts}:{user: UserData, organisation: InstituteData&ProviderData, cohort?: CohortData, school?: InstituteData, eventId?: string, type: "home"|"cohort"|"employerDatabase"|"event", schools?: {[key: string]: InstituteData}, cohorts?: {[key: string]: CohortData}}):Promise => { let fCohort:CohortData|[string, CohortData][]|undefined = cohort || (cohorts ? Object.entries(cohorts) : undefined); let fSchool:InstituteData|[string, InstituteData][]|undefined = school || (schools ? Object.entries(schools) : undefined); const getCohortsOrSchoolsIfNotProvided = async (type: "cohorts"|"schools", school?: InstituteData | [string, InstituteData][]) => { console.log("TASKS", type, "View", user[`view${capitaliseWords(type)}`], user[`visible${capitaliseWords(type)}`]); if (type === "schools" && organisation.package === "institutes-one") return; if (user[`view${capitaliseWords(type)}`] === "none") return([]); if (user.userGroup === "admin" || user[`view${capitaliseWords(type)}`] === "all") { const constraints = [where("oId", "==", user.oId)]; if (type === "cohorts") { constraints.push(where("product", "==", user.product), where("stage", "==", "created")); if (school && (Array.isArray(school) ? school.length > 0 : typeof school === "object")) { constraints.push(where("schoolId", Array.isArray(school) ? "in" : "==", Array.isArray(school) ? school.map(([schId]) => schId) : school.id)) } } const cohorts = await firebaseQuery.getDocsWhere(type, constraints) as {[key:string]: CohortData|SchoolData}; return Object.entries(cohorts); } if (user[`view${capitaliseWords(type)}`] === "some") { const items = await ((user[`visible${capitaliseWords(type)}`] || []) as string[])?.reduce(async (acc, itemId) => { const item = await firebaseQuery.getDocData([type, itemId]) as CohortData; if (school && item.schoolId) { if (Array.isArray(school)) { if (!school.map(([schId]) => schId).includes(item.schoolId)) return (await acc); } else { if (item.schoolId !== school.id) return (await acc); } } if (type === "cohorts" && item.stage !== "created") { return (await acc); } (await acc).push([itemId, item]) return (await acc); }, Promise.resolve<[string, any][]>([])); return items; } return []; } // Get cohort based on schools. if (!fSchool) { fSchool = await getCohortsOrSchoolsIfNotProvided("schools"); } console.log("FSCHOOLS FOR TASKS", fSchool); if (!fCohort) { fCohort = await getCohortsOrSchoolsIfNotProvided("cohorts", fSchool); } console.log("FCOHORTS FOR TASKS", fCohort); const processedTasks = await Object.entries(instituteTasks).reduce(async (accPromise, [itemName, item]) => { if (!fCohort) { console.log("No cohorts to retrieve tasks for"); return({}); } const acc = await accPromise; const queryResult = await item.callback({user, organisation, cohorts: fCohort, schools: fSchool, type, eventId}); if (!queryResult) return acc; return { ...acc, ...queryResult}; // Merge the query result into the accumulator object }, Promise.resolve({})); return processedTasks; } /* export const getTasks = async (user: UserData):Promise<{[key:string]: TaskQueryReturnObject}> => { return Object.fromEntries((await Promise.all(Object.entries(tasks).filter(([k]) => !user.dismissedTasks?.includes(k)).map(async ([taskName, task]) => { const queryResult = await task.callback(user); return [taskName, queryResult ? {...queryResult, ...task} : undefined]; }))).filter(([, v]) => v)); }; export const dismissTask = (user: UserData, taskName:TaskNames) => { firebaseQuery.update(["users", user.id], {dismissedTasks: arrayUnion(taskName)}); }; */ export const dismissTip = async (user: UserData, itemName:InstituteTaskNames | ProviderTaskNames | StudentTaskNames | InstituteTipNames | ProviderTipNames | StudentTipNames | undefined) => { if (!itemName) return; return await firebaseQuery.update(["users", user.id], {dismissedTips: arrayUnion(itemName)}); };