import { DocumentData, arrayRemove, arrayUnion, documentId, orderBy, where } from "firebase/firestore" import { useEffect, useState } from "react" import { TypedUseSelectorHook, useDispatch, useSelector } from "react-redux" import { AppDispatch, RootState } from "./config" import { addContact, deleteContact, setContacts, updateContact } from "./features/contacts/contactsSlice" import { Job, addJob, setJobStatus, setJobs, setMarkRead, updateJob } from "./features/jobs/jobsSlice" import { editActivePlacement, fetchActivePlacement, setActivePlacement } from "./features/placements/studentPlacements/activePlacement" import { addCompletedStudentPlacements, deleteCompletedStudentPlacement, setCompletedStudentPlacements, updateCompletedStudentPlacement } from "./features/placements/studentPlacements/completedStudentPlacementsSlice" import { addUpcomingStudentPlacements, deleteUpcomingStudentPlacement, setUpcoming, setUpcomingStudentPlacements, updateUpcomingStudentPlacement } from "./features/placements/studentPlacements/upcomingStudentPlacementsSlice" import FirebaseQuery from "./firebase/firebaseQuery" import { getOrganisation, getPlacementsWhere } from "./firebase/readDatabase" import { convertDate, getRandomNumber } from "./firebase/util" import { deleteStorageItem, uploadFiles } from "./firebase/writeDatabase" import { Address, Application, CohortData, Contact, EmailTemplate, FileItem, InstituteData, PlacementListing, ProviderData, SavedPlacement, StudentPlacementData, UserData, UserGroupData } from "./typeDefinitions" import { deleteObject, getDownloadURL, ref } from "firebase/storage" import { storage } from "./firebase/firebaseConfig" export const useAppSelector: TypedUseSelectorHook = useSelector export const useAppDispatch = () => useDispatch(); export function useStudent({user} : {user: UserData}) { if (!user || user.userType !== "Students") return {error: "Unauthorized"}; const upcomingPlacements = useAppSelector((state) => state.upcomingStudentPlacements.values) const completedPlacements = useAppSelector((state) => state.completedStudentPlacements.values) const activePlacement = useAppSelector((state) => state.activePlacement.values) const upcoming = useAppSelector((state) => state.upcomingStudentPlacements.upcoming) const contacts = useAppSelector((state) => state.contacts.values) const [applications, setApplications] = useState<{[key: string]: Application&{listing: PlacementListing|false, provider: ProviderData|false, address: Address}}>(); const [files, setFiles] = useState<[string, FileItem][]>(); const [uploadedDocument, setUploadedDocument] = useState(null); const firebaseQuery = new FirebaseQuery(); const dispatch = useAppDispatch(); const today = new Date(); const applicationsConstraints = [where("uid", "==", user.id)] const upcomingPlacementsConstraints = [where("uid", "==", user.id), where("inProgress", "==", true)] const completedPlacementsConstraints = [where("uid", "==", user.id), where("completed", "==", true)] const contactsConstraints = [where("uid", "==", user.id)] useEffect(() => { firebaseQuery.collectionSnapshot(setUpcomingStudentPlacements, "placements", upcomingPlacementsConstraints, dispatch); firebaseQuery.collectionSnapshot(setCompletedStudentPlacements, "placements", completedPlacementsConstraints, dispatch); firebaseQuery.collectionSnapshot(setContacts, "contacts", contactsConstraints, dispatch); firebaseQuery.collectionSnapshot(async (fApplications: {[key: string]: Application}) => { const applicationsWithProviderAndListing:{[key: string]: Application&{listing: PlacementListing, provider: ProviderData, address: Address}} = Object.fromEntries(await Promise.all(Object.entries(fApplications).map(async ([id, application]) => { const provider = await firebaseQuery.getDocData(["providers", application.providerId]).catch(() => false) as ProviderData|false; const listing = await firebaseQuery.getDocData(["placementListings", application.listingId]).catch(() => false) as PlacementListing|false; return [id, {...application, listing: listing, provider: provider}]; }))); setApplications(applicationsWithProviderAndListing); }, "applications", applicationsConstraints); }, [user.id]); /* useEffect(() => { if (!user.id) return dispatch(fetchActivePlacement({userId: user.id})) }, [dispatch, user.id, upcomingPlacements]); */ const fetchFiles = (oId: string) => { firebaseQuery.collectionSnapshot(async (docs: {[key: string]: FileItem}) => { setFiles(await Promise.all(Object.entries(docs as {[key: string]: FileItem}).map(async ([id, file]) => { const url = await getDownloadURL(ref(storage, `userFiles/${file.fileName}`)); return [id, {...file, url: url}]; }))); }, "files", [where("product", "==", user.product), where("oId", "==", oId)]); } const uploadFile = async (e: any, oId: string) => { console.log("files", e); const fileName = e.name; const file = e.file[0] as File; const newFileName = oId + "_" + getRandomNumber(0, 1000000) + "_" + file.name; const newFile: FileItem = { product: user.product, oId: oId, fileName: newFileName, name: fileName, added: convertDate(new Date(), "visual") as string, }; const docRef = await firebaseQuery.add("files", newFile); await uploadFiles(file, `userFiles/${newFileName}`); setFiles((prevFiles) => { const newEntry: [string, FileItem] = [docRef.id, newFile]; return prevFiles ? [...prevFiles, newEntry] : [newEntry]; }); }; const uploadDocument = async (e: { name: string }, oId: string) => { const file = await fetch(uploadedDocument?.uri as string); const blob = await file.blob(); const newFileName = `${oId}_${getRandomNumber(0, 1000000)}_${uploadedDocument?.name}`; await uploadFiles(blob, `userFiles/${newFileName}`); await firebaseQuery.add("files", { product: user.product, oId: oId, fileName: newFileName, name: e.name, added: convertDate(new Date(), "visual"), } as FileItem); setUploadedDocument(null); } const removeFile = async (file: string) => { if (!file) return; const fileToDelete = files?.find(([id]) => id === file)?.[1]; if (!fileToDelete) { console.error("File not found in state"); return; } try { await firebaseQuery.delete(["files", file]); await deleteObject(ref(storage, `userFiles/${fileToDelete.fileName}`)); setFiles((prevFiles) => prevFiles?.filter(([id]) => id !== file) || []); } catch (error) { console.error("Error deleting file:", error); } } const fetchUpcomingPlacements = () => { const constraints = [where("uid", "==", user.id), where("inProgress", "==", true), orderBy("startDate")] firebaseQuery.collectionSnapshot(setUpcomingStudentPlacements, "placements", constraints, dispatch); } const fetchCompletedPlacements = () => { const constraints = [where("uid", "==", user.id), where("completed", "==", true), orderBy("startDate")] firebaseQuery.collectionSnapshot(setCompletedStudentPlacements, "placements", constraints, dispatch); } const fetchContacts = () => { const constraints = [where("uid", "==", user.id)] firebaseQuery.collectionSnapshot(setContacts, "contacts", constraints, dispatch); } const updateUpcomingPlacement = () => { if (!upcomingPlacements || !Object.entries(upcomingPlacements).length) { dispatch(setUpcoming(undefined)); return; } const upcomingPlacement = Object.entries(upcomingPlacements).find(([, v]) => new Date(v.startDate).getTime() > today.getTime()) upcomingPlacement ? dispatch(setUpcoming(upcomingPlacement[1])) : dispatch(setUpcoming(undefined)) } const updateActivePlacement = () => { const key = Object.keys(upcomingPlacements).find((key) => { const placement = upcomingPlacements[key] return placement.active }) key ? dispatch(setActivePlacement(upcomingPlacements[key])) : dispatch(setActivePlacement(null)) } const getItemById = async (path: "contacts"|"placements", id: string) => { return await firebaseQuery.getDocData([path, id]) } const handleDeletePlacement = async (id: string) => { if (upcomingPlacements[id]) dispatch(deleteUpcomingStudentPlacement({placementId: id})) else dispatch(deleteCompletedStudentPlacement({placementId: id})) } const handleUpdatePlacement = async (id: string, attributes: Partial) => { if (upcomingPlacements[id]) dispatch(updateUpcomingStudentPlacement({placementId: id, attributes})) else dispatch(updateCompletedStudentPlacement({placementId: id, attributes})) } const handleAddPlacement = async (formData: StudentPlacementData) => { if (formData.completed) dispatch(addCompletedStudentPlacements({formData})) else dispatch(addUpcomingStudentPlacements({formData})) } const getUserPlacements = async () => { return await getPlacementsWhere({w: where("uid", "==", user?.id)}) } const getPlacementsStart = async (start: Date, end: Date) => { return await firebaseQuery.getDocsWhere("placements", [where("uid", "==", user?.id), where("startDate", ">=", convertDate(start, "dbstring")), where("startDate", "<=", convertDate(end, "dbstring"))]) as {[key:string]: StudentPlacementData}; } const getPlacementsEnd = async (end: Date) => { return await firebaseQuery.getDocsWhere("placements", [where("uid", "==", user?.id), where("endDate", ">=", convertDate(end, "dbstring")), where("endDate", "<=", convertDate(end, "dbstring"))]) as {[key:string]: StudentPlacementData}; } const dispatchActivePlacement = () => { dispatch(fetchActivePlacement({userId: user.id})) } return { contacts: { add: async (contactForm: Contact) => await dispatch(addContact({contactForm: contactForm, userId: user.id})), update: async (contactId: string, data: Partial) => await dispatch(updateContact({contactId: contactId, attributes: data})), delete: async (contactId: string) => await dispatch(deleteContact({contactId: contactId})), contacts, fetchContacts, getById: async (item: string) => await getItemById("contacts", item) }, placements: { add: async (formData: StudentPlacementData) => await handleAddPlacement(formData), update: async (placementId: string, attributes: Partial) => await handleUpdatePlacement(placementId, attributes), delete: async (id: string) => await handleDeletePlacement(id), getUserPlacements, getPlacementsStart, getPlacementsEnd, activePlacement, editActivePlacement: (attributes: Partial) => dispatch(editActivePlacement(attributes)), updateActivePlacement, updateUpcomingPlacement, fetchUpcomingPlacements, fetchCompletedPlacements, dispatchActivePlacement, upcoming, upcomingPlacements: upcomingPlacements || {}, completedPlacements: completedPlacements || {}, getById: async (item: string) => await getItemById("placements", item), }, applications: applications, files: { fetchFiles, uploadDocument, uploadedDocument, uploadFile, removeFile, files, } } ; }; export function useInstituteStaff({user}: {user: UserData}) { if (!user || user?.product !== "institutes" || user.userType !== "Staff") return {error: "Unauthorized"}; const firebaseQuery = new FirebaseQuery(); const dispatch = useAppDispatch(); const {userGroups, forms, institute} = useInstitute({user}) const [cohorts, setCohorts] = useState<{[key:string]: CohortData}>(); const [emailTemplates, setEmailTemplates] = useState<{[key:string]: EmailTemplate}>(); useEffect(() => { const cohortConstraints = [where("oId", "==", user.oId)]; if (user.userGroup !== "admin") { if (user.viewAddresses !== "all") { cohortConstraints.push(where("addressId", "in", user.visibleAddresses)); } if (user.viewCohorts !== "all") { cohortConstraints.push(where(documentId(), "in", user.visibleCohorts)); } } firebaseQuery.collectionSnapshot(setCohorts, "cohorts", cohortConstraints); firebaseQuery.collectionSnapshot(setEmailTemplates, "emailTemplates", [where("product", "==", user.product), where("oId", "==", user.oId)]); }, [user.oId, user.userGroup, user.viewAddresses, user.viewCohorts, user.visibleAddresses, user.visibleCohorts, user.product]) const getJobById = async (id: string) => { try { const job = await firebaseQuery.getDocData(["jobs", id]) return job } catch (error) { throw error } } return { jobs: { setJobs: async (jobs: {[jobId: string]: Job}) => dispatch(setJobs(jobs)), addJob: async ({job, jobId} : {job: Partial, jobId: string}) => dispatch(addJob({job: job, jobId: jobId})), setMarkRead: async (payload) => dispatch(setMarkRead(payload)), setJobStatus: async ({jobId, status}: {jobId: string, status: string}) => dispatch(setJobStatus({jobId: jobId, status: status})), updateJob: async ({jobId, data}: {jobId: string, data: string}) => dispatch(updateJob({jobId: jobId, data: data})), getById: getJobById }, institute, userGroups, forms, cohorts, emailTemplates, } } export function useInstitute({user}: {user: UserData}) { if (!user || user?.product !== "institutes") return {error: "Unauthorized"}; const [userGroups, setUserGroups] = useState<{[key:string]: UserGroupData}>(); const [forms, setForms] = useState<{[key:string]: unknown}>(); const [institute, setInstitute] = useState(); const firebaseQuery = new FirebaseQuery(); useEffect(() => { getOrganisation(user, (data) => setInstitute(data as InstituteData)); firebaseQuery.collectionSnapshot(setUserGroups, "userGroups", [where("oId", "==", user.oId), where("product", "==", user.product)]); firebaseQuery.collectionSnapshot(setForms, "forms", [where("oId", "==", user.oId), where("product", "==", user.product)]); }, [user.product, user.oId]) return { userGroups, forms, institute, } } export function useInstituteStudent({user}: {user: UserData}) { if (!user || user?.product !== "institutes" || user.userType !== "Students") return {error: "Unauthorized"}; const {contacts, placements, applications, files} = useStudent({user}); const [cohorts, setCohorts] = useState<{[key:string]: CohortData}>(); const firebaseQuery = new FirebaseQuery(); const {userGroups, forms, institute} = useInstitute({user}) const [logs, setLogs] = useState<{[key: string]: DocumentData}>({}) // const dispatch = useAppDispatch() useEffect(() => { const isAdmin = user.userGroup === "admin"; const canViewCohorts = user.viewCohorts !== "none"; const canViewAddresses = user.viewAddresses !== "none"; if (isAdmin || (canViewCohorts && canViewAddresses)) { const cohortConstraints = [where("oId", "==", user.oId)]; firebaseQuery.collectionSnapshot(setCohorts, "cohorts", cohortConstraints); } else { setCohorts({}); } }, [user.oId, user.id, user.userGroup, user.viewCohorts, user.viewAddresses, user.product]); const getPlacementListings = async () => { const savedPlacements = await firebaseQuery.getDocsWhere(["savedPlacements"], [where("savedById", "==", user.oId), where("savedByProduct", "==", "institutes"), where("listed", "==", true)]) as {[key:string]: SavedPlacement}; console.log("SP", Object.keys(savedPlacements)); const listings = Object.fromEntries((await Promise.all(Object.values(savedPlacements).map(async (value) => { const listing = await firebaseQuery.getDocData(["placementListings", value.placementId]) as PlacementListing; if (value.concurrentPlacements !== undefined) { const concurrentPlacements = await firebaseQuery.getCount(["placements"], [where("placementId", "==", value.placementId), where("oId", "==", user.oId), where("inProgress", "==", true)]); console.log("placementId", value.placementId, "Concurrent placements", concurrentPlacements); console.log("max placements", value.concurrentPlacements, "limit reached", value.concurrentPlacements && (concurrentPlacements >= value.concurrentPlacements)); if (concurrentPlacements >= value.concurrentPlacements) return false; } if (!listing?.addressId || !listing.providerId) return false; const address:DocumentData|false = await firebaseQuery.getDocData(["addresses", listing.addressId]).catch(() => false); const provider:DocumentData|false = await firebaseQuery.getDocData(["providers", listing.providerId]).catch(() => false); if (!address || !provider) return false; const placementStatus = value.status === "Accepted" ? "Accepted" : "Not accepted"; return [value.placementId, {...listing, ...address, ...provider, status: placementStatus, savedPlacement: value}]; }))).filter((i) => i) as [string, PlacementListing][]); return listings } const fetchLogs = async (placementId: string, selectedDate: string) => { try { const data = await firebaseQuery.getDocData(["logs", `${placementId}-${selectedDate}`]) setLogs(prev => ({...prev, [selectedDate]: data || {}})) } catch { console.log("No log error") setLogs(prev => ({...prev, [selectedDate]: {}})) } } const saveLog = async (value: any[] | {[key: string]: unknown}, selectedDate: string, userType: string, placementId: string) => { const empty = (Array.isArray(value) && value.length === 0) || (Object.entries(Object.values(value)[0] as { [key: string]: unknown }) .filter(([k, v]) => (![`${userType}Uid`, "completed"].includes(k)) && v) .length === 0); const updatedValue = empty ? null : Array.isArray(value) ? value : Object.values(value)[0]; await firebaseQuery.set( ["logs", `${placementId}-${selectedDate}`], { [userType]: updatedValue }, true ); await firebaseQuery.update( ["placements", placementId], { [userType + "Logs"]: empty ? arrayRemove(selectedDate) : arrayUnion(selectedDate) } ); const updatedLog = { ...logs[selectedDate], [userType]: updatedValue, }; setLogs(prev => ({ ...prev, [selectedDate]: updatedLog })); } const addLogFile = async (files: {file: FileList}, userType: string, selectedDate: string, placementId: string) => { const newFileNames = Array.from(files.file).map(file => file.name); await uploadFiles( Array.from(files.file), ["placements", placementId, "log", `${userType}_files`, selectedDate] ); await firebaseQuery.set( ["logs", `${placementId}-${selectedDate}`], { [`${userType}_files`]: arrayUnion(...newFileNames) }, true ); setLogs(prevLogs => { const currentLog = prevLogs[selectedDate] || {}; const updatedFiles = [...(currentLog[`${userType}_files`] || []), ...newFileNames]; return { ...prevLogs, [selectedDate]: { ...currentLog, [`${userType}_files`]: updatedFiles } }; }); } const deleteLogFile = async (openFile: {name: string, url: string}, userType: string, selectedDate: string, placementId: string) => { await deleteStorageItem(`placements/${placementId}/log/${userType}_files/${selectedDate}/${openFile.name}`); await firebaseQuery.set( ["logs", `${placementId}-${selectedDate}`], { [`${userType}_files`]: arrayRemove(openFile.name) }, true ); setLogs(prevLogs => { const currentLog = prevLogs[selectedDate] || {}; const updatedFiles = currentLog[`${userType}_files`]?.filter((file: string) => file !== openFile.name) || []; return { ...prevLogs, [selectedDate]: { ...currentLog, [`${userType}_files`]: updatedFiles } }; }); } return { contacts, placements, institute, userGroups, forms, cohorts, applications, getPlacementListings, files, logs: { values: logs, fetchLogs, saveLog, addLogFile, deleteLogFile } } } export function useProvider({user}: {user: UserData}) { if (!user || user?.product !== "providers") return {error: "Unauthorized"}; const {jobs} = useInstituteStaff({user}); return { jobs } } // function MyContactComponent() { // const {contacts} = useStudent({}); // contacts.add({data}) // return null // } // function MyPlacementComponent() { // const {placements} = useStudent(); // placements.add({data}) // return null // }