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
package/src/hooks.tsx CHANGED
@@ -1,5 +1,5 @@
1
1
  import {
2
- DocumentData, QueryConstraint, QueryDocumentSnapshot, QueryFieldFilterConstraint, QuerySnapshot, Unsubscribe, collection, documentId,
2
+ DocumentData, QueryConstraint, QueryDocumentSnapshot, QueryFieldFilterConstraint, QueryOrderByConstraint, QuerySnapshot, Unsubscribe, collection, documentId,
3
3
  endAt, endBefore, getDocs, limit, limitToLast, onSnapshot, orderBy, query, startAfter, where
4
4
  } from "firebase/firestore";
5
5
  import { useCallback, useEffect, useRef, useState } from "react";
@@ -15,9 +15,9 @@ import {
15
15
  ApplicantStage,
16
16
  ApplicantWorkflow,
17
17
  Application,
18
- ArrowObject, CohortData, FileItem, FlagCodes, InstituteData, OnboardingDocs, PlacementListing, Products,
18
+ ArrowObject, CohortData, CustomFormSchema, FileItem, FlagCodes, InstituteData, OnboardingDocs, PlacementListing, Products,
19
19
  ProviderData,
20
- QueryObject, SavedPlacement, StudentPlacementData, UserData, UserGroupData, WorkflowStage
20
+ QueryObject, QueryObjectConstraint, SavedPlacement, Sorts, StudentPlacementData, UserData, UserGroupData, WorkflowStage
21
21
  } from "./typeDefinitions";
22
22
  import algoliasearch from "algoliasearch";
23
23
  import {getDownloadURL, ref} from "firebase/storage";
@@ -28,7 +28,7 @@ type StudentPlacementListParams = {
28
28
  user: UserData,
29
29
  student?: UserData,
30
30
  queryConstraint?: QueryConstraint[],
31
- ql?: number
31
+ ql?: number // query limit
32
32
  }
33
33
 
34
34
  const DEFAULTQUERYLIMIT = 5;
@@ -60,20 +60,20 @@ export function useStudentPlacementList({user, student, queryConstraint, ql=DEFA
60
60
  setStudentId(student.id);
61
61
  return;
62
62
  }
63
- if (!user.viewCohorts || !user.viewUsers || user.viewCohorts === "none") {
63
+ if (!user.viewCohorts || !user.viewStudents || user.viewCohorts === "none") {
64
64
  setStudentId(undefined);
65
65
  return;
66
66
  }
67
- if (user.viewCohorts === "all" && user.viewUsers == "all") {
67
+ if (user.viewCohorts === "all" && user.viewStudents == "all") {
68
68
  setStudentId(student.id);
69
69
  }
70
- if (user.viewCohorts === "some" && user.visibleCohorts?.split(",").includes(student.cohort || "")) {
71
- if (user.viewUsers === "all") {
70
+ if (user.viewCohorts === "some" && user.visibleCohorts?.includes(student.cohort || "")) {
71
+ if (user.viewStudents === "all") {
72
72
  setStudentId(student.id);
73
73
  return;
74
74
  }
75
75
 
76
- if (user.studentFilterValues?.split(", ").includes(student.details[user.studentFilter || ""])) {
76
+ if (user.studentFilterValues?.includes(student.details[user.studentFilter || ""])) {
77
77
  setStudentId(student.id);
78
78
  return;
79
79
  }
@@ -152,7 +152,22 @@ type InstitutePlacementParams = {
152
152
  }
153
153
 
154
154
 
155
- export function useNewInstitutePlacementList({id, user, cohort, queryConstraint, ql=DEFAULTQUERYLIMIT, inProgress}:InstitutePlacementParams) {
155
+ type NewInstitutePlacementParams = {
156
+ id?: string,
157
+ user: UserData,
158
+ cohort?: string,
159
+ queryConstraints?: QueryObjectConstraint,
160
+ ql?: number,
161
+ inProgress?: boolean,
162
+ view: "list"|"table",
163
+ filters?: FilterObject,
164
+ }
165
+
166
+
167
+
168
+
169
+
170
+ export function useOldInstitutePlacementList({id, user, cohort, queryConstraint, ql=DEFAULTQUERYLIMIT, inProgress}:InstitutePlacementParams) {
156
171
  const [loadMoreIcon, setLoadMoreIcon] = useState(true);
157
172
  const [query, setQuery] = useState<string>();
158
173
  const [initialQueryLimit, setInitialQueryLimit] = useState(ql);
@@ -165,7 +180,6 @@ export function useNewInstitutePlacementList({id, user, cohort, queryConstraint,
165
180
 
166
181
  const algoliaClient = algoliasearch(process.env.NODE_ENV === "development" ? "A0ZB50I7VS" : "XMPXCMUUOJ", user.algoliaKey);
167
182
  const placementsIndex = algoliaClient.initIndex("placements");
168
- const usersIndex = algoliaClient.initIndex("users");
169
183
 
170
184
  useEffect(() => {
171
185
  if (user.product !== "institutes" || user.userType !== "Staff") {
@@ -190,36 +204,6 @@ export function useNewInstitutePlacementList({id, user, cohort, queryConstraint,
190
204
  student: UserData;
191
205
  }} = {}
192
206
 
193
- let userSearchString = `userType:Students AND status:active AND oId:${user.oId} AND product:${user.product}`
194
- if (cohort) {
195
- userSearchString = userSearchString + ` AND cohort:${cohort}`;
196
- }
197
- if (user.product === "institutes" && user.userType === "Staff") {
198
- const searchStudentHits = await usersIndex.search<UserData>(query, {
199
- filters: userSearchString
200
- });
201
-
202
- if (searchStudentHits) {
203
- console.log("FOUND", searchStudentHits.hits.length, "students");
204
-
205
- await Promise.all(searchStudentHits.hits.map(async (hit) => {
206
- console.log("STUDENT", hit.objectID);
207
- const constraints = [...(cohort ? [where("cohort", "==", cohort)] : [])]
208
- if (inProgress !== undefined) {
209
- constraints.push(inProgress ? where("inProgress", "==", true) : where("completed", "==", true));
210
- }
211
- const fPlacements = await getPlacementsWhere({w: constraints, uid: hit.objectID, oId: hit.oId}) as {[key: string]: StudentPlacementData};
212
-
213
- console.log("PLACEMENTS", fPlacements)
214
- Object.entries(fPlacements).forEach(([k, v]) => {
215
- placementsFound[k] = {...v, student: hit};
216
- })
217
- }))
218
- }
219
- }
220
-
221
-
222
-
223
207
  let placementSearchString = `oId:${user.oId} AND ` + (inProgress !== undefined ? (inProgress ? "inProgress:true" : "completed:true" ) : "")
224
208
  if (cohort) {
225
209
  placementSearchString = placementSearchString + ` AND cohort:${cohort}`;
@@ -248,7 +232,6 @@ export function useNewInstitutePlacementList({id, user, cohort, queryConstraint,
248
232
  setPlacements(placementsFound);
249
233
  setLoadMoreIcon(false);
250
234
  }
251
-
252
235
  searchPlacements()
253
236
  }, [query]);
254
237
 
@@ -275,11 +258,13 @@ export function useNewInstitutePlacementList({id, user, cohort, queryConstraint,
275
258
  return;
276
259
  }
277
260
 
261
+
262
+
278
263
  setLoadMoreIcon(true);
279
264
 
280
265
  let fPlacements:{[key:string]: StudentPlacementData&{student:UserData}} = {};
281
266
 
282
- if (((user.userGroup === "admin" || (cohort && user.viewCohorts === "some" && user?.visibleCohorts?.split(",").includes(cohort as string)) || (user.viewCohorts === "all" && user.viewUsers === "all")))) {
267
+ if (((user.userGroup === "admin" || (cohort && user.viewCohorts === "some" && user?.visibleCohorts?.includes(cohort as string)) || (user.viewCohorts === "all" && user.viewStudents === "all")))) {
283
268
 
284
269
  const queryConstraintOrdered = Boolean(queryConstraints && queryConstraints.find((v) => v.type === "orderBy"));
285
270
 
@@ -295,16 +280,17 @@ export function useNewInstitutePlacementList({id, user, cohort, queryConstraint,
295
280
 
296
281
 
297
282
  const placementsQuery = await getPlacementsWhere({w: constraints, oId: oId, raw: true}) as QuerySnapshot;
298
-
283
+ console.log("PLACEMENTS RETRIEVED", placementsQuery.size);
299
284
 
300
285
  const placementsWithStudentData:([string, StudentPlacementData&{student:UserData}]|false)[] = placementsQuery.empty ? [] : await Promise.all(placementsQuery.docs.map(async (placement) => {
301
286
  const pData = placement.data() as StudentPlacementData;
302
287
  const student = pData.uid === user.id ? user : (await getUserById(pData.uid).catch(() => false)) as UserData;
303
-
304
- if (user.viewUsers === "some") {
288
+ console.log("STUDENT", student, "PLACEMENT", id);
289
+ // if (!student) return false;
290
+ if (user.viewStudents === "some") {
305
291
  if (!(user.studentFilter && user.studentFilterValues)) return false;
306
292
 
307
- if (!user.studentFilterValues.split(", ").includes(student.details[user.studentFilter])) {
293
+ if (!user.studentFilterValues.includes(student.details[user.studentFilter])) {
308
294
  return false;
309
295
  }
310
296
  }
@@ -332,11 +318,157 @@ export function useNewInstitutePlacementList({id, user, cohort, queryConstraint,
332
318
  }
333
319
  };
334
320
 
321
+
335
322
  return ({...{placements, loadMoreIcon, loadMorePlacements, setQuery, setInitialQueryLimit, reset, changeQueryConstraints}});
336
323
  }
337
324
 
338
325
 
339
- export function useOldInstitutePlacementList({user, cohort, queryConstraint, ql=DEFAULTQUERYLIMIT}:InstitutePlacementParams) {
326
+ export function useNewInstitutePlacementList({id, user, filters, view, cohort, queryConstraints, ql=DEFAULTQUERYLIMIT, inProgress}:NewInstitutePlacementParams) {
327
+ const [query, setQuery] = useState<QueryObject[]>();
328
+
329
+ const sorts:Sorts = {
330
+ ["Student Forename - Asc"]: {
331
+ value: "studentForename",
332
+ direction: "asc",
333
+ },
334
+ ["Student Forename - Desc"]: {
335
+ value: "studentForename",
336
+ direction: "desc",
337
+ },
338
+ ["Student Surname - Asc"]: {
339
+ value: "studentSurname",
340
+ direction: "asc",
341
+ },
342
+ ["Student Surname - Desc"]: {
343
+ value: "studentSurname",
344
+ direction: "desc",
345
+ },
346
+ ["Student Email - Asc"]: {
347
+ value: "studentEmail",
348
+ direction: "asc",
349
+ },
350
+ ["Student Email - Desc"]: {
351
+ value: "studentEmaile",
352
+ direction: "desc",
353
+ },
354
+ ["Provider email - Asc"]: {
355
+ value: "providerEmail",
356
+ direction: "asc",
357
+ },
358
+ ["Provider email - Desc"]: {
359
+ value: "providerEmail",
360
+ direction: "desc",
361
+ }
362
+ }
363
+
364
+ const additionalProcessing = async (k, placement:StudentPlacementData) => {
365
+ if (user.viewStudents === "some") {
366
+ if (!(user.studentFilter && user.studentFilterValues)) return false;
367
+
368
+ const student = await getUserById(placement.uid).catch(() => false) as UserData|false;
369
+
370
+ if (!student) return;
371
+
372
+ if (!user.studentFilterValues.includes(student.details[user.studentFilter])) {
373
+ return false;
374
+ }
375
+ }
376
+ return {...placement, id: k};
377
+ }
378
+
379
+
380
+ const {tableData, pageUp, pageDown, setFilters, page, setView, loading, updateSearch, updateSort, sort} = useDataViewerPaginator({view, filters, sorts, queryLimit: ql, data: query, additionalEntryProcessing: additionalProcessing, onSearch: async (s, sort, page, filters, limit) => await algoliaPlacementSearch(user, s, sort, page, filters, limit, cohort, inProgress)})
381
+
382
+ useEffect(() => {
383
+ // Sets the query of for the DataViewerPaginator
384
+ if(user.product !== "institutes" || user.userType !== "Staff") {
385
+ setQuery(undefined);
386
+ return;
387
+ }
388
+ if (((user.userGroup === "admin" || (cohort && user.viewCohorts === "some" && user?.visibleCohorts?.includes(cohort as string)) || (user.viewCohorts === "all" && user.viewStudents === "all")))) {
389
+ const constraints:QueryObjectConstraint = [["oId", "==", user.oId], ["draft", "==", false]];
390
+
391
+ cohort && constraints.push(["cohort", "==", cohort]);
392
+ queryConstraints && constraints.unshift(...queryConstraints);
393
+ inProgress !== undefined && constraints.push(["inProgress", "==", inProgress]);
394
+
395
+ setQuery([{
396
+ path: ["placements"],
397
+ where: constraints
398
+ }])
399
+ return;
400
+ }
401
+ setQuery(undefined)
402
+ }, [user, queryConstraints, cohort])
403
+
404
+ return {tableData, page, loading, updateSearch, setFilters, setView, pageUp, pageDown, sorts, updateSort, sort}
405
+ }
406
+
407
+
408
+ const algoliaPlacementSearch = async (user: UserData, query?: string, sort?: [string, {value: string, direction: "asc"|"desc"}], page?: number, filters?: FilterObject, limit?: number, cohort?: string, inProgress?: boolean) => {
409
+ const algoliaClient = algoliasearch(process.env.NODE_ENV === "development" ? "A0ZB50I7VS" : "XMPXCMUUOJ", user.algoliaKey);
410
+ const placementsIndex = algoliaClient.initIndex(sort ? Object.values(sort[1]).join("_") : "placements");
411
+ // const usersIndex = algoliaClient.initIndex("users");
412
+
413
+ // let userSearchString = `userType:Students AND status:active AND oId:${user.oId} AND product:${user.product}`
414
+ // if (cohort) {
415
+ // userSearchString = userSearchString + ` AND cohort:${cohort}`;
416
+ // }
417
+
418
+ // if (user.product === "institutes" && user.userType === "Staff") {
419
+ // const searchStudentHits = await usersIndex.search<UserData>(query, {
420
+ // filters: userSearchString,
421
+ // hitsPerPage: limit,
422
+ // page: page
423
+ // });
424
+
425
+ // if (searchStudentHits) {
426
+ // console.log("FOUND", searchStudentHits.hits.length, "students");
427
+
428
+ // await Promise.all(searchStudentHits.hits.map(async (hit) => {
429
+ // console.log("STUDENT", hit.objectID);
430
+ // const constraints = [...(cohort ? [where("cohort", "==", cohort)] : [])]
431
+ // if (inProgress !== undefined) {
432
+ // constraints.push(inProgress ? where("inProgress", "==", true) : where("completed", "==", true));
433
+ // }
434
+ // const fPlacements = await getPlacementsWhere({w: constraints, uid: hit.objectID, oId: hit.oId}) as {[key: string]: StudentPlacementData};
435
+
436
+ // console.log("PLACEMENTS", fPlacements)
437
+ // Object.entries(fPlacements).forEach(([k, v]) => {
438
+ // placementsFound[k] = {...v, student: hit};
439
+ // })
440
+ // }))
441
+ // }
442
+ // }
443
+
444
+
445
+ let placementSearchString = `oId:${user.oId} AND ` + (inProgress !== undefined ? (inProgress ? "inProgress:true" : "completed:true" ) : "")
446
+ if (cohort) {
447
+ placementSearchString = placementSearchString + ` AND cohort:${cohort}`;
448
+ }
449
+ filters && Object.entries(filters).filter(([, filter]) => filter.value).map(([id, filter]) => {
450
+ placementSearchString = placementSearchString + ` AND ${id}:${filter.value}`;
451
+ });
452
+
453
+ const options = {
454
+ filters: placementSearchString,
455
+ hitsPerPage: limit,
456
+ page: page ? page - 1 : undefined,
457
+ }
458
+
459
+ const searchPlacementHits = await placementsIndex.search<StudentPlacementData>(query || "", options);
460
+
461
+ console.log(searchPlacementHits.hits);
462
+
463
+ const i = (searchPlacementHits ? (await Promise.all(searchPlacementHits.hits.map(async (hit) => {
464
+ return [hit.objectID, hit];
465
+ }))) : []).filter((e) => e !== undefined) as [string, StudentPlacementData][];
466
+
467
+ return Object.fromEntries(i)
468
+ }
469
+
470
+
471
+ export function useVeryOldInstitutePlacementList({user, cohort, queryConstraint, ql=DEFAULTQUERYLIMIT}:InstitutePlacementParams) {
340
472
  const [loadMoreIcon, setLoadMoreIcon] = useState(true);
341
473
  const [query, setQuery] = useState("");
342
474
  const [initialQueryLimit, setInitialQueryLimit] = useState(ql);
@@ -391,7 +523,7 @@ export function useOldInstitutePlacementList({user, cohort, queryConstraint, ql=
391
523
 
392
524
  let fPlacements:{[key:string]: StudentPlacementData&{student:UserData}} = {};
393
525
 
394
- if (((user.userGroup === "admin" || (cohort && user.viewCohorts === "some" && user?.visibleCohorts?.split(", ").includes(cohort as string)) || (user.viewCohorts === "all" && user.viewUsers === "all") || query) && (fStartPlacementAfter && fStartPlacementAfter[0] === "placement"))) {
526
+ if (((user.userGroup === "admin" || (cohort && user.viewCohorts === "some" && user?.visibleCohorts?.includes(cohort as string)) || (user.viewCohorts === "all" && user.viewStudents === "all") || query) && (fStartPlacementAfter && fStartPlacementAfter[0] === "placement"))) {
395
527
  const queryConstraintOrdered = Boolean(queryConstraints && queryConstraints.find((v) => v.type === "orderBy"));
396
528
 
397
529
  const constraints:QueryConstraint[] = fStartPlacementAfter?.length === 4 && fStartPlacementAfter[1] ?
@@ -413,10 +545,10 @@ export function useOldInstitutePlacementList({user, cohort, queryConstraint, ql=
413
545
  const pData = placement.data() as StudentPlacementData;
414
546
  const student = pData.uid === user.id ? user : (await getUserById(pData.uid).catch(() => false)) as UserData;
415
547
 
416
- if (user.viewUsers === "some") {
548
+ if (user.viewStudents === "some") {
417
549
  if (!(user.studentFilter && user.studentFilterValues)) return false;
418
550
 
419
- if (!user.studentFilterValues.split(", ").includes(student.details[user.studentFilter])) {
551
+ if (!user.studentFilterValues.includes(student.details[user.studentFilter])) {
420
552
  return false;
421
553
  }
422
554
  }
@@ -446,7 +578,7 @@ export function useOldInstitutePlacementList({user, cohort, queryConstraint, ql=
446
578
  }
447
579
  }
448
580
 
449
- if (!(user.studentFilterValues && user.studentFilter && user.viewUsers === "some")) {
581
+ if (!(user.studentFilterValues && user.studentFilter && user.viewStudents === "some")) {
450
582
  console.log("fenaibn");
451
583
  return;
452
584
  };
@@ -468,7 +600,7 @@ export function useOldInstitutePlacementList({user, cohort, queryConstraint, ql=
468
600
  );
469
601
 
470
602
  if (ffStartPlacementAfter && ffStartPlacementAfter[3]) {
471
- const currentFilter = user?.studentFilterValues?.split(", ")[ffStartPlacementAfter[3]];
603
+ const currentFilter = user?.studentFilterValues?.[ffStartPlacementAfter[3]];
472
604
 
473
605
  if (currentFilter && user.studentFilter) {
474
606
  constraints.push(where(user.studentFilter, "==", currentFilter));
@@ -481,7 +613,7 @@ export function useOldInstitutePlacementList({user, cohort, queryConstraint, ql=
481
613
  // console.log("student", student);
482
614
 
483
615
  // If there is no students but more filters
484
- if (!student && ffStartPlacementAfter && ffStartPlacementAfter[3] + 1 < (user?.studentFilterValues?.split(", ") || []).length) {
616
+ if (!student && ffStartPlacementAfter && ffStartPlacementAfter[3] + 1 < (user?.studentFilterValues || []).length) {
485
617
  // console.log("No more students. Calling recursion");
486
618
  return await getStudentPlacements(["student", undefined, undefined, ffStartPlacementAfter[3]+1], ffPlacements);
487
619
  }
@@ -726,7 +858,7 @@ export function useFilterTablePaginator({data}:{data:{[key:string]:{[key:string]
726
858
  const listenForUpdates = () => {
727
859
  if (!Object.keys(itemList).length) return;
728
860
  console.log("Fetching filter table updates")
729
- const itemListUpdateQuery = query(collection(db, "users"), where(documentId(), "in", Object.keys(itemList)));
861
+ const itemListUpdateQuery = query(collection(db, ...(querySchema.path as [any])), where(documentId(), "in", Object.keys(itemList)));
730
862
  const itemUpdateSnapshot = onSnapshot(itemListUpdateQuery, (querySnapshot) => {
731
863
  querySnapshot.docs.forEach((doc) => {
732
864
  setTableData((data) => {
@@ -918,9 +1050,9 @@ export function usePlacementListingPaginator({data, user}:{data:QueryObject[], u
918
1050
 
919
1051
  const finalData = Object.fromEntries(await Promise.all(Object.entries(itemList).map(async ([k, v]:[string, SavedPlacement]) => {
920
1052
  const placement = await firebaseQuery.getDocData(["placementListings", v.placementId || ""]) as PlacementListing;
921
- const address = await firebaseQuery.getDocData(["addresses", placement.addressId || ""]);
922
- const provider = await firebaseQuery.getDocData(["providers", placement.providerId || ""]) as ProviderData;
923
- const status = getPlacementStatus(provider, placement, v);
1053
+ const address = await firebaseQuery.getDocData(["addresses", placement.addressId || ""]).catch(() => ({["address-line1"]: "Unknown address"}));
1054
+ const provider = await firebaseQuery.getDocData(["providers", placement.providerId || ""]).catch(() => ({name: "Unknown"})) as ProviderData;
1055
+ const status = provider.name !== "Unknown" ? getPlacementStatus(provider, placement, v) : "Deleted";
924
1056
  return [k, {...address, ...provider, ...placement, email: placement.providerEmail, status: status, savedPlacement: v}];
925
1057
  })));
926
1058
  setTableData(finalData);
@@ -963,13 +1095,15 @@ type UserPaginatorParams = {
963
1095
  cohort?: string,
964
1096
  data: QueryObject[],
965
1097
  search?: string,
966
- userType: "Staff"|"Students"
1098
+ userType: "Staff"|"Students",
1099
+ sort?: string
967
1100
  }
968
1101
 
969
- export function useCohortUserPaginator({user, cohort, data, search, userType}:UserPaginatorParams) {
1102
+ export function useCohortUserPaginator({user, cohort, data, search, userType, sort}:UserPaginatorParams) {
970
1103
  const [tableData, setTableData] = useState<{[key:string]:{[key:string]: unknown}}>({});
971
- const [queryAnchor, setQueryAnchor] = useState<{startKey: string, endKey: string, startQueryPos: number, endQueryPos: number}>({startKey: "", endKey: "", startQueryPos: 0, endQueryPos: 0});
1104
+ const [queryAnchor, setQueryAnchor] = useState<{startDoc?: QueryDocumentSnapshot, endDoc?: QueryDocumentSnapshot, startQueryPos: number, endQueryPos: number}>({startDoc: undefined, endDoc: undefined, startQueryPos: 0, endQueryPos: 0});
972
1105
  const [filters, setFilters] = useState<{[key:string]: unknown}>();
1106
+ const [sortResultsBy, setSortResultsBy] = useState<string|undefined>(sort);
973
1107
  const [prevEntryIds, setPrevEntryIds] = useState<{[key:string]:number}>({});
974
1108
  const [page, setPage] = useState([1, 0]);
975
1109
  const [dataListenerUnsubscribe, setDataListenerUnsubscribe] = useState<Unsubscribe|undefined>();
@@ -978,6 +1112,15 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
978
1112
 
979
1113
  const algoliaClient = algoliasearch(process.env.NODE_ENV === "development" ? "A0ZB50I7VS" : "XMPXCMUUOJ", user.algoliaKey);
980
1114
  const usersIndex = algoliaClient.initIndex("users");
1115
+
1116
+ const sortOptions = {
1117
+ ["Forename - Asc"]: ["details.forename", "asc"],
1118
+ ["Forename - Desc"]: ["details.forename", "desc"],
1119
+ ["Surname - Asc"]: ["details.surname", "desc"],
1120
+ ["Surname - Desc"]: ["details.surname", "asc"],
1121
+ ["Email - Asc"]: ["email", "asc"],
1122
+ ["Email - Desc"]: ["email", "desc"],
1123
+ }
981
1124
 
982
1125
  useEffect(() => {
983
1126
  if (user.userType !== "Staff") {
@@ -988,7 +1131,7 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
988
1131
  if (
989
1132
  (!user.viewCohorts && user.userGroup !== "admin") ||
990
1133
  user.viewCohorts === "none" ||
991
- (user.viewCohorts === "some" && cohort !== "all" && !user.visibleCohorts?.split(",").includes(cohort || ""))) {
1134
+ (user.viewCohorts === "some" && cohort !== "all" && !user.visibleCohorts?.includes(cohort || ""))) {
992
1135
  setQueries(undefined);
993
1136
  return;
994
1137
  }
@@ -1006,12 +1149,12 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1006
1149
  constraints.push(where("oId", "==", user.oId));
1007
1150
  cohort && cohort !== "all" && constraints.push(where("cohort", "==", cohort));
1008
1151
 
1009
- if (user.userGroup === "admin" || user.viewUsers === "all") {
1152
+ if (user.userGroup === "admin" || user.viewStudents === "all") {
1010
1153
  finalConstraints.push(constraints);
1011
1154
  continue
1012
1155
  }
1013
1156
  if (!user.studentFilter || !user.studentFilterValues) continue;
1014
- user.studentFilterValues.split(", ").forEach((filterValue) => {
1157
+ user.studentFilterValues.forEach((filterValue) => {
1015
1158
  user.studentFilter && finalConstraints.push([...constraints, where("details."+user.studentFilter, "==", filterValue)])
1016
1159
  })
1017
1160
  };
@@ -1022,9 +1165,9 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1022
1165
 
1023
1166
 
1024
1167
  const getDataFromQuery = async (
1025
- itemList: {[key: string]: any} = {}, currentQueryAnchor=queryAnchor, cursorDirection?:"increase"|"decrease"|undefined, prevEntries=prevEntryIds, loadMoreFromQuery=false):Promise<any> => {
1168
+ itemList: {[key: string]: QueryDocumentSnapshot<DocumentData>} = {}, currentQueryAnchor=queryAnchor, cursorDirection?:"increase"|"decrease"|undefined, prevEntries=prevEntryIds, loadMoreFromQuery=false):Promise<any> => {
1026
1169
 
1027
- if (!queries) {
1170
+ if (!queries?.length) {
1028
1171
  setTableData({});
1029
1172
  return;
1030
1173
  }
@@ -1043,18 +1186,41 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1043
1186
  console.log("mConstraints", mConstraints);
1044
1187
  const fConstraints = [...mConstraints];
1045
1188
 
1189
+ let addOrderBy = true;
1046
1190
  filters && Object.entries(filters).forEach(([key, value]) => {
1047
- if (typeof value === "object") {
1048
- fConstraints.push(value as QueryFieldFilterConstraint);
1191
+ if (Array.isArray(value)) {
1192
+ value.forEach((v) => {
1193
+ if (typeof v === "object") {
1194
+ if (v instanceof QueryOrderByConstraint) {
1195
+ addOrderBy = false;
1196
+ }
1197
+ fConstraints.push(v as QueryFieldFilterConstraint|QueryOrderByConstraint);
1198
+ } else {
1199
+ fConstraints.push(where(key, "==", v));
1200
+ }
1201
+ })
1202
+ } else if (typeof value === "object") {
1203
+ if (value instanceof QueryOrderByConstraint) {
1204
+ addOrderBy = false;
1205
+ }
1206
+ fConstraints.push(value as QueryFieldFilterConstraint|QueryOrderByConstraint);
1049
1207
  } else {
1050
1208
  fConstraints.push(where(key, "==", value));
1051
1209
  }
1052
1210
  });
1053
- fConstraints.push(orderBy(documentId()));
1211
+ console.log("Add order by", addOrderBy);
1212
+ if (addOrderBy) {
1213
+ if (sortResultsBy) {
1214
+ fConstraints.push(orderBy(sortOptions[sortResultsBy][0], sortOptions[sortResultsBy][1]));
1215
+ } else {
1216
+ fConstraints.push(orderBy(documentId()));
1217
+
1218
+ }
1219
+ }
1054
1220
 
1055
1221
 
1056
1222
  if (page[0] > page[1] && !cursorDirection) { // Going up
1057
- currentQueryAnchor.endKey && fConstraints.push(startAfter(currentQueryAnchor.endKey));
1223
+ currentQueryAnchor.endDoc && fConstraints.push(startAfter(currentQueryAnchor.endDoc));
1058
1224
  fConstraints.push(limit(10));
1059
1225
  if (!loadMoreFromQuery) {
1060
1226
  currentQueryAnchor = {...currentQueryAnchor, startQueryPos: currentQueryAnchor.endQueryPos};
@@ -1064,10 +1230,10 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1064
1230
  currentQueryAnchor = {...currentQueryAnchor, endQueryPos: currentQueryAnchor.startQueryPos};
1065
1231
  }
1066
1232
  fConstraints.push(limitToLast(10));
1067
- if (currentQueryAnchor.startKey) {
1068
- currentQueryAnchor.startKey && fConstraints.push(endBefore(currentQueryAnchor.startKey));
1233
+ if (currentQueryAnchor.startDoc) {
1234
+ currentQueryAnchor.startDoc && fConstraints.push(endBefore(currentQueryAnchor.startDoc));
1069
1235
  } else {
1070
- currentQueryAnchor.startKey && fConstraints.push(endAt(currentQueryAnchor.startKey));
1236
+ currentQueryAnchor.startDoc && fConstraints.push(endAt(currentQueryAnchor.startDoc));
1071
1237
  }
1072
1238
  } else {
1073
1239
  if (cursorDirection === "decrease") {
@@ -1083,7 +1249,7 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1083
1249
  // console.log(queryId, "constraints", constraints)
1084
1250
 
1085
1251
  const q = query(collection(db, "users"), ...(constraints));
1086
- const queryResults:{[key:string]: unknown} = {};
1252
+ const queryResults:{[key:string]: QueryDocumentSnapshot<DocumentData>} = {};
1087
1253
 
1088
1254
  const queryData = await getDocs(q);
1089
1255
 
@@ -1115,9 +1281,7 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1115
1281
  return;
1116
1282
  }
1117
1283
 
1118
- const item = doc.data();
1119
- item.id = doc.id;
1120
- queryResults[doc.id] = item;
1284
+ queryResults[doc.id] = doc;
1121
1285
  index = index+1;
1122
1286
 
1123
1287
  if (prevEntries[doc.id]) return;
@@ -1141,7 +1305,7 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1141
1305
  }
1142
1306
 
1143
1307
  if (Object.keys(itemList).length < 10 && queryData.size === 10) {
1144
- return getDataFromQuery(itemList, {...currentQueryAnchor, startKey: Object.keys(itemList)[0], endKey: Object.keys(itemList).slice(-1)[0]}, undefined, prevEntries, true);
1308
+ return getDataFromQuery(itemList, {...currentQueryAnchor, startDoc: Object.values(itemList)[0], endDoc: Object.values(itemList).slice(-1)[0]}, undefined, prevEntries, true);
1145
1309
  }
1146
1310
 
1147
1311
  if (queryData.size === 0 &&
@@ -1170,8 +1334,8 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1170
1334
  };
1171
1335
  listenForUpdates();
1172
1336
 
1173
- setQueryAnchor({...currentQueryAnchor, startKey: Object.keys(itemList)[0], endKey: Object.keys(itemList).slice(-1)[0]});
1174
- setTableData(itemList);
1337
+ setQueryAnchor({...currentQueryAnchor, startDoc: Object.values(itemList)[0], endDoc: Object.values(itemList).slice(-1)[0]});
1338
+ setTableData(Object.fromEntries(Object.entries(itemList).map(([k, v]) => [k, {id: k, ...v.data()}])));
1175
1339
  };
1176
1340
 
1177
1341
  const searchUsers = async () => {
@@ -1215,10 +1379,10 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1215
1379
  console.log("Set page");
1216
1380
 
1217
1381
  setTableData({});
1218
- setQueryAnchor({startKey: "", endKey: "", startQueryPos: 0, endQueryPos: 0});
1382
+ setQueryAnchor({startQueryPos: 0, endQueryPos: 0});
1219
1383
  setPrevEntryIds({});
1220
1384
  dataListenerUnsubscribe && dataListenerUnsubscribe();
1221
- }, [filters, search]);
1385
+ }, [filters, search, sortResultsBy]);
1222
1386
 
1223
1387
  // Fetch new data when queries or page change
1224
1388
  useEffect(() => {
@@ -1231,7 +1395,10 @@ export function useCohortUserPaginator({user, cohort, data, search, userType}:Us
1231
1395
  dataListenerUnsubscribe && dataListenerUnsubscribe();
1232
1396
  }, [page, queries, prevSearch]);
1233
1397
 
1234
- return ({...{tableData, setPage, page, setFilters}});
1398
+
1399
+ const setSort = (s: string) => setSortResultsBy(s);
1400
+
1401
+ return ({...{tableData, setPage, page, setFilters, setSort, sortOptions: Object.keys(sortOptions), sortBy: sortResultsBy}});
1235
1402
  }
1236
1403
 
1237
1404
 
@@ -1356,7 +1523,7 @@ export function usePublicPlacementListingLoader({providerId, number=5}:PublicPla
1356
1523
  const applicantWorkflow = (await firebaseQuery.getDocData(["applicantWorkflows", itemObj.applicantWorkflowId]) as ApplicantWorkflow).workflow.filter((i) => i.id === 1)[0];
1357
1524
  const applicantFiles = applicantWorkflow.files ? Object.fromEntries(await Promise.all(applicantWorkflow.files?.map(async (fileId) => {
1358
1525
  const file = await firebaseQuery.getDocData(["files", fileId]);
1359
- file.url = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`));
1526
+ file.url = await getDownloadURL(ref(storage, `providers/${itemObj.providerId}/${file.fileName}`));
1360
1527
 
1361
1528
  return [fileId, file];
1362
1529
  }))) : [];
@@ -1408,7 +1575,7 @@ export type ApplicationHookParams = {
1408
1575
  profileUrl: string | undefined;
1409
1576
  successPopup: "submitted" | "draftSaved" | "stageComplete" | "outcome" | undefined;
1410
1577
  fApplicationId: string | undefined;
1411
- fListing: PlacementListing | undefined;
1578
+ fListing: PlacementListing | false | undefined;
1412
1579
  student: UserData | undefined;
1413
1580
  fProvider: {
1414
1581
  details?: ProviderData;
@@ -1458,13 +1625,14 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1458
1625
  const [fApplication, setFApplication] = useState<Partial<Application>>(application ? applicationWithoutAdditionalData : {
1459
1626
  uid: user.userType === "Students" ? user.id : undefined,
1460
1627
  listingId: listingId,
1628
+ addressId: listing?.addressId,
1461
1629
  stage: 1,
1462
1630
  reqUserType: "Students",
1463
1631
  status: "draft"});
1464
1632
  const [fApplicationId, setFApplicationId] = useState<string|undefined>(applicationId);
1465
1633
  const [draftSaved, setDraftSaved] = useState(false);
1466
1634
  const [fProvider, setFProvider] = useState<{details?: ProviderData, profile?: string, id?: string}|undefined>(provider);
1467
- const [fListing, setFListing] = useState<PlacementListing|undefined>(listing);
1635
+ const [fListing, setFListing] = useState<PlacementListing|false|undefined>(Object.keys(listing || {}).length > 5 ? listing : undefined);
1468
1636
  const [student, setStudent] = useState<UserData|undefined>(user.userType === "Students" ? user : undefined);
1469
1637
  const [profileUrl, setProfileUrl] = useState<string>();
1470
1638
  const [successPopup, setSuccessPopup] = useState<"submitted"|"draftSaved"|"stageComplete"|"outcome">();
@@ -1476,34 +1644,44 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1476
1644
  console.log("Checking ID")
1477
1645
  if (!listingId) return;
1478
1646
  console.log("Getting listing")
1647
+ console.log("LISTING PARAM", listing, Object.keys(listing || {}).length > 5);
1479
1648
 
1480
- const listingData = listing || await firebaseQuery.getDocData(["placementListings", listingId]) as PlacementListing;
1481
- const address = (user.product === "providers" && orgContext) ? orgContext.addresses[listingData.addressId || ""] : await firebaseQuery.getDocData(["addresses", listingData.addressId as string]) as Address;
1482
- const workflow = (user.product === "providers" && orgContext) ? orgContext.applicantWorkflows[listingData.applicantWorkflowId || ""] as ApplicantWorkflow : await firebaseQuery.getDocData(["applicantWorkflows", listingData.applicantWorkflowId as string]) as ApplicantWorkflow;
1649
+ const listingData = (Object.keys(listing || {}).length > 5) ? listing : (await firebaseQuery.getDocData(["placementListings", listingId]).catch(() => false) as PlacementListing|false);
1650
+
1651
+ console.log("LISTINGDATA", listingData, Object.keys(listing || {}).length > 5 ? {a: "string"} : "AAA");
1652
+
1653
+
1654
+
1655
+ const address = listingData ? (user.product === "providers" && orgContext) ? orgContext.addresses[listingData.addressId || ""] : await firebaseQuery.getDocData(["addresses", listingData.addressId as string]) as Address : undefined;
1656
+ const workflow = listingData ? (user.product === "providers" && orgContext) ? orgContext.applicantWorkflows[listingData.applicantWorkflowId || ""] as ApplicantWorkflow : await firebaseQuery.getDocData(["applicantWorkflows", listingData.applicantWorkflowId as string]) as ApplicantWorkflow : undefined;
1483
1657
 
1484
- workflow.workflow = await Promise.all(workflow.workflow.map(async (s) => {
1485
- const applicantFiles = s.files ? Object.fromEntries(await Promise.all(s.files?.map(async (fileId: string) => {
1486
- const file = await firebaseQuery.getDocData(["files", fileId]);
1487
- file.url = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`));
1488
-
1489
- return [fileId, file];
1490
- }))) : [];
1491
- const applicantForms = s.forms ? Object.fromEntries(await Promise.all(s.forms?.map(async (formId: string) => {
1492
- return [formId, await firebaseQuery.getDocData(["forms", formId])];
1493
- }))) : [];
1494
-
1495
- return {...s, viewableFiles: applicantFiles, formDetails: applicantForms};
1496
- }));
1658
+ if (workflow && listingData) {
1659
+ workflow.workflow = await Promise.all(workflow.workflow.map(async (s) => {
1660
+ const applicantFiles = s.files ? Object.fromEntries(await Promise.all(s.files?.map(async (fileId: string) => {
1661
+ const file = await firebaseQuery.getDocData(["files", fileId]);
1662
+ file.url = await getDownloadURL(ref(storage, `providers/${listingData?.providerId}/${file.fileName}`));
1663
+
1664
+ return [fileId, file];
1665
+ }))) : [];
1666
+ const applicantForms = s.forms ? Object.fromEntries(await Promise.all(s.forms?.map(async (formId: string) => {
1667
+ return [formId, await firebaseQuery.getDocData(["forms", formId])];
1668
+ }))) : [];
1669
+
1670
+ return {...s, viewableFiles: applicantFiles, formDetails: applicantForms};
1671
+ }));
1672
+ delete workflow.id;
1673
+ }
1674
+ if (address) {
1675
+ delete address.id;
1676
+ }
1497
1677
 
1498
- delete address.id;
1499
- delete workflow.id;
1500
1678
  console.log("Setting listing")
1501
- setFListing({...listingData, ...address, applicantWorkflow: workflow.workflow});
1679
+ setFListing((listingData && workflow) ? {...listingData, ...address, applicantWorkflow: workflow.workflow} as PlacementListing : false);
1680
+
1502
1681
 
1503
- console.log("Getting provider", listingData?.providerId);
1504
1682
  if ((fProvider?.id === application?.providerId) && user.product === "providers") {
1505
1683
  setFProvider({details: orgContext?.details, id: user.oId});
1506
- } else if (listingData?.providerId) {
1684
+ } else if (listingData && listingData?.providerId) {
1507
1685
  console.log("Getting provider from DB");
1508
1686
  const provider = await firebaseQuery.getDocData(["providers", listingData.providerId]) as ProviderData;
1509
1687
  console.log("Provider", provider);
@@ -1525,7 +1703,7 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1525
1703
  }, []);
1526
1704
 
1527
1705
  useEffect(() => {
1528
- setFListing(listing);
1706
+ setFListing(Object.keys(listing || {}).length > 5 ? listing : undefined);
1529
1707
  }, [listing]);
1530
1708
 
1531
1709
  useEffect(() => {
@@ -1550,7 +1728,18 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1550
1728
  });
1551
1729
  return;
1552
1730
  }
1553
- if (applicationId) setFApplicationId(applicationId);
1731
+ if (applicationId) {
1732
+ setFApplicationId(applicationId);
1733
+ if (application) {
1734
+ let applicationWithoutAdditionalData = {...(application || {})} as any;
1735
+ delete applicationWithoutAdditionalData.listing;
1736
+ delete applicationWithoutAdditionalData.address;
1737
+ delete applicationWithoutAdditionalData.provider;
1738
+ setFApplication(applicationWithoutAdditionalData);
1739
+ } else {
1740
+ firebaseQuery.getDocData(["applications", applicationId]).then(setFApplication);
1741
+ }
1742
+ }
1554
1743
  }, [application, applicationId]);
1555
1744
 
1556
1745
  const getCurrentStage = async (stage: number): Promise<{
@@ -1565,6 +1754,8 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1565
1754
  } | undefined;
1566
1755
  };
1567
1756
  }> => {
1757
+ console.log("fLSITING CURRENT STAGE", fListing);
1758
+ if (!fListing) throw new Error("Listing deleted");
1568
1759
  if (!fListing?.applicantWorkflowId) throw new Error("No workflow stage");
1569
1760
 
1570
1761
  const mApplicantWorkflow = (await firebaseQuery.getDocData(["applicantWorkflows", fListing.applicantWorkflowId]) as ApplicantWorkflow);
@@ -1575,7 +1766,7 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1575
1766
 
1576
1767
  const applicantFiles = stageObj.files ? Object.fromEntries(await Promise.all(stageObj.files?.map(async (fileId) => {
1577
1768
  const file = await firebaseQuery.getDocData(["files", fileId]);
1578
- file.url = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`));
1769
+ file.url = await getDownloadURL(ref(storage, `providers/${mApplicantWorkflow?.oId}/${file.fileName}`));
1579
1770
 
1580
1771
  return [fileId, file];
1581
1772
  }))) : [];
@@ -1615,11 +1806,12 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1615
1806
 
1616
1807
  const addApplication = async () => {
1617
1808
  console.log("ADDING APPLICATION");
1618
- if (!fListing?.id || !fProvider?.id || !student?.id) return;
1809
+ if (!fListing || !fListing?.id || !fProvider?.id || !student?.id) return;
1619
1810
 
1620
1811
  const applicationData = {
1621
1812
  uid: student.id,
1622
1813
  listingId: fListing?.id,
1814
+ addressId: fListing.addressId,
1623
1815
  applicantWorkflowId: fListing?.applicantWorkflowId,
1624
1816
  providerId: fProvider.id,
1625
1817
  stage: 1,
@@ -1637,9 +1829,9 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1637
1829
  getUploadedFiles();
1638
1830
 
1639
1831
  console.log("Checking IDs");
1640
- console.log(fListing?.id, fProvider?.id, student?.id);
1641
- if (!fListing?.id || !fProvider?.id || !student?.id) return;
1642
- if (user.product === "providers") throw new Error("Providers cannot create applications");
1832
+
1833
+ if (!fListing || !fListing?.id || !fProvider?.id || !student?.id) return;
1834
+ if (user.product === "providers") return;
1643
1835
 
1644
1836
  console.log("Checking dates and sections");
1645
1837
  if (!fApplication.completedSections && !fApplication.startDate && !fApplication.endDate) return;
@@ -1709,6 +1901,7 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1709
1901
  const viewFile = (file: string, onOpen: (url: string) => void) => {
1710
1902
  if (fApplication.reqUserType !== user.userType) return;
1711
1903
  if (fApplication.stage === undefined) throw new Error("Missing applciation stage.")
1904
+ if (!fListing) throw new Error("No associated listing.");
1712
1905
 
1713
1906
  const viewableFiles = fListing?.applicantWorkflow?.find((stage) => stage.id === fApplication.stage)?.viewableFiles;
1714
1907
 
@@ -1763,21 +1956,29 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1763
1956
  openSuccessPopup("draftSaved")
1764
1957
  return;
1765
1958
  }
1959
+
1960
+ if (!fApplicationId) return;
1766
1961
 
1767
1962
  // Check all items have been filled in.
1768
1963
  if (!fApplication.startDate || !fApplication.endDate) throw new Error("Please select dates for your placement.");
1769
1964
 
1770
1965
  await executeCallable("applications-submit", {applicationId: fApplicationId});
1966
+
1967
+ const newApplication = await firebaseQuery.getDocData(["applications", fApplicationId]) as Application;
1968
+ setFApplication(newApplication);
1771
1969
  openSuccessPopup("submitted")
1772
1970
  };
1773
1971
 
1774
1972
  const progressStage = async (type: number|"accept"|"reject", e?: {feedback?: string}) => {
1775
1973
  // Check all stages completed.
1974
+ if (!fApplicationId) return;
1776
1975
  if (!currentStageComplete) throw new Error("Complete all forms before submitting.");
1777
1976
  if (fApplication.reqUserType !== user.userType) throw new Error(`Incorrect user type. Expected ${fApplication.reqUserType}, got: ${user.userType}`);
1778
1977
  if (fApplication.stage === undefined) throw new Error("Missing applciation stage.")
1779
1978
 
1780
1979
  await executeCallable("applications-changeStage", {applicationId: fApplicationId, type: type, feedback: e?.feedback});
1980
+ const newApplication = await firebaseQuery.getDocData(["applications", fApplicationId]) as Application;
1981
+ setFApplication(newApplication);
1781
1982
  };
1782
1983
 
1783
1984
 
@@ -1795,8 +1996,6 @@ export function useProposePlacementRenderer({user, orgContext, placement}:
1795
1996
  const [student, setStudent] = useState<UserData|undefined>((user && (user?.userType === "Students")) ? user : undefined);
1796
1997
  const [cohort, setCohort] = useState<CohortData>();
1797
1998
  const [complete, setComplete] = useState(false);
1798
-
1799
-
1800
1999
  const [stage, setStage] = useState<"provider"|"address"|"dates"|"review">("dates");
1801
2000
 
1802
2001
  const sections:Array<"dates"|"provider"|"address"|"review"> = ["dates", "provider", "address", "review"];
@@ -1865,16 +2064,33 @@ export function useProposePlacementRenderer({user, orgContext, placement}:
1865
2064
  setFormData(undefined);
1866
2065
  };
1867
2066
 
1868
- const proposePlacement = async (draft=false) => {
2067
+ const proposePlacement = async (draft = false) => {
2068
+ const getPlacementStatus = (startDate: Date, endDate: Date) => {
2069
+ const today = new Date()
2070
+ if (startDate <= today && endDate >= today) return { active: !draft ? true : false, inProgress: true, completed: false};
2071
+ else if (endDate <= today) return { completed: true, inProgress: false, active: false};
2072
+ return { completed: false, inProgress: true, active: false };
2073
+ }
2074
+
1869
2075
  if (!formData || !student) {
1870
2076
  throw new Error("Cannot find placement details.");
1871
2077
  }
1872
- console.log("formData", formData);
1873
-
1874
- if (formData.id && formData.uid && formData.id && draft === formData.draft) {
1875
- return await firebaseQuery.update(["placements", formData.id], formData);
2078
+
2079
+ try {
2080
+ console.log("formData", formData);
2081
+ const status = getPlacementStatus(new Date(formData.startDate), new Date(formData.endDate));
2082
+ const newFormData = {...formData, ...status}
2083
+
2084
+ if (newFormData.id && newFormData.uid && draft === newFormData.draft) await firebaseQuery.update(["placements", newFormData.id], newFormData);
2085
+ else await addPlacement(newFormData, student.id, draft)
2086
+ setComplete(true);
2087
+ return {status}
2088
+
2089
+ } catch (error) {
2090
+ console.log("Error:", error);
2091
+ throw error;
1876
2092
  }
1877
-
2093
+ /*
1878
2094
  console.log("STUDENTID", student.id);
1879
2095
 
1880
2096
  return await addPlacement(formData, student.id, draft).catch((e) => {
@@ -1885,9 +2101,9 @@ export function useProposePlacementRenderer({user, orgContext, placement}:
1885
2101
  setComplete(true);
1886
2102
  return e;
1887
2103
  });
1888
-
1889
-
2104
+ */
1890
2105
  };
2106
+
1891
2107
 
1892
2108
  const deletePlacement = async (id:string) => {
1893
2109
  return await firebaseQuery.delete(["placements", id]);
@@ -1989,7 +2205,7 @@ export function useCreateCohortRenderer({oId, product, initialData=defaultCohort
1989
2205
  type UserUploadParams = {
1990
2206
  userType: "Staff"|"Students",
1991
2207
  user: UserData,
1992
- onComplete?: () => void,
2208
+ onComplete?: (e: string) => void,
1993
2209
  cohortId?: string,
1994
2210
  userGroupId?: string,
1995
2211
  shareNameWithReferralLeaderboardConsent?: string,
@@ -2004,9 +2220,6 @@ export function useUserUploadHandler({product, oId, userType, user, onComplete,
2004
2220
  const {execute} = useExecuteCallableJob({user: user});
2005
2221
 
2006
2222
  const requiredFields = ["forename", "surname", "email"];
2007
- if (userType === "Students") {
2008
- requiredFields.push("year");
2009
- }
2010
2223
 
2011
2224
  const checkData = (userData: {email?: string, parentEmail?: string, year?: number, [key:string]: unknown}[]) => {
2012
2225
  setAlert(undefined);
@@ -2061,11 +2274,6 @@ export function useUserUploadHandler({product, oId, userType, user, onComplete,
2061
2274
  }
2062
2275
  }
2063
2276
  // Add more test cases
2064
-
2065
- if (userType === "Students" && (!user.year || !((user.year | 0) > 0 && user.year % 1 === 0))) {
2066
- setAlert({msg: "'Year' must be a number, e.g. 10.", severity: "error"});
2067
- return false;
2068
- }
2069
2277
  }
2070
2278
 
2071
2279
  if (emptyRequiredCell) {
@@ -2111,10 +2319,10 @@ export function useUserUploadHandler({product, oId, userType, user, onComplete,
2111
2319
  setAlert(undefined);
2112
2320
  if (fUsers.length) {
2113
2321
  console.log("fUsers", fUsers);
2114
- execute("userManagement-addUsers", {product: product, oId: oId, users: fUsers, userType: userType, userGroupId: userGroupId, cohortId: cohortId});
2322
+ const jobId = await execute("userManagement-addUsers", {product: product, oId: oId, users: fUsers, userType: userType, userGroupId: userGroupId, cohortId: cohortId});
2323
+ onComplete && onComplete(jobId);
2115
2324
  }
2116
2325
  console.log("Complete", onComplete);
2117
- onComplete && onComplete();
2118
2326
  console.log("Finished");
2119
2327
  };
2120
2328
 
@@ -2962,7 +3170,7 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
2962
3170
  const [workflow, setWorkflow] = useState<WorkflowStage[]>();
2963
3171
  const [cohort, setCohort] = useState<CohortData>();
2964
3172
  const [student, setStudent] = useState<UserData>();
2965
- const [wStage, setWStage] = useState<WorkflowStage>();
3173
+ const [wStage, setWStage] = useState<WorkflowStage&{files: {[fileId: string]: {name: string, url: string}}}>();
2966
3174
  const [snackbar, setSnackbar] = useState<{ open: boolean; message?: string}>({open: false});
2967
3175
  const [disableEmail, setDisableEmail] = useState({parent: false, provider: false});
2968
3176
  const [rejectELIPopup, setRejectELIPopup] = useState(false);
@@ -2982,6 +3190,10 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
2982
3190
  const [uploadRA, setUploadRA] = useState(false);
2983
3191
  const [uploadDBS, setUploadDBS] = useState(false);
2984
3192
  const [onboardingPopup, setOnboardingPopup] = useState(false);
3193
+ const [dismissOnboardingPopup, setDismissOnboardingPopup] = useState(false);
3194
+ const [addOnboardingDocsPopup, setAddOnboardingDocsPopup] = useState(false);
3195
+ const [shareStudentRequestPopup, setShareStudentRequestPopup] = useState(false);
3196
+
2985
3197
  const [editable, setEditable] = useState(false);
2986
3198
 
2987
3199
  const [withdrawFromPlacementPopup, setWithdrawFromPlacementPopup] = useState(false);
@@ -3031,7 +3243,17 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3031
3243
  if (user.userType === "Students") {
3032
3244
  setStudent(user);
3033
3245
  } else {
3034
- getUserById(placement.uid).then(setStudent);
3246
+ if (placement.uid) {
3247
+ getUserById(placement.uid, undefined, false).then(setStudent);
3248
+ } else {
3249
+ setStudent({
3250
+ details: {
3251
+ forename: placement.studentForename || "",
3252
+ surname: placement.studentSurname || "",
3253
+ },
3254
+ email: placement.studentEmail || ""
3255
+ } as UserData)
3256
+ }
3035
3257
  }
3036
3258
 
3037
3259
  }, [placement]);
@@ -3041,19 +3263,29 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3041
3263
  useEffect(() => {
3042
3264
  if (!workflow || !placement) return;
3043
3265
 
3044
- const currentWorkflowStage = {...workflow.find((obj) => obj.id === placement.status) as WorkflowStage};
3045
- // console.log("currentWorkflowStage", currentWorkflowStage)
3046
- currentWorkflowStage.id = placement.status;
3266
+ const getAdditionalStageData = async ():Promise<WorkflowStage&{files: {[fileId: string]: {name: string, url: string}}}> => {
3047
3267
 
3048
- // Get form data for current stage
3049
- if (currentWorkflowStage.forms) {
3050
- getFormsFromId(["forms"], currentWorkflowStage.forms).then((details) => {
3051
- currentWorkflowStage.formDetails = details as [{name:string}];
3052
- setWStage(currentWorkflowStage);
3053
- });
3054
- } else {
3055
- setWStage(currentWorkflowStage);
3268
+ const currentWorkflowStage = {...workflow.find((obj) => obj.id === placement.status) as WorkflowStage&{files: {[fileId: string]: {name: string, url: string}}}};
3269
+ // console.log("currentWorkflowStage", currentWorkflowStage)
3270
+ currentWorkflowStage.id = placement.status;
3271
+
3272
+ // Get form data for current stage
3273
+ if (currentWorkflowStage.forms) {
3274
+ getFormsFromId(["forms"], currentWorkflowStage.forms).then((details) => {
3275
+ currentWorkflowStage.formDetails = details as [{name:string}];
3276
+ });
3277
+ }
3278
+ if (currentWorkflowStage.files) {
3279
+ currentWorkflowStage.files = Object.fromEntries(await Promise.all(currentWorkflowStage.files.map(async (file) => {
3280
+ const fileItem = await firebaseQuery.getDocData(["files", file]) as FileItem;
3281
+ const url = await getDownloadURL(ref(storage, `institutes/${placement.oId}/${fileItem.fileName}`))
3282
+ return [file, {name: fileItem.name, url: url}];
3283
+ })));
3284
+ }
3285
+ return currentWorkflowStage;
3056
3286
  }
3287
+
3288
+ getAdditionalStageData().then(setWStage);
3057
3289
  }, [placement, workflow]);
3058
3290
 
3059
3291
 
@@ -3097,15 +3329,14 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3097
3329
  };
3098
3330
 
3099
3331
 
3100
- const onboardingStatus:"Add onboarding documents"|"Onboarding sent"|"Onboarding docs completed"|"Onboarding docs approved"|"Complete onboarding" = placement?.onboarding ? placement.onboarding.completed?.submitted ? placement.onboarding.completed.accepted ? "Onboarding docs approved" : "Onboarding docs completed" : (user.userType === "Staff" ? "Onboarding sent" : "Complete onboarding") : "Add onboarding documents";
3332
+ const onboardingStatus:"Add onboarding documents"|"Onboarding sent"|"Onboarding docs completed"|"Onboarding docs approved"|"Complete onboarding" =
3333
+ placement?.onboarding ? placement.onboarding.completed?.submitted ? placement.onboarding.completed.accepted ? "Onboarding docs approved" : "Onboarding docs completed" : (user.userType === "Staff" ? "Onboarding sent" : "Complete onboarding") : "Add onboarding documents";
3101
3334
 
3102
3335
  const signOffPlacements = getAccess(user, "signOffPlacements");
3103
3336
 
3104
3337
  let canEdit = false;
3105
3338
 
3106
- if ((wStage?.userType === "Staff" && user.product === "providers") && user.userGroup === "admin") {
3107
- canEdit = true;
3108
- } else if ((wStage?.userType === "Staff" && user.userType === "Staff" && user.product === "institutes") || (user.product === "providers" && wStage?.userType === "Provider") || user.userType === "Students" && wStage?.userType === "Students") {
3339
+ if ((wStage?.userType === "Staff" && user.userType === "Staff" && user.product === "institutes") || (user.product === "providers" && wStage?.userType === "Provider") || user.userType === "Students" && wStage?.userType === "Students") {
3109
3340
  console.log("ALMOST CAN EDIT");
3110
3341
  if (user.userType === "Staff" && !signOffPlacements) {
3111
3342
  canEdit = false;
@@ -3116,11 +3347,14 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3116
3347
 
3117
3348
 
3118
3349
 
3119
- const onFlagClick = async (e:FlagCodes) => {
3350
+ const onFlagClick = async (e:FlagCodes, onClose?: boolean) => {
3120
3351
  if (!placement) return;
3121
3352
  if (e === "completeOnboarding" || e === "reviewOnboarding") {
3122
3353
  setOnboardingPopup(true);
3123
3354
  }
3355
+ if (e === "studentNotAccepted") {
3356
+ setShareStudentRequestPopup(true);
3357
+ }
3124
3358
  if (e === "noInsurance") {
3125
3359
  if (!eliURL) {
3126
3360
  const storageRef = ref(storage, `insurance/${placement.providerId}.pdf`);
@@ -3146,6 +3380,13 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3146
3380
  }
3147
3381
  setExternalDocPopupOpen("dbsCheck");
3148
3382
  }
3383
+ if (e === "addOnboarding") {
3384
+ if (onClose) {
3385
+ setDismissOnboardingPopup(true);
3386
+ } else {
3387
+ setAddOnboardingDocsPopup(true);
3388
+ }
3389
+ }
3149
3390
  };
3150
3391
 
3151
3392
  const approveELI = async () => {
@@ -3187,7 +3428,7 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3187
3428
  throw e;
3188
3429
  });
3189
3430
  console.log("RETURN", res.data);
3190
- setPlacement((p) => ({...p, ...res.data}));
3431
+ setPlacement((p) => ({...p, ...res.data as any}));
3191
3432
  if (uploadProviderDocPopup === "insurance") {
3192
3433
  setUploadProviderDocPopup(undefined);
3193
3434
  setUploadInsurance(true);
@@ -3206,26 +3447,28 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3206
3447
  await executeCallable("placement-withdraw", {placementId: placementId});
3207
3448
  }
3208
3449
 
3209
- return {placement, wStage, student, workflow, editable, withdrawFromPlacementPopup, setWithdrawFromPlacementPopup, withdrawFromPlacement, onFlagClick, setUploadInsurance, setUploadProviderDocPopup, setUploadRA, setUploadDBS, onboardingStatus, setSkipStagePopup, onboardingPopup, setViewExternalLinkPopup, setOnboardingPopup, setRejectELIPopup, eliURL, riskAssessmentURL, dbsCheckURL, setExternalLinkCopied, skipStagePopup, snackbar, setSnackbar, cohort, disableEmail, rejectELIPopup, eliPopupOpen, rejectExternalDocPopup, externalDocPopupOpen, viewExternalLinkPopup, externalLinkCopied, uploadInsurance, uploadRA, uploadDBS, editStage, sendEmail, canEdit, approveELI, setEliPopupOpen, uploadProviderDocPopup, rejectELI, setRejectExternalDocPopup, setExternalDocPopupOpen, approveProviderDoc, rejectProviderDoc, manuallyConfigureProvider, institute}
3450
+ return {placement, wStage, student, workflow, editable, withdrawFromPlacementPopup, addOnboardingDocsPopup, setAddOnboardingDocsPopup, dismissOnboardingPopup, setDismissOnboardingPopup, setWithdrawFromPlacementPopup, withdrawFromPlacement, onFlagClick, setUploadInsurance, setUploadProviderDocPopup, setUploadRA, setUploadDBS, onboardingStatus, setSkipStagePopup, onboardingPopup, setViewExternalLinkPopup, setOnboardingPopup, setRejectELIPopup, eliURL, riskAssessmentURL, dbsCheckURL, setExternalLinkCopied, skipStagePopup, snackbar, setSnackbar, cohort, disableEmail, rejectELIPopup, eliPopupOpen, rejectExternalDocPopup, externalDocPopupOpen, viewExternalLinkPopup, externalLinkCopied, uploadInsurance, uploadRA, uploadDBS, editStage, sendEmail, canEdit, approveELI, setEliPopupOpen, uploadProviderDocPopup, rejectELI, setRejectExternalDocPopup, setExternalDocPopupOpen, approveProviderDoc, rejectProviderDoc, manuallyConfigureProvider, institute, shareStudentRequestPopup, setShareStudentRequestPopup}
3210
3451
  }
3211
3452
 
3212
- export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onboarding: (
3453
+ export function useOnboardingPopup({onboarding, providerId, placementId, user, onClose}:{onboarding: (
3213
3454
  OnboardingDocs&{
3214
3455
  completed: {
3215
- submitted: string|false,
3456
+ submitted: boolean,
3457
+ submittedDate?: string,
3216
3458
  accepted?: boolean,
3217
3459
  filesViewed?: string[],
3218
3460
  formsCompleted?: {[key: string]: unknown},
3219
3461
  filesUploaded?: {[key: number]: string[]},
3220
3462
  }}
3221
- ), placementId: string, user: UserData, onClose: () => void}) {
3463
+ ), placementId: string, user: UserData, providerId: string, onClose: () => void}) {
3222
3464
 
3223
3465
  const [fileUploadPopup, setFileUploadPopup] = useState<false|number>(false);
3224
3466
  const [form, setForm] = useState<{id: string, name: string, [key:string]: unknown}>();
3225
3467
  const [rejectPopup, setRejectPopup] = useState(false);
3226
3468
  const [mOnboarding, setMOnboarding] = useState(onboarding);
3227
3469
  const [completedSections, setCompletedSections] = useState<{
3228
- submitted: string | false;
3470
+ submitted: boolean,
3471
+ submittedDate?: string,
3229
3472
  filesViewed?: string[] | undefined;
3230
3473
  formsCompleted?: {
3231
3474
  [key: string]: unknown;
@@ -3234,7 +3477,19 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3234
3477
  [key: number]: string[];
3235
3478
  } | undefined;
3236
3479
  }>();
3480
+
3237
3481
  const [uploadedFiles, setUploadedFiles] = useState<{[key: string]: FileItem}>({});
3482
+ const [viewableFiles, setViewableFiles] = useState<{[key: string]: FileItem}>();
3483
+ const [formDetails, setFormDetails] = useState<{
3484
+ [key: string]: {
3485
+ name: string;
3486
+ id: string;
3487
+ description?: string;
3488
+ product: Products;
3489
+ oId: string;
3490
+ form: CustomFormSchema;
3491
+ };
3492
+ }>({});
3238
3493
 
3239
3494
  const firebaseQuery = new FirebaseQuery();
3240
3495
 
@@ -3252,7 +3507,7 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3252
3507
  const viewedFiles = a.completed ? a.completed?.filesViewed || [] : [];
3253
3508
  if (viewedFiles?.includes(file)) return a;
3254
3509
 
3255
- a?.viewableFiles?.[file].url && onOpen(a?.viewableFiles?.[file].url);
3510
+ viewableFiles?.[file].url && onOpen(viewableFiles?.[file].url);
3256
3511
  viewedFiles?.push(file);
3257
3512
 
3258
3513
  const newA = editNestedObject(["completed", "filesViewed"], oldA, viewedFiles) as any;
@@ -3275,7 +3530,7 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3275
3530
 
3276
3531
  const onboardingFiles = onboarding.files ? Object.fromEntries(await Promise.all(onboarding.files?.map(async (fileId) => {
3277
3532
  const file = await firebaseQuery.getDocData(["files", fileId]);
3278
- file.url = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`));
3533
+ file.url = await getDownloadURL(ref(storage, `providers/${providerId}/${file.fileName}`));
3279
3534
 
3280
3535
  return [fileId, file];
3281
3536
  }))) : [];
@@ -3283,8 +3538,8 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3283
3538
  return [formId, await firebaseQuery.getDocData(["forms", formId])];
3284
3539
  }))) : [];
3285
3540
 
3286
- onboardingNew.viewableFiles = onboardingFiles;
3287
- onboardingNew.formDetails = onboardingForms;
3541
+ setViewableFiles(onboardingFiles);
3542
+ setFormDetails(onboardingForms);
3288
3543
  return onboardingNew;
3289
3544
  };
3290
3545
  getOnboardingData().then(setMOnboarding);
@@ -3346,7 +3601,8 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3346
3601
  // Check all stages completed.
3347
3602
  if (!placementId) return;
3348
3603
  if (!stagesCompleted()) throw new Error("Complete all forms before submitting.");
3349
- await firebaseQuery.update(["placements", placementId], {["onboarding.completed.submitted"]: convertDate(new Date(), "dbstring") as string});
3604
+ await firebaseQuery.update(["placements", placementId], {["onboarding.completed.accepted"]: false, ["onboarding.completed.submitted"]: true, ["onboarding.completed.submittedDate"]: convertDate(new Date(), "dbstring") as string});
3605
+ //executeCallable("sendOnboardingSubmittedEmail", {});
3350
3606
  };
3351
3607
 
3352
3608
  useEffect(() => {
@@ -3391,5 +3647,707 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3391
3647
  addCompletedSectionURLs();
3392
3648
  }, [mOnboarding]);
3393
3649
 
3394
- return {addFile, viewFile, uploadedFiles, setFormComplete, setRejectPopup, stagesCompleted, setForm, mOnboarding, form, submit, acceptOnboarding, rejectOnboarding, rejectPopup, completedSections, fileUploadPopup, setFileUploadPopup};
3395
- }
3650
+ return {addFile, viewFile, uploadedFiles, setFormComplete, setRejectPopup, stagesCompleted, setForm, mOnboarding, form, submit, acceptOnboarding, rejectOnboarding, rejectPopup, completedSections, fileUploadPopup, setFileUploadPopup, viewableFiles, formDetails};
3651
+ }
3652
+
3653
+
3654
+
3655
+
3656
+
3657
+ export function useLoadAddresses(user: UserData, limitItems?: number, queryConstraint?: QueryConstraint[], request?: boolean) {
3658
+ const [addresses, setAddresses] = useState<{[key: string]: OrganisationAddress&{listings: number}}>({});
3659
+ const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
3660
+ const [loading, setLoading] = useState<boolean>(false);
3661
+
3662
+ const firebaseQuery = new FirebaseQuery();
3663
+
3664
+ const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
3665
+
3666
+ const changeQueryConstraints = (e: QueryConstraint[]) => {
3667
+ setQueryConstraints([...(queryConstraint || []), ...e]);
3668
+ };
3669
+
3670
+ const loadAddresses = () => {
3671
+ const constraints:QueryConstraint[] = [where("oId", "==", user.oId), where("product", "==", user.product), orderBy(documentId())]
3672
+
3673
+ if (limitItems) {
3674
+ constraints.push(limit(limitItems));
3675
+ }
3676
+
3677
+ if (user.viewAddresses === "all" || user.userGroup === "admin" || request) {
3678
+ if (lastDoc?.id) {
3679
+ constraints.push(startAfter(lastDoc?.id));
3680
+ }
3681
+ } else if (user.viewAddresses === "request") {
3682
+ if (!user.visibleAddresses?.length) return;
3683
+
3684
+ constraints.push(where(documentId(), "in", user.visibleAddresses))
3685
+ if (lastDoc?.id) {
3686
+ constraints.push(startAfter(lastDoc?.id));
3687
+ }
3688
+ } else {
3689
+ setLoading(false);
3690
+ return; // viewAddresses === "none", no need to load anything
3691
+ }
3692
+ queryConstraints && constraints.unshift(...queryConstraints);
3693
+
3694
+ return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
3695
+ const deletedAddresses = snapshot.docChanges().map((change) => {
3696
+ if (change.type === "removed") {
3697
+ return change.doc.id;
3698
+ }
3699
+ return;
3700
+ })
3701
+
3702
+ setAddresses((prev) => Object.fromEntries(Object.entries(prev).filter(([k]) => !deletedAddresses.includes(k))))
3703
+
3704
+ if (!snapshot.empty) {
3705
+
3706
+
3707
+ const newAddresses:[string, OrganisationAddress][] = snapshot.docs.map(doc => ([doc.id, {id: doc.id, ...doc.data() as OrganisationAddress}]));
3708
+
3709
+ const withListings:{[key: string]: OrganisationAddress&{listings: number}} = Object.fromEntries(await Promise.all(newAddresses.map(async ([k, address]) => {
3710
+ const listings = await firebaseQuery.getCount("placementListings", [where("providerId", "==", user.oId), where("addressId", "==", k)]);
3711
+ return [k, {...address, listings: listings}];
3712
+ })));
3713
+
3714
+ setAddresses(prev => ({...prev, ...withListings}));
3715
+ setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
3716
+ }
3717
+ setLoading(false);
3718
+ }), "addresses", constraints, undefined, true);
3719
+ };
3720
+
3721
+ useEffect(() => {
3722
+ const unsubscribe = loadAddresses();
3723
+
3724
+ return () => {
3725
+ if (unsubscribe) {
3726
+ unsubscribe(); // Unsubscribe from the snapshot listener when the component unmounts
3727
+ }
3728
+ };
3729
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3730
+ }, [queryConstraints]);
3731
+
3732
+ const onScrollBottom = () => {
3733
+ if (!limitItems) return;
3734
+ if (!loading) {
3735
+ setLoading(true);
3736
+ loadAddresses();
3737
+ }
3738
+ };
3739
+
3740
+ return { addresses, onScrollBottom, loading, changeQueryConstraints };
3741
+ }
3742
+
3743
+
3744
+
3745
+
3746
+ export function useLoadListings(user: UserData, queryConstraint?: QueryConstraint[], request?: boolean) {
3747
+ const [listings, setListings] = useState<[string, PlacementListing&{applicants?: number, scheduled?: number, active?: number}][]>([]);
3748
+ const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
3749
+ const [loading, setLoading] = useState<boolean>(false);
3750
+
3751
+ const firebaseQuery = new FirebaseQuery();
3752
+
3753
+ const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
3754
+
3755
+ const changeQueryConstraints = (e: QueryConstraint[]) => {
3756
+ setQueryConstraints([...(queryConstraint || []), ...e]);
3757
+ };
3758
+
3759
+ const loadListings = () => {
3760
+ const constraints:QueryConstraint[] = [where("providerId", "==", user.oId), limit(10), orderBy(documentId())]
3761
+
3762
+ if (user.viewPlacementListings === "all" || user.userGroup === "admin" || request) {
3763
+ if (lastDoc?.id) {
3764
+ constraints.push(startAfter(lastDoc));
3765
+ }
3766
+ } else {
3767
+ if (!user.visibleListings?.length) return;
3768
+ constraints.push(where(documentId(), 'in', user.visibleListings));
3769
+ if (lastDoc?.id) {
3770
+ constraints.push(startAfter(lastDoc));
3771
+ }
3772
+ }
3773
+
3774
+ if (user.viewAddresses !== "all" && user.viewPlacementListings === "all" && user.userGroup !== "admin") {
3775
+ if (!user.visibleAddresses?.length) return;
3776
+
3777
+ constraints.push(where('addressId', 'in', user.visibleAddresses));
3778
+ }
3779
+ queryConstraints && constraints.unshift(...queryConstraints);
3780
+
3781
+ return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
3782
+ const deletedListings = snapshot.docChanges().map((change) => {
3783
+ if (change.type === "removed") {
3784
+ return change.doc.id;
3785
+ }
3786
+ return;
3787
+ })
3788
+
3789
+ setListings((prev) => prev.filter(([k]) => !deletedListings.includes(k)))
3790
+
3791
+ if (!snapshot.empty) {
3792
+ const newListings:[string, PlacementListing][] = snapshot.docs.map(doc => ([doc.id, {...doc.data() as PlacementListing, id: doc.id}]));
3793
+
3794
+ const listingsWithAdditionalData:[string, PlacementListing&{applicants?: number, scheduled?: number, active?: number}][] = await Promise.all(newListings.map(async ([id, listing]) => {
3795
+ const listingWithAdditionalData = {...listing} as PlacementListing&{applicants?: number, scheduled?: number, active?: number};
3796
+
3797
+ if (listingWithAdditionalData.applicants !== undefined) return [id, listingWithAdditionalData];
3798
+
3799
+ listingWithAdditionalData.applicants = await firebaseQuery.getCount("applications", [where("providerId", "==", user.oId), where("placementId", "==", id), where("status", "==", "submitted")]);
3800
+ listingWithAdditionalData.scheduled = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("placementId", "==", id), where("inProgress", "==", true), where("startDate", ">", convertDate(new Date(), "dbstring"))]);
3801
+ listingWithAdditionalData.active = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("placementId", "==", id), where("active", "==", true)]);
3802
+
3803
+ return [id, listingWithAdditionalData];
3804
+ }));
3805
+ setListings(prev => (Object.entries({...Object.fromEntries(prev), ...Object.fromEntries(listingsWithAdditionalData)})));
3806
+ setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
3807
+ }
3808
+ setLoading(false);
3809
+ }), "placementListings", constraints, undefined, true);
3810
+ };
3811
+
3812
+ useEffect(() => {
3813
+ let unsubscribe: () => void;
3814
+
3815
+
3816
+
3817
+ loadListings();
3818
+
3819
+ return () => {
3820
+ if (unsubscribe) {
3821
+ unsubscribe(); // Unsubscribe from the snapshot listener when the component unmounts
3822
+ }
3823
+ };
3824
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3825
+ }, [queryConstraints]);
3826
+
3827
+ const onScrollBottom = () => {
3828
+ if (!loading) {
3829
+ setLoading(true);
3830
+ loadListings();
3831
+ }
3832
+ };
3833
+
3834
+ return { listings, onScrollBottom, loading, changeQueryConstraints };
3835
+ }
3836
+
3837
+
3838
+
3839
+ export function useLoadProviderPlacements(user: UserData, queryConstraint?: QueryConstraint[], placementId?: string) {
3840
+ const [placements, setPlacements] = useState<[string, StudentPlacementData&{applicants?: number, scheduled?: number, active?: number}][]>([]);
3841
+ const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
3842
+ const [loading, setLoading] = useState<boolean>(false);
3843
+
3844
+ const firebaseQuery = new FirebaseQuery();
3845
+
3846
+ const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
3847
+
3848
+ if (user.product !== "providers") throw new Error("Only providers can use this hook.");
3849
+
3850
+ const changeQueryConstraints = (e: QueryConstraint[]) => {
3851
+ setQueryConstraints([...(queryConstraint || []), ...e]);
3852
+ };
3853
+
3854
+ const loadListings = () => {
3855
+ const constraints:QueryConstraint[] = [where("providerId", "==", user.oId), limit(10), orderBy(documentId())]
3856
+
3857
+ if (placementId) {
3858
+ constraints.push(where("placementId", "==", placementId));
3859
+ }
3860
+
3861
+ if (user.viewPlacementListings === "all" || user.userGroup === "admin") {
3862
+ if (lastDoc?.id) {
3863
+ constraints.push(startAfter(lastDoc));
3864
+ }
3865
+ } else {
3866
+ if (!user.visibleListings?.length) return;
3867
+ constraints.push(where("placementId", 'in', user.visibleListings));
3868
+ if (lastDoc?.id) {
3869
+ constraints.push(startAfter(lastDoc));
3870
+ }
3871
+ }
3872
+
3873
+ if (user.viewAddresses !== "all" && user.viewPlacementListings === "all" && user.userGroup !== "admin") {
3874
+ if (!user.visibleAddresses?.length) return;
3875
+
3876
+ constraints.push(where('addressId', 'in', user.visibleAddresses));
3877
+ }
3878
+ queryConstraints && constraints.unshift(...queryConstraints);
3879
+
3880
+ return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
3881
+ const deletedListings = snapshot.docChanges().map((change) => {
3882
+ if (change.type === "removed") {
3883
+ return change.doc.id;
3884
+ }
3885
+ return;
3886
+ })
3887
+
3888
+ setPlacements((prev) => prev.filter(([k]) => !deletedListings.includes(k)))
3889
+
3890
+ if (!snapshot.empty) {
3891
+ const newPlacements:[string, StudentPlacementData][] = snapshot.docs.map(doc => ([doc.id, {...doc.data() as StudentPlacementData, id: doc.id}]));
3892
+
3893
+ const withAdditionalData = await Promise.all(newPlacements.map(async ([id, placement]) => {
3894
+ const student = placement.uid ? await firebaseQuery.getDocData(["users", placement.uid || ""]).catch(() => false) as UserData|false :
3895
+ {
3896
+ details: {
3897
+ forename: placement.studentForename,
3898
+ surname: placement.studentSurname,
3899
+ },
3900
+ email: placement.studentEmail
3901
+ };
3902
+ const listing = await firebaseQuery.getDocData(["placementListings", placement.placementId || ""]).catch(() => false) as PlacementListing;
3903
+
3904
+ return [id, {...placement, student: student, listing: listing}];
3905
+ }));
3906
+ setPlacements(prev => (Object.entries({...Object.fromEntries(prev), ...Object.fromEntries(withAdditionalData)})));
3907
+ setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
3908
+ }
3909
+ setLoading(false);
3910
+ }), "placements", constraints, undefined, true);
3911
+ };
3912
+
3913
+ useEffect(() => {
3914
+ let unsubscribe: () => void;
3915
+
3916
+
3917
+
3918
+ loadListings();
3919
+
3920
+ return () => {
3921
+ if (unsubscribe) {
3922
+ unsubscribe(); // Unsubscribe from the snapshot listener when the component unmounts
3923
+ }
3924
+ };
3925
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3926
+ }, [queryConstraints]);
3927
+
3928
+ const onScrollBottom = () => {
3929
+ if (!loading) {
3930
+ setLoading(true);
3931
+ loadListings();
3932
+ }
3933
+ };
3934
+
3935
+ return { placements: Object.fromEntries(placements), onScrollBottom, loading, changeQueryConstraints };
3936
+ }
3937
+
3938
+
3939
+
3940
+ export function useLoadApplications({user, applicationType, listingId, queryConstraint}:{user: UserData, applicationType?: "all"|"actionRequired"|"awaitingStudent"|"closed", listingId?: string, queryConstraint?: QueryConstraint[]}) {
3941
+ const [applications, setApplications] = useState<[string, Application][]>([]);
3942
+ const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
3943
+ const [loading, setLoading] = useState<boolean>(false);
3944
+ const [type, setType] = useState<"actionRequired"|"awaitingStudent"|"closed"|"all">(applicationType || "all");
3945
+
3946
+ const firebaseQuery = new FirebaseQuery();
3947
+
3948
+ const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
3949
+
3950
+ const changeQueryConstraints = (e: QueryConstraint[]) => {
3951
+ setQueryConstraints([...(queryConstraint || []), ...e]);
3952
+ };
3953
+
3954
+ const loadApplications = () => {
3955
+ const constraints:QueryConstraint[] = [where("providerId", "==", user.oId), limit(10), orderBy(documentId())]
3956
+
3957
+
3958
+ if (lastDoc?.id) {
3959
+ constraints.push(startAfter(lastDoc));
3960
+ }
3961
+
3962
+ switch (type) {
3963
+ case "actionRequired":
3964
+ constraints.push(where("status", "==", "submitted"), where("reqUserType", "==", "Staff"));
3965
+ break;
3966
+ case "awaitingStudent":
3967
+ constraints.push(where("status", "==", "submitted"), where("reqUserType", "==", "Students"));
3968
+ break;
3969
+ case "closed":
3970
+ constraints.push(where("status", "in", ["approved", "declined"]));
3971
+ break;
3972
+ default:
3973
+ constraints.push(where("status", "==", "submitted"));
3974
+ }
3975
+ if (listingId) {
3976
+ constraints.push(where("listingId", "==", listingId));
3977
+ }
3978
+
3979
+ console.log("Constraints before user group check", constraints);
3980
+ if (user.viewAddresses !== "all" && user.userGroup !== "admin") {
3981
+ if (user.viewPlacementListings === "all") {
3982
+ if (!user.visibleAddresses?.length) return;
3983
+ constraints.push(where('addressId', 'in', user.visibleAddresses));
3984
+ } else {
3985
+ if (!user.visibleListings?.length) return;
3986
+ constraints.push(where('placementId', 'in', user.visibleListings));
3987
+ }
3988
+ }
3989
+
3990
+ queryConstraints && constraints.unshift(...queryConstraints);
3991
+ console.log("Constraints after user group check", constraints);
3992
+
3993
+ return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
3994
+ const deletedApplications = snapshot.docChanges().map((change) => {
3995
+ if (change.type === "removed") {
3996
+ return change.doc.id;
3997
+ }
3998
+ return;
3999
+ })
4000
+
4001
+ console.log("applicantCount", snapshot.size);
4002
+
4003
+ setApplications((prev) => prev.filter(([k]) => !deletedApplications.includes(k)))
4004
+
4005
+ if (!snapshot.empty) {
4006
+ const newApplications:[string, Application][] = snapshot.docs.map(doc => ([doc.id, {id: doc.id, ...doc.data() as Application}])).filter(([, v]) => (v as Application).status !== "draft") as [string, Application][];
4007
+
4008
+ setApplications(prev => ([...prev, ...newApplications]));
4009
+ setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
4010
+ } else {
4011
+ setApplications([]);
4012
+ setLastDoc(null);
4013
+ }
4014
+ setLoading(false);
4015
+ }), "applications", constraints, undefined, true);
4016
+ };
4017
+
4018
+ useEffect(() => {
4019
+ let unsubscribe: () => void;
4020
+
4021
+ loadApplications();
4022
+ return () => {
4023
+ if (unsubscribe) {
4024
+ unsubscribe(); // Unsubscribe from the snapshot listener when the component unmounts
4025
+ }
4026
+ };
4027
+ // eslint-disable-next-line react-hooks/exhaustive-deps
4028
+ }, [type, queryConstraints]);
4029
+
4030
+ const onScrollBottom = () => {
4031
+ if (!loading) {
4032
+ setLoading(true);
4033
+ loadApplications();
4034
+ }
4035
+ };
4036
+
4037
+ return { applications, type, setType, onScrollBottom, loading, changeQueryConstraints };
4038
+ }
4039
+
4040
+
4041
+
4042
+ export type FilterObject = {
4043
+ [key: string]: {
4044
+ label: string,
4045
+ value?: unknown,
4046
+ values?: {[key:string|number]: string|{label: string, test: QueryFieldFilterConstraint}},
4047
+ type: "dropdown"|"string"|"number"
4048
+ }
4049
+ }
4050
+
4051
+ export type DataViewerPaginater = {
4052
+ view: "list"|"table";
4053
+ queryLimit?: number;
4054
+ formatItems?: (key: string, item: any) => Promise<{ key: string, item: any }> | { key: string, item: any };
4055
+ snapshot?: boolean;
4056
+ filters?: FilterObject;
4057
+ onSearch?: boolean | ((search?: string, sort?: [string, {value: string, direction: "asc"|"desc"}], page?: number, filters?:FilterObject, limit?: number) => Promise<{ [key: string]: any }>);
4058
+ data?: {[key:string]:{[key:string]: unknown}}|QueryObject[];
4059
+ additionalEntryProcessing?: (k: string, v: any) => Promise<any> | any,
4060
+ sorts?: Sorts
4061
+ }
4062
+
4063
+ export function useDataViewerPaginator({view: initialView, sorts, queryLimit, additionalEntryProcessing, formatItems, snapshot, filters: initialFilters, onSearch, data}:DataViewerPaginater) {
4064
+ const [tableData, setTableData] = useState<{[key:string]:{[key:string]: unknown}}>(Array.isArray(data) ? Object.fromEntries(Object.entries(data).slice(0, queryLimit)) : {});
4065
+ const [page, setPage] = useState([1, 0]);
4066
+ const [view, setView] = useState(initialView);
4067
+ const [filters, setFilters] = useState<FilterObject|undefined>(initialFilters);
4068
+ const [queryAnchor, setQueryAnchor] = useState<{startKey: string, endKey: string, startQueryPos: number, endQueryPos: number}>({startKey: "", endKey: "", startQueryPos: 0, endQueryPos: 0});
4069
+ const [prevEntryIds, setPrevEntryIds] = useState<{[key:string]:number}>({});
4070
+ const [dataListenerUnsubscribe, setDataListenerUnsubscribe] = useState<{[key: string] : Unsubscribe}>();
4071
+ const [loading, setLoading] = useState<boolean|"loaded">(true);
4072
+ const [searchString, setSearchString] = useState<string>();
4073
+ const [sort, setSort] = useState<[string, {value: string, direction: "asc"|"desc"}]>();
4074
+
4075
+
4076
+
4077
+ const processedData = async (k: string, v: any) => additionalEntryProcessing ? await additionalEntryProcessing(k, v) : v;
4078
+
4079
+ const setTableDataFromDefinedData = async () => {
4080
+ if (!data || Array.isArray(data)) return;
4081
+
4082
+ const dataWithAdditionalProcessingPossibleNulls: [string, any][] = (await Promise.all(Object.entries(data).map(async ([k, v]) => [k, await processedData(k, v)])))
4083
+ const dataWithAdditionalProcessing = dataWithAdditionalProcessingPossibleNulls.filter(([k, v]) => v);
4084
+
4085
+ const searchedData: [string, any][] = searchString ? dataWithAdditionalProcessing.filter(([, v]) => {
4086
+ const values = Object.values(v).join(", ");
4087
+ console.log("VALUESTRING", v);
4088
+ return values.includes(searchString);
4089
+ }) : dataWithAdditionalProcessing;
4090
+
4091
+ const filteredData:[string, any][] = filters && Object.keys(filters).length > 0 ? searchedData.filter(([, dataValue]) => Object.entries(filters).every(([filterKey, filterValue]) => {
4092
+ const value = dataValue[filterKey];
4093
+ if ((typeof value === "number") && value === parseInt(filterValue.value as string)) return true;
4094
+ if ((typeof value === "boolean") && value === (filterValue.value === "true")) return true;
4095
+ if ((typeof value === "boolean") && value === (filterValue.value === "false")) return true;
4096
+
4097
+ if ((typeof value === "string" || Array.isArray(value)) && value.includes(filterValue.value as string)) return true;
4098
+ return false;
4099
+ })) : searchedData;
4100
+ if (view === "table") {
4101
+ if (!queryLimit) throw new Error("Tables must have a limit defined.");
4102
+ const newData:[string, any][] = filteredData.slice((page[0] - 1)*queryLimit, page[0]*queryLimit);
4103
+ setTableData(Object.fromEntries(newData));
4104
+ if (Object.keys(Object.fromEntries(newData)).pop() === Object.keys(data).pop()) {
4105
+ setLoading("loaded");
4106
+ } else {
4107
+ setLoading(false);
4108
+ }
4109
+ return;
4110
+ }
4111
+ if (view === "list") {
4112
+ setTableData(Object.fromEntries(filteredData));
4113
+ setLoading("loaded");
4114
+ }
4115
+ };
4116
+
4117
+ const getDataFromQuery = async (
4118
+ itemList: {[key: string]: any} = {}, currentQueryAnchor=queryAnchor, cursorDirection?:"increase"|"decrease"|undefined, prevEntries=prevEntryIds, loadMoreFromQuery=false):Promise<any> => {
4119
+
4120
+ if (!filters) return;
4121
+ setLoading(true);
4122
+ if (!Array.isArray(data)) {
4123
+ setTableDataFromDefinedData();
4124
+ return;
4125
+ }
4126
+ if (!queryLimit) throw new Error("Firestore queries must have a limit defined.");
4127
+
4128
+ if (onSearch && (searchString || sort)) {
4129
+ if (typeof onSearch === "boolean") throw new Error("When using Firestore queries, an onSearch function should be passed to retrieve data externally. Additional processing is however completed in this hook.");
4130
+ const data = await onSearch(searchString, sort, page[0], filters, queryLimit);
4131
+ const dataWithAdditionalProcessing: [string, any][] = await Promise.all(Object.entries(data).map(async ([k, v]) => [k, await processedData(k, v)]));
4132
+
4133
+ setTableData((old) => {
4134
+ if (view === "table") {
4135
+ return {...Object.fromEntries(dataWithAdditionalProcessing)};
4136
+ }
4137
+ return {...old, ...Object.fromEntries(dataWithAdditionalProcessing)};
4138
+ });
4139
+
4140
+ if (dataWithAdditionalProcessing.length < queryLimit) {
4141
+ setLoading("loaded");
4142
+ } else {
4143
+ setLoading(false);
4144
+ }
4145
+ return;
4146
+ }
4147
+
4148
+
4149
+
4150
+ let cursorPos:number;
4151
+ if (page[0] > page[1]) {
4152
+ cursorPos = currentQueryAnchor.endQueryPos;
4153
+ } else {
4154
+ cursorPos = currentQueryAnchor.startQueryPos;
4155
+ }
4156
+
4157
+ const querySchema:QueryObject = data[cursorPos];
4158
+
4159
+ const createQuery = (queryData:QueryObject) => {
4160
+ const constraints:any[] = [];
4161
+ queryData.where && queryData.where.forEach((w) => {
4162
+ constraints.push(where(...w));
4163
+ });
4164
+
4165
+ filters && Object.entries(filters).filter(([, value]) => value.value).forEach(([key, value]) => {
4166
+ const filterValue = (value.type === "number" || value.type === "dropdown") ? parseInt(value.value as string) || value.value : value.value;
4167
+ constraints.push(where(key, "==", filterValue));
4168
+ });
4169
+
4170
+ constraints.push(orderBy(queryData.orderBy ? queryData.orderBy === "documentId" ? documentId() : queryData.orderBy : documentId()));
4171
+
4172
+ if (page[0] > page[1] && !cursorDirection) { // Going up
4173
+ currentQueryAnchor.endKey && constraints.push(startAfter(currentQueryAnchor.endKey));
4174
+ constraints.push(limit(queryLimit));
4175
+ if (!loadMoreFromQuery) {
4176
+ currentQueryAnchor = {...currentQueryAnchor, startQueryPos: currentQueryAnchor.endQueryPos};
4177
+ }
4178
+ } else if (page[0] < page[1] && !cursorDirection) { // Going down
4179
+ if (!loadMoreFromQuery) {
4180
+ currentQueryAnchor = {...currentQueryAnchor, endQueryPos: currentQueryAnchor.startQueryPos};
4181
+ }
4182
+ constraints.push(limitToLast(queryLimit));
4183
+ if (currentQueryAnchor.startKey) {
4184
+ currentQueryAnchor.startKey && constraints.push(endBefore(currentQueryAnchor.startKey));
4185
+ } else {
4186
+ currentQueryAnchor.startKey && constraints.push(endAt(currentQueryAnchor.startKey));
4187
+ }
4188
+ } else {
4189
+ if (cursorDirection === "decrease") {
4190
+ constraints.push(limitToLast(queryLimit));
4191
+ } else {
4192
+ constraints.push(limit(queryLimit));
4193
+ }
4194
+ }
4195
+ return constraints;
4196
+ };
4197
+
4198
+ const constraints = createQuery(querySchema);
4199
+ const q = query(collection(db, ...(querySchema.path as [any])), ...(constraints));
4200
+
4201
+ console.log("Fetching docs", constraints);
4202
+
4203
+ if (snapshot) {
4204
+ // Use onSnapshot to get real-time updates
4205
+ const unsubscribe = onSnapshot(q, (querySnapshot) => {
4206
+ handleQuerySnapshot(querySnapshot);
4207
+ });
4208
+ // Save the unsubscribe function to state so we can dispose of it later
4209
+ setDataListenerUnsubscribe((d) => ({...(d || {}), [cursorPos]: unsubscribe}));
4210
+ return;
4211
+ } else {
4212
+ // Just get the docs without setting up a listener
4213
+ const queryData = await getDocs(q);
4214
+ handleQuerySnapshot(queryData);
4215
+ }
4216
+
4217
+ // Function to handle query snapshot
4218
+ async function handleQuerySnapshot(querySnapshot: QuerySnapshot) {
4219
+ if (!Array.isArray(data)) throw new Error("Called querySnapshot but data is defined.");
4220
+ if (!queryLimit) throw new Error("Firestore queries must have a limit defined.");
4221
+
4222
+ const queryResults: { [key: string]: { [key: string]: unknown } } = {};
4223
+ let index = 0; // Declare the index variable
4224
+
4225
+ const reverseIfBack = (docs: QueryDocumentSnapshot<DocumentData>[]) =>
4226
+ page[0] < page[1] ? docs.reverse() : docs;
4227
+
4228
+ // Process each document in the querySnapshot
4229
+ for (const doc of reverseIfBack(querySnapshot.docs)) {
4230
+ if ((Object.keys(queryResults).length + Object.keys(itemList).length) === queryLimit) {
4231
+ break;
4232
+ }
4233
+
4234
+ let position = Object.keys(itemList).length+(page[0]-1)*queryLimit+index+1;
4235
+ if (page[0] < page[1]) {
4236
+ position = (page[0])*queryLimit-index-Object.keys(itemList).length;
4237
+ }
4238
+
4239
+
4240
+ if (itemList[doc.id] || (prevEntries[doc.id] && prevEntries[doc.id] !== position)) {
4241
+ console.log("Removing ", doc.id, ": E=", prevEntries[doc.id], ", G=", position);
4242
+ continue;
4243
+ }
4244
+
4245
+ let item: {[key: string]: unknown}|false = doc.data();
4246
+ item.id = doc.id;
4247
+
4248
+ // Apply additionalEntryProcessing if provided
4249
+ if (additionalEntryProcessing) {
4250
+ item = await additionalEntryProcessing(doc.id, item);
4251
+ }
4252
+ if (!item) continue;
4253
+
4254
+ queryResults[doc.id] = item;
4255
+ index += 1;
4256
+
4257
+ if (prevEntries[doc.id]) continue;
4258
+ prevEntries[doc.id] = position;
4259
+ }
4260
+
4261
+ if (cursorDirection === "decrease" || page[0] < page[1]) {
4262
+ itemList = {...Object.fromEntries(Object.entries(queryResults).reverse()), ...itemList};
4263
+ } else {
4264
+ itemList = {...itemList, ...queryResults};
4265
+ }
4266
+
4267
+ // Updating state with the new data and query anchors
4268
+
4269
+
4270
+ setPrevEntryIds(prevEntries);
4271
+
4272
+ if (querySnapshot.size < queryLimit && Object.keys(itemList).length < queryLimit) {
4273
+ // If we have ran out of entries, increase or decrease the index.
4274
+ if (page[0] > page[1] && cursorPos+1 < data.length) {
4275
+ return getDataFromQuery(itemList, {...currentQueryAnchor, endQueryPos: currentQueryAnchor.endQueryPos+1}, "increase", prevEntries);
4276
+ } else if (page[0] < page[1] && cursorPos > 0) {
4277
+ return getDataFromQuery(itemList, {...currentQueryAnchor, startQueryPos: currentQueryAnchor.startQueryPos-1}, "decrease", prevEntries);
4278
+ }
4279
+ }
4280
+
4281
+ if (Object.keys(itemList).length < queryLimit && querySnapshot.size === queryLimit) {
4282
+ console.log("Shorter than ten");
4283
+ return getDataFromQuery(itemList, {...currentQueryAnchor, startKey: Object.keys(itemList)[0], endKey: Object.keys(itemList).slice(-1)[0]}, undefined, prevEntries, true);
4284
+ }
4285
+
4286
+ if (querySnapshot.size === 0 &&
4287
+ Object.keys(itemList).length === 0 &&
4288
+ currentQueryAnchor.endQueryPos+1 === data.length &&
4289
+ page[0] > 1) {
4290
+ setTableData({});
4291
+ setQueryAnchor((a) => ({...a, startKey: ""}));
4292
+ return;
4293
+ }
4294
+
4295
+ if (querySnapshot.size < queryLimit) {
4296
+ setLoading("loaded");
4297
+ } else {
4298
+ setLoading(false);
4299
+ }
4300
+
4301
+ setQueryAnchor({...currentQueryAnchor, startKey: Object.keys(itemList)[0], endKey: Object.keys(itemList).slice(-1)[0]});
4302
+
4303
+ setTableData((old) => {
4304
+ if (view === "table") {
4305
+ return {...itemList};
4306
+ }
4307
+ return {...old, ...itemList};
4308
+ });
4309
+ }
4310
+ };
4311
+
4312
+ const reset = () => {
4313
+ console.log("Resetting after filters?");
4314
+ setPage([1, 0]);
4315
+ setTableData({});
4316
+ setQueryAnchor({startKey: "", endKey: "", startQueryPos: 0, endQueryPos: 0});
4317
+ setPrevEntryIds({});
4318
+ dataListenerUnsubscribe && Object.values(dataListenerUnsubscribe).map((u) => u());
4319
+ setDataListenerUnsubscribe(undefined);
4320
+ };
4321
+
4322
+ useEffect(() => {
4323
+ console.log("Filters updates", filters);
4324
+ if (!filters) return;
4325
+ console.log("Resetting cus filters");
4326
+ reset();
4327
+ }, [filters]);
4328
+
4329
+ useEffect(() => {
4330
+ reset();
4331
+ }, [view, searchString, data, sort]);
4332
+
4333
+ // Fetch new data when queries or page change
4334
+ useEffect(() => {
4335
+ getDataFromQuery();
4336
+ dataListenerUnsubscribe && Object.values(dataListenerUnsubscribe).map((u) => u());
4337
+ }, [page]);
4338
+
4339
+ const pageUp = () => {
4340
+ setPage((p) => ([p[0]+1, p[0]]));
4341
+ };
4342
+
4343
+ const pageDown = () => {
4344
+ setPage((p) => ([p[0]-1, p[0]]));
4345
+ };
4346
+
4347
+ const updateSort = (sortLabel: string) => {
4348
+ if (!sorts || !sorts[sortLabel]) return;
4349
+ setSort([sortLabel, sorts[sortLabel]]);
4350
+ }
4351
+
4352
+ return ({...{tableData, pageUp, pageDown, setFilters, page: page[0], sorts, loading, sort, updateSort: updateSort, setView, updateSearch: setSearchString}});
4353
+ };