placementt-core 11.0.533 → 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 (76) hide show
  1. package/lib/constants.d.ts +13 -1
  2. package/lib/constants.js +86 -1
  3. package/lib/constants.js.map +1 -1
  4. package/lib/features/analytics/useAnalytics.d.ts +2 -0
  5. package/lib/features/analytics/useAnalytics.js +22 -19
  6. package/lib/features/analytics/useAnalytics.js.map +1 -1
  7. package/lib/features/global/downtime/useDowntime.d.ts +1 -0
  8. package/lib/features/global/downtime/useDowntime.js +9 -7
  9. package/lib/features/global/downtime/useDowntime.js.map +1 -1
  10. package/lib/features/global/users/useUserFunctions.js +1 -1
  11. package/lib/features/global/users/useUserFunctions.js.map +1 -1
  12. package/lib/features/jobs/jobsSlice.d.ts +10 -2
  13. package/lib/features/jobs/jobsSlice.js +5 -2
  14. package/lib/features/jobs/jobsSlice.js.map +1 -1
  15. package/lib/features/placements/studentPlacements/activePlacement.d.ts +5 -1
  16. package/lib/features/placements/studentPlacements/activePlacement.js +7 -3
  17. package/lib/features/placements/studentPlacements/activePlacement.js.map +1 -1
  18. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.d.ts +3 -2
  19. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.js +4 -1
  20. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.js.map +1 -1
  21. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.d.ts +2 -2
  22. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.js +1 -1
  23. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.js.map +1 -1
  24. package/lib/features/placements/studentPlacements/useStudentPlacements.d.ts +2 -12
  25. package/lib/features/placements/studentPlacements/useStudentPlacements.js +1 -26
  26. package/lib/features/placements/studentPlacements/useStudentPlacements.js.map +1 -1
  27. package/lib/features/updates/useUpdates.d.ts +1 -0
  28. package/lib/features/updates/useUpdates.js +13 -12
  29. package/lib/features/updates/useUpdates.js.map +1 -1
  30. package/lib/firebase/firebase.d.ts +5 -3
  31. package/lib/firebase/firebase.js +15 -9
  32. package/lib/firebase/firebase.js.map +1 -1
  33. package/lib/firebase/firebaseConfig.js.map +1 -1
  34. package/lib/firebase/firebaseQuery.d.ts +6 -2
  35. package/lib/firebase/firebaseQuery.js +11 -3
  36. package/lib/firebase/firebaseQuery.js.map +1 -1
  37. package/lib/firebase/readDatabase.d.ts +2 -4
  38. package/lib/firebase/readDatabase.js +30 -5
  39. package/lib/firebase/readDatabase.js.map +1 -1
  40. package/lib/firebase/writeDatabase.d.ts +6 -2
  41. package/lib/firebase/writeDatabase.js +2 -1
  42. package/lib/firebase/writeDatabase.js.map +1 -1
  43. package/lib/hooks.d.ts +226 -16
  44. package/lib/hooks.js +938 -146
  45. package/lib/hooks.js.map +1 -1
  46. package/lib/reduxHooks.d.ts +56 -4
  47. package/lib/reduxHooks.js +63 -19
  48. package/lib/reduxHooks.js.map +1 -1
  49. package/lib/tasksAndTips.d.ts +7 -5
  50. package/lib/tasksAndTips.js +487 -23
  51. package/lib/tasksAndTips.js.map +1 -1
  52. package/lib/typeDefinitions.d.ts +149 -30
  53. package/lib/util.d.ts +7 -2
  54. package/lib/util.js +32 -9
  55. package/lib/util.js.map +1 -1
  56. package/package.json +1 -1
  57. package/src/constants.ts +91 -3
  58. package/src/features/analytics/useAnalytics.tsx +25 -17
  59. package/src/features/global/downtime/useDowntime.tsx +11 -7
  60. package/src/features/global/users/useUserFunctions.tsx +1 -1
  61. package/src/features/jobs/jobsSlice.ts +9 -3
  62. package/src/features/placements/studentPlacements/activePlacement.ts +8 -3
  63. package/src/features/placements/studentPlacements/completedStudentPlacementsSlice.ts +5 -2
  64. package/src/features/placements/studentPlacements/upcomingStudentPlacementsSlice.ts +2 -2
  65. package/src/features/placements/studentPlacements/useStudentPlacements.tsx +4 -28
  66. package/src/features/updates/useUpdates.tsx +14 -12
  67. package/src/firebase/firebase.tsx +19 -12
  68. package/src/firebase/firebaseConfig.tsx +1 -1
  69. package/src/firebase/firebaseQuery.tsx +11 -3
  70. package/src/firebase/readDatabase.tsx +34 -6
  71. package/src/firebase/writeDatabase.tsx +3 -1
  72. package/src/hooks.tsx +1124 -166
  73. package/src/reduxHooks.ts +71 -22
  74. package/src/tasksAndTips.ts +495 -30
  75. package/src/typeDefinitions.ts +140 -35
  76. package/src/util.ts +45 -17
@@ -1,18 +1,18 @@
1
- import {arrayUnion, orderBy, where} from "firebase/firestore";
1
+ import {arrayUnion, documentId, orderBy, where} from "firebase/firestore";
2
2
  import FirebaseQuery from "./firebase/firebaseQuery";
3
- import {CohortData, InstituteData, ProviderData, StudentPlacementData, UserData} from "./typeDefinitions";
4
- import {getAccess} from "./firebase/util";
3
+ import {CohortData, InstituteData, OrganisationAddress, PlacementListing, ProviderData, RegistrationRequest, StudentPlacementData, UserData} from "./typeDefinitions";
4
+ import {camelCaseToNormal, capitaliseWords, dateToString, getAccess} from "./firebase/util";
5
5
  import { convertDate } from "./firebase/util";
6
6
 
7
7
  const firebaseQuery = new FirebaseQuery;
8
8
 
9
- type InstituteTipNames = "createCohort"|"uploadStaff"|"assignStaffRoles"|"uploadPlacements"|"allowExternalPlacementUpload"|"uploadStaffGuidance"|"uploadStudentGuidance"
9
+ type InstituteTipNames = "createCohort"|"addSchools"|"uploadStaff"|"assignStaffRoles"|"uploadPlacements"|"allowExternalPlacementUpload"|"uploadStaffGuidance"|"uploadStudentGuidance"
10
10
  type ProviderTipNames = string
11
11
  type StudentTipNames = string
12
12
 
13
- export type InstituteTaskNames = "missingParentEmail"|"verifyInsurance"|"verifyRiskAssessment"|"verifyDbsCheck"|"inactiveStudents"|"uploadStudents"|"inactiveStaff"|"requiredStage"|"approveExternalPlacement"|"overdueStage"
13
+ export type InstituteTaskNames = "missingParentEmail"|"outstandingReminders"|"invalidStaffEmails"|"invalidStudentEmails"|"invalidParentEmails"|"invalidProviderEmails"|"verifyInsurance"|"verifyRiskAssessment"|"verifyDbsCheck"|"inactiveStudents"|"uploadStudents"|"inactiveStaff"|"requiredStage"|"approveExternalPlacement"|"overdueStage"
14
14
  export type StudentTaskNames = "completeOnboarding"
15
- export type ProviderTaskNames = "applicationRequireReview"|"requestedVisiblePlacementListings"|"requestedVisibleAddresses"|"completeStudentDocs"|"reviewOnboarding"|"completeListing"|"completeAddress"|"registrationRequests"|"placementStarting"|"completeFeedback"|"setUpFeedback"
15
+ export type ProviderTaskNames = "applicationRequireReview"|"activateStaff"|"requestedVisiblePlacementListings"|"requestedVisibleAddresses"|"completeStudentDocs"|"uploadOnboarding"|"reviewOnboarding"|"completeListing"|"completeAddress"|"registrationRequests"|"placementStarting"|"completeFeedback"|"setUpFeedback"
16
16
 
17
17
  // IF UPDATING LOGIC WITHIN THIS FILE, PLACEMENTT-BACKEND LOGIC MUST ALSO BE CHANGED ACCORDINGLY
18
18
 
@@ -34,7 +34,7 @@ type TasksObject = {
34
34
  */
35
35
  type InstituteTipsObject = {
36
36
  [key in InstituteTipNames]: {
37
- callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
37
+ callback: (user: UserData, organisation?:InstituteData|ProviderData, additional?:{[key: string]: OrganisationAddress}) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
38
38
  };
39
39
  };
40
40
  type ProviderTipsObject = {
@@ -69,6 +69,21 @@ const studentTips:StudentTipsObject = {}
69
69
 
70
70
 
71
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
+ },
72
87
  createCohort: {
73
88
  callback: async (user) => {
74
89
  if (!getAccess(user, "createCohorts")) return;
@@ -183,9 +198,162 @@ const instituteTips:InstituteTipsObject = {
183
198
  // Accept a cohort to any task
184
199
 
185
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
+ },
186
354
  requiredStage: {
187
355
  callback: async (user, _, cohorts) => {
188
- if (!getAccess(user, "signOffPlacements")) return;
356
+ if (!getAccess(user, "signOffPlacements") || user.viewStudents === "none") return;
189
357
 
190
358
  if (!Array.isArray(cohorts)) {
191
359
  const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("reqUserType", "==", user.userType)]));
@@ -220,7 +388,7 @@ const instituteTasks:InstituteTaskObject = {
220
388
  },
221
389
  verifyInsurance: {
222
390
  callback: async (user, _, cohorts) => {
223
- if (!getAccess(user, "verifyInsurance")) return;
391
+ if (!getAccess(user, "verifyInsurance") || user.viewStudents === "none") return;
224
392
 
225
393
  if (!Array.isArray(cohorts)) {
226
394
  const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("insurance", "==", "awaitingReview")]));
@@ -255,7 +423,7 @@ const instituteTasks:InstituteTaskObject = {
255
423
  },
256
424
  verifyDbsCheck: {
257
425
  callback: async (user, _, cohorts) => {
258
- if (!getAccess(user, "verifyDbsChecks")) return;
426
+ if (!getAccess(user, "verifyDbsChecks") || user.viewStudents === "none") return;
259
427
 
260
428
  if (!Array.isArray(cohorts)) {
261
429
  const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("dbsCheck", "==", "awaitingReview")]));
@@ -290,7 +458,7 @@ const instituteTasks:InstituteTaskObject = {
290
458
  },
291
459
  verifyRiskAssessment: {
292
460
  callback: async (user, _, cohorts) => {
293
- if (!getAccess(user, "verifyRiskAssessments")) return;
461
+ if (!getAccess(user, "verifyRiskAssessments") || user.viewStudents === "none") return;
294
462
 
295
463
  if (!Array.isArray(cohorts)) {
296
464
  const placementCount = (await firebaseQuery.getCount(["placements"], [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("riskAssessment", "==", "awaitingReview")]));
@@ -325,7 +493,7 @@ const instituteTasks:InstituteTaskObject = {
325
493
  },
326
494
  missingParentEmail: {
327
495
  callback: async (user, _, cohorts) => {
328
- if (!getAccess(user, "editStudents")) return;
496
+ if (!getAccess(user, "editStudents") || user.viewStudents === "none") return;
329
497
  if (!Array.isArray(cohorts)) {
330
498
  const requiresParents = Boolean(cohorts.workflow.find((node) => node.userType === "Parent"))
331
499
  if (!requiresParents) return;
@@ -372,7 +540,7 @@ const instituteTasks:InstituteTaskObject = {
372
540
  },
373
541
  inactiveStudents: {
374
542
  callback: async (user, _, cohorts) => {
375
- if (!getAccess(user, "activateStudents")) return;
543
+ if (!getAccess(user, "activateStudents") || user.viewStudents === "none") return;
376
544
  if (!Array.isArray(cohorts)) {
377
545
  const constraints = [where("oId", "==", user.oId), where("cohort", "==", cohorts.id), where("status", "==", "inactive")];
378
546
 
@@ -406,7 +574,7 @@ const instituteTasks:InstituteTaskObject = {
406
574
  title: `${studentCount} students in '${cohort.name}' are inactive.`,
407
575
  message: `Your cohort '${cohort.name}' has inactive students. Activate them to enable them to use the platform.`,
408
576
  link: `/institutes/cohorts/${cohort.id}/students?status=inactive`,
409
- buttonTitle: "Review placements",
577
+ buttonTitle: "Review students",
410
578
  } as TaskQueryReturnObject;
411
579
  }
412
580
  return;
@@ -417,7 +585,7 @@ const instituteTasks:InstituteTaskObject = {
417
585
  uploadStudents: {
418
586
  callback: async (user, _, cohorts) => {
419
587
  if (!Array.isArray(cohorts)) return;
420
- if (!getAccess(user, "addStudents")) return;
588
+ if (!getAccess(user, "addStudents") || user.viewStudents === "none") return;
421
589
  if (user.product !== "institutes") return;
422
590
  const returnObj: TaskQueryReturnObject = {
423
591
  link: "",
@@ -493,13 +661,12 @@ const studentTasks:StudentTaskObject = {
493
661
  completeOnboarding: {
494
662
  callback: async (user) => {
495
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};
496
- console.log("placementsWithoutONboarding", placementsWithoutOnboarding);
497
664
  if (Object.keys(placementsWithoutOnboarding).length === 0) return;
498
665
  const items = Object.entries(placementsWithoutOnboarding).map(([k, placement]) => ({
499
666
  dismissible: false,
500
667
  severity: "primary",
501
- title: `Complete required onboarding documents for ${placement.name}`,
502
- message: `${placement.name} has requested you to complete some onboarding documents.`,
668
+ title: `Complete onboarding for ${placement.name}`,
669
+ message: `Review onboarding for your placement starting on ${convertDate(placement.startDate, "visual")}`,
503
670
  link: `/${user.product}/placements/${k}`,
504
671
  buttonTitle: "View onboarding",
505
672
  } as TaskQueryReturnObject))
@@ -511,10 +678,11 @@ const studentTasks:StudentTaskObject = {
511
678
  const providerTasks:ProviderTaskObject = {
512
679
  requestedVisibleAddresses: {
513
680
  callback: async (user) => {
514
- const accessRequests = await firebaseQuery.getCount("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleAddresses")]);
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);
515
683
  if (accessRequests === 0) return;
516
684
  if (accessRequests === 1) {
517
- const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleAddresses")]) || {})[0] as [string, UserData];
685
+ const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", "providers"), where("oId", "==", user.oId), orderBy("requestedVisibleAddresses")]) || {})[0] as [string, UserData];
518
686
  return {
519
687
  dismissible: false,
520
688
  severity: "primary",
@@ -537,7 +705,8 @@ const providerTasks:ProviderTaskObject = {
537
705
  },
538
706
  requestedVisiblePlacementListings: {
539
707
  callback: async (user) => {
540
- const accessRequests = await firebaseQuery.getCount("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleListings")]);
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);;
541
710
  if (accessRequests === 0) return;
542
711
  if (accessRequests === 1) {
543
712
  const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleListings")]) || {})[0] as [string, UserData];
@@ -563,7 +732,26 @@ const providerTasks:ProviderTaskObject = {
563
732
  },
564
733
  applicationRequireReview: {
565
734
  callback: async (user) => {
566
- const applicationCount = await firebaseQuery.getCount("applications", [where("providerId", "==", user.oId), where("reqUserType", "==", "Staff"), where("status", "==", "submitted")]);
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);
567
755
  if (applicationCount === 0) return;
568
756
 
569
757
  return {
@@ -578,47 +766,319 @@ const providerTasks:ProviderTaskObject = {
578
766
  },
579
767
  completeStudentDocs: {
580
768
  callback: async (user) => {
769
+ return;
581
770
  return {} as TaskQueryReturnObject;
582
771
  },
583
772
  },
584
773
  reviewOnboarding: {
585
774
  callback: async (user) => {
586
- return {} as TaskQueryReturnObject;
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;
587
868
  },
588
869
  },
589
870
  completeListing: {
590
871
  callback: async (user) => {
591
- return {} as TaskQueryReturnObject;
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;
592
915
  },
593
916
  },
594
917
  completeAddress: {
595
918
  callback: async (user) => {
596
- return {} as TaskQueryReturnObject;
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;
597
952
  },
598
953
  },
599
954
  registrationRequests: {
600
955
  callback: async (user) => {
601
- return {} as TaskQueryReturnObject;
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;
602
1010
  },
603
1011
  },
604
1012
  placementStarting: {
605
1013
  callback: async (user) => {
606
- return {} as TaskQueryReturnObject;
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;
607
1065
  },
608
1066
  },
609
1067
  completeFeedback: {
610
1068
  callback: async (user) => {
1069
+ return;
611
1070
  return {} as TaskQueryReturnObject;
612
1071
  },
613
1072
  },
614
1073
  setUpFeedback: {
615
1074
  callback: async (user) => {
1075
+ return;
616
1076
  return {} as TaskQueryReturnObject;
617
1077
  },
618
1078
  },
619
1079
  }
620
1080
 
621
- export const getTips = async (user: UserData, organisation: InstituteData|ProviderData):Promise<(TaskQueryReturnObject)[]> => {
1081
+ export const getTips = async (user: UserData, organisation: InstituteData|ProviderData, addresses?: {[key: string]: OrganisationAddress}):Promise<(TaskQueryReturnObject)[]> => {
622
1082
  const tipsObject = {
623
1083
  providers: providerTips,
624
1084
  institutes: instituteTips,
@@ -627,9 +1087,14 @@ export const getTips = async (user: UserData, organisation: InstituteData|Provid
627
1087
  const includedItems = Object.entries(tipsObject[user.product]).filter(([k]) => !user.dismissedTips?.includes(k));
628
1088
 
629
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
+ }
630
1095
  const queryResult = await (item as {
631
1096
  callback: (user: UserData, organisation?:InstituteData|ProviderData) => Promise<TaskQueryReturnObject|TaskQueryReturnObject[]>,
632
- }).callback(user, organisation);
1097
+ }).callback(...callbackParams);
633
1098
  if (!queryResult) return await acc;
634
1099
 
635
1100
  const queryResultArray = Array.isArray(queryResult) ? queryResult : [queryResult];
@@ -697,7 +1162,7 @@ const getInstituteTasks = async (user: UserData, organisation: InstituteData|Pro
697
1162
  fCohort = Object.entries(cohorts);
698
1163
  }
699
1164
  if (user.viewCohorts === "some") {
700
- const cohorts = await user.visibleCohorts?.split(",").reduce(async (acc, cohortId) => {
1165
+ const cohorts = await user.visibleCohorts?.reduce(async (acc, cohortId) => {
701
1166
  const cohort = await firebaseQuery.getDocData(["cohorts", cohortId]) as CohortData;
702
1167
  if (cohort.stage !== "created") {
703
1168
  return acc;