placementt-core 11.0.533 → 11.10.151

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/lib/constants.d.ts +1 -0
  2. package/lib/constants.js +6 -0
  3. package/lib/constants.js.map +1 -1
  4. package/lib/features/analytics/useAnalytics.d.ts +2 -0
  5. package/lib/features/analytics/useAnalytics.js +21 -16
  6. package/lib/features/analytics/useAnalytics.js.map +1 -1
  7. package/lib/features/global/downtime/useDowntime.d.ts +1 -0
  8. package/lib/features/global/downtime/useDowntime.js +9 -7
  9. package/lib/features/global/downtime/useDowntime.js.map +1 -1
  10. package/lib/features/global/users/useUserFunctions.js +1 -1
  11. package/lib/features/global/users/useUserFunctions.js.map +1 -1
  12. package/lib/features/placements/studentPlacements/activePlacement.d.ts +5 -1
  13. package/lib/features/placements/studentPlacements/activePlacement.js +7 -3
  14. package/lib/features/placements/studentPlacements/activePlacement.js.map +1 -1
  15. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.d.ts +3 -2
  16. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.js +4 -1
  17. package/lib/features/placements/studentPlacements/completedStudentPlacementsSlice.js.map +1 -1
  18. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.d.ts +2 -2
  19. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.js +1 -1
  20. package/lib/features/placements/studentPlacements/upcomingStudentPlacementsSlice.js.map +1 -1
  21. package/lib/features/placements/studentPlacements/useStudentPlacements.d.ts +2 -12
  22. package/lib/features/placements/studentPlacements/useStudentPlacements.js +1 -26
  23. package/lib/features/placements/studentPlacements/useStudentPlacements.js.map +1 -1
  24. package/lib/features/updates/useUpdates.d.ts +1 -0
  25. package/lib/features/updates/useUpdates.js +13 -12
  26. package/lib/features/updates/useUpdates.js.map +1 -1
  27. package/lib/firebase/firebase.d.ts +3 -1
  28. package/lib/firebase/firebase.js +4 -3
  29. package/lib/firebase/firebase.js.map +1 -1
  30. package/lib/firebase/firebaseQuery.d.ts +3 -1
  31. package/lib/firebase/firebaseQuery.js +8 -1
  32. package/lib/firebase/firebaseQuery.js.map +1 -1
  33. package/lib/firebase/readDatabase.d.ts +2 -4
  34. package/lib/firebase/readDatabase.js +28 -5
  35. package/lib/firebase/readDatabase.js.map +1 -1
  36. package/lib/firebase/writeDatabase.d.ts +6 -2
  37. package/lib/firebase/writeDatabase.js +2 -1
  38. package/lib/firebase/writeDatabase.js.map +1 -1
  39. package/lib/hooks.d.ts +108 -9
  40. package/lib/hooks.js +547 -58
  41. package/lib/hooks.js.map +1 -1
  42. package/lib/reduxHooks.d.ts +41 -3
  43. package/lib/reduxHooks.js +61 -18
  44. package/lib/reduxHooks.js.map +1 -1
  45. package/lib/tasksAndTips.d.ts +1 -1
  46. package/lib/tasksAndTips.js +179 -8
  47. package/lib/tasksAndTips.js.map +1 -1
  48. package/lib/typeDefinitions.d.ts +14 -22
  49. package/lib/util.js +14 -8
  50. package/lib/util.js.map +1 -1
  51. package/package.json +1 -1
  52. package/src/constants.ts +7 -1
  53. package/src/features/analytics/useAnalytics.tsx +24 -15
  54. package/src/features/global/downtime/useDowntime.tsx +11 -7
  55. package/src/features/global/users/useUserFunctions.tsx +1 -1
  56. package/src/features/placements/studentPlacements/activePlacement.ts +8 -3
  57. package/src/features/placements/studentPlacements/completedStudentPlacementsSlice.ts +5 -2
  58. package/src/features/placements/studentPlacements/upcomingStudentPlacementsSlice.ts +2 -2
  59. package/src/features/placements/studentPlacements/useStudentPlacements.tsx +4 -28
  60. package/src/features/updates/useUpdates.tsx +14 -12
  61. package/src/firebase/firebase.tsx +5 -3
  62. package/src/firebase/firebaseQuery.tsx +8 -1
  63. package/src/firebase/readDatabase.tsx +33 -6
  64. package/src/firebase/writeDatabase.tsx +3 -1
  65. package/src/hooks.tsx +695 -61
  66. package/src/reduxHooks.ts +68 -20
  67. package/src/tasksAndTips.ts +184 -11
  68. package/src/typeDefinitions.ts +14 -20
  69. package/src/util.ts +14 -9
package/src/hooks.tsx CHANGED
@@ -15,7 +15,7 @@ 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
20
  QueryObject, SavedPlacement, StudentPlacementData, UserData, UserGroupData, WorkflowStage
21
21
  } from "./typeDefinitions";
@@ -1356,7 +1356,7 @@ export function usePublicPlacementListingLoader({providerId, number=5}:PublicPla
1356
1356
  const applicantWorkflow = (await firebaseQuery.getDocData(["applicantWorkflows", itemObj.applicantWorkflowId]) as ApplicantWorkflow).workflow.filter((i) => i.id === 1)[0];
1357
1357
  const applicantFiles = applicantWorkflow.files ? Object.fromEntries(await Promise.all(applicantWorkflow.files?.map(async (fileId) => {
1358
1358
  const file = await firebaseQuery.getDocData(["files", fileId]);
1359
- file.url = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`));
1359
+ file.url = await getDownloadURL(ref(storage, `providers/${itemObj.providerId}/${file.fileName}`));
1360
1360
 
1361
1361
  return [fileId, file];
1362
1362
  }))) : [];
@@ -1408,7 +1408,7 @@ export type ApplicationHookParams = {
1408
1408
  profileUrl: string | undefined;
1409
1409
  successPopup: "submitted" | "draftSaved" | "stageComplete" | "outcome" | undefined;
1410
1410
  fApplicationId: string | undefined;
1411
- fListing: PlacementListing | undefined;
1411
+ fListing: PlacementListing | false | undefined;
1412
1412
  student: UserData | undefined;
1413
1413
  fProvider: {
1414
1414
  details?: ProviderData;
@@ -1458,13 +1458,14 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1458
1458
  const [fApplication, setFApplication] = useState<Partial<Application>>(application ? applicationWithoutAdditionalData : {
1459
1459
  uid: user.userType === "Students" ? user.id : undefined,
1460
1460
  listingId: listingId,
1461
+ addressId: listing?.addressId,
1461
1462
  stage: 1,
1462
1463
  reqUserType: "Students",
1463
1464
  status: "draft"});
1464
1465
  const [fApplicationId, setFApplicationId] = useState<string|undefined>(applicationId);
1465
1466
  const [draftSaved, setDraftSaved] = useState(false);
1466
1467
  const [fProvider, setFProvider] = useState<{details?: ProviderData, profile?: string, id?: string}|undefined>(provider);
1467
- const [fListing, setFListing] = useState<PlacementListing|undefined>(listing);
1468
+ const [fListing, setFListing] = useState<PlacementListing|false|undefined>(Object.keys(listing || {}).length > 5 ? listing : undefined);
1468
1469
  const [student, setStudent] = useState<UserData|undefined>(user.userType === "Students" ? user : undefined);
1469
1470
  const [profileUrl, setProfileUrl] = useState<string>();
1470
1471
  const [successPopup, setSuccessPopup] = useState<"submitted"|"draftSaved"|"stageComplete"|"outcome">();
@@ -1476,34 +1477,44 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1476
1477
  console.log("Checking ID")
1477
1478
  if (!listingId) return;
1478
1479
  console.log("Getting listing")
1480
+ console.log("LISTING PARAM", listing, Object.keys(listing || {}).length > 5);
1479
1481
 
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;
1482
+ const listingData = (Object.keys(listing || {}).length > 5) ? listing : (await firebaseQuery.getDocData(["placementListings", listingId]).catch(() => false) as PlacementListing|false);
1483
+
1484
+ console.log("LISTINGDATA", listingData, Object.keys(listing || {}).length > 5 ? {a: "string"} : "AAA");
1485
+
1486
+
1487
+
1488
+ const address = listingData ? (user.product === "providers" && orgContext) ? orgContext.addresses[listingData.addressId || ""] : await firebaseQuery.getDocData(["addresses", listingData.addressId as string]) as Address : undefined;
1489
+ 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
1490
 
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
- }));
1491
+ if (workflow && listingData) {
1492
+ workflow.workflow = await Promise.all(workflow.workflow.map(async (s) => {
1493
+ const applicantFiles = s.files ? Object.fromEntries(await Promise.all(s.files?.map(async (fileId: string) => {
1494
+ const file = await firebaseQuery.getDocData(["files", fileId]);
1495
+ file.url = await getDownloadURL(ref(storage, `providers/${listingData?.providerId}/${file.fileName}`));
1496
+
1497
+ return [fileId, file];
1498
+ }))) : [];
1499
+ const applicantForms = s.forms ? Object.fromEntries(await Promise.all(s.forms?.map(async (formId: string) => {
1500
+ return [formId, await firebaseQuery.getDocData(["forms", formId])];
1501
+ }))) : [];
1502
+
1503
+ return {...s, viewableFiles: applicantFiles, formDetails: applicantForms};
1504
+ }));
1505
+ delete workflow.id;
1506
+ }
1507
+ if (address) {
1508
+ delete address.id;
1509
+ }
1497
1510
 
1498
- delete address.id;
1499
- delete workflow.id;
1500
1511
  console.log("Setting listing")
1501
- setFListing({...listingData, ...address, applicantWorkflow: workflow.workflow});
1512
+ setFListing((listingData && workflow) ? {...listingData, ...address, applicantWorkflow: workflow.workflow} : false);
1513
+
1502
1514
 
1503
- console.log("Getting provider", listingData?.providerId);
1504
1515
  if ((fProvider?.id === application?.providerId) && user.product === "providers") {
1505
1516
  setFProvider({details: orgContext?.details, id: user.oId});
1506
- } else if (listingData?.providerId) {
1517
+ } else if (listingData && listingData?.providerId) {
1507
1518
  console.log("Getting provider from DB");
1508
1519
  const provider = await firebaseQuery.getDocData(["providers", listingData.providerId]) as ProviderData;
1509
1520
  console.log("Provider", provider);
@@ -1525,7 +1536,7 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1525
1536
  }, []);
1526
1537
 
1527
1538
  useEffect(() => {
1528
- setFListing(listing);
1539
+ setFListing(Object.keys(listing || {}).length > 5 ? listing : undefined);
1529
1540
  }, [listing]);
1530
1541
 
1531
1542
  useEffect(() => {
@@ -1550,7 +1561,18 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1550
1561
  });
1551
1562
  return;
1552
1563
  }
1553
- if (applicationId) setFApplicationId(applicationId);
1564
+ if (applicationId) {
1565
+ setFApplicationId(applicationId);
1566
+ if (application) {
1567
+ let applicationWithoutAdditionalData = {...(application || {})} as any;
1568
+ delete applicationWithoutAdditionalData.listing;
1569
+ delete applicationWithoutAdditionalData.address;
1570
+ delete applicationWithoutAdditionalData.provider;
1571
+ setFApplication(applicationWithoutAdditionalData);
1572
+ } else {
1573
+ firebaseQuery.getDocData(["applications", applicationId]).then(setFApplication);
1574
+ }
1575
+ }
1554
1576
  }, [application, applicationId]);
1555
1577
 
1556
1578
  const getCurrentStage = async (stage: number): Promise<{
@@ -1565,6 +1587,8 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1565
1587
  } | undefined;
1566
1588
  };
1567
1589
  }> => {
1590
+ console.log("fLSITING CURRENT STAGE", fListing);
1591
+ if (!fListing) throw new Error("Listing deleted");
1568
1592
  if (!fListing?.applicantWorkflowId) throw new Error("No workflow stage");
1569
1593
 
1570
1594
  const mApplicantWorkflow = (await firebaseQuery.getDocData(["applicantWorkflows", fListing.applicantWorkflowId]) as ApplicantWorkflow);
@@ -1575,7 +1599,7 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1575
1599
 
1576
1600
  const applicantFiles = stageObj.files ? Object.fromEntries(await Promise.all(stageObj.files?.map(async (fileId) => {
1577
1601
  const file = await firebaseQuery.getDocData(["files", fileId]);
1578
- file.url = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`));
1602
+ file.url = await getDownloadURL(ref(storage, `providers/${mApplicantWorkflow?.oId}/${file.fileName}`));
1579
1603
 
1580
1604
  return [fileId, file];
1581
1605
  }))) : [];
@@ -1615,11 +1639,12 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1615
1639
 
1616
1640
  const addApplication = async () => {
1617
1641
  console.log("ADDING APPLICATION");
1618
- if (!fListing?.id || !fProvider?.id || !student?.id) return;
1642
+ if (!fListing || !fListing?.id || !fProvider?.id || !student?.id) return;
1619
1643
 
1620
1644
  const applicationData = {
1621
1645
  uid: student.id,
1622
1646
  listingId: fListing?.id,
1647
+ addressId: fListing.addressId,
1623
1648
  applicantWorkflowId: fListing?.applicantWorkflowId,
1624
1649
  providerId: fProvider.id,
1625
1650
  stage: 1,
@@ -1637,9 +1662,9 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1637
1662
  getUploadedFiles();
1638
1663
 
1639
1664
  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");
1665
+
1666
+ if (!fListing || !fListing?.id || !fProvider?.id || !student?.id) return;
1667
+ if (user.product === "providers") return;
1643
1668
 
1644
1669
  console.log("Checking dates and sections");
1645
1670
  if (!fApplication.completedSections && !fApplication.startDate && !fApplication.endDate) return;
@@ -1709,6 +1734,7 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1709
1734
  const viewFile = (file: string, onOpen: (url: string) => void) => {
1710
1735
  if (fApplication.reqUserType !== user.userType) return;
1711
1736
  if (fApplication.stage === undefined) throw new Error("Missing applciation stage.")
1737
+ if (!fListing) throw new Error("No associated listing.");
1712
1738
 
1713
1739
  const viewableFiles = fListing?.applicantWorkflow?.find((stage) => stage.id === fApplication.stage)?.viewableFiles;
1714
1740
 
@@ -1763,21 +1789,29 @@ export function useCreateApplicationRenderer({user, listingId, listing, provider
1763
1789
  openSuccessPopup("draftSaved")
1764
1790
  return;
1765
1791
  }
1792
+
1793
+ if (!fApplicationId) return;
1766
1794
 
1767
1795
  // Check all items have been filled in.
1768
1796
  if (!fApplication.startDate || !fApplication.endDate) throw new Error("Please select dates for your placement.");
1769
1797
 
1770
1798
  await executeCallable("applications-submit", {applicationId: fApplicationId});
1799
+
1800
+ const newApplication = await firebaseQuery.getDocData(["applications", fApplicationId]) as Application;
1801
+ setFApplication(newApplication);
1771
1802
  openSuccessPopup("submitted")
1772
1803
  };
1773
1804
 
1774
1805
  const progressStage = async (type: number|"accept"|"reject", e?: {feedback?: string}) => {
1775
1806
  // Check all stages completed.
1807
+ if (!fApplicationId) return;
1776
1808
  if (!currentStageComplete) throw new Error("Complete all forms before submitting.");
1777
1809
  if (fApplication.reqUserType !== user.userType) throw new Error(`Incorrect user type. Expected ${fApplication.reqUserType}, got: ${user.userType}`);
1778
1810
  if (fApplication.stage === undefined) throw new Error("Missing applciation stage.")
1779
1811
 
1780
1812
  await executeCallable("applications-changeStage", {applicationId: fApplicationId, type: type, feedback: e?.feedback});
1813
+ const newApplication = await firebaseQuery.getDocData(["applications", fApplicationId]) as Application;
1814
+ setFApplication(newApplication);
1781
1815
  };
1782
1816
 
1783
1817
 
@@ -1795,8 +1829,6 @@ export function useProposePlacementRenderer({user, orgContext, placement}:
1795
1829
  const [student, setStudent] = useState<UserData|undefined>((user && (user?.userType === "Students")) ? user : undefined);
1796
1830
  const [cohort, setCohort] = useState<CohortData>();
1797
1831
  const [complete, setComplete] = useState(false);
1798
-
1799
-
1800
1832
  const [stage, setStage] = useState<"provider"|"address"|"dates"|"review">("dates");
1801
1833
 
1802
1834
  const sections:Array<"dates"|"provider"|"address"|"review"> = ["dates", "provider", "address", "review"];
@@ -1865,16 +1897,32 @@ export function useProposePlacementRenderer({user, orgContext, placement}:
1865
1897
  setFormData(undefined);
1866
1898
  };
1867
1899
 
1868
- const proposePlacement = async (draft=false) => {
1900
+ const proposePlacement = async (draft = false) => {
1901
+ const getPlacementStatus = (startDate: Date, endDate: Date) => {
1902
+ const today = new Date()
1903
+ if (startDate <= today && endDate >= today) return { active: !draft ? true : false, inProgress: true, completed: false};
1904
+ else if (endDate <= today) return { completed: true, inProgress: false, active: false};
1905
+ return { completed: false, inProgress: true, active: false };
1906
+ }
1907
+
1869
1908
  if (!formData || !student) {
1870
1909
  throw new Error("Cannot find placement details.");
1871
1910
  }
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);
1911
+
1912
+ try {
1913
+ console.log("formData", formData);
1914
+ const status = getPlacementStatus(new Date(formData.startDate), new Date(formData.endDate));
1915
+ const newFormData = {...formData, ...status}
1916
+
1917
+ if (newFormData.id && newFormData.uid && draft === newFormData.draft) await firebaseQuery.update(["placements", newFormData.id], newFormData);
1918
+ else await addPlacement(newFormData, student.id, draft)
1919
+ return {status}
1920
+
1921
+ } catch (error) {
1922
+ console.log("Error:", error);
1923
+ throw error;
1876
1924
  }
1877
-
1925
+ /*
1878
1926
  console.log("STUDENTID", student.id);
1879
1927
 
1880
1928
  return await addPlacement(formData, student.id, draft).catch((e) => {
@@ -1885,9 +1933,9 @@ export function useProposePlacementRenderer({user, orgContext, placement}:
1885
1933
  setComplete(true);
1886
1934
  return e;
1887
1935
  });
1888
-
1889
-
1936
+ */
1890
1937
  };
1938
+
1891
1939
 
1892
1940
  const deletePlacement = async (id:string) => {
1893
1941
  return await firebaseQuery.delete(["placements", id]);
@@ -2982,6 +3030,9 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
2982
3030
  const [uploadRA, setUploadRA] = useState(false);
2983
3031
  const [uploadDBS, setUploadDBS] = useState(false);
2984
3032
  const [onboardingPopup, setOnboardingPopup] = useState(false);
3033
+ const [dismissOnboardingPopup, setDismissOnboardingPopup] = useState(false);
3034
+ const [addOnboardingDocsPopup, setAddOnboardingDocsPopup] = useState(false);
3035
+
2985
3036
  const [editable, setEditable] = useState(false);
2986
3037
 
2987
3038
  const [withdrawFromPlacementPopup, setWithdrawFromPlacementPopup] = useState(false);
@@ -3031,7 +3082,7 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3031
3082
  if (user.userType === "Students") {
3032
3083
  setStudent(user);
3033
3084
  } else {
3034
- getUserById(placement.uid).then(setStudent);
3085
+ getUserById(placement.uid, undefined, false).then(setStudent);
3035
3086
  }
3036
3087
 
3037
3088
  }, [placement]);
@@ -3097,15 +3148,14 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3097
3148
  };
3098
3149
 
3099
3150
 
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";
3151
+ const onboardingStatus:"Add onboarding documents"|"Onboarding sent"|"Onboarding docs completed"|"Onboarding docs approved"|"Complete onboarding" =
3152
+ 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
3153
 
3102
3154
  const signOffPlacements = getAccess(user, "signOffPlacements");
3103
3155
 
3104
3156
  let canEdit = false;
3105
3157
 
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") {
3158
+ if ((wStage?.userType === "Staff" && user.userType === "Staff" && user.product === "institutes") || (user.product === "providers" && wStage?.userType === "Provider") || user.userType === "Students" && wStage?.userType === "Students") {
3109
3159
  console.log("ALMOST CAN EDIT");
3110
3160
  if (user.userType === "Staff" && !signOffPlacements) {
3111
3161
  canEdit = false;
@@ -3116,7 +3166,7 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3116
3166
 
3117
3167
 
3118
3168
 
3119
- const onFlagClick = async (e:FlagCodes) => {
3169
+ const onFlagClick = async (e:FlagCodes, onClose?: boolean) => {
3120
3170
  if (!placement) return;
3121
3171
  if (e === "completeOnboarding" || e === "reviewOnboarding") {
3122
3172
  setOnboardingPopup(true);
@@ -3146,6 +3196,13 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3146
3196
  }
3147
3197
  setExternalDocPopupOpen("dbsCheck");
3148
3198
  }
3199
+ if (e === "addOnboarding") {
3200
+ if (onClose) {
3201
+ setDismissOnboardingPopup(true);
3202
+ } else {
3203
+ setAddOnboardingDocsPopup(true);
3204
+ }
3205
+ }
3149
3206
  };
3150
3207
 
3151
3208
  const approveELI = async () => {
@@ -3187,7 +3244,7 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3187
3244
  throw e;
3188
3245
  });
3189
3246
  console.log("RETURN", res.data);
3190
- setPlacement((p) => ({...p, ...res.data}));
3247
+ setPlacement((p) => ({...p, ...res.data as any}));
3191
3248
  if (uploadProviderDocPopup === "insurance") {
3192
3249
  setUploadProviderDocPopup(undefined);
3193
3250
  setUploadInsurance(true);
@@ -3206,26 +3263,28 @@ export function useGetIndividualPlacementForPlacementPage({user, placementId, or
3206
3263
  await executeCallable("placement-withdraw", {placementId: placementId});
3207
3264
  }
3208
3265
 
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}
3266
+ 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}
3210
3267
  }
3211
3268
 
3212
- export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onboarding: (
3269
+ export function useOnboardingPopup({onboarding, providerId, placementId, user, onClose}:{onboarding: (
3213
3270
  OnboardingDocs&{
3214
3271
  completed: {
3215
- submitted: string|false,
3272
+ submitted: boolean,
3273
+ submittedDate?: string,
3216
3274
  accepted?: boolean,
3217
3275
  filesViewed?: string[],
3218
3276
  formsCompleted?: {[key: string]: unknown},
3219
3277
  filesUploaded?: {[key: number]: string[]},
3220
3278
  }}
3221
- ), placementId: string, user: UserData, onClose: () => void}) {
3279
+ ), placementId: string, user: UserData, providerId: string, onClose: () => void}) {
3222
3280
 
3223
3281
  const [fileUploadPopup, setFileUploadPopup] = useState<false|number>(false);
3224
3282
  const [form, setForm] = useState<{id: string, name: string, [key:string]: unknown}>();
3225
3283
  const [rejectPopup, setRejectPopup] = useState(false);
3226
3284
  const [mOnboarding, setMOnboarding] = useState(onboarding);
3227
3285
  const [completedSections, setCompletedSections] = useState<{
3228
- submitted: string | false;
3286
+ submitted: boolean,
3287
+ submittedDate?: string,
3229
3288
  filesViewed?: string[] | undefined;
3230
3289
  formsCompleted?: {
3231
3290
  [key: string]: unknown;
@@ -3234,7 +3293,19 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3234
3293
  [key: number]: string[];
3235
3294
  } | undefined;
3236
3295
  }>();
3296
+
3237
3297
  const [uploadedFiles, setUploadedFiles] = useState<{[key: string]: FileItem}>({});
3298
+ const [viewableFiles, setViewableFiles] = useState<{[key: string]: FileItem}>();
3299
+ const [formDetails, setFormDetails] = useState<{
3300
+ [key: string]: {
3301
+ name: string;
3302
+ id: string;
3303
+ description?: string;
3304
+ product: Products;
3305
+ oId: string;
3306
+ form: CustomFormSchema;
3307
+ };
3308
+ }>({});
3238
3309
 
3239
3310
  const firebaseQuery = new FirebaseQuery();
3240
3311
 
@@ -3252,7 +3323,7 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3252
3323
  const viewedFiles = a.completed ? a.completed?.filesViewed || [] : [];
3253
3324
  if (viewedFiles?.includes(file)) return a;
3254
3325
 
3255
- a?.viewableFiles?.[file].url && onOpen(a?.viewableFiles?.[file].url);
3326
+ viewableFiles?.[file].url && onOpen(viewableFiles?.[file].url);
3256
3327
  viewedFiles?.push(file);
3257
3328
 
3258
3329
  const newA = editNestedObject(["completed", "filesViewed"], oldA, viewedFiles) as any;
@@ -3275,7 +3346,7 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3275
3346
 
3276
3347
  const onboardingFiles = onboarding.files ? Object.fromEntries(await Promise.all(onboarding.files?.map(async (fileId) => {
3277
3348
  const file = await firebaseQuery.getDocData(["files", fileId]);
3278
- file.url = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`));
3349
+ file.url = await getDownloadURL(ref(storage, `providers/${providerId}/${file.fileName}`));
3279
3350
 
3280
3351
  return [fileId, file];
3281
3352
  }))) : [];
@@ -3283,8 +3354,8 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3283
3354
  return [formId, await firebaseQuery.getDocData(["forms", formId])];
3284
3355
  }))) : [];
3285
3356
 
3286
- onboardingNew.viewableFiles = onboardingFiles;
3287
- onboardingNew.formDetails = onboardingForms;
3357
+ setViewableFiles(onboardingFiles);
3358
+ setFormDetails(onboardingForms);
3288
3359
  return onboardingNew;
3289
3360
  };
3290
3361
  getOnboardingData().then(setMOnboarding);
@@ -3346,7 +3417,7 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3346
3417
  // Check all stages completed.
3347
3418
  if (!placementId) return;
3348
3419
  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});
3420
+ await firebaseQuery.update(["placements", placementId], {["onboarding.completed.accepted"]: false, ["onboarding.completed.submitted"]: true, ["onboarding.completed.submittedDate"]: convertDate(new Date(), "dbstring") as string});
3350
3421
  };
3351
3422
 
3352
3423
  useEffect(() => {
@@ -3391,5 +3462,568 @@ export function useOnboardingPopup({onboarding, placementId, user, onClose}:{onb
3391
3462
  addCompletedSectionURLs();
3392
3463
  }, [mOnboarding]);
3393
3464
 
3394
- return {addFile, viewFile, uploadedFiles, setFormComplete, setRejectPopup, stagesCompleted, setForm, mOnboarding, form, submit, acceptOnboarding, rejectOnboarding, rejectPopup, completedSections, fileUploadPopup, setFileUploadPopup};
3395
- }
3465
+ return {addFile, viewFile, uploadedFiles, setFormComplete, setRejectPopup, stagesCompleted, setForm, mOnboarding, form, submit, acceptOnboarding, rejectOnboarding, rejectPopup, completedSections, fileUploadPopup, setFileUploadPopup, viewableFiles, formDetails};
3466
+ }
3467
+
3468
+
3469
+
3470
+
3471
+
3472
+ export function useLoadAddresses(user: UserData, limitItems?: number, queryConstraint?: QueryConstraint[]) {
3473
+ const [addresses, setAddresses] = useState<{[key: string]: OrganisationAddress}>({});
3474
+ const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
3475
+ const [loading, setLoading] = useState<boolean>(false);
3476
+
3477
+ const firebaseQuery = new FirebaseQuery();
3478
+
3479
+ const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
3480
+
3481
+ const changeQueryConstraints = (e: QueryConstraint[]) => {
3482
+ setQueryConstraints([...(queryConstraint || []), ...e]);
3483
+ };
3484
+
3485
+ const loadAddresses = () => {
3486
+ const constraints:QueryConstraint[] = [where("oId", "==", user.oId), where("product", "==", user.product), orderBy(documentId())]
3487
+
3488
+ if (limitItems) {
3489
+ constraints.push(limit(limitItems));
3490
+ }
3491
+
3492
+ if (user.groupData?.viewAddresses === "all" || user.userGroup === "admin") {
3493
+ if (lastDoc?.id) {
3494
+ constraints.push(startAfter(lastDoc?.id));
3495
+ }
3496
+ } else if (user.groupData?.viewAddresses === "request") {
3497
+ if (!user.visibleAddresses?.length) return;
3498
+
3499
+ constraints.push(where(documentId(), "in", user.visibleAddresses))
3500
+ if (lastDoc?.id) {
3501
+ constraints.push(startAfter(lastDoc?.id));
3502
+ }
3503
+ } else {
3504
+ setLoading(false);
3505
+ return; // viewAddresses === "none", no need to load anything
3506
+ }
3507
+ queryConstraints && constraints.unshift(...queryConstraints);
3508
+
3509
+ return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
3510
+ const deletedAddresses = snapshot.docChanges().map((change) => {
3511
+ if (change.type === "removed") {
3512
+ return change.doc.id;
3513
+ }
3514
+ return;
3515
+ })
3516
+
3517
+ setAddresses((prev) => Object.fromEntries(Object.entries(prev).filter(([k]) => !deletedAddresses.includes(k))))
3518
+
3519
+ if (!snapshot.empty) {
3520
+
3521
+
3522
+ const newAddresses:[string, OrganisationAddress][] = snapshot.docs.map(doc => ([doc.id, {id: doc.id, ...doc.data() as OrganisationAddress}]));
3523
+
3524
+ const withListings:{[key: string]: OrganisationAddress&{listings: number}} = Object.fromEntries(await Promise.all(newAddresses.map(async ([k, address]) => {
3525
+ const listings = await firebaseQuery.getCount("placementListings", [where("providerId", "==", user.oId), where("addressId", "==", k)]);
3526
+ return [k, {...address, listings: listings}];
3527
+ })));
3528
+
3529
+ setAddresses(prev => ({...prev, ...withListings}));
3530
+ setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
3531
+ }
3532
+ setLoading(false);
3533
+ }), "addresses", constraints, undefined, true);
3534
+ };
3535
+
3536
+ useEffect(() => {
3537
+ const unsubscribe = loadAddresses();
3538
+
3539
+ return () => {
3540
+ if (unsubscribe) {
3541
+ unsubscribe(); // Unsubscribe from the snapshot listener when the component unmounts
3542
+ }
3543
+ };
3544
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3545
+ }, [queryConstraints]);
3546
+
3547
+ const onScrollBottom = () => {
3548
+ if (!limitItems) return;
3549
+ if (!loading) {
3550
+ setLoading(true);
3551
+ loadAddresses();
3552
+ }
3553
+ };
3554
+
3555
+ return { addresses, onScrollBottom, loading, changeQueryConstraints };
3556
+ }
3557
+
3558
+
3559
+
3560
+
3561
+ export function useLoadListings(user: UserData, queryConstraint?: QueryConstraint[]) {
3562
+ const [listings, setListings] = useState<[string, PlacementListing&{applicants?: number, scheduled?: number, active?: number}][]>([]);
3563
+ const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
3564
+ const [loading, setLoading] = useState<boolean>(false);
3565
+
3566
+ const firebaseQuery = new FirebaseQuery();
3567
+
3568
+ const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
3569
+
3570
+ const changeQueryConstraints = (e: QueryConstraint[]) => {
3571
+ setQueryConstraints([...(queryConstraint || []), ...e]);
3572
+ };
3573
+
3574
+ const loadListings = () => {
3575
+ const constraints:QueryConstraint[] = [where("providerId", "==", user.oId), limit(10), orderBy(documentId())]
3576
+
3577
+ if (user.groupData?.viewPlacementListings === "all" || user.userGroup === "admin") {
3578
+ if (lastDoc?.id) {
3579
+ constraints.push(startAfter(lastDoc));
3580
+ }
3581
+ } else if (user.groupData?.viewPlacementListings === "request") {
3582
+ if (!user.visibleListings?.length) return;
3583
+ constraints.push(where(documentId(), 'in', user.visibleListings));
3584
+ if (lastDoc?.id) {
3585
+ constraints.push(startAfter(lastDoc));
3586
+ }
3587
+ }
3588
+
3589
+ if (user.groupData?.viewAddresses !== "all" && user.userGroup !== "admin") {
3590
+ if (!user.visibleAddresses?.length) return;
3591
+
3592
+ constraints.push(where('addressId', 'in', user.visibleAddresses));
3593
+ }
3594
+ queryConstraints && constraints.unshift(...queryConstraints);
3595
+
3596
+ return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
3597
+ const deletedListings = snapshot.docChanges().map((change) => {
3598
+ if (change.type === "removed") {
3599
+ return change.doc.id;
3600
+ }
3601
+ return;
3602
+ })
3603
+
3604
+ setListings((prev) => prev.filter(([k]) => !deletedListings.includes(k)))
3605
+
3606
+ if (!snapshot.empty) {
3607
+ const newListings:[string, PlacementListing][] = snapshot.docs.map(doc => ([doc.id, {...doc.data() as PlacementListing, id: doc.id}]));
3608
+
3609
+ const listingsWithAdditionalData:[string, PlacementListing&{applicants?: number, scheduled?: number, active?: number}][] = await Promise.all(newListings.map(async ([id, listing]) => {
3610
+ const listingWithAdditionalData = {...listing} as PlacementListing&{applicants?: number, scheduled?: number, active?: number};
3611
+
3612
+ if (listingWithAdditionalData.applicants !== undefined) return [id, listingWithAdditionalData];
3613
+
3614
+ listingWithAdditionalData.applicants = await firebaseQuery.getCount("applications", [where("providerId", "==", user.oId), where("placementId", "==", id), where("status", "==", "submitted")]);
3615
+ listingWithAdditionalData.scheduled = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("placementId", "==", id), where("inProgress", "==", true), where("startDate", ">", convertDate(new Date(), "dbstring"))]);
3616
+ listingWithAdditionalData.active = await firebaseQuery.getCount("placements", [where("providerId", "==", user.oId), where("placementId", "==", id), where("active", "==", true)]);
3617
+
3618
+ return [id, listingWithAdditionalData];
3619
+ }));
3620
+ setListings(prev => (Object.entries({...Object.fromEntries(prev), ...Object.fromEntries(listingsWithAdditionalData)})));
3621
+ setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
3622
+ }
3623
+ setLoading(false);
3624
+ }), "placementListings", constraints, undefined, true);
3625
+ };
3626
+
3627
+ useEffect(() => {
3628
+ let unsubscribe: () => void;
3629
+
3630
+
3631
+
3632
+ loadListings();
3633
+
3634
+ return () => {
3635
+ if (unsubscribe) {
3636
+ unsubscribe(); // Unsubscribe from the snapshot listener when the component unmounts
3637
+ }
3638
+ };
3639
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3640
+ }, [queryConstraints]);
3641
+
3642
+ const onScrollBottom = () => {
3643
+ if (!loading) {
3644
+ setLoading(true);
3645
+ loadListings();
3646
+ }
3647
+ };
3648
+
3649
+ return { listings, onScrollBottom, loading, changeQueryConstraints };
3650
+ }
3651
+
3652
+
3653
+
3654
+
3655
+
3656
+
3657
+ export function useLoadApplications({user, applicationType, listingId, queryConstraint}:{user: UserData, applicationType?: "all"|"actionRequired"|"awaitingStudent"|"closed", listingId?: string, queryConstraint?: QueryConstraint[]}) {
3658
+ const [applications, setApplications] = useState<[string, Application][]>([]);
3659
+ const [lastDoc, setLastDoc] = useState<QueryDocumentSnapshot|null>(null);
3660
+ const [loading, setLoading] = useState<boolean>(false);
3661
+ const [type, setType] = useState<"actionRequired"|"awaitingStudent"|"closed"|"all">(applicationType || "all");
3662
+
3663
+ const firebaseQuery = new FirebaseQuery();
3664
+
3665
+ const [queryConstraints, setQueryConstraints] = useState<QueryConstraint[]>(queryConstraint || []);
3666
+
3667
+ const changeQueryConstraints = (e: QueryConstraint[]) => {
3668
+ setQueryConstraints([...(queryConstraint || []), ...e]);
3669
+ };
3670
+
3671
+ const loadApplications = () => {
3672
+ const constraints:QueryConstraint[] = [where("providerId", "==", user.oId), limit(10), orderBy(documentId())]
3673
+
3674
+
3675
+ if (lastDoc?.id) {
3676
+ constraints.push(startAfter(lastDoc));
3677
+ }
3678
+
3679
+ switch (type) {
3680
+ case "actionRequired":
3681
+ constraints.push(where("status", "==", "submitted"), where("reqUserType", "==", "Staff"));
3682
+ break;
3683
+ case "awaitingStudent":
3684
+ constraints.push(where("status", "==", "submitted"), where("reqUserType", "==", "Students"));
3685
+ break;
3686
+ case "closed":
3687
+ constraints.push(where("status", "in", ["approved", "declined"]));
3688
+ break;
3689
+ default:
3690
+ constraints.push(where("status", "==", "submitted"));
3691
+ }
3692
+ if (listingId) {
3693
+ constraints.push(where("listingId", "==", listingId));
3694
+ }
3695
+
3696
+ console.log("Constraints before user group check", constraints);
3697
+ if (user.groupData?.viewAddresses !== "all" && user.userGroup !== "admin") {
3698
+ if (user.groupData?.viewPlacementListings === "all") {
3699
+ if (!user.visibleAddresses?.length) return;
3700
+ constraints.push(where('addressId', 'in', user.visibleAddresses));
3701
+ } else {
3702
+ if (!user.visibleListings?.length) return;
3703
+ constraints.push(where('placementId', 'in', user.visibleListings));
3704
+ }
3705
+ }
3706
+
3707
+ queryConstraints && constraints.unshift(...queryConstraints);
3708
+ console.log("Constraints after user group check", constraints);
3709
+
3710
+ return firebaseQuery.collectionSnapshot((async (snapshot: QuerySnapshot<DocumentData>) => {
3711
+ const deletedApplications = snapshot.docChanges().map((change) => {
3712
+ if (change.type === "removed") {
3713
+ return change.doc.id;
3714
+ }
3715
+ return;
3716
+ })
3717
+
3718
+ console.log("applicantCount", snapshot.size);
3719
+
3720
+ setApplications((prev) => prev.filter(([k]) => !deletedApplications.includes(k)))
3721
+
3722
+ if (!snapshot.empty) {
3723
+ 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][];
3724
+
3725
+ setApplications(prev => ([...prev, ...newApplications]));
3726
+ setLastDoc(snapshot.docs[snapshot.docs.length - 1]);
3727
+ }
3728
+ setLoading(false);
3729
+ }), "applications", constraints, undefined, true);
3730
+ };
3731
+
3732
+ useEffect(() => {
3733
+ let unsubscribe: () => void;
3734
+
3735
+ loadApplications();
3736
+ return () => {
3737
+ if (unsubscribe) {
3738
+ unsubscribe(); // Unsubscribe from the snapshot listener when the component unmounts
3739
+ }
3740
+ };
3741
+ // eslint-disable-next-line react-hooks/exhaustive-deps
3742
+ }, [type, queryConstraints]);
3743
+
3744
+ const onScrollBottom = () => {
3745
+ if (!loading) {
3746
+ setLoading(true);
3747
+ loadApplications();
3748
+ }
3749
+ };
3750
+
3751
+ return { applications, type, setType, onScrollBottom, loading, changeQueryConstraints };
3752
+ }
3753
+
3754
+
3755
+
3756
+
3757
+
3758
+
3759
+
3760
+ type ViewType = 'list' | 'table';
3761
+
3762
+ export interface UseFirestoreQueryProps {
3763
+ path?: string | string[];
3764
+ constraints?: QueryConstraint[];
3765
+ view: ViewType;
3766
+ limit: number;
3767
+ formatItems?: (key: string, item: any) => Promise<{ key: string, item: any }> | { key: string, item: any };
3768
+ snapshot?: boolean;
3769
+ filters?: { [key: string]: any };
3770
+ search?: string;
3771
+ onSearch?: (search: string, page: number, limit: number) => Promise<{ [key: string]: any }>;
3772
+ data?: { [key: string]: any };
3773
+ }
3774
+
3775
+ export interface UseFirestoreQueryReturn {
3776
+ data: { [key: string]: any };
3777
+ loading: boolean | 'loaded';
3778
+ pageUp: () => void;
3779
+ pageDown?: () => void;
3780
+ setQueries: (newConstraints: QueryConstraint[]) => void;
3781
+ reset: () => void;
3782
+ setFilters: (newFilters: { [key: string]: any }) => void;
3783
+ setSearch: (search: string) => void;
3784
+ }
3785
+
3786
+ export const useFirestoreQuery = (firebaseQuery: FirebaseQuery, props: UseFirestoreQueryProps): UseFirestoreQueryReturn => {
3787
+ const { path, constraints = [], view, limit: itemsPerPage, formatItems, snapshot = false, filters = {}, search, onSearch, data: predefinedData } = props;
3788
+
3789
+ const [data, setData] = useState<{ [key: string]: any }>(predefinedData || {});
3790
+ const [loading, setLoading] = useState<boolean | 'loaded'>(true);
3791
+ const [lastVisible, setLastVisible] = useState<QueryDocumentSnapshot<DocumentData> | null>(null);
3792
+ const [firstVisible, setFirstVisible] = useState<QueryDocumentSnapshot<DocumentData> | null>(null);
3793
+ const [currentPage, setCurrentPage] = useState(0);
3794
+ const listenersRef = useRef<{ [key: string]: () => void }>({});
3795
+
3796
+ const buildConstraints = useCallback(() => {
3797
+ const allConstraints = [...constraints];
3798
+
3799
+ Object.entries(filters).forEach(([key, value]) => {
3800
+ allConstraints.push(where(key, '==', value));
3801
+ });
3802
+
3803
+ return allConstraints;
3804
+ }, [constraints, filters]);
3805
+
3806
+ const paginateData = useCallback((items: { [key: string]: any }, page: number) => {
3807
+ const startIndex = page * itemsPerPage;
3808
+ const endIndex = startIndex + itemsPerPage;
3809
+ const paginatedData = Object.entries(items).slice(startIndex, endIndex).reduce((acc, [key, value]) => {
3810
+ acc[key] = value;
3811
+ return acc;
3812
+ }, {} as { [key: string]: any });
3813
+
3814
+ return paginatedData;
3815
+ }, [itemsPerPage]);
3816
+
3817
+ const filterData = useCallback((items: { [key: string]: any }, searchString: string) => {
3818
+ const filteredData = Object.entries(items).filter(([key, value]) => {
3819
+ const itemString = JSON.stringify(value).toLowerCase();
3820
+ return itemString.includes(searchString.toLowerCase());
3821
+ }).reduce((acc, [key, value]) => {
3822
+ acc[key] = value;
3823
+ return acc;
3824
+ }, {} as { [key: string]: any });
3825
+
3826
+ return filteredData;
3827
+ }, []);
3828
+
3829
+ const applyFormatItems = useCallback(
3830
+ async (key: string, item: any): Promise<{ key: string, item: any }> => {
3831
+ if (formatItems) {
3832
+ const result = formatItems(key, item);
3833
+ return result instanceof Promise ? await result : result;
3834
+ }
3835
+ return { key, item };
3836
+ },
3837
+ [formatItems]
3838
+ );
3839
+
3840
+ const setSearch = useCallback(async (newSearch: string, page: number = 0) => {
3841
+ if (predefinedData) {
3842
+ let results = predefinedData;
3843
+
3844
+ if (newSearch) {
3845
+ results = filterData(results, newSearch);
3846
+ }
3847
+
3848
+ const paginatedResults = paginateData(results, page);
3849
+ setData(paginatedResults);
3850
+
3851
+ setLoading(Object.keys(paginatedResults).length < itemsPerPage ? 'loaded' : false);
3852
+ return;
3853
+ }
3854
+
3855
+ if (onSearch) {
3856
+ setLoading(true);
3857
+ const results = await onSearch(newSearch, page, itemsPerPage);
3858
+ setData(results);
3859
+ setLoading(results && Object.keys(results).length < itemsPerPage ? 'loaded' : false);
3860
+ }
3861
+ }, [onSearch, itemsPerPage, filterData, paginateData, predefinedData]);
3862
+
3863
+ const fetchData = useCallback(async (direction?: 'up' | 'down') => {
3864
+ if (snapshot) {
3865
+ return;
3866
+ }
3867
+
3868
+ if (predefinedData) {
3869
+ setLoading(false);
3870
+ let results = predefinedData;
3871
+
3872
+ if (search) {
3873
+ results = filterData(results, search);
3874
+ }
3875
+
3876
+ const paginatedResults = paginateData(results, currentPage);
3877
+ setData(paginatedResults);
3878
+
3879
+ setLoading(Object.keys(paginatedResults).length < itemsPerPage ? 'loaded' : false);
3880
+ return;
3881
+ }
3882
+
3883
+ setLoading(true);
3884
+ const constraintsWithLimit = [...buildConstraints(), limit(itemsPerPage)];
3885
+
3886
+ if (view === 'list' && lastVisible && direction === 'up') {
3887
+ constraintsWithLimit.push(startAfter(lastVisible));
3888
+ }
3889
+
3890
+ if (view === 'table' && direction) {
3891
+ if (direction === 'up' && lastVisible) {
3892
+ constraintsWithLimit.push(startAfter(lastVisible));
3893
+ }
3894
+ if (direction === 'down' && firstVisible) {
3895
+ constraintsWithLimit.push(endBefore(firstVisible));
3896
+ }
3897
+ }
3898
+
3899
+ const results = await firebaseQuery.getDocsWhere(path!, constraintsWithLimit, true) as QuerySnapshot<DocumentData>;
3900
+
3901
+ if (Object.keys(results).length === 0) {
3902
+ setLoading('loaded');
3903
+ return;
3904
+ }
3905
+
3906
+ const formattedResults: { [key: string]: any } = {};
3907
+ for (const [key, item] of Object.entries(results)) {
3908
+ const formatted = await applyFormatItems(key, item);
3909
+ formattedResults[formatted.key] = formatted.item;
3910
+ }
3911
+
3912
+ setData(prevData => (direction === 'down' ? formattedResults : { ...prevData, ...formattedResults }));
3913
+
3914
+ const docEntries = Object.entries(results);
3915
+ if (direction === 'up' || !direction) {
3916
+ setLastVisible(docEntries[docEntries.length - 1][1]);
3917
+ }
3918
+ if (direction === 'down' || !direction) {
3919
+ setFirstVisible(docEntries[0][1]);
3920
+ }
3921
+
3922
+ setLoading(false);
3923
+ }, [buildConstraints, firebaseQuery, applyFormatItems, lastVisible, firstVisible, view, snapshot, predefinedData, filterData, paginateData, currentPage, itemsPerPage]);
3924
+
3925
+ const pageUp = useCallback(() => {
3926
+ setCurrentPage(prevPage => {
3927
+ const newPage = prevPage + 1;
3928
+
3929
+ if (search && onSearch) {
3930
+ setSearch(search, newPage);
3931
+ } else {
3932
+ fetchData('up');
3933
+ }
3934
+
3935
+ return newPage;
3936
+ });
3937
+ }, [fetchData, onSearch, search, setSearch]);
3938
+
3939
+ const pageDown = useCallback(() => {
3940
+ setCurrentPage(prevPage => {
3941
+ const newPage = Math.max(prevPage - 1, 0);
3942
+
3943
+ if (search && onSearch) {
3944
+ setSearch(search, newPage);
3945
+ } else {
3946
+ fetchData('down');
3947
+ }
3948
+
3949
+ return newPage;
3950
+ });
3951
+ }, [fetchData, onSearch, search, setSearch]);
3952
+
3953
+ const reset = useCallback(() => {
3954
+ setData(predefinedData || {});
3955
+ setLastVisible(null);
3956
+ setFirstVisible(null);
3957
+ setCurrentPage(0);
3958
+ setLoading(true);
3959
+ fetchData();
3960
+ }, [fetchData, predefinedData]);
3961
+
3962
+ const setQueries = useCallback((newConstraints: QueryConstraint[]) => {
3963
+ reset();
3964
+ }, [reset]);
3965
+
3966
+ const setFilters = useCallback((newFilters: { [key: string]: any }) => {
3967
+ reset();
3968
+ }, [reset]);
3969
+
3970
+ useEffect(() => {
3971
+ if (!search) {
3972
+ fetchData();
3973
+ } else {
3974
+ setSearch(search, 0);
3975
+ }
3976
+ }, [fetchData, search, setSearch]);
3977
+
3978
+ useEffect(() => {
3979
+ if (snapshot && path) {
3980
+ const constraintsWithLimit = [...buildConstraints(), limit(itemsPerPage)];
3981
+
3982
+ const handleSnapshot = (querySnapshot: DocumentData) => {
3983
+ const items: { [key: string]: any } = {};
3984
+ querySnapshot.docs.forEach((doc: QueryDocumentSnapshot<DocumentData>) => {
3985
+ items[doc.id] = doc.data();
3986
+ });
3987
+
3988
+ if (search) {
3989
+ const filteredData = filterData(items, search);
3990
+ const paginatedData = paginateData(filteredData, currentPage);
3991
+ setData(paginatedData);
3992
+ } else {
3993
+ const paginatedData = paginateData(items, currentPage);
3994
+ setData(paginatedData);
3995
+ }
3996
+
3997
+ setLastVisible(querySnapshot.docs[querySnapshot.docs.length - 1] || null);
3998
+ setFirstVisible(querySnapshot.docs[0] || null);
3999
+
4000
+ setLoading(Object.keys(items).length < itemsPerPage ? 'loaded' : false);
4001
+ };
4002
+
4003
+ const unsubscribe = firebaseQuery.collectionSnapshot(handleSnapshot, path, constraintsWithLimit, undefined, true);
4004
+
4005
+ listenersRef.current[path.toString()] = unsubscribe;
4006
+
4007
+ return () => {
4008
+ if (listenersRef.current[path.toString()]) {
4009
+ listenersRef.current[path.toString()]();
4010
+ }
4011
+ };
4012
+ }
4013
+
4014
+ return () => {
4015
+ Object.values(listenersRef.current).forEach(unsub => unsub());
4016
+ };
4017
+ }, [path, snapshot, buildConstraints, itemsPerPage, filterData, paginateData, currentPage, search, firebaseQuery]);
4018
+
4019
+ return {
4020
+ data,
4021
+ loading,
4022
+ pageUp,
4023
+ pageDown: view === 'table' ? pageDown : undefined,
4024
+ setQueries,
4025
+ reset,
4026
+ setFilters,
4027
+ setSearch,
4028
+ };
4029
+ };