import React, { createContext, useContext, useState, useEffect, useRef } from ‘react’; import { Patient, Appointment, AppointmentType, Staff, Invoice, Expense, Clinic, ConsentForm } from ‘./types’; import { MOCK_PATIENTS, MOCK_APPOINTMENTS, MOCK_INVOICES, MOCK_STAFF, MOCK_APPOINTMENT_TYPES, MOCK_EXPENSES, MOCK_CLINICS } from ‘./services/mockData’; import toast from ‘react-hot-toast’; import { db, auth, handleFirestoreError, OperationType } from ‘./services/firebase’; import { signInAnonymously, onAuthStateChanged } from ‘firebase/auth’; import { collection, getDocs, doc, setDoc, deleteDoc, writeBatch } from ‘firebase/firestore’; interface ClinicContextType { patients: Patient[]; appointments: Appointment[]; invoices: Invoice[]; expenses: Expense[]; staff: Staff[]; clinics: Clinic[]; appointmentTypes: AppointmentType[]; consentForms: ConsentForm[]; addPatient: (patient: Omit) => Patient; addAppointment: (appointment: Omit) => Appointment; updateAppointment: (appointment: Appointment) => void; updateInvoice: (updatedInvoice: Invoice) => void; addInvoice: (invoice: Omit) => Invoice; addExpense: (expense: Omit) => Expense; updateExpense: (expense: Expense) => void; deleteExpense: (id: string) => void; addAppointmentType: (type: Omit) => AppointmentType; updateAppointmentType: (type: AppointmentType) => void; updateStaff: (updatedStaff: Staff) => void; addStaff: (staff: Omit) => Staff; updateClinic: (clinic: Clinic) => void; addClinic: (clinic: Omit) => Clinic; addConsentForm: (form: Omit) => ConsentForm; updateConsentForm: (form: ConsentForm) => void; deleteConsentForm: (id: string) => void; resetData: () => void; isCloudSynced: boolean; isSyncing: boolean; } const ClinicContext = createContext(undefined); export function ClinicProvider({ children }: { children: React.ReactNode }) { // Offline-first initial states from localStorage or standard mock-dataset const [patients, setPatients] = useState(() => { const saved = localStorage.getItem(‘df_patients’); return saved ? JSON.parse(saved) : MOCK_PATIENTS; }); const [clinics, setClinics] = useState(() => { const saved = localStorage.getItem(‘df_clinics’); return saved ? JSON.parse(saved) : MOCK_CLINICS; }); const [appointments, setAppointments] = useState(() => { const saved = localStorage.getItem(‘df_appointments’); return saved ? JSON.parse(saved) : MOCK_APPOINTMENTS; }); const [invoices, setInvoices] = useState(() => { const saved = localStorage.getItem(‘df_invoices’); return saved ? JSON.parse(saved) : MOCK_INVOICES; }); const [appointmentTypes, setAppointmentTypes] = useState(() => { const saved = localStorage.getItem(‘df_treatment_types’); return saved ? JSON.parse(saved) : MOCK_APPOINTMENT_TYPES; }); const [expenses, setExpenses] = useState(() => { const saved = localStorage.getItem(‘df_expenses’); return saved ? JSON.parse(saved) : MOCK_EXPENSES; }); const [staff, setStaff] = useState(() => { const saved = localStorage.getItem(‘df_staff’); return saved ? JSON.parse(saved) : MOCK_STAFF; }); const [consentForms, setConsentForms] = useState(() => { const saved = localStorage.getItem(‘df_consent_forms’); return saved ? JSON.parse(saved) : []; }); const [isCloudSynced, setIsCloudSynced] = useState(false); const [isSyncing, setIsSyncing] = useState(false); const seedingRef = useRef(false); // Sync state helpers to update localStorage useEffect(() => { localStorage.setItem(‘df_patients’, JSON.stringify(patients)); }, [patients]); useEffect(() => { localStorage.setItem(‘df_clinics’, JSON.stringify(clinics)); }, [clinics]); useEffect(() => { localStorage.setItem(‘df_appointments’, JSON.stringify(appointments)); }, [appointments]); useEffect(() => { localStorage.setItem(‘df_invoices’, JSON.stringify(invoices)); }, [invoices]); useEffect(() => { localStorage.setItem(‘df_expenses’, JSON.stringify(expenses)); }, [expenses]); useEffect(() => { localStorage.setItem(‘df_treatment_types’, JSON.stringify(appointmentTypes)); }, [appointmentTypes]); useEffect(() => { localStorage.setItem(‘df_staff’, JSON.stringify(staff)); }, [staff]); useEffect(() => { localStorage.setItem(‘df_consent_forms’, JSON.stringify(consentForms)); }, [consentForms]); // Auth & Cloud Syncing Process useEffect(() => { // Authenticate via Anonymous Auth on startup if not already logged in signInAnonymously(auth).catch((error) => { console.warn(“Firebase Anonymous Auth warning/disabled in project:”, error); }); const unsubscribe = onAuthStateChanged(auth, async (user) => { if (user) { console.log(“Firebase Authenticated as User:”, user.uid); await syncDataWithFirestore(); } else { console.log(“Firebase Auth State: Offline/Signed Out.”); } }); return () => unsubscribe(); }, []); // Sync function to pull from Firestore or seed if Firestore metadata is empty const syncDataWithFirestore = async () => { if (seedingRef.current) return; setIsSyncing(true); try { // 1. Fetch Collections const fetchCollectionData = async (colPath: string) => { try { const snap = await getDocs(collection(db, colPath)); const list: any[] = []; snap.forEach((doc) => { list.push({ id: doc.id, …doc.data() }); }); return list; } catch (err) { handleFirestoreError(err, OperationType.LIST, colPath); return []; } }; const firestorePatients = await fetchCollectionData(‘patients’); const firestoreClinics = await fetchCollectionData(‘clinics’); const firestoreAppointments = await fetchCollectionData(‘appointments’); const firestoreInvoices = await fetchCollectionData(‘invoices’); const firestoreTypes = await fetchCollectionData(‘appointmentTypes’); const firestoreExpenses = await fetchCollectionData(‘expenses’); const firestoreStaffList = await fetchCollectionData(‘staff’); const firestoreConsent = await fetchCollectionData(‘consentForms’); const isDbEmpty = firestorePatients.length === 0 && firestoreClinics.length === 0 && firestoreAppointments.length === 0; if (isDbEmpty) { console.log(“Firestore cloud database is empty. Seeding initial records to Google Cloud…”); seedingRef.current = true; // Seed database using atomic batch writes/sets to keep records synced const seedCollection = async (colPath: string, items: any[]) => { for (const item of items) { const { id, …dataWithoutId } = item; try { await setDoc(doc(db, colPath, id), dataWithoutId); } catch (err) { handleFirestoreError(err, OperationType.CREATE, `${colPath}/${id}`); } } }; await seedCollection(‘patients’, patients); await seedCollection(‘clinics’, clinics); await seedCollection(‘appointments’, appointments); await seedCollection(‘invoices’, invoices); await seedCollection(‘appointmentTypes’, appointmentTypes); await seedCollection(‘expenses’, expenses); await seedCollection(‘staff’, staff); await seedCollection(‘consentForms’, consentForms); console.log(“Seeding complete!”); setIsCloudSynced(true); } else { console.log(“Data pulled successfully from Firestore!”); // Update local state with latest Cloud Firestore data if (firestorePatients.length > 0) setPatients(firestorePatients); if (firestoreClinics.length > 0) setClinics(firestoreClinics); if (firestoreAppointments.length > 0) setAppointments(firestoreAppointments); if (firestoreInvoices.length > 0) setInvoices(firestoreInvoices); if (firestoreTypes.length > 0) setAppointmentTypes(firestoreTypes); if (firestoreExpenses.length > 0) setExpenses(firestoreExpenses); if (firestoreStaffList.length > 0) setStaff(firestoreStaffList); if (firestoreConsent.length > 0) setConsentForms(firestoreConsent); setIsCloudSynced(true); } } catch (error) { console.warn(“Could not sync with Google Firebase database fully, using local cache. Error details:”, error); } finally { setIsSyncing(false); } }; // Helper to persist single record mutation const writeToCloud = async (collectionName: string, id: string, dataWithoutId: any) => { try { await setDoc(doc(db, collectionName, id), dataWithoutId); } catch (err) { console.warn(`Local-write only context. Cloud sync failed for ${collectionName}/${id}:`, err); } }; const deleteFromCloud = async (collectionName: string, id: string) => { try { await deleteDoc(doc(db, collectionName, id)); } catch (err) { console.warn(`Local-delete only context. Cloud deletion failed for ${collectionName}/${id}:`, err); } }; // State Mutators const addClinic = (clinic: Omit) => { const id = `c${Date.now()}`; const newClinic = { …clinic, id }; setClinics(prev => […prev, newClinic]); writeToCloud(‘clinics’, id, clinic); return newClinic; }; const addConsentForm = (form: Omit) => { const id = `cf${Date.now()}`; const newForm = { …form, id }; setConsentForms(prev => [newForm, …prev]); writeToCloud(‘consentForms’, id, form); return newForm; }; const updateConsentForm = (updatedForm: ConsentForm) => { setConsentForms(prev => prev.map(f => f.id === updatedForm.id ? updatedForm : f)); const { id, …data } = updatedForm; writeToCloud(‘consentForms’, id, data); }; const deleteConsentForm = (id: string) => { setConsentForms(prev => prev.filter(f => f.id !== id)); deleteFromCloud(‘consentForms’, id); }; const updateClinic = (updatedClinic: Clinic) => { setClinics(prev => prev.map(c => c.id === updatedClinic.id ? updatedClinic : c)); const { id, …data } = updatedClinic; writeToCloud(‘clinics’, id, data); }; const addAppointmentType = (type: Omit) => { const id = `t${Date.now()}`; const newType = { …type, id }; setAppointmentTypes(prev => [newType, …prev]); writeToCloud(‘appointmentTypes’, id, type); return newType; }; const updateAppointmentType = (type: AppointmentType) => { setAppointmentTypes(prev => prev.map(t => t.id === type.id ? type : t)); const { id, …data } = type; writeToCloud(‘appointmentTypes’, id, data); }; const addPatient = (patient: Omit) => { const id = `p${Date.now()}`; const newPatient = { …patient, id }; setPatients(prev => [newPatient, …prev]); writeToCloud(‘patients’, id, patient); return newPatient; }; const addAppointment = (appointment: Omit) => { const id = `a${Date.now()}`; const newApt = { …appointment, id }; setAppointments(prev => [newApt, …prev]); writeToCloud(‘appointments’, id, appointment); return newApt; }; const updateAppointment = (updatedApt: Appointment) => { setAppointments(prev => prev.map(apt => apt.id === updatedApt.id ? updatedApt : apt)); const { id, …data } = updatedApt; writeToCloud(‘appointments’, id, data); }; const updateInvoice = (updatedInvoice: Invoice) => { setInvoices(prev => prev.map(inv => inv.id === updatedInvoice.id ? updatedInvoice : inv)); const { id, …data } = updatedInvoice; writeToCloud(‘invoices’, id, data); }; const updateStaff = (updatedStaff: Staff) => { setStaff(prev => prev.map(s => s.id === updatedStaff.id ? updatedStaff : s)); const { id, …data } = updatedStaff; writeToCloud(‘staff’, id, data); }; const addStaff = (newMember: Omit) => { const id = `s${Date.now()}`; const freshStaff = { …newMember, id }; setStaff(prev => […prev, freshStaff]); writeToCloud(‘staff’, id, newMember); return freshStaff; }; const addInvoice = (invoice: Omit) => { const id = `inv${Date.now()}`; const newInvoice = { …invoice, id }; setInvoices(prev => [newInvoice, …prev]); writeToCloud(‘invoices’, id, invoice); return newInvoice; }; const addExpense = (expense: Omit) => { const id = `exp${Date.now()}`; const newExpense = { …expense, id }; setExpenses(prev => [newExpense, …prev]); writeToCloud(‘expenses’, id, expense); return newExpense; }; const updateExpense = (updatedExpense: Expense) => { setExpenses(prev => prev.map(exp => exp.id === updatedExpense.id ? updatedExpense : exp)); const { id, …data } = updatedExpense; writeToCloud(‘expenses’, id, data); }; const deleteExpense = (id: string) => { setExpenses(prev => prev.filter(exp => exp.id !== id)); deleteFromCloud(‘expenses’, id); }; const resetData = async () => { localStorage.removeItem(‘df_patients’); localStorage.removeItem(‘df_appointments’); localStorage.removeItem(‘df_invoices’); localStorage.removeItem(‘df_staff’); localStorage.removeItem(‘df_treatment_types’); localStorage.removeItem(‘df_expenses’); localStorage.removeItem(‘df_consent_forms’); setPatients(MOCK_PATIENTS); setAppointments(MOCK_APPOINTMENTS); setInvoices(MOCK_INVOICES); setExpenses(MOCK_EXPENSES); setStaff(MOCK_STAFF); setAppointmentTypes(MOCK_APPOINTMENT_TYPES); setConsentForms([]); // Optional Cloud purge & repopulate back to default try { const purgeAndSeed = async () => { const resetCol = async (path: string, list: any[]) => { const snap = await getDocs(collection(db, path)); for (const docItem of snap.docs) { await deleteDoc(doc(db, path, docItem.id)); } for (const item of list) { const { id, …v } = item; await setDoc(doc(db, path, id), v); } }; await resetCol(‘patients’, MOCK_PATIENTS); await resetCol(‘clinics’, MOCK_CLINICS); await resetCol(‘appointments’, MOCK_APPOINTMENTS); await resetCol(‘invoices’, MOCK_INVOICES); await resetCol(‘appointmentTypes’, MOCK_APPOINTMENT_TYPES); await resetCol(‘expenses’, MOCK_EXPENSES); await resetCol(‘staff’, MOCK_STAFF); const consentSnap = await getDocs(collection(db, ‘consentForms’)); for (const docItem of consentSnap.docs) { await deleteDoc(doc(db, ‘consentForms’, docItem.id)); } }; toast.promise(purgeAndSeed(), { loading: ‘Resetting cloud Database…’, success: ‘Cloud data seeded successfully!’, error: ‘Reset completed locally (Cloud storage reset skipped).’ }); } catch { toast.success(‘Demo data reset to defaults locally.’); } }; const value = { patients, appointments, invoices, expenses, staff, clinics, appointmentTypes, consentForms, addPatient, addAppointment, updateAppointment, updateInvoice, addInvoice, addExpense, updateExpense, deleteExpense, addAppointmentType, updateAppointmentType, updateStaff, addStaff, updateClinic, addClinic, addConsentForm, updateConsentForm, deleteConsentForm, resetData, isCloudSynced, isSyncing }; return ( {children} ); } export function useClinicData() { const context = useContext(ClinicContext); if (context === undefined) { throw new Error(‘useClinicData must be used within a ClinicProvider’); } return context; }

Leave a Comment

Your email address will not be published. Required fields are marked *

Scroll to Top