placementt-core 11.0.533 → 11.10.151

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 (69) hide show
  1. package/lib/constants.d.ts +1 -0
  2. package/lib/constants.js +6 -0
  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 +21 -16
  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/placements/studentPlacements/activePlacement.d.ts +5 -1
  13. package/lib/features/placements/studentPlacements/activePlacement.js +7 -3
  14. package/lib/features/placements/studentPlacements/activePlacement.js.map +1 -1
  15. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.d.ts +3 -2
  16. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.js +4 -1
  17. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.js.map +1 -1
  18. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.d.ts +2 -2
  19. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.js +1 -1
  20. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.js.map +1 -1
  21. package/lib/features/placements/studentPlacements/useStudentPlacements.d.ts +2 -12
  22. package/lib/features/placements/studentPlacements/useStudentPlacements.js +1 -26
  23. package/lib/features/placements/studentPlacements/useStudentPlacements.js.map +1 -1
  24. package/lib/features/updates/useUpdates.d.ts +1 -0
  25. package/lib/features/updates/useUpdates.js +13 -12
  26. package/lib/features/updates/useUpdates.js.map +1 -1
  27. package/lib/firebase/firebase.d.ts +3 -1
  28. package/lib/firebase/firebase.js +4 -3
  29. package/lib/firebase/firebase.js.map +1 -1
  30. package/lib/firebase/firebaseQuery.d.ts +3 -1
  31. package/lib/firebase/firebaseQuery.js +8 -1
  32. package/lib/firebase/firebaseQuery.js.map +1 -1
  33. package/lib/firebase/readDatabase.d.ts +2 -4
  34. package/lib/firebase/readDatabase.js +28 -5
  35. package/lib/firebase/readDatabase.js.map +1 -1
  36. package/lib/firebase/writeDatabase.d.ts +6 -2
  37. package/lib/firebase/writeDatabase.js +2 -1
  38. package/lib/firebase/writeDatabase.js.map +1 -1
  39. package/lib/hooks.d.ts +108 -9
  40. package/lib/hooks.js +547 -58
  41. package/lib/hooks.js.map +1 -1
  42. package/lib/reduxHooks.d.ts +41 -3
  43. package/lib/reduxHooks.js +61 -18
  44. package/lib/reduxHooks.js.map +1 -1
  45. package/lib/tasksAndTips.d.ts +1 -1
  46. package/lib/tasksAndTips.js +179 -8
  47. package/lib/tasksAndTips.js.map +1 -1
  48. package/lib/typeDefinitions.d.ts +14 -22
  49. package/lib/util.js +14 -8
  50. package/lib/util.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/constants.ts +7 -1
  53. package/src/features/analytics/useAnalytics.tsx +24 -15
  54. package/src/features/global/downtime/useDowntime.tsx +11 -7
  55. package/src/features/global/users/useUserFunctions.tsx +1 -1
  56. package/src/features/placements/studentPlacements/activePlacement.ts +8 -3
  57. package/src/features/placements/studentPlacements/completedStudentPlacementsSlice.ts +5 -2
  58. package/src/features/placements/studentPlacements/upcomingStudentPlacementsSlice.ts +2 -2
  59. package/src/features/placements/studentPlacements/useStudentPlacements.tsx +4 -28
  60. package/src/features/updates/useUpdates.tsx +14 -12
  61. package/src/firebase/firebase.tsx +5 -3
  62. package/src/firebase/firebaseQuery.tsx +8 -1
  63. package/src/firebase/readDatabase.tsx +33 -6
  64. package/src/firebase/writeDatabase.tsx +3 -1
  65. package/src/hooks.tsx +695 -61
  66. package/src/reduxHooks.ts +68 -20
  67. package/src/tasksAndTips.ts +184 -11
  68. package/src/typeDefinitions.ts +14 -20
  69. package/src/util.ts +14 -9
package/src/reduxHooks.ts CHANGED
@@ -1,14 +1,16 @@
1
- import { where } from "firebase/firestore"
1
+ import { orderBy, where } from "firebase/firestore"
2
2
  import { useEffect, useState } from "react"
3
3
  import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux"
4
4
  import { AppDispatch, RootState } from "./config"
5
5
  import { addContact, deleteContact, setContacts, updateContact } from "./features/contacts/contactsSlice"
6
6
  import { Job, addJob, setJobStatus, setJobs, setMarkRead } from "./features/jobs/jobsSlice"
7
- import { fetchActivePlacement } from "./features/placements/studentPlacements/activePlacement"
7
+ import { editActivePlacement, fetchActivePlacement, setActivePlacement } from "./features/placements/studentPlacements/activePlacement"
8
8
  import { addCompletedStudentPlacements, deleteCompletedStudentPlacement, setCompletedStudentPlacements, updateCompletedStudentPlacement } from "./features/placements/studentPlacements/completedStudentPlacementsSlice"
9
9
  import { addUpcomingStudentPlacements, deleteUpcomingStudentPlacement, setUpcoming, setUpcomingStudentPlacements, updateUpcomingStudentPlacement } from "./features/placements/studentPlacements/upcomingStudentPlacementsSlice"
10
10
  import FirebaseQuery from "./firebase/firebaseQuery"
11
11
  import { Address, Application, Contact, PlacementListing, ProviderData, StudentPlacementData, UserData } from "./typeDefinitions"
12
+ import {getPlacementsWhere} from "./firebase/readDatabase"
13
+ import {convertDate} from "./firebase/util"
12
14
 
13
15
  export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
14
16
  export const useAppDispatch = () => useDispatch<AppDispatch>();
@@ -22,7 +24,7 @@ export function useStudent({user} : {user: UserData}) {
22
24
  const activePlacement = useAppSelector((state) => state.activePlacement.values)
23
25
  const upcoming = useAppSelector((state) => state.upcomingStudentPlacements.upcoming)
24
26
  const contacts = useAppSelector((state) => state.contacts.values)
25
- const [applications, setApplications] = useState<{[key: string]: Application&{listing: PlacementListing, provider: ProviderData, address: Address}}>();
27
+ const [applications, setApplications] = useState<{[key: string]: Application&{listing: PlacementListing|false, provider: ProviderData, address: Address}}>();
26
28
  const firebaseQuery = new FirebaseQuery();
27
29
 
28
30
  const dispatch = useAppDispatch();
@@ -42,8 +44,8 @@ export function useStudent({user} : {user: UserData}) {
42
44
 
43
45
  const applicationsWithProviderAndListing:{[key: string]: Application&{listing: PlacementListing, provider: ProviderData, address: Address}} = Object.fromEntries(await Promise.all(Object.entries(fApplications).map(async ([id, application]) => {
44
46
  const provider = await firebaseQuery.getDocData(["providers", application.providerId]) as ProviderData;
45
- const listing = await firebaseQuery.getDocData(["placementListings", application.listingId]) as PlacementListing;
46
- const address = listing.addressId && await firebaseQuery.getDocData(["addresses", listing.addressId]) as Address;
47
+ const listing = await firebaseQuery.getDocData(["placementListings", application.listingId]).catch(() => false) as PlacementListing|false;
48
+ const address = listing && listing?.addressId && await firebaseQuery.getDocData(["addresses", listing.addressId]) as Address;
47
49
 
48
50
  return [id, {...application, listing: listing, provider: provider, address: address}];
49
51
  })));
@@ -51,25 +53,44 @@ export function useStudent({user} : {user: UserData}) {
51
53
  setApplications(applicationsWithProviderAndListing);
52
54
 
53
55
  }, "applications", applicationsConstraints);
54
- }, [dispatch, user.id]);
55
-
56
+ }, [user.id]);
57
+ /*
56
58
  useEffect(() => {
57
59
  if (!user.id) return
58
60
  dispatch(fetchActivePlacement({userId: user.id}))
59
- }, [dispatch, user.id, upcomingPlacements])
61
+ }, [dispatch, user.id, upcomingPlacements]);
62
+ */
63
+ const fetchUpcomingPlacements = () => {
64
+ const constraints = [where("uid", "==", user.id), where("inProgress", "==", true), orderBy("startDate")]
65
+ firebaseQuery.collectionSnapshot(setUpcomingStudentPlacements, "placements", constraints, dispatch);
66
+ }
60
67
 
61
- useEffect(() => {
62
- console.log("UPDATE UPCOMING");
63
-
68
+ const fetchCompletedPlacements = () => {
69
+ const constraints = [where("uid", "==", user.id), where("completed", "==", true), orderBy("startDate")]
70
+ firebaseQuery.collectionSnapshot(setCompletedStudentPlacements, "placements", constraints, dispatch);
71
+ }
72
+
73
+ const fetchContacts = () => {
74
+ const constraints = [where("uid", "==", user.id)]
75
+ firebaseQuery.collectionSnapshot(setContacts, "contacts", constraints, dispatch);
76
+ }
77
+
78
+ const updateUpcomingPlacement = () => {
64
79
  if (!upcomingPlacements || !Object.entries(upcomingPlacements).length) {
65
80
  dispatch(setUpcoming(undefined));
66
81
  return;
67
82
  }
68
83
  const upcomingPlacement = Object.entries(upcomingPlacements).find(([, v]) => new Date(v.startDate).getTime() > today.getTime())
69
84
  upcomingPlacement ? dispatch(setUpcoming(upcomingPlacement[1])) : dispatch(setUpcoming(undefined))
85
+ }
70
86
 
71
- }, [dispatch, upcomingPlacements]);
72
-
87
+ const updateActivePlacement = () => {
88
+ const key = Object.keys(upcomingPlacements).find((key) => {
89
+ const placement = upcomingPlacements[key]
90
+ return placement.active
91
+ })
92
+ key ? dispatch(setActivePlacement(upcomingPlacements[key])) : dispatch(setActivePlacement(null))
93
+ }
73
94
 
74
95
  const getItemById = async (path: "contacts"|"placements", id: string) => {
75
96
  return await firebaseQuery.getDocData([path, id])
@@ -80,9 +101,9 @@ export function useStudent({user} : {user: UserData}) {
80
101
  else dispatch(deleteCompletedStudentPlacement({placementId: id}))
81
102
  }
82
103
 
83
- const handleUpdatePlacement = async (id: string, data: StudentPlacementData) => {
84
- if (upcomingPlacements[id]) dispatch(updateUpcomingStudentPlacement({placementId: id, attributes: data}))
85
- else dispatch(updateCompletedStudentPlacement({placementId: id, attributes: data}))
104
+ const handleUpdatePlacement = async (id: string, attributes: Partial<StudentPlacementData>) => {
105
+ if (upcomingPlacements[id]) dispatch(updateUpcomingStudentPlacement({placementId: id, attributes}))
106
+ else dispatch(updateCompletedStudentPlacement({placementId: id, attributes}))
86
107
  }
87
108
 
88
109
  const handleAddPlacement = async (formData: StudentPlacementData) => {
@@ -90,19 +111,46 @@ export function useStudent({user} : {user: UserData}) {
90
111
  else dispatch(addUpcomingStudentPlacements({formData}))
91
112
  }
92
113
 
114
+ const getUserPlacements = async () => {
115
+ return await getPlacementsWhere({w: where("uid", "==", user?.id)})
116
+ }
117
+
118
+ const getPlacementsStart = async (start: Date, end: Date) => {
119
+ return await firebaseQuery.getDocsWhere("placements", [where("uid", "==", user?.id), where("startDate", ">=", convertDate(start, "dbstring")), where("startDate", "<=", convertDate(end, "dbstring"))]) as {[key:string]: StudentPlacementData};
120
+ }
121
+
122
+ const getPlacementsEnd = async (end: Date) => {
123
+ return await firebaseQuery.getDocsWhere("placements", [where("uid", "==", user?.id), where("endDate", ">=", convertDate(end, "dbstring")), where("endDate", "<=", convertDate(end, "dbstring"))]) as {[key:string]: StudentPlacementData};
124
+ }
125
+
126
+ const dispatchActivePlacement = () => {
127
+ dispatch(fetchActivePlacement({userId: user.id}))
128
+ }
129
+
93
130
  return {
94
131
  contacts: {
95
- add: async (contactForm: Contact) => dispatch(addContact({contactForm: contactForm, userId: user.id})),
96
- update: async (contactId: string, data: Partial<Contact>) => dispatch(updateContact({contactId: contactId, attributes: data})),
97
- delete: async (contactId: string) => dispatch(deleteContact({contactId: contactId})),
132
+ add: async (contactForm: Contact) => await dispatch(addContact({contactForm: contactForm, userId: user.id})),
133
+ update: async (contactId: string, data: Partial<Contact>) => await dispatch(updateContact({contactId: contactId, attributes: data})),
134
+ delete: async (contactId: string) => await dispatch(deleteContact({contactId: contactId})),
98
135
  contacts,
136
+ fetchContacts,
99
137
  getById: async (item: string) => await getItemById("contacts", item)
100
138
  },
139
+
101
140
  placements: {
102
141
  add: async (formData: StudentPlacementData) => await handleAddPlacement(formData),
103
- update: async (placementId: string, data: StudentPlacementData) => handleUpdatePlacement(placementId, data),
142
+ update: async (placementId: string, attributes: Partial<StudentPlacementData>) => await handleUpdatePlacement(placementId, attributes),
104
143
  delete: async (id: string) => await handleDeletePlacement(id),
144
+ getUserPlacements,
145
+ getPlacementsStart,
146
+ getPlacementsEnd,
105
147
  activePlacement,
148
+ editActivePlacement: (attributes: Partial<StudentPlacementData>) => dispatch(editActivePlacement(attributes)),
149
+ updateActivePlacement,
150
+ updateUpcomingPlacement,
151
+ fetchUpcomingPlacements,
152
+ fetchCompletedPlacements,
153
+ dispatchActivePlacement,
106
154
  upcoming,
107
155
  upcomingPlacements: upcomingPlacements || {},
108
156
  completedPlacements: completedPlacements || {},
@@ -1,7 +1,7 @@
1
1
  import {arrayUnion, 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;
@@ -12,7 +12,7 @@ type StudentTipNames = string
12
12
 
13
13
  export type InstituteTaskNames = "missingParentEmail"|"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
 
@@ -511,10 +511,11 @@ const studentTasks:StudentTaskObject = {
511
511
  const providerTasks:ProviderTaskObject = {
512
512
  requestedVisibleAddresses: {
513
513
  callback: async (user) => {
514
- const accessRequests = await firebaseQuery.getCount("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleAddresses")]);
514
+ if (!getAccess(user, "addStaff")) return;
515
+ 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
516
  if (accessRequests === 0) return;
516
517
  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];
518
+ const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", "providers"), where("oId", "==", user.oId), orderBy("requestedVisibleAddresses")]) || {})[0] as [string, UserData];
518
519
  return {
519
520
  dismissible: false,
520
521
  severity: "primary",
@@ -537,7 +538,8 @@ const providerTasks:ProviderTaskObject = {
537
538
  },
538
539
  requestedVisiblePlacementListings: {
539
540
  callback: async (user) => {
540
- const accessRequests = await firebaseQuery.getCount("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleListings")]);
541
+ if (!getAccess(user, "addStaff")) return;
542
+ 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
543
  if (accessRequests === 0) return;
542
544
  if (accessRequests === 1) {
543
545
  const userRequestingAccess = Object.entries(await firebaseQuery.getDocsWhere("users", [where("oId", "==", user.oId), where("product", "==", "providers"), orderBy("requestedVisibleListings")]) || {})[0] as [string, UserData];
@@ -578,41 +580,212 @@ const providerTasks:ProviderTaskObject = {
578
580
  },
579
581
  completeStudentDocs: {
580
582
  callback: async (user) => {
583
+ return;
581
584
  return {} as TaskQueryReturnObject;
582
585
  },
583
586
  },
584
587
  reviewOnboarding: {
585
588
  callback: async (user) => {
586
- return {} as TaskQueryReturnObject;
589
+ const toReview = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("onboarding.completed.accepted", "==", false), where("onboarding.completed.submitted", "==", true), where("endDate", ">=", dateToString(new Date()))]);
590
+
591
+ if (toReview === 0) return;
592
+ if (toReview === 1) {
593
+ const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", [where("providerId", "==", user.oId), where("onboarding.completed.accepted", "==", false), where("onboarding.completed.submitted", "==", true), where("endDate", ">=", dateToString(new Date()))]) || {})[0] as [string, StudentPlacementData];
594
+ const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData;
595
+ return {
596
+ dismissible: false,
597
+ severity: "primary",
598
+ 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")}`,
599
+ message: `Click to view the placement and review the onboarding.`,
600
+ link: `/${user.product}/placements/${placement[0]}`,
601
+ buttonTitle: "View",
602
+ } as TaskQueryReturnObject;
603
+ }
604
+
605
+ return {
606
+ dismissible: false,
607
+ severity: "primary",
608
+ title: `${toReview} student have completed their onboarding.`,
609
+ message: `Click to view your placements and approve completed onboarding`,
610
+ link: `/${user.product}/placementListings/placements`,
611
+ buttonTitle: "View placements",
612
+ } as TaskQueryReturnObject;
613
+ },
614
+ },
615
+ uploadOnboarding: {
616
+ callback: async (user) => {
617
+ const withoutOnboarding = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("onboarding", "==", null), where("endDate", ">=", dateToString(new Date()))]);
618
+
619
+ if (withoutOnboarding === 0) return;
620
+ if (withoutOnboarding === 1) {
621
+ const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", [where("providerId", "==", user.oId), where("onboarding", "==", null), where("endDate", ">=", dateToString(new Date()))]) || {})[0] as [string, StudentPlacementData];
622
+ const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData;
623
+ return {
624
+ dismissible: false,
625
+ severity: "primary",
626
+ 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")}`,
627
+ message: `Click to view the placement and add or dismiss onboarding reminders.`,
628
+ link: `/${user.product}/placements/${placement[0]}`,
629
+ buttonTitle: "View",
630
+ } as TaskQueryReturnObject;
631
+ }
632
+
633
+ return {
634
+ dismissible: false,
635
+ severity: "primary",
636
+ title: `Set up onboarding for ${withoutOnboarding} placements to prepare yourself and your students.`,
637
+ message: `Click to view your placements and add or dismiss onboarding reminders.`,
638
+ link: `/${user.product}/placementListings/placements`,
639
+ buttonTitle: "View placements",
640
+ } as TaskQueryReturnObject;
587
641
  },
588
642
  },
589
643
  completeListing: {
590
644
  callback: async (user) => {
591
- return {} as TaskQueryReturnObject;
645
+ const incompleteListings = await firebaseQuery.getCount("placementListings", [where("providerId", "==", user.oId), where("status", "==", "draft")]);
646
+ if (incompleteListings === 0) return;
647
+ if (incompleteListings === 1) {
648
+ const incompleteListing = Object.entries(await firebaseQuery.getDocsWhere("placementListings", [where("providerId", "==", user.oId), where("status", "==", "draft")]) || {})[0] as [string, PlacementListing];
649
+ const address = incompleteListing[1].addressId ? await firebaseQuery.getDocData(["addresses", incompleteListing[1].addressId]) as OrganisationAddress : undefined;
650
+ return {
651
+ dismissible: false,
652
+ severity: "info",
653
+ 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.`,
654
+ message: `Click to complete and publish the placement listing.`,
655
+ link: `/${user.product}/addListing/${incompleteListing[0]}`,
656
+ buttonTitle: "View listing",
657
+ } as TaskQueryReturnObject;
658
+ }
659
+
660
+ return {
661
+ dismissible: false,
662
+ severity: "info",
663
+ title: `You have ${incompleteListings} draft listings waiting to be published.`,
664
+ message: `Click to review and publish the placement listings.`,
665
+ link: `/${user.product}/placementListings/listings`,
666
+ buttonTitle: "View listings",
667
+ } as TaskQueryReturnObject;
592
668
  },
593
669
  },
594
670
  completeAddress: {
595
671
  callback: async (user) => {
596
- return {} as TaskQueryReturnObject;
672
+ const incompleteAddresses = await firebaseQuery.getCount("addresses", [where("product", "==", "providers"), where("oId", "==", user.oId), where("stage", "!=", "complete")]);
673
+ if (incompleteAddresses === 0) return;
674
+ if (incompleteAddresses === 1) {
675
+ const address = Object.entries(await firebaseQuery.getDocsWhere("addresses", [where("product", "==", "providers"), where("oId", "==", user.oId), where("stage", "!=", "complete")]) || {})[0] as [string, OrganisationAddress];
676
+ return {
677
+ dismissible: false,
678
+ severity: "info",
679
+ title: `Your address: ${address[1]["address-line1"]}, ${address[1].postal_code.toUpperCase()}, ${capitaliseWords(camelCaseToNormal(address[1].country))} is currently incomplete.`,
680
+ message: `Click to complete the addresses.`,
681
+ link: `/${user.product}/addAddress/${address[0]}`,
682
+ buttonTitle: "View address",
683
+ } as TaskQueryReturnObject;
684
+ }
685
+
686
+ return {
687
+ dismissible: false,
688
+ severity: "info",
689
+ title: `You have ${incompleteAddresses} draft addresses waiting to be published.`,
690
+ message: `Click to review the addresses.`,
691
+ link: `/${user.product}/organisation/addresses`,
692
+ buttonTitle: "View addresses",
693
+ } as TaskQueryReturnObject;
597
694
  },
598
695
  },
599
696
  registrationRequests: {
600
697
  callback: async (user) => {
601
- return {} as TaskQueryReturnObject;
698
+ const regRequests = await firebaseQuery.getCount("requests", [where("product", "==", user.product), where("oId", "==", user.oId)]);
699
+
700
+ if (regRequests === 0) return;
701
+ if (regRequests === 1) {
702
+ const request = Object.entries(await firebaseQuery.getDocsWhere("requests", [where("product", "==", user.product), where("oId", "==", user.oId)]) || {})[0] as [string, RegistrationRequest];
703
+ return {
704
+ dismissible: false,
705
+ severity: "primary",
706
+ title: `${request[1].forename} ${request[1].surname} has requested to access your organisation.`,
707
+ message: `Click to review these request.`,
708
+ link: `/${user.product}/organisation/staff/requests`,
709
+ buttonTitle: "View requests",
710
+ } as TaskQueryReturnObject;
711
+ }
712
+
713
+ return {
714
+ dismissible: false,
715
+ severity: "primary",
716
+ title: `${regRequests} people have requested to register with your organisation.`,
717
+ message: `Click to review these requests.`,
718
+ link: `/${user.product}/organisation/staff/requests`,
719
+ buttonTitle: "View requests",
720
+ } as TaskQueryReturnObject;
721
+ },
722
+ },
723
+ activateStaff: {
724
+ callback: async (user) => {
725
+ const inactiveAccounts = await firebaseQuery.getCount("users", [where("product", "==", user.product), where("oId", "==", user.oId), where("status", "==", "inactive")]);
726
+
727
+ if (inactiveAccounts === 0) return;
728
+ if (inactiveAccounts === 1) {
729
+ const account = Object.entries(await firebaseQuery.getDocsWhere("users", [where("product", "==", user.product), where("oId", "==", user.oId), where("status", "==", "inactive")]) || {})[0] as [string, UserData];
730
+ return {
731
+ dismissible: false,
732
+ severity: "info",
733
+ title: `Activate ${account[1].details.forename} ${account[1].details.surname}'s staff account.`,
734
+ message: "Activate this account to give the user access to Placementt.",
735
+ link: `/${user.product}/organisation/staff/all`,
736
+ buttonTitle: "View accounts",
737
+ } as TaskQueryReturnObject;
738
+ }
739
+
740
+ return {
741
+ dismissible: false,
742
+ severity: "info",
743
+ title: `${inactiveAccounts} staff have inactive active accounts.`,
744
+ message: "Activate these accounts to give the user access to Placementt.",
745
+ link: `/${user.product}/organisation/staff/all`,
746
+ buttonTitle: "View accounts",
747
+ } as TaskQueryReturnObject;
602
748
  },
603
749
  },
604
750
  placementStarting: {
605
751
  callback: async (user) => {
606
- return {} as TaskQueryReturnObject;
752
+ const sevenDaysInFuture = new Date();
753
+ sevenDaysInFuture.setDate(sevenDaysInFuture.getDate() + 7)
754
+ const placementsStartingSoon = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("startDate", "<=", convertDate(sevenDaysInFuture, "dbstring")), where("startDate", ">", dateToString(new Date()))]);
755
+
756
+ if (placementsStartingSoon === 0) return;
757
+ if (placementsStartingSoon === 1) {
758
+ const placement = Object.entries(await firebaseQuery.getDocsWhere("placements", [where("providerId", "==", user.oId), where("startDate", "<=", convertDate(sevenDaysInFuture, "dbstring")), where("startDate", ">", dateToString(new Date()))]) || {})[0] as [string, StudentPlacementData];
759
+ const student = await firebaseQuery.getDocData(["users", placement[1].uid]) as UserData;
760
+ return {
761
+ dismissible: false,
762
+ severity: "success",
763
+ 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.`,
764
+ message: `Click to view the placement and acquaint yourself with the student.`,
765
+ link: `/${user.product}/placements/${placement[0]}`,
766
+ buttonTitle: "View",
767
+ } as TaskQueryReturnObject;
768
+ }
769
+
770
+ return {
771
+ dismissible: false,
772
+ severity: "success",
773
+ title: `${placementsStartingSoon} placements starting soon.`,
774
+ message: `Click to view scheduled placements.`,
775
+ link: `/${user.product}/placementListings/placements`,
776
+ buttonTitle: "View placements",
777
+ } as TaskQueryReturnObject;
607
778
  },
608
779
  },
609
780
  completeFeedback: {
610
781
  callback: async (user) => {
782
+ return;
611
783
  return {} as TaskQueryReturnObject;
612
784
  },
613
785
  },
614
786
  setUpFeedback: {
615
787
  callback: async (user) => {
788
+ return;
616
789
  return {} as TaskQueryReturnObject;
617
790
  },
618
791
  },
@@ -85,13 +85,15 @@ export type StudentPlacementData = {
85
85
  },
86
86
  },
87
87
  onboarding: (OnboardingDocs&{
88
+ deadline?: string,
88
89
  completed: {
89
- submitted: string|false,
90
+ submitted: boolean,
91
+ submittedDate?: string,
90
92
  accepted?: boolean,
91
93
  filesViewed?: string[],
92
94
  formsCompleted?: {[key: string]: unknown},
93
95
  filesUploaded?: {[key: number]: string[]},
94
- }})|false,
96
+ }})|false|null,
95
97
  }
96
98
 
97
99
  export type PlacementQuestions = {
@@ -178,29 +180,17 @@ export type PlacementListing = {
178
180
  applicationType?: "simple"|"complex",
179
181
  applicantWorkflowId?: string,
180
182
  applicantWorkflow?: ApplicantStage[],
181
- onboarding: OnboardingDocs|false,
183
+ onboarding: (OnboardingDocs&{deadline: number})|false,
182
184
  maxApplicants?: number,
183
185
  maxStudents?: number,
184
186
  primaryContactId: string,
185
187
  }
186
188
 
187
189
  export type OnboardingDocs = {
188
- deadline?: string,
189
190
  message?: string,
190
- deadlineType?: "date"|"days"
191
191
  forms?: string[],
192
192
  files?: string[],
193
- formDetails?: {[key: string]:
194
- {
195
- name: string,
196
- id: string,
197
- description?: string,
198
- product: Products,
199
- oId: string,
200
- form: CustomFormSchema
201
- }},
202
193
  requiredFiles?: {fileName: string, fileType: string}[]
203
- viewableFiles?: {[key: string]: FileItem},
204
194
  }
205
195
 
206
196
  export type PlacementTemplate = PlacementListing&{templateName: string};
@@ -273,13 +263,15 @@ export type FlagCodes =
273
263
  "completeOnboarding"|
274
264
  "reviewOnboarding"|
275
265
  "requestedVisibleAddresses"|
276
- "requestedVisiblePlacementListings"
266
+ "requestedVisiblePlacementListings"|
267
+ "addOnboarding"
277
268
 
278
269
 
279
270
  export type UserData = {
280
271
  details: {
281
272
  forename: string,
282
273
  surname: string,
274
+ pronouns?: string,
283
275
  parentEmail?: string,
284
276
  year?: string
285
277
  },
@@ -318,7 +310,7 @@ export type UserData = {
318
310
  visibleCohorts?: string,
319
311
  readUpdates?: string[],
320
312
  contactProviderConsent?: boolean,
321
- contactProviderConsenDate?: boolean
313
+ contactProviderConsentDate?: boolean
322
314
  referrals?: number,
323
315
  shareNameWithReferralLeaderboardConsent?: boolean,
324
316
  shareNameWithReferralLeaderboardConsentDate?: boolean,
@@ -329,7 +321,8 @@ export type UserData = {
329
321
  visibleAddresses?: string[],
330
322
  visibleListings?: string[],
331
323
  requestedVisibleAddresses?: string[],
332
- requestedVisibleListings?: string[]
324
+ requestedVisibleListings?: string[],
325
+ packageData?: BillingPackage
333
326
  };
334
327
 
335
328
  export type AnalyticsItem = {
@@ -497,7 +490,7 @@ export type ProviderData = {
497
490
  staffActive: number,
498
491
  admin: string,
499
492
  name: string,
500
- insurance: boolean,
493
+ insurance: boolean|"awaitingReview",
501
494
  regNumber?: string,
502
495
  insuranceExpiry: string,
503
496
  mapConsent?: boolean|"unlisted",
@@ -525,7 +518,7 @@ export type ProviderData = {
525
518
  locations?: number,
526
519
  },
527
520
  discount?: string,
528
- package?: string,
521
+ package: string,
529
522
  splitBilling?: boolean,
530
523
  adminBillingContactId: string,
531
524
  addOnPackages?: string[],
@@ -829,6 +822,7 @@ export type Application = {
829
822
  uid: string,
830
823
  applicantWorkflowId: string,
831
824
  listingId: string,
825
+ addressId: string,
832
826
  providerId: string,
833
827
  reqUserType: "Students"|"Staff",
834
828
  completedSections: {[stage: number]: {
package/src/util.ts CHANGED
@@ -32,51 +32,57 @@ export const getPlacementFlagCodes = ({placement, studentData, workflow, institu
32
32
  const dbsCheckNotVerified = !institute?.verifiedDbsChecks?.includes(placement.placementId || placement.id);
33
33
  const awaitingDbsCheck = !placement.dbsCheck || institute?.awaitingPlacementDbsChecks?.includes(placement.placementId || placement.id);
34
34
 
35
- if (user.userType === "Staff" && placement.insurance && placementIsPostProviderReview && placementNotEnded && providerUnverified && !awaitingProviderInsurance) {
35
+ if (user.userType === "Staff" && user.product === "institutes" && placement.insurance && placementIsPostProviderReview && placementNotEnded && providerUnverified && !awaitingProviderInsurance) {
36
36
  console.log("Add insurance flag!");
37
37
  flags.includes("noInsurance") || flags.push("noInsurance");
38
38
  } else {
39
39
  flags = flags.filter((x) => x !== "noInsurance");
40
40
  }
41
41
 
42
- if (user.userType === "Staff" && placementIsPostProviderReview && placementNotEnded && awaitingProviderInsurance) {
42
+ if (user.userType === "Staff" && user.product === "institutes" && placementIsPostProviderReview && placementNotEnded && awaitingProviderInsurance) {
43
43
  flags.includes("awaitingInsurance") || flags.push("awaitingInsurance");
44
44
  } else {
45
45
  flags = flags.filter((x) => x !== "awaitingInsurance");
46
46
  }
47
47
 
48
- if (user.userType === "Staff" && placement.dbsCheck && workflow.find((stage) => stage.dbsCheck) && placement.dbsCheck !== true && placementIsPostProviderReview && placementNotEnded && dbsCheckNotVerified && !awaitingDbsCheck) {
48
+ if (user.userType === "Staff" && user.product === "institutes" && placement.dbsCheck && workflow.find((stage) => stage.dbsCheck) && placement.dbsCheck !== true && placementIsPostProviderReview && placementNotEnded && dbsCheckNotVerified && !awaitingDbsCheck) {
49
49
  flags.includes("noDbsCheck") || flags.push("noDbsCheck");
50
50
  } else {
51
51
  flags = flags.filter((x) => x !== "noDbsCheck");
52
52
  }
53
53
 
54
- if (user.userType === "Staff" && placementIsPostProviderReview && workflow.find((stage) => stage.dbsCheck) && placementNotEnded && awaitingProviderInsurance) {
54
+ if (user.userType === "Staff" && user.product === "institutes" && placementIsPostProviderReview && workflow.find((stage) => stage.dbsCheck) && placementNotEnded && awaitingProviderInsurance) {
55
55
  flags.includes("awaitingDbsCheck") || flags.push("awaitingDbsCheck");
56
56
  } else {
57
57
  flags = flags.filter((x) => x !== "awaitingDbsCheck");
58
58
  }
59
59
 
60
- if (user.userType === "Staff" && placement.riskAssessment && workflow.find((stage) => stage.riskAssessment) && placement.riskAssessment !== true && placementIsPostProviderReview && placementNotEnded && riskAssessmentNotVerified && !awaitingRiskAssessment) {
60
+ if (user.userType === "Staff" && user.product === "institutes" && placement.riskAssessment && workflow.find((stage) => stage.riskAssessment) && placement.riskAssessment !== true && placementIsPostProviderReview && placementNotEnded && riskAssessmentNotVerified && !awaitingRiskAssessment) {
61
61
  console.log("Add RA flag!");
62
62
  flags.includes("noRiskAssessment") || flags.push("noRiskAssessment");
63
63
  } else {
64
64
  flags = flags.filter((x) => x !== "noRiskAssessment");
65
65
  }
66
66
 
67
- if (user.userType === "Staff" && placementIsPostProviderReview && workflow.find((stage) => stage.riskAssessment) && placementNotEnded && awaitingProviderInsurance) {
67
+ if (user.userType === "Staff" && user.product === "institutes" && placementIsPostProviderReview && workflow.find((stage) => stage.riskAssessment) && placementNotEnded && awaitingProviderInsurance) {
68
68
  flags.includes("awaitingRiskAssessment") || flags.push("awaitingRiskAssessment");
69
69
  } else {
70
70
  flags = flags.filter((x) => x !== "awaitingRiskAssessment");
71
71
  }
72
72
 
73
- if (user.userType === "Staff" && user.product === "providers" && user.oId === placement.providerId && placement.onboarding && (placement.onboarding.completed?.submitted && !placement.onboarding.completed.accepted) && (placement.inProgress || placement.active)) {
73
+ if (placement.inProgress && user.userType === "Staff" && user.product === "providers" && user.oId === placement.providerId && placement.onboarding === null) {
74
+ flags.includes("addOnboarding") || flags.push("addOnboarding");
75
+ } else {
76
+ flags = flags.filter((x) => x !== "addOnboarding");
77
+ }
78
+
79
+ if (placement.inProgress && user.userType === "Staff" && user.product === "providers" && user.oId === placement.providerId && placement.onboarding && (placement.onboarding.completed?.submitted && !placement.onboarding.completed.accepted) && (placement.inProgress || placement.active)) {
74
80
  flags.includes("reviewOnboarding") || flags.push("reviewOnboarding");
75
81
  } else {
76
82
  flags = flags.filter((x) => x !== "reviewOnboarding");
77
83
  }
78
84
 
79
- if (user.userType === "Students" && placement.onboarding && (!placement.onboarding.completed || (placement.onboarding.completed && !placement.onboarding.completed?.submitted)) && (placement.inProgress || placement.active)) {
85
+ if (placement.inProgress && user.userType === "Students" && placement.onboarding && (!placement.onboarding.completed || (placement.onboarding.completed && !placement.onboarding.completed?.submitted)) && (placement.inProgress || placement.active)) {
80
86
  flags.includes("completeOnboarding") || flags.push("completeOnboarding");
81
87
  } else {
82
88
  flags = flags.filter((x) => x !== "completeOnboarding");
@@ -87,7 +93,6 @@ export const getPlacementFlagCodes = ({placement, studentData, workflow, institu
87
93
  };
88
94
 
89
95
 
90
-
91
96
  export function objectsEqualNew(a: any, b: any): boolean { // Works with arrays or objects.
92
97
  if (a === b) {
93
98
  return true;