placementt-core 1.20.217 → 11.0.803

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/.eslintrc.js +40 -40
  2. package/.gitattributes +2 -2
  3. package/lib/constants.js +10 -1
  4. package/lib/constants.js.map +1 -1
  5. package/lib/features/config.d.ts +133 -133
  6. package/lib/features/config.js +35 -35
  7. package/lib/features/contacts/contacts.d.ts +75 -75
  8. package/lib/features/contacts/contacts.js +105 -105
  9. package/lib/features/downtime/useDowntime.d.ts +11 -11
  10. package/lib/features/downtime/useDowntime.js +22 -22
  11. package/lib/features/placements/studentPlacements/studentPlacementsSlice.d.ts +63 -63
  12. package/lib/features/placements/studentPlacements/studentPlacementsSlice.js +81 -81
  13. package/lib/features/providerPlacements/providerPlacementsSlice.d.ts +19 -19
  14. package/lib/features/providerPlacements/providerPlacementsSlice.js +24 -24
  15. package/lib/features/studentPlacements/studentPlacementsSlice.d.ts +62 -62
  16. package/lib/features/studentPlacements/studentPlacementsSlice.js +87 -87
  17. package/lib/features/studentPlacements/useStudentPlacements.d.ts +6 -6
  18. package/lib/features/studentPlacements/useStudentPlacements.js +18 -18
  19. package/lib/features/userSlice.d.ts +26 -26
  20. package/lib/features/userSlice.js +23 -23
  21. package/lib/features/users/useUserFunctions.d.ts +25 -25
  22. package/lib/features/users/useUserFunctions.js +124 -124
  23. package/lib/features/users/userSlice.d.ts +46 -46
  24. package/lib/features/users/userSlice.js +48 -48
  25. package/lib/firebase/firebase.d.ts +1 -1
  26. package/lib/firebase/firebase.js +6 -2
  27. package/lib/firebase/firebase.js.map +1 -1
  28. package/lib/firebase/readDatabase.js +3 -1
  29. package/lib/firebase/readDatabase.js.map +1 -1
  30. package/lib/hooks.d.ts +33 -5
  31. package/lib/hooks.js +143 -107
  32. package/lib/hooks.js.map +1 -1
  33. package/lib/images/GatsbyBenchmarks.d.ts +1 -2
  34. package/lib/reduxHooks.d.ts +1 -66
  35. package/lib/reduxHooks.js +9 -69
  36. package/lib/reduxHooks.js.map +1 -1
  37. package/lib/tasksAndTips.d.ts +2 -2
  38. package/lib/tasksAndTips.js +37 -6
  39. package/lib/tasksAndTips.js.map +1 -1
  40. package/lib/typeDefinitions.d.ts +50 -5
  41. package/lib/util.d.ts +1 -1
  42. package/lib/util.js +12 -1
  43. package/lib/util.js.map +1 -1
  44. package/package.json +52 -56
  45. package/src/DatabaseDefinitions.ts +18 -18
  46. package/src/apiCalls.ts +128 -128
  47. package/src/config.ts +50 -50
  48. package/src/constants.ts +796 -787
  49. package/src/databaseTypes.ts +42 -42
  50. package/src/features/analytics/useAnalytics.tsx +63 -63
  51. package/src/features/contacts/contactsSlice.ts +147 -147
  52. package/src/features/contacts/useContacts.tsx +73 -73
  53. package/src/features/dropdown/useDropdown.tsx +52 -52
  54. package/src/features/global/downtime/useDowntime.tsx +23 -23
  55. package/src/features/global/users/useUserFunctions.tsx +132 -132
  56. package/src/features/jobs/jobsSlice.ts +71 -71
  57. package/src/features/placements/studentPlacements/activePlacement.ts +68 -68
  58. package/src/features/placements/studentPlacements/completedStudentPlacementsSlice.ts +97 -97
  59. package/src/features/placements/studentPlacements/upcomingStudentPlacementsSlice.ts +108 -108
  60. package/src/features/placements/studentPlacements/useStudentPlacements.tsx +9 -9
  61. package/src/features/placements/types.ts +10 -10
  62. package/src/features/referrals/useReferrals.tsx +56 -56
  63. package/src/features/updates/useUpdates.tsx +38 -38
  64. package/src/firebase/firebase.tsx +149 -145
  65. package/src/firebase/firebaseConfig.tsx +45 -45
  66. package/src/firebase/firebaseQuery.tsx +151 -151
  67. package/src/firebase/persistence.ts +84 -84
  68. package/src/firebase/readDatabase.tsx +236 -235
  69. package/src/firebase/util.tsx +352 -352
  70. package/src/firebase/writeDatabase.tsx +77 -77
  71. package/src/hooks.tsx +4353 -4323
  72. package/src/images/GatsbyBenchmarks.tsx +711 -711
  73. package/src/images/LogFuturePlacement.jsx +64 -64
  74. package/src/images/LogPreviousPlacement.jsx +228 -228
  75. package/src/images/gatsby_benchmarks.svg +466 -466
  76. package/src/images/log_future_placement.svg +114 -114
  77. package/src/images/log_previous_placement.svg +199 -199
  78. package/src/index.ts +34 -34
  79. package/src/readDatabase.tsx +3 -3
  80. package/src/reduxHooks.ts +232 -297
  81. package/src/tasksAndTips.ts +1209 -1177
  82. package/src/tutorialTips.ts +58 -58
  83. package/src/typeDefinitions.ts +1003 -958
  84. package/src/util.ts +160 -150
  85. package/tsconfig.dev.json +5 -5
  86. package/tsconfig.json +21 -22
@@ -1,1177 +1,1209 @@
1
- import {arrayUnion, documentId, orderBy, where} from "firebase/firestore";
2
- import FirebaseQuery from "./firebase/firebaseQuery";
3
- import {CohortData, InstituteData, OrganisationAddress, PlacementListing, ProviderData, RegistrationRequest, StudentPlacementData, UserData} from "./typeDefinitions";
4
- import {camelCaseToNormal, capitaliseWords, dateToString, getAccess} from "./firebase/util";
5
- import { convertDate } from "./firebase/util";
6
-
7
- const firebaseQuery = new FirebaseQuery;
8
-
9
- type InstituteTipNames = "createCohort"|"addAddresses"|"uploadStaff"|"assignStaffRoles"|"uploadPlacements"|"allowExternalPlacementUpload"|"uploadStaffGuidance"|"uploadStudentGuidance"
10
- type ProviderTipNames = string
11
- type StudentTipNames = string
12
-
13
- export type InstituteTaskNames = "missingParentEmail"|"invalidStaffEmails"|"invalidStudentEmails"|"invalidParentEmails"|"invalidProviderEmails"|"verifyInsurance"|"verifyRiskAssessment"|"verifyDbsCheck"|"inactiveStudents"|"uploadStudents"|"inactiveStaff"|"requiredStage"|"approveExternalPlacement"|"overdueStage"
14
- export type StudentTaskNames = "completeOnboarding"
15
- export type ProviderTaskNames = "applicationRequireReview"|"activateStaff"|"requestedVisiblePlacementListings"|"requestedVisibleAddresses"|"completeStudentDocs"|"uploadOnboarding"|"reviewOnboarding"|"completeListing"|"completeAddress"|"registrationRequests"|"placementStarting"|"completeFeedback"|"setUpFeedback"
16
-
17
- // IF UPDATING LOGIC WITHIN THIS FILE, PLACEMENTT-BACKEND LOGIC MUST ALSO BE CHANGED ACCORDINGLY
18
-
19
- export type TaskQueryReturnObject = {
20
- itemName?: InstituteTaskNames|InstituteTipNames|StudentTipNames|ProviderTipNames,
21
- title?: string,
22
- message?: string,
23
- link?: string,
24
- buttonTitle?: string,
25
- dismissible?: boolean,
26
- severity?: "error"|"warning"|"success"|"primary"|"info"
27
- }|undefined;
28
- /*
29
- type TasksObject = {
30
- [key in TaskNames]: {
31
- callback: (user: UserData) => Promise<TaskQueryReturnObject>,
32
- };
33
- };
34
- */
35
- type InstituteTipsObject = {
36
- [key in InstituteTipNames]: {
37
- callback: (user: UserData, organisation?:InstituteData|ProviderData, additional?:{[key: string]: OrganisationAddress}) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
38
- };
39
- };
40
- type ProviderTipsObject = {
41
- [key in ProviderTipNames]: {
42
- callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
43
- };
44
- };
45
- type StudentTipsObject = {
46
- [key in StudentTipNames]: {
47
- callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
48
- };
49
- };
50
-
51
- export type InstituteTaskObject = {
52
- [key in InstituteTaskNames]: {
53
- callback: (user: UserData, organisation:InstituteData|ProviderData, cohort: CohortData|[string, CohortData][]) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
54
- };
55
- };
56
-
57
- export type StudentTaskObject = {
58
- [key in StudentTaskNames]: {
59
- callback: (user: UserData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
60
- };
61
- };
62
- export type ProviderTaskObject = {
63
- [key in ProviderTaskNames]: {
64
- callback: (user: UserData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
65
- };
66
- };
67
- const providerTips:ProviderTipsObject = {}
68
- const studentTips:StudentTipsObject = {}
69
-
70
-
71
- const instituteTips:InstituteTipsObject = {
72
- addAddresses: {
73
- callback: async (user, institute, addresses) => {
74
- if (!getAccess(user, "addAddresses") || institute?.package === "institutes-one") return;
75
- if (Object.keys(addresses as {[key: string]: OrganisationAddress}).length < 2) {
76
- return {
77
- title: "Add your schools",
78
- message: "Add your school addresses. These will show up in the 'Cohorts' tab where you can assign cohorts of students to them.",
79
- link: "/institutes/organisation/overview",
80
- buttonTitle: "Add schools",
81
- dismissible: true
82
- };
83
- }
84
- return;
85
- },
86
- },
87
- createCohort: {
88
- callback: async (user) => {
89
- if (!getAccess(user, "createCohorts")) return;
90
- const cohorts = await firebaseQuery.getCount("cohorts", [where("product", "==", user.product), where("oId", "==", user.oId)])
91
- if (cohorts === 0) {
92
- return {
93
- title: "Create a cohort",
94
- message: "Create a cohort to manage your students, process their placements and track their progress",
95
- link: "/institutes/cohorts/new",
96
- buttonTitle: "Create cohort",
97
- };
98
- }
99
- return;
100
- },
101
- },
102
- uploadStaff: {
103
- callback: async (user) => {
104
- const returnObj: TaskQueryReturnObject = {
105
- link: "",
106
- dismissible: true,
107
- };
108
- if ((await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff")])) == 1) {
109
- return {
110
- ...returnObj,
111
- title: "Upload staff",
112
- message: "Upload staff to help manage your students",
113
- link: "/institutes/cohorts/staff/all",
114
- buttonTitle: "Upload staff",
115
- };
116
- }
117
- return;
118
- },
119
- },
120
- assignStaffRoles: {
121
- callback: async () => {
122
- return undefined;
123
- },
124
- },
125
- uploadPlacements: {
126
- callback: async (user) => {
127
- const returnObj: TaskQueryReturnObject = {
128
- link: "",
129
- dismissible: true,
130
- };
131
- if (await firebaseQuery.getCount(["placementListings"], where(`savedBy.${user.oId}.exists`, "==", true)) === 0) {
132
- return {
133
- ...returnObj,
134
- title: "List placements",
135
- message: "You can list placements to students, to give them places to contact. Check out the 'Placements' tab to list opportunitites.",
136
- link: "/institutes/placements",
137
- buttonTitle: "Add placements",
138
- };
139
- }
140
- return;
141
- },
142
- },
143
- allowExternalPlacementUpload: {
144
- callback: async (user, organisation) => {
145
- if (user.product !== "institutes") return;
146
- if ((organisation as InstituteData).externalProviderUploads) return;
147
-
148
- return {
149
- dismissible: true,
150
- title: "Allow external uploads",
151
- message: "Get a link to share with businesses, allowing them to list their own opportunities to your students.",
152
- link: "/institutes/placements",
153
- buttonTitle: "View placements",
154
- };
155
- },
156
- },
157
- uploadStaffGuidance: {
158
- callback: async (user, organisation) => {
159
- if (user.product !== "institutes") return;
160
- const returnObj: TaskQueryReturnObject = {
161
- link: "",
162
- dismissible: true,
163
- };
164
- const guidanceTips:TaskQueryReturnObject[] = [];
165
- if (!Object.keys((organisation as InstituteData).staffGuidance || {}).length) {
166
- guidanceTips.push({
167
- ...returnObj,
168
- title: "Upload staff guidance",
169
- message: "Upload guidance documents to support your staff in coordinating work experience.",
170
- buttonTitle: "Staff guidance",
171
- link: `/${user.product}/setup/guidance`,
172
- });
173
- }
174
- return guidanceTips;
175
- },
176
- },
177
- uploadStudentGuidance: {
178
- callback: async (user, organisation) => {
179
- if (user.product !== "institutes") return;
180
- const returnObj: TaskQueryReturnObject = {
181
- link: "",
182
- dismissible: true,
183
- };
184
- const guidanceTips:TaskQueryReturnObject[] = [];
185
- if (!Object.keys((organisation as InstituteData).studentsGuidance || {}).length) {
186
- guidanceTips.push({
187
- ...returnObj,
188
- title: "Upload student guidance",
189
- message: "Upload guidance documents to support your staff in preparing for their placements.",
190
- buttonTitle: "Student guidance",
191
- link: `/${user.product}/setup/guidance`,
192
- });
193
- }
194
- return guidanceTips;
195
- },
196
- },
197
- };
198
- // Accept a cohort to any task
199
-
200
- const instituteTasks:InstituteTaskObject = {
201
- invalidStaffEmails: {
202
- callback: async (user, organisation, cohort) => {
203
- if (!getAccess(user, "viewStaff") || !Array.isArray(cohort)) return;
204
-
205
- const staffCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff"), where("flags", "array-contains", "userEmailFailed")]));
206
- if (staffCount > 0) {
207
- return {
208
- dismissible: false,
209
- severity: "error",
210
- title: `${staffCount} staff have invalid emails.`,
211
- message: `${staffCount} staff accounts have invalid email addresses. Delete and reupload these users.`,
212
- link: "/institutes/cohorts/staff/all",
213
- };
214
- }
215
- return;
216
- },
217
- },
218
- invalidStudentEmails: {
219
- callback: async (user, organisation, cohorts) => {
220
- if (!getAccess(user, "viewStudents") || user.viewStudents === "none") return;
221
-
222
- if (!Array.isArray(cohorts)) {
223
- const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Students"), where("cohort", "==", cohorts.id), where("flags", "array-contains", "userEmailFailed")]));
224
- if (studentCount > 0) {
225
- return {
226
- dismissible: false,
227
- severity: "error",
228
- title: `${studentCount} students have invalid emails.`,
229
- message: `${studentCount} student accounts have invalid email addresses. Delete and reupload these users.`,
230
- link: `/institutes/cohorts/${cohorts.id}/students`,
231
- };
232
- }
233
- return;
234
- }
235
-
236
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
237
- const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Students"), where("cohort", "==", id), where("flags", "array-contains", "userEmailFailed")]));
238
- if (studentCount > 0) {
239
- return {
240
- dismissible: false,
241
- severity: "error",
242
- title: `${studentCount} students have invalid emails.`,
243
- message: `Your cohort, ${cohort.name}, has ${studentCount} student accounts with invalid email addresses. Delete and reupload these users.`,
244
- link: `/institutes/cohorts/${id}/students`,
245
- } as TaskQueryReturnObject;
246
- }
247
- return;
248
- }));
249
- return items.filter((v) => v);
250
- },
251
- },
252
- invalidParentEmails: {
253
- callback: async (user, _, cohorts) => {
254
- if (!getAccess(user, "signOffPlacements") || user.viewStudents === "none") return;
255
-
256
- if (!Array.isArray(cohorts)) {
257
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("flags", "array-contains", "parentEmailFailed")]));
258
- if (placementCount > 0) {
259
- return {
260
- dismissible: false,
261
- severity: "error",
262
- title: `${placementCount} placements have invalid parent emails.`,
263
- message: `Your cohort '${cohorts.name}' has placements with invalid parent emails. Click to view these placements.`,
264
- link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming`,
265
- };
266
- }
267
- return;
268
- }
269
-
270
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
271
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("flags", "array-contains", "parentEmailFailed")]));
272
- if (placementCount > 0) {
273
- return {
274
- dismissible: false,
275
- severity: "error",
276
- title: `${placementCount} placements in '${cohort.name}' have invalid parent emails.`,
277
- message: `Your cohort '${cohort.name}' has placements with invalid parent emails. Click to view these placements.`,
278
- link: `/institutes/cohorts/${id}/placements?id=upcoming`,
279
- buttonTitle: "Review placements",
280
- } as TaskQueryReturnObject;
281
- }
282
- return;
283
- }));
284
- return items.filter((v) => v);
285
- },
286
- },
287
- invalidProviderEmails: {
288
- callback: async (user, organisation, cohorts) => {
289
- if (!getAccess(user, "signOffPlacements") || user.viewStudents === "none") return;
290
-
291
- if (!Array.isArray(cohorts)) {
292
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("flags", "array-contains", "providerEmailFailed")]));
293
- if (placementCount > 0) {
294
- return {
295
- dismissible: false,
296
- severity: "error",
297
- title: `${placementCount} placements have invalid provider emails.`,
298
- message: `Your cohort '${cohorts.name}' has placements with invalid provider emails. Click to view these placements.`,
299
- link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming`,
300
- };
301
- }
302
- return;
303
- }
304
-
305
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
306
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("flags", "array-contains", "providerEmailFailed")]));
307
- if (placementCount > 0) {
308
- return {
309
- dismissible: false,
310
- severity: "error",
311
- title: `${placementCount} placements in '${cohort.name}' have invalid provider emails.`,
312
- message: `Your cohort '${cohort.name}' has placements with invalid provider emails. Click to view these placements.`,
313
- link: `/institutes/cohorts/${id}/placements?id=upcoming`,
314
- buttonTitle: "Review placements",
315
- } as TaskQueryReturnObject;
316
- }
317
- return;
318
- }));
319
- return items.filter((v) => v);
320
- },
321
- },
322
- requiredStage: {
323
- callback: async (user, _, cohorts) => {
324
- if (!getAccess(user, "signOffPlacements") || user.viewStudents === "none") return;
325
-
326
- if (!Array.isArray(cohorts)) {
327
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("reqUserType", "==", user.userType)]));
328
- if (placementCount > 0) {
329
- return {
330
- dismissible: false,
331
- severity: "primary",
332
- title: `${placementCount} placements require your attention.`,
333
- message: `Your cohort '${cohorts.name}' has placements for your to review. Click to view these placements.`,
334
- link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming&reqUserType=${user.userType}`,
335
- };
336
- }
337
- return;
338
- }
339
-
340
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
341
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("reqUserType", "==", user.userType)]));
342
- if (placementCount > 0) {
343
- return {
344
- dismissible: false,
345
- severity: "primary",
346
- title: `${placementCount} placements in '${cohort.name}' require your attention.`,
347
- message: `Your cohort '${cohort.name}' has placements for your to review. Click to view these placements.`,
348
- link: `/institutes/cohorts/${id}/placements?id=upcoming&reqUserType=${user.userType}`,
349
- buttonTitle: "Review placements",
350
- } as TaskQueryReturnObject;
351
- }
352
- return;
353
- }));
354
- return items.filter((v) => v);
355
- },
356
- },
357
- verifyInsurance: {
358
- callback: async (user, _, cohorts) => {
359
- if (!getAccess(user, "verifyInsurance") || user.viewStudents === "none") return;
360
-
361
- if (!Array.isArray(cohorts)) {
362
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("insurance", "==", "awaitingReview")]));
363
- if (placementCount > 0) {
364
- return {
365
- dismissible: false,
366
- severity: "primary",
367
- title: `${placementCount} placements have insurance that require review.`,
368
- message: `Your cohort '${cohorts.name}' has employer's liability insurance documents that require review`,
369
- link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming&insurance=awaitingReview`,
370
- };
371
- }
372
- return;
373
- }
374
-
375
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
376
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("insurance", "==", "awaitingReview")]));
377
- if (placementCount > 0) {
378
- return {
379
- dismissible: false,
380
- severity: "primary",
381
- title: `${placementCount} placements in '${cohort.name}' have insurance that require review.`,
382
- message: `Your cohort '${cohort.name}' has employer's liability insurance documents that require review`,
383
- link: `/institutes/cohorts/${cohort.id}/placements?id=upcoming&insurance=awaitingReview`,
384
- buttonTitle: "Review placements",
385
- } as TaskQueryReturnObject;
386
- }
387
- return;
388
- }));
389
- return items.filter((v) => v);
390
- },
391
- },
392
- verifyDbsCheck: {
393
- callback: async (user, _, cohorts) => {
394
- if (!getAccess(user, "verifyDbsChecks") || user.viewStudents === "none") return;
395
-
396
- if (!Array.isArray(cohorts)) {
397
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("dbsCheck", "==", "awaitingReview")]));
398
- if (placementCount > 0) {
399
- return {
400
- dismissible: false,
401
- severity: "primary",
402
- title: `${placementCount} placements have DBS checks that require review.`,
403
- message: `Your cohort '${cohorts.name}' has DBS checks that require review`,
404
- link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming&dbsCheck=awaitingReview`,
405
- };
406
- }
407
- return;
408
- }
409
-
410
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
411
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("dbsCheck", "==", "awaitingReview")]));
412
- if (placementCount > 0) {
413
- return {
414
- dismissible: false,
415
- severity: "primary",
416
- title: `${placementCount} placements in '${cohort.name}' have DBS checks that require review.`,
417
- message: `Your cohort '${cohort.name}' has DBS checks that require review`,
418
- link: `/institutes/cohorts/${cohort.id}/placements?id=upcoming&dbsCheck=awaitingReview`,
419
- buttonTitle: "Review placements",
420
- } as TaskQueryReturnObject;
421
- }
422
- return;
423
- }));
424
- return items.filter((v) => v);
425
- },
426
- },
427
- verifyRiskAssessment: {
428
- callback: async (user, _, cohorts) => {
429
- if (!getAccess(user, "verifyRiskAssessments") || user.viewStudents === "none") return;
430
-
431
- if (!Array.isArray(cohorts)) {
432
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("riskAssessment", "==", "awaitingReview")]));
433
- if (placementCount > 0) {
434
- return {
435
- dismissible: false,
436
- severity: "primary",
437
- title: `${placementCount} placements have risk assessments that require review.`,
438
- message: `Your cohort '${cohorts.name}' has risk assessents that require review`,
439
- link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming&riskAssessment=awaitingReview`,
440
- };
441
- }
442
- return;
443
- }
444
-
445
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
446
- const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("riskAssessment", "==", "awaitingReview")]));
447
- if (placementCount > 0) {
448
- return {
449
- dismissible: false,
450
- severity: "primary",
451
- title: `${placementCount} placements in '${cohort.name}' have risk assessments that require review.`,
452
- message: `Your cohort '${cohort.name}' has risk assessments that require review`,
453
- link: `/institutes/cohorts/${cohort.id}/placements?id=upcoming&riskAssessment=awaitingReview`,
454
- buttonTitle: "Review placements",
455
- } as TaskQueryReturnObject;
456
- }
457
- return;
458
- }));
459
- return items.filter((v) => v);
460
- },
461
- },
462
- missingParentEmail: {
463
- callback: async (user, _, cohorts) => {
464
- if (!getAccess(user, "editStudents") || user.viewStudents === "none") return;
465
- if (!Array.isArray(cohorts)) {
466
- const requiresParents = Boolean(cohorts.workflow.find((node) => node.userType === "Parent"))
467
- if (!requiresParents) return;
468
- const constraints = [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("details.parentEmail", "==", "null")];
469
-
470
- if (user.groupData?.viewStudents === "filter") {
471
- constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || ""));
472
- }
473
- const studentCount = (await firebaseQuery.getCount(["users"], constraints));
474
- if (studentCount > 0) {
475
- return {
476
- dismissible: false,
477
- severity: "info",
478
- title: `${studentCount} students do not have a parent email.`,
479
- message: `Your cohort '${cohorts.name}' has students without a parent email. To allow proper processing of their placements, add these emails.`,
480
- link: `/institutes/cohorts/${cohorts.id}/students?parentEmail=false`,
481
- };
482
- }
483
- return;
484
- }
485
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
486
- const requiresParents = Boolean(cohort.workflow.find((node) => node.userType === "Parent"))
487
- if (!requiresParents) return;
488
- const constraints = [where("oId", "==", user.oId), where("cohort", "==", id), where("details.parentEmail", "==", "null")];
489
-
490
- if (user.groupData?.viewStudents === "filter") {
491
- constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || ""));
492
- }
493
- const studentCount = (await firebaseQuery.getCount(["users"], constraints));
494
- if (studentCount > 0) {
495
- return {
496
- dismissible: false,
497
- severity: "info",
498
- title: `${studentCount} students in '${cohort.name}' do not have a parent email.`,
499
- message: `Your cohort '${cohort.name}' has students without a parent email. To allow proper processing of their placements, add these emails.`,
500
- link: `/institutes/cohorts/${cohort.id}/students?parentEmail=false`,
501
- buttonTitle: "View students",
502
- } as TaskQueryReturnObject;
503
- }
504
- return;
505
- }));
506
- return items.filter((v) => v);
507
- },
508
- },
509
- inactiveStudents: {
510
- callback: async (user, _, cohorts) => {
511
- if (!getAccess(user, "activateStudents") || user.viewStudents === "none") return;
512
- if (!Array.isArray(cohorts)) {
513
- const constraints = [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("status", "==", "inactive")];
514
-
515
- if (user.groupData?.viewStudents === "filter") {
516
- constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || ""));
517
- }
518
- const studentCount = (await firebaseQuery.getCount(["users"], constraints));
519
- if (studentCount > 0) {
520
- return {
521
- dismissible: false,
522
- severity: "info",
523
- title: `${studentCount} students in are inactive.`,
524
- message: `Your cohort '${cohorts.name}' has inactive students. Activate them to enable them to use the platform.`,
525
- link: `/institutes/cohorts/${cohorts.id}/students?status=inactive`,
526
- };
527
- }
528
- return;
529
- }
530
-
531
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
532
- const constraints = [where("oId", "==", user.oId), where("cohort", "==", id), where("status", "==", "inactive")];
533
-
534
- if (user.groupData?.viewStudents === "filter") {
535
- constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || ""));
536
- }
537
- const studentCount = (await firebaseQuery.getCount(["users"], constraints));
538
- if (studentCount > 0) {
539
- return {
540
- dismissible: false,
541
- severity: "info",
542
- title: `${studentCount} students in '${cohort.name}' are inactive.`,
543
- message: `Your cohort '${cohort.name}' has inactive students. Activate them to enable them to use the platform.`,
544
- link: `/institutes/cohorts/${cohort.id}/students?status=inactive`,
545
- buttonTitle: "Review students",
546
- } as TaskQueryReturnObject;
547
- }
548
- return;
549
- }));
550
- return items.filter((v) => v);
551
- },
552
- },
553
- uploadStudents: {
554
- callback: async (user, _, cohorts) => {
555
- if (!Array.isArray(cohorts)) return;
556
- if (!getAccess(user, "addStudents") || user.viewStudents === "none") return;
557
- if (user.product !== "institutes") return;
558
- const returnObj: TaskQueryReturnObject = {
559
- link: "",
560
- dismissible: true,
561
- severity: "warning",
562
- };
563
- const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
564
- const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Students"), where("cohort", "==", id)]));
565
- if (studentCount === 0) {
566
- return {
567
- ...returnObj,
568
- title: "Upload students",
569
- message: `Your cohort '${cohort.name}' has no students. Add them in the cohort 'Students' tab`,
570
- link: `/institutes/cohorts/${id}/students`,
571
- buttonTitle: "Upload students",
572
- };
573
- }
574
- return;
575
- }));
576
- return items.filter((v) => v);
577
- },
578
- },
579
- inactiveStaff: {
580
- callback: async (user, _, cohorts) => {
581
- if (!getAccess(user, "addStaff")) return;
582
-
583
- if (!Array.isArray(cohorts) || user.product === "students") return;
584
- const returnObj: TaskQueryReturnObject = {
585
- link: `/${user.product}/cohorts/staff/all?status=inactive`,
586
- dismissible: false,
587
- severity: "info",
588
- };
589
- const inactiveStaff = await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff"), where("status", "==", "inactive")]);
590
- if (inactiveStaff > 0) {
591
- return {
592
- ...returnObj,
593
- title: "Inactive staff",
594
- message: `You have ${inactiveStaff} inactive staff members. You can activate them in the 'Staff' cohorts tab.`,
595
- };
596
- }
597
- return;
598
- },
599
- },
600
- overdueStage: {
601
- callback: async (user, _, cohort) => {
602
- console.log(user, cohort);
603
- return undefined;
604
- },
605
- },
606
- approveExternalPlacement: {
607
- callback: async (user, _, cohort) => {
608
- if (!getAccess(user, "verifyListings")) return;
609
- if (!Array.isArray(cohort)) return;
610
- const externalCount = await firebaseQuery.getCount(["placementListings"],
611
- [where(`savedBy.${user.oId}.exists`, "==", true), where(`savedBy.${user.oId}.status`, "==", "uploaded"), where("mapConsent", "==", "institute")]);
612
- console.log("EXT", externalCount);
613
- if (externalCount > 0) {
614
- return {
615
- dismissible: false,
616
- severity: "warning",
617
- title: `${externalCount} external placements require approval`,
618
- message: "Review and approve these placements to add them to your institute placement map",
619
- link: "/institutes/placements",
620
- buttonTitle: "View placements",
621
- };
622
- }
623
- return;
624
- },
625
- },
626
- };
627
-
628
- const studentTasks:StudentTaskObject = {
629
- completeOnboarding: {
630
- callback: async (user) => {
631
- 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};
632
- if (Object.keys(placementsWithoutOnboarding).length === 0) return;
633
- const items = Object.entries(placementsWithoutOnboarding).map(([k, placement]) => ({
634
- dismissible: false,
635
- severity: "primary",
636
- title: `Complete onboarding for ${placement.name}`,
637
- message: `Review onboarding for your placement starting on ${convertDate(placement.startDate, "visual")}`,
638
- link: `/${user.product}/placements/${k}`,
639
- buttonTitle: "View onboarding",
640
- } as TaskQueryReturnObject))
641
- return items;
642
- },
643
- }
644
- }
645
-
646
- const providerTasks:ProviderTaskObject = {
647
- requestedVisibleAddresses: {
648
- callback: async (user) => {
649
- if (!getAccess(user, "addStaff")) return;
650
- 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);
651
- if (accessRequests === 0) return;
652
- if (accessRequests === 1) {
653
- const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", "providers"), where("oId", "==", user.oId), orderBy("requestedVisibleAddresses")]) || {})[0] as [string, UserData];
654
- return {
655
- dismissible: false,
656
- severity: "primary",
657
- title: `${userRequestingAccess[1].details.forename} ${userRequestingAccess[1].details.surname} has requested access to view addresses.`,
658
- message: `Click to review the addresses and grant access to the user.`,
659
- link: `/${user.product}/users/${userRequestingAccess[0]}`,
660
- buttonTitle: "View request",
661
- } as TaskQueryReturnObject;
662
- }
663
-
664
- return {
665
- dismissible: false,
666
- severity: "warning",
667
- title: `Multiple users have requested access to view addresses.`,
668
- message: `Click to review the addresses and grant access to the user.`,
669
- link: `/${user.product}/organisation/staff/all`,
670
- buttonTitle: "View request",
671
- } as TaskQueryReturnObject;
672
- },
673
- },
674
- requestedVisiblePlacementListings: {
675
- callback: async (user) => {
676
- if (!getAccess(user, "addStaff")) return;
677
- 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);;
678
- if (accessRequests === 0) return;
679
- if (accessRequests === 1) {
680
- const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleListings")]) || {})[0] as [string, UserData];
681
- return {
682
- dismissible: false,
683
- severity: "primary",
684
- title: `${userRequestingAccess[1].details.forename} ${userRequestingAccess[1].details.surname} has requested access to view placement listings.`,
685
- message: `Click to review the placement listings and grant access to the user.`,
686
- link: `/${user.product}/users/${userRequestingAccess[0]}`,
687
- buttonTitle: "View request",
688
- } as TaskQueryReturnObject;
689
- }
690
-
691
- return {
692
- dismissible: false,
693
- severity: "warning",
694
- title: `Multiple users have requested access to view placement listings.`,
695
- message: `Click to review the placement listings and grant access to the user.`,
696
- link: `/${user.product}/organisation/staff/all`,
697
- buttonTitle: "View request",
698
- } as TaskQueryReturnObject;
699
- },
700
- },
701
- applicationRequireReview: {
702
- callback: async (user) => {
703
- const constraints = [where("providerId", "==", user.oId), where("reqUserType", "==", "Staff"), where("status", "==", "submitted")];
704
-
705
- if (user.userGroup !== "admin") {
706
-
707
- if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
708
- if (!user.viewAddresses || user.viewAddresses === "none") return;
709
-
710
- if (user.viewPlacementListings === "request") {
711
- if (!user.visibleListings || user.visibleListings?.length === 0) return;
712
- constraints.push(where("listingId", 'in', user.visibleListings));
713
- } else {
714
- // viewPlacementListings must be 'all'
715
-
716
- if (user.viewAddresses === "request") {
717
- if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
718
- constraints.push(where("addressId", 'in', user.visibleAddresses));
719
- }
720
- }
721
- }
722
- const applicationCount = await firebaseQuery.getCount("applications", constraints);
723
- if (applicationCount === 0) return;
724
-
725
- return {
726
- dismissible: false,
727
- severity: "primary",
728
- title: `${applicationCount} applications require your review.`,
729
- message: `Click to view ${applicationCount} that require your attention.`,
730
- link: `/${user.product}/placementListings/applicants`,
731
- buttonTitle: "View applications",
732
- } as TaskQueryReturnObject;
733
- },
734
- },
735
- completeStudentDocs: {
736
- callback: async (user) => {
737
- return;
738
- return {} as TaskQueryReturnObject;
739
- },
740
- },
741
- reviewOnboarding: {
742
- callback: async (user) => {
743
- const constraints = [where("providerId", "==", user.oId), where("onboarding.completed.accepted", "==", false), where("onboarding.completed.submitted", "==", true), where("endDate", ">=", dateToString(new Date()))]
744
-
745
- if (user.userGroup !== "admin") {
746
-
747
- if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
748
- if (!user.viewAddresses || user.viewAddresses === "none") return;
749
-
750
- if (user.viewPlacementListings === "request") {
751
- if (!user.visibleListings || user.visibleListings?.length === 0) return;
752
- constraints.push(where("placementId", 'in', user.visibleListings));
753
- } else {
754
- // viewPlacementListings must be 'all'
755
-
756
- if (user.viewAddresses === "request") {
757
- if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
758
- constraints.push(where("addressId", 'in', user.visibleAddresses));
759
- }
760
- }
761
- }
762
- const toReview = await firebaseQuery.getCount("placements", constraints);
763
-
764
- if (toReview === 0) return;
765
- if (toReview === 1) {
766
- const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData];
767
- const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData;
768
- return {
769
- dismissible: false,
770
- severity: "primary",
771
- 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")}`,
772
- message: `Click to view the placement and review the onboarding.`,
773
- link: `/${user.product}/placements/${placement[0]}`,
774
- buttonTitle: "View",
775
- } as TaskQueryReturnObject;
776
- }
777
-
778
- return {
779
- dismissible: false,
780
- severity: "primary",
781
- title: `${toReview} student have completed their onboarding.`,
782
- message: `Click to view your placements and approve completed onboarding`,
783
- link: `/${user.product}/placementListings/placements`,
784
- buttonTitle: "View placements",
785
- } as TaskQueryReturnObject;
786
- },
787
- },
788
- uploadOnboarding: {
789
- callback: async (user) => {
790
-
791
- const constraints = [where("providerId", "==", user.oId), where("onboarding", "==", null), where("endDate", ">=", dateToString(new Date()))];
792
-
793
-
794
- if (user.userGroup !== "admin") {
795
-
796
- if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
797
- if (!user.viewAddresses || user.viewAddresses === "none") return;
798
-
799
- if (user.viewPlacementListings === "request") {
800
- if (!user.visibleListings || user.visibleListings?.length === 0) return;
801
- constraints.push(where("placementId", 'in', user.visibleListings));
802
- } else {
803
- // viewPlacementListings must be 'all'
804
-
805
- if (user.viewAddresses === "request") {
806
- if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
807
- constraints.push(where("addressId", 'in', user.visibleAddresses));
808
- }
809
- }
810
- }
811
-
812
- const withoutOnboarding = await firebaseQuery.getCount("placements", constraints);
813
-
814
- if (withoutOnboarding === 0) return;
815
- if (withoutOnboarding === 1) {
816
- const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData];
817
- const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData;
818
- return {
819
- dismissible: false,
820
- severity: "primary",
821
- 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")}`,
822
- message: `Click to view the placement and add or dismiss onboarding reminders.`,
823
- link: `/${user.product}/placements/${placement[0]}`,
824
- buttonTitle: "View",
825
- } as TaskQueryReturnObject;
826
- }
827
-
828
- return {
829
- dismissible: false,
830
- severity: "primary",
831
- title: `Set up onboarding for ${withoutOnboarding} placements to prepare yourself and your students.`,
832
- message: `Click to view your placements and add or dismiss onboarding reminders.`,
833
- link: `/${user.product}/placementListings/placements`,
834
- buttonTitle: "View placements",
835
- } as TaskQueryReturnObject;
836
- },
837
- },
838
- completeListing: {
839
- callback: async (user) => {
840
- const constraints = [where("providerId", "==", user.oId), where("status", "==", "draft")];
841
-
842
-
843
- if (user.userGroup !== "admin") {
844
-
845
- if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
846
- if (!user.viewAddresses || user.viewAddresses === "none") return;
847
-
848
- if (user.viewPlacementListings === "request") {
849
- if (!user.visibleListings || user.visibleListings?.length === 0) return;
850
- constraints.push(where(documentId(), 'in', user.visibleListings));
851
- } else {
852
- // viewPlacementListings must be 'all'
853
-
854
- if (user.viewAddresses === "request") {
855
- if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
856
- constraints.push(where("addressId", 'in', user.visibleAddresses));
857
- }
858
- }
859
- }
860
- const incompleteListings = await firebaseQuery.getCount("placementListings", constraints);
861
- if (incompleteListings === 0) return;
862
- if (incompleteListings === 1) {
863
- const incompleteListing = Object.entries(await firebaseQuery.getDocsWhere("placementListings", constraints) || {})[0] as [string, PlacementListing];
864
- const address = incompleteListing[1].addressId ? await firebaseQuery.getDocData(["addresses", incompleteListing[1].addressId]) as OrganisationAddress : undefined;
865
- return {
866
- dismissible: false,
867
- severity: "info",
868
- 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.`,
869
- message: `Click to complete and publish the placement listing.`,
870
- link: `/${user.product}/addListing/${incompleteListing[0]}`,
871
- buttonTitle: "View listing",
872
- } as TaskQueryReturnObject;
873
- }
874
-
875
- return {
876
- dismissible: false,
877
- severity: "info",
878
- title: `You have ${incompleteListings} draft listings waiting to be published.`,
879
- message: `Click to review and publish the placement listings.`,
880
- link: `/${user.product}/placementListings/listings`,
881
- buttonTitle: "View listings",
882
- } as TaskQueryReturnObject;
883
- },
884
- },
885
- completeAddress: {
886
- callback: async (user) => {
887
- const constraints = [where("product", "==", "providers"), where("oId", "==", user.oId), where("stage", "!=", "complete")];
888
-
889
-
890
- if (user.userGroup !== "admin") {
891
- if (!user.viewAddresses || user.viewAddresses === "none") return;
892
-
893
- if (user.viewAddresses === "request") {
894
- if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
895
- constraints.push(where("addressId", 'in', user.visibleAddresses));
896
- }
897
- }
898
- const incompleteAddresses = await firebaseQuery.getCount("addresses", constraints);
899
- if (incompleteAddresses === 0) return;
900
- if (incompleteAddresses === 1) {
901
- const address = Object.entries(await firebaseQuery.getDocsWhere("addresses", constraints) || {})[0] as [string, OrganisationAddress];
902
- return {
903
- dismissible: false,
904
- severity: "info",
905
- title: `Your address: ${address[1]["address-line1"]}, ${address[1].postal_code.toUpperCase()}, ${capitaliseWords(camelCaseToNormal(address[1].country))} is currently incomplete.`,
906
- message: `Click to complete the addresses.`,
907
- link: `/${user.product}/addAddress/${address[0]}`,
908
- buttonTitle: "View address",
909
- } as TaskQueryReturnObject;
910
- }
911
-
912
- return {
913
- dismissible: false,
914
- severity: "info",
915
- title: `You have ${incompleteAddresses} draft addresses waiting to be published.`,
916
- message: `Click to review the addresses.`,
917
- link: `/${user.product}/organisation/addresses`,
918
- buttonTitle: "View addresses",
919
- } as TaskQueryReturnObject;
920
- },
921
- },
922
- registrationRequests: {
923
- callback: async (user) => {
924
- if (!getAccess(user, "addStaff")) return;
925
-
926
- const regRequests = await firebaseQuery.getCount("requests", [where("product", "==", user.product), where("oId", "==", user.oId)]);
927
-
928
- if (regRequests === 0) return;
929
- if (regRequests === 1) {
930
- const request = Object.entries(await firebaseQuery.getDocsWhere("requests", [where("product", "==", user.product), where("oId", "==", user.oId)]) || {})[0] as [string, RegistrationRequest];
931
- return {
932
- dismissible: false,
933
- severity: "primary",
934
- title: `${request[1].forename} ${request[1].surname} has requested to access your organisation.`,
935
- message: `Click to review these request.`,
936
- link: `/${user.product}/organisation/staff/requests`,
937
- buttonTitle: "View requests",
938
- } as TaskQueryReturnObject;
939
- }
940
-
941
- return {
942
- dismissible: false,
943
- severity: "primary",
944
- title: `${regRequests} people have requested to register with your organisation.`,
945
- message: `Click to review these requests.`,
946
- link: `/${user.product}/organisation/staff/requests`,
947
- buttonTitle: "View requests",
948
- } as TaskQueryReturnObject;
949
- },
950
- },
951
- activateStaff: {
952
- callback: async (user) => {
953
- if (!getAccess(user, "addStaff")) return;
954
-
955
- const inactiveAccounts = await firebaseQuery.getCount("users", [where("product", "==", user.product), where("oId", "==", user.oId), where("status", "==", "inactive")]);
956
-
957
- if (inactiveAccounts === 0) return;
958
- if (inactiveAccounts === 1) {
959
- const account = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", user.product), where("oId", "==", user.oId), where("status", "==", "inactive")]) || {})[0] as [string, UserData];
960
- return {
961
- dismissible: false,
962
- severity: "info",
963
- title: `Activate ${account[1].details.forename} ${account[1].details.surname}'s staff account.`,
964
- message: "Activate this account to give the user access to Placementt.",
965
- link: `/${user.product}/organisation/staff/all`,
966
- buttonTitle: "View accounts",
967
- } as TaskQueryReturnObject;
968
- }
969
-
970
- return {
971
- dismissible: false,
972
- severity: "info",
973
- title: `${inactiveAccounts} staff have inactive active accounts.`,
974
- message: "Activate these accounts to give the user access to Placementt.",
975
- link: `/${user.product}/organisation/staff/all`,
976
- buttonTitle: "View accounts",
977
- } as TaskQueryReturnObject;
978
- },
979
- },
980
- placementStarting: {
981
- callback: async (user) => {
982
- const sevenDaysInFuture = new Date();
983
- sevenDaysInFuture.setDate(sevenDaysInFuture.getDate() + 7)
984
-
985
- const constraints = [where("providerId", "==", user.oId), where("startDate", "<=", convertDate(sevenDaysInFuture, "dbstring")), where("startDate", ">", dateToString(new Date()))];
986
-
987
- if (user.userGroup !== "admin") {
988
-
989
- if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
990
- if (!user.viewAddresses || user.viewAddresses === "none") return;
991
-
992
- if (user.viewPlacementListings === "request") {
993
- if (!user.visibleListings || user.visibleListings?.length === 0) return;
994
- constraints.push(where(documentId(), 'in', user.visibleListings));
995
- } else {
996
- // viewPlacementListings must be 'all'
997
-
998
- if (user.viewAddresses === "request") {
999
- if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
1000
- constraints.push(where("addressId", 'in', user.visibleAddresses));
1001
- }
1002
- }
1003
- }
1004
- const placementsStartingSoon = await firebaseQuery.getCount("placements", constraints);
1005
-
1006
- if (placementsStartingSoon === 0) return;
1007
- if (placementsStartingSoon === 1) {
1008
- const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData];
1009
- const student = placement[1].uid ? await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData : {
1010
- details: {
1011
- forename: placement[1].studentForename,
1012
- surname: placement[1].studentSurname,
1013
- }
1014
- };
1015
- return {
1016
- dismissible: false,
1017
- severity: "success",
1018
- 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.`,
1019
- message: `Click to view the placement and acquaint yourself with the student.`,
1020
- link: `/${user.product}/placements/${placement[0]}`,
1021
- buttonTitle: "View",
1022
- } as TaskQueryReturnObject;
1023
- }
1024
-
1025
- return {
1026
- dismissible: false,
1027
- severity: "success",
1028
- title: `${placementsStartingSoon} placements starting soon.`,
1029
- message: `Click to view scheduled placements.`,
1030
- link: `/${user.product}/placementListings/placements`,
1031
- buttonTitle: "View placements",
1032
- } as TaskQueryReturnObject;
1033
- },
1034
- },
1035
- completeFeedback: {
1036
- callback: async (user) => {
1037
- return;
1038
- return {} as TaskQueryReturnObject;
1039
- },
1040
- },
1041
- setUpFeedback: {
1042
- callback: async (user) => {
1043
- return;
1044
- return {} as TaskQueryReturnObject;
1045
- },
1046
- },
1047
- }
1048
-
1049
- export const getTips = async (user: UserData, organisation: InstituteData|ProviderData, addresses?: {[key: string]: OrganisationAddress}):Promise<(TaskQueryReturnObject)[]> => {
1050
- const tipsObject = {
1051
- providers: providerTips,
1052
- institutes: instituteTips,
1053
- studentTips: studentTips,
1054
- }
1055
- const includedItems = Object.entries(tipsObject[user.product]).filter(([k]) => !user.dismissedTips?.includes(k));
1056
-
1057
- const processedTips = await includedItems.reduce(async (acc, [itemName, item]) => {
1058
- const callbackParams:[UserData, InstituteData|ProviderData] = [user, organisation];
1059
-
1060
- if (itemName === "addAddresses" && addresses) {
1061
- callbackParams.push(addresses as any);
1062
- }
1063
- const queryResult = await (item as {
1064
- callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
1065
- }).callback(...callbackParams);
1066
- if (!queryResult) return await acc;
1067
-
1068
- const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
1069
- const results = queryResultArray.map((r) => ({itemName: itemName as InstituteTipNames|ProviderTipNames|StudentTipNames|InstituteTaskNames|ProviderTaskNames|StudentTaskNames, ...r}));
1070
-
1071
- (await acc).push(...results);
1072
- return await acc;
1073
- }, Promise.resolve<TaskQueryReturnObject[]>([]));
1074
- return processedTips;
1075
- }
1076
-
1077
- export const getTasks = async (user: UserData, organisation?: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => {
1078
- // Cohort is either a specific one or all.
1079
-
1080
- if (user.product === "institutes" && user.userType === "Staff" && organisation) {
1081
- return await getInstituteTasks(user, organisation, cohort);
1082
- }
1083
- if (user.product === "students" || user.userType === "Students") {
1084
- return await getStudentTasks(user, organisation, cohort);
1085
- }
1086
- if (user.product === "providers" && organisation) {
1087
- return await getProviderTasks(user, organisation);
1088
- }
1089
-
1090
- return [];
1091
- };
1092
-
1093
- const getStudentTasks = async (user: UserData, organisation?: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => {
1094
- const processedTasks = await Object.entries(studentTasks).reduce(async (acc, [itemName, item]) => {
1095
-
1096
- const queryResult = await item.callback(user);
1097
- if (!queryResult) return await acc;
1098
-
1099
- const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
1100
- const results = queryResultArray.map((r) => ({itemName: itemName as StudentTaskNames, ...r}));
1101
-
1102
- (await acc).push(...results);
1103
- return await acc;
1104
- }, Promise.resolve<TaskQueryReturnObject[]>([]));
1105
- return processedTasks;
1106
- };
1107
-
1108
- const getProviderTasks = async (user: UserData, organisation: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => {
1109
- const processedTasks = await Object.entries(providerTasks).reduce(async (acc, [itemName, item]) => {
1110
-
1111
- const queryResult = await item.callback(user);
1112
- if (!queryResult) return await acc;
1113
-
1114
- const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
1115
- const results = queryResultArray.map((r) => ({itemName: itemName as ProviderTaskNames, ...r}));
1116
-
1117
- (await acc).push(...results);
1118
- return await acc;
1119
- }, Promise.resolve<TaskQueryReturnObject[]>([]));
1120
- return processedTasks;};
1121
-
1122
- const getInstituteTasks = async (user: UserData, organisation: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => {
1123
- let fCohort:CohortData|[string, CohortData][]|undefined = cohort;
1124
-
1125
- if (!fCohort) {
1126
- // get all associated cohorts.
1127
- if (user.viewCohorts === "none") return([]);
1128
- if (user.userGroup === "admin" || user.viewCohorts === "all") {
1129
- const cohorts = await firebaseQuery.getDocsWhere("cohorts", [where("product", "==", user.product), where("oId", "==", user.oId), where("stage", "==", "created")]) as {[key:string]: CohortData};
1130
- fCohort = Object.entries(cohorts);
1131
- }
1132
- if (user.viewCohorts === "some") {
1133
- const cohorts = await user.visibleCohorts?.reduce(async (acc, cohortId) => {
1134
- const cohort = await firebaseQuery.getDocData(["cohorts", cohortId]) as CohortData;
1135
- if (cohort.stage !== "created") {
1136
- return acc;
1137
- }
1138
- acc[cohortId] = cohort;
1139
- return acc;
1140
- }, Promise.resolve<[string, CohortData][]>([]));
1141
- fCohort = cohorts;
1142
- }
1143
- }
1144
-
1145
- const processedTasks = await Object.entries(instituteTasks).reduce(async (acc, [itemName, item]) => {
1146
- if (!fCohort) {
1147
- console.log("No cohorts to retrieve tasks for");
1148
- return([]);
1149
- }
1150
- const queryResult = await item.callback(user, organisation, fCohort);
1151
- if (!queryResult) return await acc;
1152
-
1153
- const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
1154
- const results = queryResultArray.map((r) => ({itemName: itemName as InstituteTaskNames, ...r}));
1155
-
1156
- (await acc).push(...results);
1157
- return await acc;
1158
- }, Promise.resolve<TaskQueryReturnObject[]>([]));
1159
- return processedTasks;
1160
- }
1161
-
1162
- /*
1163
- export const getTasks = async (user: UserData):Promise<{[key:string]: TaskQueryReturnObject}> => {
1164
- return Object.fromEntries((await Promise.all(Object.entries(tasks).filter(([k]) => !user.dismissedTasks?.includes(k)).map(async ([taskName, task]) => {
1165
- const queryResult = await task.callback(user);
1166
- return [taskName, queryResult ? {...queryResult, ...task} : undefined];
1167
- }))).filter(([, v]) => v));
1168
- };
1169
-
1170
- export const dismissTask = (user: UserData, taskName:TaskNames) => {
1171
- firebaseQuery.update(["users", user.id], {dismissedTasks: arrayUnion(taskName)});
1172
- };
1173
- */
1174
- export const dismissTip = async (user: UserData, itemName:InstituteTaskNames | ProviderTaskNames | StudentTaskNames | InstituteTipNames | ProviderTipNames | StudentTipNames | undefined) => {
1175
- if (!itemName) return;
1176
- return await firebaseQuery.update(["users", user.id], {dismissedTips: arrayUnion(itemName)});
1177
- };
1
+ import {arrayUnion, documentId, orderBy, where} from "firebase/firestore";
2
+ import FirebaseQuery from "./firebase/firebaseQuery";
3
+ import {CohortData, InstituteData, OrganisationAddress, PlacementListing, ProviderData, RegistrationRequest, StudentPlacementData, UserData} from "./typeDefinitions";
4
+ import {camelCaseToNormal, capitaliseWords, dateToString, getAccess} from "./firebase/util";
5
+ import { convertDate } from "./firebase/util";
6
+
7
+ const firebaseQuery = new FirebaseQuery;
8
+
9
+ type InstituteTipNames = "createCohort"|"addSchools"|"uploadStaff"|"assignStaffRoles"|"uploadPlacements"|"allowExternalPlacementUpload"|"uploadStaffGuidance"|"uploadStudentGuidance"
10
+ type ProviderTipNames = string
11
+ type StudentTipNames = string
12
+
13
+ export type InstituteTaskNames = "missingParentEmail"|"outstandingReminders"|"invalidStaffEmails"|"invalidStudentEmails"|"invalidParentEmails"|"invalidProviderEmails"|"verifyInsurance"|"verifyRiskAssessment"|"verifyDbsCheck"|"inactiveStudents"|"uploadStudents"|"inactiveStaff"|"requiredStage"|"approveExternalPlacement"|"overdueStage"
14
+ export type StudentTaskNames = "completeOnboarding"
15
+ export type ProviderTaskNames = "applicationRequireReview"|"activateStaff"|"requestedVisiblePlacementListings"|"requestedVisibleAddresses"|"completeStudentDocs"|"uploadOnboarding"|"reviewOnboarding"|"completeListing"|"completeAddress"|"registrationRequests"|"placementStarting"|"completeFeedback"|"setUpFeedback"
16
+
17
+ // IF UPDATING LOGIC WITHIN THIS FILE, PLACEMENTT-BACKEND LOGIC MUST ALSO BE CHANGED ACCORDINGLY
18
+
19
+ export type TaskQueryReturnObject = {
20
+ itemName?: InstituteTaskNames|InstituteTipNames|StudentTipNames|ProviderTipNames,
21
+ title?: string,
22
+ message?: string,
23
+ link?: string,
24
+ buttonTitle?: string,
25
+ dismissible?: boolean,
26
+ severity?: "error"|"warning"|"success"|"primary"|"info"
27
+ }|undefined;
28
+ /*
29
+ type TasksObject = {
30
+ [key in TaskNames]: {
31
+ callback: (user: UserData) => Promise<TaskQueryReturnObject>,
32
+ };
33
+ };
34
+ */
35
+ type InstituteTipsObject = {
36
+ [key in InstituteTipNames]: {
37
+ callback: (user: UserData, organisation?:InstituteData|ProviderData, additional?:{[key: string]: OrganisationAddress}) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
38
+ };
39
+ };
40
+ type ProviderTipsObject = {
41
+ [key in ProviderTipNames]: {
42
+ callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
43
+ };
44
+ };
45
+ type StudentTipsObject = {
46
+ [key in StudentTipNames]: {
47
+ callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
48
+ };
49
+ };
50
+
51
+ export type InstituteTaskObject = {
52
+ [key in InstituteTaskNames]: {
53
+ callback: (user: UserData, organisation:InstituteData|ProviderData, cohort: CohortData|[string, CohortData][]) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
54
+ };
55
+ };
56
+
57
+ export type StudentTaskObject = {
58
+ [key in StudentTaskNames]: {
59
+ callback: (user: UserData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
60
+ };
61
+ };
62
+ export type ProviderTaskObject = {
63
+ [key in ProviderTaskNames]: {
64
+ callback: (user: UserData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
65
+ };
66
+ };
67
+ const providerTips:ProviderTipsObject = {}
68
+ const studentTips:StudentTipsObject = {}
69
+
70
+
71
+ const instituteTips:InstituteTipsObject = {
72
+ addSchools: {
73
+ callback: async (user, institute, schools) => {
74
+ if (!getAccess(user, "addSchools") || institute?.package !== "institutes-two") return;
75
+ if (Object.keys(schools as {[key: string]: OrganisationAddress}).length < 2) {
76
+ return {
77
+ title: "Add your schools",
78
+ message: "Add your schools. These will show up in the 'Cohorts' tab where you can assign cohorts of students to them.",
79
+ link: "/institutes/organisation/overview",
80
+ buttonTitle: "Add schools",
81
+ dismissible: true
82
+ };
83
+ }
84
+ return;
85
+ },
86
+ },
87
+ createCohort: {
88
+ callback: async (user) => {
89
+ if (!getAccess(user, "createCohorts")) return;
90
+ const cohorts = await firebaseQuery.getCount("cohorts", [where("product", "==", user.product), where("oId", "==", user.oId)])
91
+ if (cohorts === 0) {
92
+ return {
93
+ title: "Create a cohort",
94
+ message: "Create a cohort to manage your students, process their placements and track their progress",
95
+ link: "/institutes/cohorts/new",
96
+ buttonTitle: "Create cohort",
97
+ };
98
+ }
99
+ return;
100
+ },
101
+ },
102
+ uploadStaff: {
103
+ callback: async (user) => {
104
+ const returnObj: TaskQueryReturnObject = {
105
+ link: "",
106
+ dismissible: true,
107
+ };
108
+ if ((await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff")])) == 1) {
109
+ return {
110
+ ...returnObj,
111
+ title: "Upload staff",
112
+ message: "Upload staff to help manage your students",
113
+ link: "/institutes/cohorts/staff/all",
114
+ buttonTitle: "Upload staff",
115
+ };
116
+ }
117
+ return;
118
+ },
119
+ },
120
+ assignStaffRoles: {
121
+ callback: async () => {
122
+ return undefined;
123
+ },
124
+ },
125
+ uploadPlacements: {
126
+ callback: async (user) => {
127
+ const returnObj: TaskQueryReturnObject = {
128
+ link: "",
129
+ dismissible: true,
130
+ };
131
+ if (await firebaseQuery.getCount(["placementListings"], where(`savedBy.${user.oId}.exists`, "==", true)) === 0) {
132
+ return {
133
+ ...returnObj,
134
+ title: "List placements",
135
+ message: "You can list placements to students, to give them places to contact. Check out the 'Placements' tab to list opportunitites.",
136
+ link: "/institutes/placements",
137
+ buttonTitle: "Add placements",
138
+ };
139
+ }
140
+ return;
141
+ },
142
+ },
143
+ allowExternalPlacementUpload: {
144
+ callback: async (user, organisation) => {
145
+ if (user.product !== "institutes") return;
146
+ if ((organisation as InstituteData).externalProviderUploads) return;
147
+
148
+ return {
149
+ dismissible: true,
150
+ title: "Allow external uploads",
151
+ message: "Get a link to share with businesses, allowing them to list their own opportunities to your students.",
152
+ link: "/institutes/placements",
153
+ buttonTitle: "View placements",
154
+ };
155
+ },
156
+ },
157
+ uploadStaffGuidance: {
158
+ callback: async (user, organisation) => {
159
+ if (user.product !== "institutes") return;
160
+ const returnObj: TaskQueryReturnObject = {
161
+ link: "",
162
+ dismissible: true,
163
+ };
164
+ const guidanceTips:TaskQueryReturnObject[] = [];
165
+ if (!Object.keys((organisation as InstituteData).staffGuidance || {}).length) {
166
+ guidanceTips.push({
167
+ ...returnObj,
168
+ title: "Upload staff guidance",
169
+ message: "Upload guidance documents to support your staff in coordinating work experience.",
170
+ buttonTitle: "Staff guidance",
171
+ link: `/${user.product}/setup/guidance`,
172
+ });
173
+ }
174
+ return guidanceTips;
175
+ },
176
+ },
177
+ uploadStudentGuidance: {
178
+ callback: async (user, organisation) => {
179
+ if (user.product !== "institutes") return;
180
+ const returnObj: TaskQueryReturnObject = {
181
+ link: "",
182
+ dismissible: true,
183
+ };
184
+ const guidanceTips:TaskQueryReturnObject[] = [];
185
+ if (!Object.keys((organisation as InstituteData).studentsGuidance || {}).length) {
186
+ guidanceTips.push({
187
+ ...returnObj,
188
+ title: "Upload student guidance",
189
+ message: "Upload guidance documents to support your staff in preparing for their placements.",
190
+ buttonTitle: "Student guidance",
191
+ link: `/${user.product}/setup/guidance`,
192
+ });
193
+ }
194
+ return guidanceTips;
195
+ },
196
+ },
197
+ };
198
+ // Accept a cohort to any task
199
+
200
+ const instituteTasks:InstituteTaskObject = {
201
+ invalidStaffEmails: {
202
+ callback: async (user, organisation, cohort) => {
203
+ if (!getAccess(user, "viewStaff") || !Array.isArray(cohort)) return;
204
+
205
+ const staffCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff"), where("flags", "array-contains", "userEmailFailed")]));
206
+ if (staffCount > 0) {
207
+ return {
208
+ dismissible: false,
209
+ severity: "error",
210
+ title: `${staffCount} staff have invalid emails.`,
211
+ message: `${staffCount} staff accounts have invalid email addresses. Delete and reupload these users.`,
212
+ link: "/institutes/cohorts/staff/all",
213
+ };
214
+ }
215
+ return;
216
+ },
217
+ },
218
+ invalidStudentEmails: {
219
+ callback: async (user, organisation, cohorts) => {
220
+ if (!getAccess(user, "viewStudents") || user.viewStudents === "none") return;
221
+
222
+ if (!Array.isArray(cohorts)) {
223
+ const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Students"), where("cohort", "==", cohorts.id), where("flags", "array-contains", "userEmailFailed")]));
224
+ if (studentCount > 0) {
225
+ return {
226
+ dismissible: false,
227
+ severity: "error",
228
+ title: `${studentCount} students have invalid emails.`,
229
+ message: `${studentCount} student accounts have invalid email addresses. Delete and reupload these users.`,
230
+ link: `/institutes/cohorts/${cohorts.id}/students`,
231
+ };
232
+ }
233
+ return;
234
+ }
235
+
236
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
237
+ const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Students"), where("cohort", "==", id), where("flags", "array-contains", "userEmailFailed")]));
238
+ if (studentCount > 0) {
239
+ return {
240
+ dismissible: false,
241
+ severity: "error",
242
+ title: `${studentCount} students have invalid emails.`,
243
+ message: `Your cohort, ${cohort.name}, has ${studentCount} student accounts with invalid email addresses. Delete and reupload these users.`,
244
+ link: `/institutes/cohorts/${id}/students`,
245
+ } as TaskQueryReturnObject;
246
+ }
247
+ return;
248
+ }));
249
+ return items.filter((v) => v);
250
+ },
251
+ },
252
+ outstandingReminders: {
253
+ callback: async (user, organisation, cohorts) => {
254
+ if (!Array.isArray(cohorts)) {
255
+ const reminderCount = (await firebaseQuery.getCount(["reminders"], [where("oId", "==", user.oId), where("uid", "==", user.id), where("dueDate", "<=", convertDate(new Date(), "dbstring") as string), where("cohort", "==", cohorts.id), where("status", "==", "upcoming")]));
256
+ if (reminderCount > 0) {
257
+ return {
258
+ dismissible: false,
259
+ severity: "primary",
260
+ title: `You have ${reminderCount} reminder${reminderCount > 1 ? "s" : ""}.`,
261
+ message: `You have ${reminderCount} outstanding placement reminder${reminderCount > 1 ? "s" : ""}. Click to view.`,
262
+ link: `/institutes/cohorts/${cohorts.id}/placements`,
263
+ };
264
+ }
265
+ return;
266
+ }
267
+
268
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
269
+ const reminderCount = (await firebaseQuery.getCount(["reminders"], [where("oId", "==", user.oId), where("uid", "==", user.id), where("dueDate", "<=", convertDate(new Date(), "dbstring") as string), where("cohort", "==", id), where("status", "==", "upcoming")]));
270
+ if (reminderCount > 0) {
271
+ return {
272
+ dismissible: false,
273
+ severity: "primary",
274
+ title: `You have ${reminderCount} reminder${reminderCount > 1 ? "s" : ""}.`,
275
+ message: `Your cohort, ${cohort.name}, has ${reminderCount} outstanding placement reminder${reminderCount > 1 ? "s" : ""}. Click to view.`,
276
+ link: `/institutes/cohorts/${id}/placements`,
277
+ } as TaskQueryReturnObject;
278
+ }
279
+ return;
280
+ }));
281
+ return items.filter((v) => v);
282
+ },
283
+ },
284
+ invalidParentEmails: {
285
+ callback: async (user, _, cohorts) => {
286
+ if (!getAccess(user, "signOffPlacements") || user.viewStudents === "none") return;
287
+
288
+ if (!Array.isArray(cohorts)) {
289
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("flags", "array-contains", "parentEmailFailed")]));
290
+ if (placementCount > 0) {
291
+ return {
292
+ dismissible: false,
293
+ severity: "error",
294
+ title: `${placementCount} placements have invalid parent emails.`,
295
+ message: `Your cohort '${cohorts.name}' has placements with invalid parent emails. Click to view these placements.`,
296
+ link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming`,
297
+ };
298
+ }
299
+ return;
300
+ }
301
+
302
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
303
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("flags", "array-contains", "parentEmailFailed")]));
304
+ if (placementCount > 0) {
305
+ return {
306
+ dismissible: false,
307
+ severity: "error",
308
+ title: `${placementCount} placements in '${cohort.name}' have invalid parent emails.`,
309
+ message: `Your cohort '${cohort.name}' has placements with invalid parent emails. Click to view these placements.`,
310
+ link: `/institutes/cohorts/${id}/placements?id=upcoming`,
311
+ buttonTitle: "Review placements",
312
+ } as TaskQueryReturnObject;
313
+ }
314
+ return;
315
+ }));
316
+ return items.filter((v) => v);
317
+ },
318
+ },
319
+ invalidProviderEmails: {
320
+ callback: async (user, organisation, cohorts) => {
321
+ if (!getAccess(user, "signOffPlacements") || user.viewStudents === "none") return;
322
+
323
+ if (!Array.isArray(cohorts)) {
324
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("flags", "array-contains", "providerEmailFailed")]));
325
+ if (placementCount > 0) {
326
+ return {
327
+ dismissible: false,
328
+ severity: "error",
329
+ title: `${placementCount} placements have invalid provider emails.`,
330
+ message: `Your cohort '${cohorts.name}' has placements with invalid provider emails. Click to view these placements.`,
331
+ link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming`,
332
+ };
333
+ }
334
+ return;
335
+ }
336
+
337
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
338
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("flags", "array-contains", "providerEmailFailed")]));
339
+ if (placementCount > 0) {
340
+ return {
341
+ dismissible: false,
342
+ severity: "error",
343
+ title: `${placementCount} placements in '${cohort.name}' have invalid provider emails.`,
344
+ message: `Your cohort '${cohort.name}' has placements with invalid provider emails. Click to view these placements.`,
345
+ link: `/institutes/cohorts/${id}/placements?id=upcoming`,
346
+ buttonTitle: "Review placements",
347
+ } as TaskQueryReturnObject;
348
+ }
349
+ return;
350
+ }));
351
+ return items.filter((v) => v);
352
+ },
353
+ },
354
+ requiredStage: {
355
+ callback: async (user, _, cohorts) => {
356
+ if (!getAccess(user, "signOffPlacements") || user.viewStudents === "none") return;
357
+
358
+ if (!Array.isArray(cohorts)) {
359
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("reqUserType", "==", user.userType)]));
360
+ if (placementCount > 0) {
361
+ return {
362
+ dismissible: false,
363
+ severity: "primary",
364
+ title: `${placementCount} placements require your attention.`,
365
+ message: `Your cohort '${cohorts.name}' has placements for your to review. Click to view these placements.`,
366
+ link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming&reqUserType=${user.userType}`,
367
+ };
368
+ }
369
+ return;
370
+ }
371
+
372
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
373
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("reqUserType", "==", user.userType)]));
374
+ if (placementCount > 0) {
375
+ return {
376
+ dismissible: false,
377
+ severity: "primary",
378
+ title: `${placementCount} placements in '${cohort.name}' require your attention.`,
379
+ message: `Your cohort '${cohort.name}' has placements for your to review. Click to view these placements.`,
380
+ link: `/institutes/cohorts/${id}/placements?id=upcoming&reqUserType=${user.userType}`,
381
+ buttonTitle: "Review placements",
382
+ } as TaskQueryReturnObject;
383
+ }
384
+ return;
385
+ }));
386
+ return items.filter((v) => v);
387
+ },
388
+ },
389
+ verifyInsurance: {
390
+ callback: async (user, _, cohorts) => {
391
+ if (!getAccess(user, "verifyInsurance") || user.viewStudents === "none") return;
392
+
393
+ if (!Array.isArray(cohorts)) {
394
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("insurance", "==", "awaitingReview")]));
395
+ if (placementCount > 0) {
396
+ return {
397
+ dismissible: false,
398
+ severity: "primary",
399
+ title: `${placementCount} placements have insurance that require review.`,
400
+ message: `Your cohort '${cohorts.name}' has employer's liability insurance documents that require review`,
401
+ link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming&insurance=awaitingReview`,
402
+ };
403
+ }
404
+ return;
405
+ }
406
+
407
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
408
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("insurance", "==", "awaitingReview")]));
409
+ if (placementCount > 0) {
410
+ return {
411
+ dismissible: false,
412
+ severity: "primary",
413
+ title: `${placementCount} placements in '${cohort.name}' have insurance that require review.`,
414
+ message: `Your cohort '${cohort.name}' has employer's liability insurance documents that require review`,
415
+ link: `/institutes/cohorts/${cohort.id}/placements?id=upcoming&insurance=awaitingReview`,
416
+ buttonTitle: "Review placements",
417
+ } as TaskQueryReturnObject;
418
+ }
419
+ return;
420
+ }));
421
+ return items.filter((v) => v);
422
+ },
423
+ },
424
+ verifyDbsCheck: {
425
+ callback: async (user, _, cohorts) => {
426
+ if (!getAccess(user, "verifyDbsChecks") || user.viewStudents === "none") return;
427
+
428
+ if (!Array.isArray(cohorts)) {
429
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("dbsCheck", "==", "awaitingReview")]));
430
+ if (placementCount > 0) {
431
+ return {
432
+ dismissible: false,
433
+ severity: "primary",
434
+ title: `${placementCount} placements have DBS checks that require review.`,
435
+ message: `Your cohort '${cohorts.name}' has DBS checks that require review`,
436
+ link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming&dbsCheck=awaitingReview`,
437
+ };
438
+ }
439
+ return;
440
+ }
441
+
442
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
443
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("dbsCheck", "==", "awaitingReview")]));
444
+ if (placementCount > 0) {
445
+ return {
446
+ dismissible: false,
447
+ severity: "primary",
448
+ title: `${placementCount} placements in '${cohort.name}' have DBS checks that require review.`,
449
+ message: `Your cohort '${cohort.name}' has DBS checks that require review`,
450
+ link: `/institutes/cohorts/${cohort.id}/placements?id=upcoming&dbsCheck=awaitingReview`,
451
+ buttonTitle: "Review placements",
452
+ } as TaskQueryReturnObject;
453
+ }
454
+ return;
455
+ }));
456
+ return items.filter((v) => v);
457
+ },
458
+ },
459
+ verifyRiskAssessment: {
460
+ callback: async (user, _, cohorts) => {
461
+ if (!getAccess(user, "verifyRiskAssessments") || user.viewStudents === "none") return;
462
+
463
+ if (!Array.isArray(cohorts)) {
464
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("riskAssessment", "==", "awaitingReview")]));
465
+ if (placementCount > 0) {
466
+ return {
467
+ dismissible: false,
468
+ severity: "primary",
469
+ title: `${placementCount} placements have risk assessments that require review.`,
470
+ message: `Your cohort '${cohorts.name}' has risk assessents that require review`,
471
+ link: `/institutes/cohorts/${cohorts.id}/placements?id=upcoming&riskAssessment=awaitingReview`,
472
+ };
473
+ }
474
+ return;
475
+ }
476
+
477
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
478
+ const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", id), where("riskAssessment", "==", "awaitingReview")]));
479
+ if (placementCount > 0) {
480
+ return {
481
+ dismissible: false,
482
+ severity: "primary",
483
+ title: `${placementCount} placements in '${cohort.name}' have risk assessments that require review.`,
484
+ message: `Your cohort '${cohort.name}' has risk assessments that require review`,
485
+ link: `/institutes/cohorts/${cohort.id}/placements?id=upcoming&riskAssessment=awaitingReview`,
486
+ buttonTitle: "Review placements",
487
+ } as TaskQueryReturnObject;
488
+ }
489
+ return;
490
+ }));
491
+ return items.filter((v) => v);
492
+ },
493
+ },
494
+ missingParentEmail: {
495
+ callback: async (user, _, cohorts) => {
496
+ if (!getAccess(user, "editStudents") || user.viewStudents === "none") return;
497
+ if (!Array.isArray(cohorts)) {
498
+ const requiresParents = Boolean(cohorts.workflow.find((node) => node.userType === "Parent"))
499
+ if (!requiresParents) return;
500
+ const constraints = [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("details.parentEmail", "==", "null")];
501
+
502
+ if (user.groupData?.viewStudents === "filter") {
503
+ constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || ""));
504
+ }
505
+ const studentCount = (await firebaseQuery.getCount(["users"], constraints));
506
+ if (studentCount > 0) {
507
+ return {
508
+ dismissible: false,
509
+ severity: "info",
510
+ title: `${studentCount} students do not have a parent email.`,
511
+ message: `Your cohort '${cohorts.name}' has students without a parent email. To allow proper processing of their placements, add these emails.`,
512
+ link: `/institutes/cohorts/${cohorts.id}/students?parentEmail=false`,
513
+ };
514
+ }
515
+ return;
516
+ }
517
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
518
+ const requiresParents = Boolean(cohort.workflow.find((node) => node.userType === "Parent"))
519
+ if (!requiresParents) return;
520
+ const constraints = [where("oId", "==", user.oId), where("cohort", "==", id), where("details.parentEmail", "==", "null")];
521
+
522
+ if (user.groupData?.viewStudents === "filter") {
523
+ constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || ""));
524
+ }
525
+ const studentCount = (await firebaseQuery.getCount(["users"], constraints));
526
+ if (studentCount > 0) {
527
+ return {
528
+ dismissible: false,
529
+ severity: "info",
530
+ title: `${studentCount} students in '${cohort.name}' do not have a parent email.`,
531
+ message: `Your cohort '${cohort.name}' has students without a parent email. To allow proper processing of their placements, add these emails.`,
532
+ link: `/institutes/cohorts/${cohort.id}/students?parentEmail=false`,
533
+ buttonTitle: "View students",
534
+ } as TaskQueryReturnObject;
535
+ }
536
+ return;
537
+ }));
538
+ return items.filter((v) => v);
539
+ },
540
+ },
541
+ inactiveStudents: {
542
+ callback: async (user, _, cohorts) => {
543
+ if (!getAccess(user, "activateStudents") || user.viewStudents === "none") return;
544
+ if (!Array.isArray(cohorts)) {
545
+ const constraints = [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("status", "==", "inactive")];
546
+
547
+ if (user.groupData?.viewStudents === "filter") {
548
+ constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || ""));
549
+ }
550
+ const studentCount = (await firebaseQuery.getCount(["users"], constraints));
551
+ if (studentCount > 0) {
552
+ return {
553
+ dismissible: false,
554
+ severity: "info",
555
+ title: `${studentCount} students in are inactive.`,
556
+ message: `Your cohort '${cohorts.name}' has inactive students. Activate them to enable them to use the platform.`,
557
+ link: `/institutes/cohorts/${cohorts.id}/students?status=inactive`,
558
+ };
559
+ }
560
+ return;
561
+ }
562
+
563
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
564
+ const constraints = [where("oId", "==", user.oId), where("cohort", "==", id), where("status", "==", "inactive")];
565
+
566
+ if (user.groupData?.viewStudents === "filter") {
567
+ constraints.push(where(`details.${user.groupData?.filterUsersBy || ""}`, "==", user.groupData?.filterUsersValue || ""));
568
+ }
569
+ const studentCount = (await firebaseQuery.getCount(["users"], constraints));
570
+ if (studentCount > 0) {
571
+ return {
572
+ dismissible: false,
573
+ severity: "info",
574
+ title: `${studentCount} students in '${cohort.name}' are inactive.`,
575
+ message: `Your cohort '${cohort.name}' has inactive students. Activate them to enable them to use the platform.`,
576
+ link: `/institutes/cohorts/${cohort.id}/students?status=inactive`,
577
+ buttonTitle: "Review students",
578
+ } as TaskQueryReturnObject;
579
+ }
580
+ return;
581
+ }));
582
+ return items.filter((v) => v);
583
+ },
584
+ },
585
+ uploadStudents: {
586
+ callback: async (user, _, cohorts) => {
587
+ if (!Array.isArray(cohorts)) return;
588
+ if (!getAccess(user, "addStudents") || user.viewStudents === "none") return;
589
+ if (user.product !== "institutes") return;
590
+ const returnObj: TaskQueryReturnObject = {
591
+ link: "",
592
+ dismissible: true,
593
+ severity: "warning",
594
+ };
595
+ const items = await Promise.all(cohorts.map(async ([id, cohort]) => {
596
+ const studentCount = (await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Students"), where("cohort", "==", id)]));
597
+ if (studentCount === 0) {
598
+ return {
599
+ ...returnObj,
600
+ title: "Upload students",
601
+ message: `Your cohort '${cohort.name}' has no students. Add them in the cohort 'Students' tab`,
602
+ link: `/institutes/cohorts/${id}/students`,
603
+ buttonTitle: "Upload students",
604
+ };
605
+ }
606
+ return;
607
+ }));
608
+ return items.filter((v) => v);
609
+ },
610
+ },
611
+ inactiveStaff: {
612
+ callback: async (user, _, cohorts) => {
613
+ if (!getAccess(user, "addStaff")) return;
614
+
615
+ if (!Array.isArray(cohorts) || user.product === "students") return;
616
+ const returnObj: TaskQueryReturnObject = {
617
+ link: `/${user.product}/cohorts/staff/all?status=inactive`,
618
+ dismissible: false,
619
+ severity: "info",
620
+ };
621
+ const inactiveStaff = await firebaseQuery.getCount(["users"], [where("oId", "==", user.oId), where("userType", "==", "Staff"), where("status", "==", "inactive")]);
622
+ if (inactiveStaff > 0) {
623
+ return {
624
+ ...returnObj,
625
+ title: "Inactive staff",
626
+ message: `You have ${inactiveStaff} inactive staff members. You can activate them in the 'Staff' cohorts tab.`,
627
+ };
628
+ }
629
+ return;
630
+ },
631
+ },
632
+ overdueStage: {
633
+ callback: async (user, _, cohort) => {
634
+ console.log(user, cohort);
635
+ return undefined;
636
+ },
637
+ },
638
+ approveExternalPlacement: {
639
+ callback: async (user, _, cohort) => {
640
+ if (!getAccess(user, "verifyListings")) return;
641
+ if (!Array.isArray(cohort)) return;
642
+ const externalCount = await firebaseQuery.getCount(["placementListings"],
643
+ [where(`savedBy.${user.oId}.exists`, "==", true), where(`savedBy.${user.oId}.status`, "==", "uploaded"), where("mapConsent", "==", "institute")]);
644
+ console.log("EXT", externalCount);
645
+ if (externalCount > 0) {
646
+ return {
647
+ dismissible: false,
648
+ severity: "warning",
649
+ title: `${externalCount} external placements require approval`,
650
+ message: "Review and approve these placements to add them to your institute placement map",
651
+ link: "/institutes/placements",
652
+ buttonTitle: "View placements",
653
+ };
654
+ }
655
+ return;
656
+ },
657
+ },
658
+ };
659
+
660
+ const studentTasks:StudentTaskObject = {
661
+ completeOnboarding: {
662
+ callback: async (user) => {
663
+ 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};
664
+ if (Object.keys(placementsWithoutOnboarding).length === 0) return;
665
+ const items = Object.entries(placementsWithoutOnboarding).map(([k, placement]) => ({
666
+ dismissible: false,
667
+ severity: "primary",
668
+ title: `Complete onboarding for ${placement.name}`,
669
+ message: `Review onboarding for your placement starting on ${convertDate(placement.startDate, "visual")}`,
670
+ link: `/${user.product}/placements/${k}`,
671
+ buttonTitle: "View onboarding",
672
+ } as TaskQueryReturnObject))
673
+ return items;
674
+ },
675
+ }
676
+ }
677
+
678
+ const providerTasks:ProviderTaskObject = {
679
+ requestedVisibleAddresses: {
680
+ callback: async (user) => {
681
+ if (!getAccess(user, "addStaff")) return;
682
+ 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);
683
+ if (accessRequests === 0) return;
684
+ if (accessRequests === 1) {
685
+ const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", "providers"), where("oId", "==", user.oId), orderBy("requestedVisibleAddresses")]) || {})[0] as [string, UserData];
686
+ return {
687
+ dismissible: false,
688
+ severity: "primary",
689
+ title: `${userRequestingAccess[1].details.forename} ${userRequestingAccess[1].details.surname} has requested access to view addresses.`,
690
+ message: `Click to review the addresses and grant access to the user.`,
691
+ link: `/${user.product}/users/${userRequestingAccess[0]}`,
692
+ buttonTitle: "View request",
693
+ } as TaskQueryReturnObject;
694
+ }
695
+
696
+ return {
697
+ dismissible: false,
698
+ severity: "warning",
699
+ title: `Multiple users have requested access to view addresses.`,
700
+ message: `Click to review the addresses and grant access to the user.`,
701
+ link: `/${user.product}/organisation/staff/all`,
702
+ buttonTitle: "View request",
703
+ } as TaskQueryReturnObject;
704
+ },
705
+ },
706
+ requestedVisiblePlacementListings: {
707
+ callback: async (user) => {
708
+ if (!getAccess(user, "addStaff")) return;
709
+ 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);;
710
+ if (accessRequests === 0) return;
711
+ if (accessRequests === 1) {
712
+ const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleListings")]) || {})[0] as [string, UserData];
713
+ return {
714
+ dismissible: false,
715
+ severity: "primary",
716
+ title: `${userRequestingAccess[1].details.forename} ${userRequestingAccess[1].details.surname} has requested access to view placement listings.`,
717
+ message: `Click to review the placement listings and grant access to the user.`,
718
+ link: `/${user.product}/users/${userRequestingAccess[0]}`,
719
+ buttonTitle: "View request",
720
+ } as TaskQueryReturnObject;
721
+ }
722
+
723
+ return {
724
+ dismissible: false,
725
+ severity: "warning",
726
+ title: `Multiple users have requested access to view placement listings.`,
727
+ message: `Click to review the placement listings and grant access to the user.`,
728
+ link: `/${user.product}/organisation/staff/all`,
729
+ buttonTitle: "View request",
730
+ } as TaskQueryReturnObject;
731
+ },
732
+ },
733
+ applicationRequireReview: {
734
+ callback: async (user) => {
735
+ const constraints = [where("providerId", "==", user.oId), where("reqUserType", "==", "Staff"), where("status", "==", "submitted")];
736
+
737
+ if (user.userGroup !== "admin") {
738
+
739
+ if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
740
+ if (!user.viewAddresses || user.viewAddresses === "none") return;
741
+
742
+ if (user.viewPlacementListings === "request") {
743
+ if (!user.visibleListings || user.visibleListings?.length === 0) return;
744
+ constraints.push(where("listingId", 'in', user.visibleListings));
745
+ } else {
746
+ // viewPlacementListings must be 'all'
747
+
748
+ if (user.viewAddresses === "request") {
749
+ if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
750
+ constraints.push(where("addressId", 'in', user.visibleAddresses));
751
+ }
752
+ }
753
+ }
754
+ const applicationCount = await firebaseQuery.getCount("applications", constraints);
755
+ if (applicationCount === 0) return;
756
+
757
+ return {
758
+ dismissible: false,
759
+ severity: "primary",
760
+ title: `${applicationCount} applications require your review.`,
761
+ message: `Click to view ${applicationCount} that require your attention.`,
762
+ link: `/${user.product}/placementListings/applicants`,
763
+ buttonTitle: "View applications",
764
+ } as TaskQueryReturnObject;
765
+ },
766
+ },
767
+ completeStudentDocs: {
768
+ callback: async (user) => {
769
+ return;
770
+ return {} as TaskQueryReturnObject;
771
+ },
772
+ },
773
+ reviewOnboarding: {
774
+ callback: async (user) => {
775
+ const constraints = [where("providerId", "==", user.oId), where("onboarding.completed.accepted", "==", false), where("onboarding.completed.submitted", "==", true), where("endDate", ">=", dateToString(new Date()))]
776
+
777
+ if (user.userGroup !== "admin") {
778
+
779
+ if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
780
+ if (!user.viewAddresses || user.viewAddresses === "none") return;
781
+
782
+ if (user.viewPlacementListings === "request") {
783
+ if (!user.visibleListings || user.visibleListings?.length === 0) return;
784
+ constraints.push(where("placementId", 'in', user.visibleListings));
785
+ } else {
786
+ // viewPlacementListings must be 'all'
787
+
788
+ if (user.viewAddresses === "request") {
789
+ if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
790
+ constraints.push(where("addressId", 'in', user.visibleAddresses));
791
+ }
792
+ }
793
+ }
794
+ const toReview = await firebaseQuery.getCount("placements", constraints);
795
+
796
+ if (toReview === 0) return;
797
+ if (toReview === 1) {
798
+ const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData];
799
+ const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData;
800
+ return {
801
+ dismissible: false,
802
+ severity: "primary",
803
+ 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")}`,
804
+ message: `Click to view the placement and review the onboarding.`,
805
+ link: `/${user.product}/placements/${placement[0]}`,
806
+ buttonTitle: "View",
807
+ } as TaskQueryReturnObject;
808
+ }
809
+
810
+ return {
811
+ dismissible: false,
812
+ severity: "primary",
813
+ title: `${toReview} student have completed their onboarding.`,
814
+ message: `Click to view your placements and approve completed onboarding`,
815
+ link: `/${user.product}/placementListings/placements`,
816
+ buttonTitle: "View placements",
817
+ } as TaskQueryReturnObject;
818
+ },
819
+ },
820
+ uploadOnboarding: {
821
+ callback: async (user) => {
822
+
823
+ const constraints = [where("providerId", "==", user.oId), where("onboarding", "==", null), where("endDate", ">=", dateToString(new Date()))];
824
+
825
+
826
+ if (user.userGroup !== "admin") {
827
+
828
+ if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
829
+ if (!user.viewAddresses || user.viewAddresses === "none") return;
830
+
831
+ if (user.viewPlacementListings === "request") {
832
+ if (!user.visibleListings || user.visibleListings?.length === 0) return;
833
+ constraints.push(where("placementId", 'in', user.visibleListings));
834
+ } else {
835
+ // viewPlacementListings must be 'all'
836
+
837
+ if (user.viewAddresses === "request") {
838
+ if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
839
+ constraints.push(where("addressId", 'in', user.visibleAddresses));
840
+ }
841
+ }
842
+ }
843
+
844
+ const withoutOnboarding = await firebaseQuery.getCount("placements", constraints);
845
+
846
+ if (withoutOnboarding === 0) return;
847
+ if (withoutOnboarding === 1) {
848
+ const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData];
849
+ const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData;
850
+ return {
851
+ dismissible: false,
852
+ severity: "primary",
853
+ 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")}`,
854
+ message: `Click to view the placement and add or dismiss onboarding reminders.`,
855
+ link: `/${user.product}/placements/${placement[0]}`,
856
+ buttonTitle: "View",
857
+ } as TaskQueryReturnObject;
858
+ }
859
+
860
+ return {
861
+ dismissible: false,
862
+ severity: "primary",
863
+ title: `Set up onboarding for ${withoutOnboarding} placements to prepare yourself and your students.`,
864
+ message: `Click to view your placements and add or dismiss onboarding reminders.`,
865
+ link: `/${user.product}/placementListings/placements`,
866
+ buttonTitle: "View placements",
867
+ } as TaskQueryReturnObject;
868
+ },
869
+ },
870
+ completeListing: {
871
+ callback: async (user) => {
872
+ const constraints = [where("providerId", "==", user.oId), where("status", "==", "draft")];
873
+
874
+
875
+ if (user.userGroup !== "admin") {
876
+
877
+ if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
878
+ if (!user.viewAddresses || user.viewAddresses === "none") return;
879
+
880
+ if (user.viewPlacementListings === "request") {
881
+ if (!user.visibleListings || user.visibleListings?.length === 0) return;
882
+ constraints.push(where(documentId(), 'in', user.visibleListings));
883
+ } else {
884
+ // viewPlacementListings must be 'all'
885
+
886
+ if (user.viewAddresses === "request") {
887
+ if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
888
+ constraints.push(where("addressId", 'in', user.visibleAddresses));
889
+ }
890
+ }
891
+ }
892
+ const incompleteListings = await firebaseQuery.getCount("placementListings", constraints);
893
+ if (incompleteListings === 0) return;
894
+ if (incompleteListings === 1) {
895
+ const incompleteListing = Object.entries(await firebaseQuery.getDocsWhere("placementListings", constraints) || {})[0] as [string, PlacementListing];
896
+ const address = incompleteListing[1].addressId ? await firebaseQuery.getDocData(["addresses", incompleteListing[1].addressId]) as OrganisationAddress : undefined;
897
+ return {
898
+ dismissible: false,
899
+ severity: "info",
900
+ 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.`,
901
+ message: `Click to complete and publish the placement listing.`,
902
+ link: `/${user.product}/addListing/${incompleteListing[0]}`,
903
+ buttonTitle: "View listing",
904
+ } as TaskQueryReturnObject;
905
+ }
906
+
907
+ return {
908
+ dismissible: false,
909
+ severity: "info",
910
+ title: `You have ${incompleteListings} draft listings waiting to be published.`,
911
+ message: `Click to review and publish the placement listings.`,
912
+ link: `/${user.product}/placementListings/listings`,
913
+ buttonTitle: "View listings",
914
+ } as TaskQueryReturnObject;
915
+ },
916
+ },
917
+ completeAddress: {
918
+ callback: async (user) => {
919
+ const constraints = [where("product", "==", "providers"), where("oId", "==", user.oId), where("stage", "!=", "complete")];
920
+
921
+
922
+ if (user.userGroup !== "admin") {
923
+ if (!user.viewAddresses || user.viewAddresses === "none") return;
924
+
925
+ if (user.viewAddresses === "request") {
926
+ if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
927
+ constraints.push(where("addressId", 'in', user.visibleAddresses));
928
+ }
929
+ }
930
+ const incompleteAddresses = await firebaseQuery.getCount("addresses", constraints);
931
+ if (incompleteAddresses === 0) return;
932
+ if (incompleteAddresses === 1) {
933
+ const address = Object.entries(await firebaseQuery.getDocsWhere("addresses", constraints) || {})[0] as [string, OrganisationAddress];
934
+ return {
935
+ dismissible: false,
936
+ severity: "info",
937
+ title: `Your address: ${address[1]["address-line1"]}, ${address[1].postal_code.toUpperCase()}, ${capitaliseWords(camelCaseToNormal(address[1].country))} is currently incomplete.`,
938
+ message: `Click to complete the addresses.`,
939
+ link: `/${user.product}/addAddress/${address[0]}`,
940
+ buttonTitle: "View address",
941
+ } as TaskQueryReturnObject;
942
+ }
943
+
944
+ return {
945
+ dismissible: false,
946
+ severity: "info",
947
+ title: `You have ${incompleteAddresses} draft addresses waiting to be published.`,
948
+ message: `Click to review the addresses.`,
949
+ link: `/${user.product}/organisation/addresses`,
950
+ buttonTitle: "View addresses",
951
+ } as TaskQueryReturnObject;
952
+ },
953
+ },
954
+ registrationRequests: {
955
+ callback: async (user) => {
956
+ if (!getAccess(user, "addStaff")) return;
957
+
958
+ const regRequests = await firebaseQuery.getCount("requests", [where("product", "==", user.product), where("oId", "==", user.oId)]);
959
+
960
+ if (regRequests === 0) return;
961
+ if (regRequests === 1) {
962
+ const request = Object.entries(await firebaseQuery.getDocsWhere("requests", [where("product", "==", user.product), where("oId", "==", user.oId)]) || {})[0] as [string, RegistrationRequest];
963
+ return {
964
+ dismissible: false,
965
+ severity: "primary",
966
+ title: `${request[1].forename} ${request[1].surname} has requested to access your organisation.`,
967
+ message: `Click to review these request.`,
968
+ link: `/${user.product}/organisation/staff/requests`,
969
+ buttonTitle: "View requests",
970
+ } as TaskQueryReturnObject;
971
+ }
972
+
973
+ return {
974
+ dismissible: false,
975
+ severity: "primary",
976
+ title: `${regRequests} people have requested to register with your organisation.`,
977
+ message: `Click to review these requests.`,
978
+ link: `/${user.product}/organisation/staff/requests`,
979
+ buttonTitle: "View requests",
980
+ } as TaskQueryReturnObject;
981
+ },
982
+ },
983
+ activateStaff: {
984
+ callback: async (user) => {
985
+ if (!getAccess(user, "addStaff")) return;
986
+
987
+ const inactiveAccounts = await firebaseQuery.getCount("users", [where("product", "==", user.product), where("oId", "==", user.oId), where("status", "==", "inactive")]);
988
+
989
+ if (inactiveAccounts === 0) return;
990
+ if (inactiveAccounts === 1) {
991
+ const account = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", user.product), where("oId", "==", user.oId), where("status", "==", "inactive")]) || {})[0] as [string, UserData];
992
+ return {
993
+ dismissible: false,
994
+ severity: "info",
995
+ title: `Activate ${account[1].details.forename} ${account[1].details.surname}'s staff account.`,
996
+ message: "Activate this account to give the user access to Placementt.",
997
+ link: `/${user.product}/organisation/staff/all`,
998
+ buttonTitle: "View accounts",
999
+ } as TaskQueryReturnObject;
1000
+ }
1001
+
1002
+ return {
1003
+ dismissible: false,
1004
+ severity: "info",
1005
+ title: `${inactiveAccounts} staff have inactive active accounts.`,
1006
+ message: "Activate these accounts to give the user access to Placementt.",
1007
+ link: `/${user.product}/organisation/staff/all`,
1008
+ buttonTitle: "View accounts",
1009
+ } as TaskQueryReturnObject;
1010
+ },
1011
+ },
1012
+ placementStarting: {
1013
+ callback: async (user) => {
1014
+ const sevenDaysInFuture = new Date();
1015
+ sevenDaysInFuture.setDate(sevenDaysInFuture.getDate() + 7)
1016
+
1017
+ const constraints = [where("providerId", "==", user.oId), where("startDate", "<=", convertDate(sevenDaysInFuture, "dbstring")), where("startDate", ">", dateToString(new Date()))];
1018
+
1019
+ if (user.userGroup !== "admin") {
1020
+
1021
+ if (!user.viewPlacementListings || user.viewPlacementListings === "none") return;
1022
+ if (!user.viewAddresses || user.viewAddresses === "none") return;
1023
+
1024
+ if (user.viewPlacementListings === "request") {
1025
+ if (!user.visibleListings || user.visibleListings?.length === 0) return;
1026
+ constraints.push(where(documentId(), 'in', user.visibleListings));
1027
+ } else {
1028
+ // viewPlacementListings must be 'all'
1029
+
1030
+ if (user.viewAddresses === "request") {
1031
+ if (!user.visibleAddresses || user.visibleAddresses?.length === 0) return;
1032
+ constraints.push(where("addressId", 'in', user.visibleAddresses));
1033
+ }
1034
+ }
1035
+ }
1036
+ const placementsStartingSoon = await firebaseQuery.getCount("placements", constraints);
1037
+
1038
+ if (placementsStartingSoon === 0) return;
1039
+ if (placementsStartingSoon === 1) {
1040
+ const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", constraints) || {})[0] as [string, StudentPlacementData];
1041
+ const student = placement[1].uid ? await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData : {
1042
+ details: {
1043
+ forename: placement[1].studentForename,
1044
+ surname: placement[1].studentSurname,
1045
+ }
1046
+ };
1047
+ return {
1048
+ dismissible: false,
1049
+ severity: "success",
1050
+ 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.`,
1051
+ message: `Click to view the placement and acquaint yourself with the student.`,
1052
+ link: `/${user.product}/placements/${placement[0]}`,
1053
+ buttonTitle: "View",
1054
+ } as TaskQueryReturnObject;
1055
+ }
1056
+
1057
+ return {
1058
+ dismissible: false,
1059
+ severity: "success",
1060
+ title: `${placementsStartingSoon} placements starting soon.`,
1061
+ message: `Click to view scheduled placements.`,
1062
+ link: `/${user.product}/placementListings/placements`,
1063
+ buttonTitle: "View placements",
1064
+ } as TaskQueryReturnObject;
1065
+ },
1066
+ },
1067
+ completeFeedback: {
1068
+ callback: async (user) => {
1069
+ return;
1070
+ return {} as TaskQueryReturnObject;
1071
+ },
1072
+ },
1073
+ setUpFeedback: {
1074
+ callback: async (user) => {
1075
+ return;
1076
+ return {} as TaskQueryReturnObject;
1077
+ },
1078
+ },
1079
+ }
1080
+
1081
+ export const getTips = async (user: UserData, organisation: InstituteData|ProviderData, addresses?: {[key: string]: OrganisationAddress}):Promise<(TaskQueryReturnObject)[]> => {
1082
+ const tipsObject = {
1083
+ providers: providerTips,
1084
+ institutes: instituteTips,
1085
+ studentTips: studentTips,
1086
+ }
1087
+ const includedItems = Object.entries(tipsObject[user.product]).filter(([k]) => !user.dismissedTips?.includes(k));
1088
+
1089
+ const processedTips = await includedItems.reduce(async (acc, [itemName, item]) => {
1090
+ const callbackParams:[UserData, InstituteData|ProviderData] = [user, organisation];
1091
+
1092
+ if ((itemName === "addAddresses" || itemName === "addSchools") && addresses) {
1093
+ callbackParams.push(addresses as any);
1094
+ }
1095
+ const queryResult = await (item as {
1096
+ callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
1097
+ }).callback(...callbackParams);
1098
+ if (!queryResult) return await acc;
1099
+
1100
+ const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
1101
+ const results = queryResultArray.map((r) => ({itemName: itemName as InstituteTipNames|ProviderTipNames|StudentTipNames|InstituteTaskNames|ProviderTaskNames|StudentTaskNames, ...r}));
1102
+
1103
+ (await acc).push(...results);
1104
+ return await acc;
1105
+ }, Promise.resolve<TaskQueryReturnObject[]>([]));
1106
+ return processedTips;
1107
+ }
1108
+
1109
+ export const getTasks = async (user: UserData, organisation?: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => {
1110
+ // Cohort is either a specific one or all.
1111
+
1112
+ if (user.product === "institutes" && user.userType === "Staff" && organisation) {
1113
+ return await getInstituteTasks(user, organisation, cohort);
1114
+ }
1115
+ if (user.product === "students" || user.userType === "Students") {
1116
+ return await getStudentTasks(user, organisation, cohort);
1117
+ }
1118
+ if (user.product === "providers" && organisation) {
1119
+ return await getProviderTasks(user, organisation);
1120
+ }
1121
+
1122
+ return [];
1123
+ };
1124
+
1125
+ const getStudentTasks = async (user: UserData, organisation?: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => {
1126
+ const processedTasks = await Object.entries(studentTasks).reduce(async (acc, [itemName, item]) => {
1127
+
1128
+ const queryResult = await item.callback(user);
1129
+ if (!queryResult) return await acc;
1130
+
1131
+ const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
1132
+ const results = queryResultArray.map((r) => ({itemName: itemName as StudentTaskNames, ...r}));
1133
+
1134
+ (await acc).push(...results);
1135
+ return await acc;
1136
+ }, Promise.resolve<TaskQueryReturnObject[]>([]));
1137
+ return processedTasks;
1138
+ };
1139
+
1140
+ const getProviderTasks = async (user: UserData, organisation: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => {
1141
+ const processedTasks = await Object.entries(providerTasks).reduce(async (acc, [itemName, item]) => {
1142
+
1143
+ const queryResult = await item.callback(user);
1144
+ if (!queryResult) return await acc;
1145
+
1146
+ const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
1147
+ const results = queryResultArray.map((r) => ({itemName: itemName as ProviderTaskNames, ...r}));
1148
+
1149
+ (await acc).push(...results);
1150
+ return await acc;
1151
+ }, Promise.resolve<TaskQueryReturnObject[]>([]));
1152
+ return processedTasks;};
1153
+
1154
+ const getInstituteTasks = async (user: UserData, organisation: InstituteData|ProviderData, cohort?: CohortData):Promise<(TaskQueryReturnObject)[]> => {
1155
+ let fCohort:CohortData|[string, CohortData][]|undefined = cohort;
1156
+
1157
+ if (!fCohort) {
1158
+ // get all associated cohorts.
1159
+ if (user.viewCohorts === "none") return([]);
1160
+ if (user.userGroup === "admin" || user.viewCohorts === "all") {
1161
+ const cohorts = await firebaseQuery.getDocsWhere("cohorts", [where("product", "==", user.product), where("oId", "==", user.oId), where("stage", "==", "created")]) as {[key:string]: CohortData};
1162
+ fCohort = Object.entries(cohorts);
1163
+ }
1164
+ if (user.viewCohorts === "some") {
1165
+ const cohorts = await user.visibleCohorts?.reduce(async (acc, cohortId) => {
1166
+ const cohort = await firebaseQuery.getDocData(["cohorts", cohortId]) as CohortData;
1167
+ if (cohort.stage !== "created") {
1168
+ return acc;
1169
+ }
1170
+ acc[cohortId] = cohort;
1171
+ return acc;
1172
+ }, Promise.resolve<[string, CohortData][]>([]));
1173
+ fCohort = cohorts;
1174
+ }
1175
+ }
1176
+
1177
+ const processedTasks = await Object.entries(instituteTasks).reduce(async (acc, [itemName, item]) => {
1178
+ if (!fCohort) {
1179
+ console.log("No cohorts to retrieve tasks for");
1180
+ return([]);
1181
+ }
1182
+ const queryResult = await item.callback(user, organisation, fCohort);
1183
+ if (!queryResult) return await acc;
1184
+
1185
+ const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
1186
+ const results = queryResultArray.map((r) => ({itemName: itemName as InstituteTaskNames, ...r}));
1187
+
1188
+ (await acc).push(...results);
1189
+ return await acc;
1190
+ }, Promise.resolve<TaskQueryReturnObject[]>([]));
1191
+ return processedTasks;
1192
+ }
1193
+
1194
+ /*
1195
+ export const getTasks = async (user: UserData):Promise<{[key:string]: TaskQueryReturnObject}> => {
1196
+ return Object.fromEntries((await Promise.all(Object.entries(tasks).filter(([k]) => !user.dismissedTasks?.includes(k)).map(async ([taskName, task]) => {
1197
+ const queryResult = await task.callback(user);
1198
+ return [taskName, queryResult ? {...queryResult, ...task} : undefined];
1199
+ }))).filter(([, v]) => v));
1200
+ };
1201
+
1202
+ export const dismissTask = (user: UserData, taskName:TaskNames) => {
1203
+ firebaseQuery.update(["users", user.id], {dismissedTasks: arrayUnion(taskName)});
1204
+ };
1205
+ */
1206
+ export const dismissTip = async (user: UserData, itemName:InstituteTaskNames | ProviderTaskNames | StudentTaskNames | InstituteTipNames | ProviderTipNames | StudentTipNames | undefined) => {
1207
+ if (!itemName) return;
1208
+ return await firebaseQuery.update(["users", user.id], {dismissedTips: arrayUnion(itemName)});
1209
+ };