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;
}