diff --git a/src/App.jsx b/src/App.jsx
index 99d55a675..59d62f311 100644
--- a/src/App.jsx
+++ b/src/App.jsx
@@ -1,6 +1,7 @@
import { createTheme, MantineProvider } from "@mantine/core";
import "@mantine/core/styles.css";
import "@mantine/notifications/styles.css";
+import { ModalsProvider } from '@mantine/modals';
import { Route, Routes, Navigate, useLocation } from "react-router-dom";
import { Notifications } from "@mantine/notifications";
import { Layout } from "./components/layout";
@@ -15,6 +16,7 @@ import InactivityHandler from "./helper/inactivityhandler";
import Examination from "./Modules/Examination/examination";
import Database from "./Modules/Database/database";
import ProgrammeCurriculumRoutes from "./Modules/Program_curriculum/programmCurriculum";
+import HealthCenterRoutes from "./routes/healthCenterRoutes";
import NotFoundPage from "./components/NotFoundPage";
const theme = createTheme({
@@ -32,9 +34,10 @@ export default function App() {
const location = useLocation();
return (
-
- {location.pathname !== "/accounts/login" && }
- {location.pathname !== "/accounts/login" && }
+
+
+ {location.pathname !== "/accounts/login" && }
+ {location.pathname !== "/accounts/login" && }
} />
@@ -82,8 +85,10 @@ export default function App() {
} />
} />
} />
+ } />
} />
+
);
}
diff --git a/src/Modules/HealthCenter/CompoundDashboard.jsx b/src/Modules/HealthCenter/CompoundDashboard.jsx
new file mode 100644
index 000000000..0ff7c6c3d
--- /dev/null
+++ b/src/Modules/HealthCenter/CompoundDashboard.jsx
@@ -0,0 +1,310 @@
+/**
+ * Compounder Dashboard
+ * ====================
+ * THIN VIEW: Composes compounder operational components
+ *
+ * Responsibility: Layout and component composition only
+ * Business logic: Delegated to individual components
+ *
+ * Composed Components (10 modules):
+ * - DoctorForm: Doctor CRUD management
+ * - ScheduleForm: Doctor schedule management
+ * - AttendanceForm: Daily attendance tracking
+ * - StockForm: Medicine inventory management
+ * - ExpiryBatchReturn: Batch return tracking
+ * - PrescriptionCreation: Prescription creation with FIFO
+ * - ComplaintResponse: Complaint response management
+ * - HospitalAdmissionForm: Patient admission/discharge
+ * - AmbulanceManagement: Ambulance CRUD
+ * - ReimbursementReview: Staff claim review
+ */
+
+import { Suspense, useState } from 'react';
+import {
+ Container,
+ Title,
+ Text,
+ Stack,
+ Tabs,
+ Loader,
+ Center,
+ ActionIcon,
+ Group,
+ ScrollArea,
+ Button,
+} from '@mantine/core';
+import {
+ IconUser,
+ IconClock,
+ IconCalendar,
+ IconPill,
+ IconRefresh,
+ IconFileText,
+ IconAlertCircle,
+ IconBed,
+ IconAmbulance,
+ IconCurrencyDollar,
+ IconChevronLeft,
+ IconChevronRight,
+ IconSend,
+ IconSpeakerphone,
+ IconReportAnalytics,
+} from '@tabler/icons-react';
+
+// Import all 10 compounder components
+import DoctorForm from './components/DoctorForm';
+import ScheduleForm from './components/ScheduleForm';
+import AttendanceForm from './components/AttendanceForm';
+import StockForm from './components/StockForm';
+import ExpiryBatchReturn from './components/ExpiryBatchReturn';
+import ConsultationForm from './components/ConsultationForm';
+import PrescriptionCreation from './components/PrescriptionCreation';
+// import ComplaintResponse from './components/ComplaintResponse';
+// import HospitalAdmissionForm from './components/HospitalAdmissionForm';
+import AmbulanceManagement from './components/AmbulanceManagement';
+import ReimbursementReview from './components/ReimbursementReview';
+import InventoryRequisitions from './components/InventoryRequisitions';
+import AnnouncementsManagement from './components/AnnouncementsManagement';
+import ReportsTab from './components/ReportsTab';
+
+
+// Loading component
+const TabLoader = () => (
+
+
+
+);
+
+export default function CompoundDashboard() {
+ const [activeTab, setActiveTab] = useState('doctors');
+ const [selectedConsultation, setSelectedConsultation] = useState(null);
+
+ const tabIds = [
+ 'doctors',
+ 'schedules',
+ 'attendance',
+ 'stock',
+ 'requisitions',
+ 'expiry',
+ 'consultations',
+ 'prescriptions',
+ // 'complaints', // Complaint Response - Commented Out
+ // 'admissions', // Hospital Admissions - Commented Out
+ 'ambulances',
+ 'reimbursement',
+ 'announcements',
+ 'reports',
+ ];
+
+ const handleTabNavigation = (direction) => {
+ const currentIndex = tabIds.indexOf(activeTab);
+ let nextIndex;
+
+ if (direction === 'left') {
+ nextIndex = currentIndex === 0 ? tabIds.length - 1 : currentIndex - 1;
+ } else {
+ nextIndex = currentIndex === tabIds.length - 1 ? 0 : currentIndex + 1;
+ }
+
+ setActiveTab(tabIds[nextIndex]);
+ };
+
+ const handleCreatePrescriptionFromConsultation = (consultation) => {
+ // Store consultation and switch to prescriptions tab
+ setSelectedConsultation(consultation);
+ setActiveTab('prescriptions');
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Compounder Dashboard
+
+ Manage hospital operations, prescriptions, and patient care
+
+
+
+
+ {/* Tabbed Interface with Direction Buttons */}
+
+ {/* Navbar with Direction Buttons */}
+
+ handleTabNavigation('left')}
+ size="lg"
+ aria-label="Previous tab"
+ >
+
+
+
+
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Doctor Management
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Doctor Schedules
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Attendance Tracking
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Medicine Stock
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Inventory Requisitions
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Batch Returns & Expiry
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ New Consultation
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Create Prescriptions
+
+ {/* } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Complaint Response
+ */}
+ {/* } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Hospital Admissions
+ */}
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Ambulance Management
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Staff Reimbursement
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ Announcements
+
+ } style={{ whiteSpace: 'nowrap', flex: '0 0 auto' }}>
+ System Reports
+
+
+
+
+ handleTabNavigation('right')}
+ size="lg"
+ aria-label="Next tab"
+ >
+
+
+
+
+ {/* Tab 1: Doctor Management */}
+
+ }>
+
+
+
+
+ {/* Tab 2: Doctor Schedules */}
+
+ }>
+
+
+
+
+ {/* Tab 3: Attendance Tracking */}
+
+ }>
+
+
+
+
+ {/* Tab 4: Medicine Stock */}
+
+ }>
+
+
+
+
+ {/* Tab 4.5: Inventory Requisitions */}
+
+ }>
+
+
+
+
+ {/* Tab 5: Batch Returns & Expiry */}
+
+ }>
+
+
+
+
+ {/* Tab 6: New Consultation */}
+
+ }>
+
+
+
+
+ {/* Tab 7: Create Prescriptions */}
+
+ }>
+ setSelectedConsultation(null)} />
+
+
+
+ {/* Tab 8: Complaint Response - COMMENTED OUT */}
+ {/*
+ }>
+
+
+ */}
+
+ {/* Tab 9: Hospital Admissions - COMMENTED OUT */}
+ {/*
+ }>
+
+
+ */}
+
+ {/* Tab 10: Ambulance Management */}
+
+ }>
+
+
+
+
+ {/* Tab 11: Staff Reimbursement */}
+
+ }>
+
+
+
+
+ {/* Tab 12: Announcements — PHC-UC-12 */}
+
+ }>
+
+
+
+
+ {/* Tab 13: System Reports — PHC-UC-13 */}
+
+ }>
+
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/PatientDashboard.jsx b/src/Modules/HealthCenter/PatientDashboard.jsx
new file mode 100644
index 000000000..03e6a042b
--- /dev/null
+++ b/src/Modules/HealthCenter/PatientDashboard.jsx
@@ -0,0 +1,169 @@
+/**
+ * Patient Dashboard
+ * =================
+ * THIN VIEW: Composes patient-related components
+ *
+ * Responsibility: Layout and component composition only
+ * Business logic: Delegated to individual components
+ *
+ * Composed Components:
+ * - ScheduleViewer: View doctor schedules
+ * - TodaysScheduleTab: View today's doctor schedule
+ * - PrescriptionList: View prescriptions
+ */
+
+import { useState, useEffect } from 'react';
+import { Container, Title, Text, Tabs, Stack, Loader, Group } from '@mantine/core';
+import {
+ IconCalendar,
+ IconFileText,
+ IconClock,
+ IconCurrencyDollar,
+ IconBell,
+} from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+
+// Import micro-components
+import DoctorSchedulesView from './components/DoctorSchedulesView';
+import TodaysScheduleTab from './components/TodaysScheduleTab';
+import PrescriptionList from './components/PrescriptionList';
+import ReimbursementSection from './components/ReimbursementSection';
+import AnnouncementsView from './components/AnnouncementsView';
+import * as api from './api';
+
+export default function PatientDashboard() {
+ const [todaysSchedule, setTodaysSchedule] = useState([]);
+ const [allDoctors, setAllDoctors] = useState([]);
+ const [loadingSchedule, setLoadingSchedule] = useState(false);
+ const [isEmployee, setIsEmployee] = useState(false);
+
+ // Fetch today's schedule and check employee status on component mount
+ useEffect(() => {
+ fetchTodaysSchedule();
+
+ // Check if user has employee privileges by verifying access to claims
+ api.getReimbursementClaims()
+ .then(() => setIsEmployee(true))
+ .catch(err => {
+ // If error is 403, they are a student. Otherwise, show tab so they can see the actual error
+ if (err.response?.status !== 403) {
+ setIsEmployee(true);
+ }
+ });
+ }, []);
+
+ const fetchTodaysSchedule = async () => {
+ try {
+ setLoadingSchedule(true);
+ // Use patient-accessible endpoint: getDoctorAvailability
+ // This returns doctor details with their schedules
+ const response = await api.getDoctorAvailability();
+ const doctors = Array.isArray(response.data) ? response.data : [];
+
+ // Store all doctors for attendance view
+ setAllDoctors(doctors);
+
+ // Get today's day of week (0 = Sunday, 1 = Monday, etc.)
+ const today = new Date();
+ const dayOfWeek = today.getDay();
+ const dayNames = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday'];
+ const todayName = dayNames[dayOfWeek];
+
+ // Extract schedules from all doctors and filter for today
+ const todaySchedules = [];
+ doctors.forEach(doctorItem => {
+ const schedules = doctorItem.schedule || doctorItem.schedules || [];
+ const doctorData = doctorItem.doctor || doctorItem;
+
+ schedules.forEach(schedule => {
+ if (schedule.day_of_week?.toLowerCase() === todayName || schedule.day_of_week === dayOfWeek) {
+ todaySchedules.push({
+ ...schedule,
+ doctor_name: doctorData.doctor_name,
+ doctor_id: doctorData.id,
+ specialization: doctorData.specialization,
+ todays_status: doctorItem.todays_status,
+ });
+ }
+ });
+ });
+
+ setTodaysSchedule(todaySchedules);
+ } catch (error) {
+
+
+ // Show empty state instead of error - no prescriptions might be normal
+ setTodaysSchedule([]);
+ setAllDoctors([]);
+ } finally {
+ setLoadingSchedule(false);
+ }
+ };
+
+ return (
+
+ {/* Header */}
+
+ Health Center
+
+ View doctor schedules, prescriptions, and manage reimbursement claims
+
+
+
+ {/* Tabbed Interface */}
+
+
+ }>
+ Announcements
+
+ }>
+ Doctor Schedules
+
+ }>
+ Today's Schedule
+
+ }>
+ Prescriptions
+
+ {isEmployee && (
+ }>
+ Reimbursement
+
+ )}
+
+
+
+
+
+
+ {/* Tab 1: Doctor Schedules */}
+
+
+
+
+ {/* Tab 2: Today's Schedule */}
+
+ {loadingSchedule ? (
+
+
+
+ ) : (
+
+ )}
+
+
+ {/* Tab 3: Prescriptions */}
+
+
+
+
+ {/* Tab 4: Reimbursement (Employees Only) */}
+ {isEmployee && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/api.js b/src/Modules/HealthCenter/api.js
new file mode 100644
index 000000000..cc3a4f3e7
--- /dev/null
+++ b/src/Modules/HealthCenter/api.js
@@ -0,0 +1,666 @@
+/**
+ * Health Center API Client
+ * =======================
+ * Axios instance for PHC module API calls
+ *
+ * Pattern: All API methods return promises with response data
+ */
+
+import axios from 'axios';
+
+// Use environment variable in dev, relative URL in prod
+const API_BASE = import.meta.env.VITE_API_BASE || '/healthcenter/api/phc';
+
+const api = axios.create({
+ baseURL: API_BASE,
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+});
+
+// Add authentication and CSRF token headers
+api.interceptors.request.use((config) => {
+ // Add authorization token from localStorage
+ const token = localStorage.getItem('authToken');
+ if (token) {
+ config.headers['Authorization'] = `Token ${token}`;
+ }
+
+ // Add CSRF token for POST/PATCH/PUT requests
+ const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]')?.value;
+ if (csrftoken) {
+ config.headers['X-CSRFToken'] = csrftoken;
+ }
+
+ return config;
+});
+
+export default api;
+
+// =========================================================================
+// ── DOCTOR AVAILABILITY ──────────────────────────────────────────────
+// =========================================================================
+
+export const getDoctorAvailability = (doctorId = null) => {
+ if (doctorId) {
+ return api.get(`/patient/doctor-availability/${doctorId}/`);
+ }
+ return api.get('/patient/doctor-availability/');
+};
+
+// =========================================================================
+// ── APPOINTMENTS ─────────────────────────────────────────────────────
+// =========================================================================
+
+export const getAppointments = (status = null) => {
+ const params = status ? { status } : {};
+ return api.get('/patient/appointments/', { params });
+};
+
+export const getAppointment = (appointmentId) => {
+ return api.get(`/patient/appointments/${appointmentId}/`);
+};
+
+export const createAppointment = (appointmentData) => {
+ return api.post('/patient/appointments/', appointmentData);
+};
+
+export const cancelAppointment = (appointmentId, reason) => {
+ return api.patch(`/patient/appointments/${appointmentId}/`, {
+ decision: 'Reject',
+ remarks: reason,
+ });
+};
+
+// =========================================================================
+// ── MEDICAL HISTORY ──────────────────────────────────────────────────
+// =========================================================================
+
+export const getMedicalHistory = () => {
+ return api.get('/patient/medical-history/').then(res => {
+ if (res.data && res.data.results && res.data.count !== undefined) {
+ res.data = res.data.results;
+ }
+ return res;
+ });
+};
+
+// =========================================================================
+// ── HEALTH PROFILE ───────────────────────────────────────────────────
+// =========================================================================
+
+export const getHealthProfile = () => {
+ return api.get('/patient/health-profile/');
+};
+
+export const updateHealthProfile = (profileData) => {
+ return api.put('/patient/health-profile/', profileData);
+};
+
+// =========================================================================
+// ── REIMBURSEMENT CLAIMS ─────────────────────────────────────────────
+// =========================================================================
+
+export const getReimbursementClaims = () => {
+ return api.get('/patient/reimbursement-claims/');
+};
+
+export const getReimbursementClaim = (claimId) => {
+ return api.get(`/patient/reimbursement-claims/${claimId}/`);
+};
+
+export const submitReimbursementClaim = (claimData) => {
+ return api.post('/patient/reimbursement-claims/', claimData);
+};
+
+export const uploadClaimDocument = (claimId, file, documentType) => {
+ const formData = new FormData();
+ formData.append('document_file', file);
+ formData.append('document_type', documentType);
+ formData.append('document_name', file.name);
+
+ return api.post(`/patient/reimbursement-claims/${claimId}/documents/`, formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ });
+};
+
+// =========================================================================
+// ── DASHBOARD ────────────────────────────────────────────────────────
+// =========================================================================
+
+export const getDashboard = () => {
+ return api.get('/dashboard/');
+};
+
+// =========================================================================
+// ── STAFF: CLAIMS PROCESSING ─────────────────────────────────────────
+// =========================================================================
+
+export const getStaffClaims = () => {
+ return api.get('/staff/claims/');
+};
+
+export const processStaffClaim = (claimId, decision, remarks) => {
+ return api.patch(`/staff/claims/${claimId}/process/`, {
+ decision,
+ remarks,
+ });
+};
+
+// =========================================================================
+// ── STAFF: INVENTORY ─────────────────────────────────────────────────
+// =========================================================================
+
+export const getInventory = () => {
+ return api.get('/staff/inventory/').then(res => {
+ if (res.data && res.data.results && res.data.count !== undefined) {
+ res.data = { stock: res.data.results };
+ }
+ return res;
+ });
+};
+
+export const updateInventoryStock = (medicineId, quantityChange, reason) => {
+ return api.post('/staff/inventory/update/', {
+ medicine_id: medicineId,
+ quantity_change: quantityChange,
+ reason,
+ });
+};
+
+export const getLowStockAlerts = () => {
+ return api.get('/staff/low-stock-alerts/');
+};
+
+// =========================================================================
+// ── COMPOUNDER: DOCTOR MANAGEMENT ────────────────────────────────────
+// =========================================================================
+
+// Register/Create Doctor
+export const createDoctor = (doctorData) => {
+ return api.post('/compounder/doctors/', {
+ doctor_name: doctorData.doctor_name,
+ specialization: doctorData.specialization,
+ doctor_phone: doctorData.doctor_phone || '',
+ email: doctorData.email || '',
+ });
+};
+
+// Get all doctors
+export const getDoctors = (activeOnly = true) => {
+ const params = {};
+ if (activeOnly !== null && activeOnly !== undefined) {
+ params.active_only = activeOnly ? 'true' : 'false';
+ }
+ return api.get('/compounder/doctors/', { params });
+};
+
+// Get single doctor
+export const getDoctor = (doctorId) => {
+ return api.get(`/compounder/doctors/${doctorId}/`);
+};
+
+// Update doctor
+export const updateDoctor = (doctorId, doctorData) => {
+ return api.patch(`/compounder/doctors/${doctorId}/`, {
+ doctor_name: doctorData.doctor_name,
+ specialization: doctorData.specialization,
+ doctor_phone: doctorData.doctor_phone || '',
+ email: doctorData.email || '',
+ is_active: doctorData.is_active !== undefined ? doctorData.is_active : true,
+ });
+};
+
+// =========================================================================
+// ── COMPOUNDER: DOCTOR SCHEDULE ──────────────────────────────────────
+// =========================================================================
+
+// Get doctor schedules
+export const getDoctorSchedules = (doctorId = null) => {
+ if (doctorId) {
+ return api.get(`/compounder/schedule/?doctor=${doctorId}`);
+ }
+ return api.get('/compounder/schedule/');
+};
+
+// Create doctor schedule
+export const createDoctorSchedule = (scheduleData) => {
+ return api.post('/compounder/schedule/', {
+ doctor: scheduleData.doctor_id,
+ day_of_week: scheduleData.day_of_week,
+ start_time: scheduleData.start_time || '09:00',
+ end_time: scheduleData.end_time || '17:00',
+ room_number: scheduleData.room_number || '',
+ });
+};
+
+// =========================================================================
+// ── COMPOUNDER: DOCTOR ATTENDANCE ───────────────────────────────────
+// =========================================================================
+
+// Get today's attendance records
+export const getTodaysAttendance = () => {
+ return api.get('/compounder/attendance/today/');
+};
+
+// Create/Update attendance for today
+export const updateDoctorAttendance = (doctorId, status) => {
+ return api.post('/compounder/attendance/', {
+ doctor: doctorId,
+ attendance_date: new Date().toISOString().split('T')[0],
+ status: status,
+ });
+};
+
+// =========================================================================
+// ── COMPOUNDER: INVENTORY REQUISITIONS ─────────────────────────────────
+// =========================================================================
+
+export const getCompounderRequisitions = () => {
+ return api.get('/compounder/requisition/');
+};
+
+export const createCompounderRequisition = (medicineId, quantity) => {
+ return api.post('/compounder/requisition/', {
+ medicine: medicineId,
+ quantity_requested: quantity
+ });
+};
+
+export const fulfillCompounderRequisition = (requisitionId, quantityFulfilled) => {
+ return api.patch(`/compounder/requisition/${requisitionId}/fulfill/`, {
+ quantity_fulfilled: quantityFulfilled
+ });
+};
+
+// =========================================================================
+// ── PATIENT: PRESCRIPTIONS ──────────────────────────────────────────
+// =========================================================================
+
+export const getPrescriptions = () => {
+ return api.get('/patient/prescriptions/');
+};
+
+export const getPrescription = (prescriptionId) => {
+ return api.get(`/patient/prescriptions/${prescriptionId}/`);
+};
+
+// =========================================================================
+// ── PATIENT: COMPLAINTS (Patient-facing) ────────────────────────────
+// =========================================================================
+
+export const getPatientComplaints = () => {
+ return api.get('/complaint/');
+};
+
+export const getPatientComplaint = (complaintId) => {
+ return api.get(`/complaint/${complaintId}/`);
+};
+
+export const createPatientComplaint = (complaintData) => {
+ return api.post('/complaint/', complaintData);
+};
+
+export const updatePatientComplaint = (complaintId, complaintData) => {
+ return api.patch(`/complaint/${complaintId}/`, complaintData);
+};
+
+// =========================================================================
+// ── COMPOUNDER: COMPLAINTS (Compounder response/management) ──────────
+// =========================================================================
+
+export const getComplaints = () => {
+ // Compounder gets complaints to respond to
+ return api.get('/compounder/complaint/');
+};
+
+export const getComplaint = (complaintId) => {
+ return api.get(`/compounder/complaint/${complaintId}/`);
+};
+
+export const respondToCompounderComplaint = (complaintId, responseData) => {
+ return api.patch(`/compounder/complaint/${complaintId}/respond/`, responseData);
+};
+
+// =========================================================================
+// ── COMPOUNDER: STOCK MANAGEMENT ────────────────────────────────────
+// =========================================================================
+
+export const getMedicines = () => {
+ return api.get('/compounder/medicine/');
+};
+
+export const addMedicine = (medicineData) => {
+ return api.post('/compounder/medicine/', medicineData);
+};
+
+export const getStock = () => {
+ return api.get('/compounder/stock/');
+};
+
+export const addStock = (stockData) => {
+ return api.post('/compounder/stock/', {
+ medicine_id: stockData.medicine_id || stockData.medication_id,
+ qty: stockData.total_qty,
+ expiry_date: stockData.expiry_date || new Date().toISOString().split('T')[0],
+ batch_no: stockData.batch_no || '',
+ });
+};
+
+export const updateStock = (stockId, stockData) => {
+ return api.patch(`/compounder/stock/${stockId}/`, {
+ medicine_id: stockData.medicine_id || stockData.medication_id,
+ qty: stockData.total_qty,
+ expiry_date: stockData.expiry_date || new Date().toISOString().split('T')[0],
+ batch_no: stockData.batch_no || '',
+ });
+};
+
+export const deleteStock = (stockId) => {
+ return api.delete(`/compounder/stock/${stockId}/`);
+};
+
+// =========================================================================
+// ── COMPOUNDER: EXPIRY & BATCH RETURNS ──────────────────────────────
+// =========================================================================
+
+export const getExpiryBatches = () => {
+ return api.get('/compounder/expiry/');
+};
+
+export const markBatchAsReturned = (batchId, returnData) => {
+ return api.patch(`/compounder/expiry/${batchId}/return/`, returnData);
+};
+
+export const deleteBatch = (batchId) => {
+ return api.delete(`/compounder/expiry/${batchId}/`);
+};
+
+// =========================================================================
+// ── COMPOUNDER: AMBULANCES ──────────────────────────────────────────
+// =========================================================================
+
+export const getAmbulances = () => {
+ return api.get('/compounder/ambulance/');
+};
+
+export const createAmbulance = (ambulanceData) => {
+ return api.post('/compounder/ambulance/', ambulanceData);
+};
+
+export const updateAmbulance = (ambulanceId, ambulanceData) => {
+ return api.patch(`/compounder/ambulance/${ambulanceId}/`, ambulanceData);
+};
+
+export const deleteAmbulance = (ambulanceId) => {
+ return api.delete(`/compounder/ambulance/${ambulanceId}/`);
+};
+
+// =========================================================================
+// ── COMPOUNDER: AMBULANCE USAGE LOG (PHC-UC-11) ──────────────────────
+// ─────────────────────────────────────────────────────────────────────────
+// Separate from fleet management — records every individual dispatch event.
+// Each entry captures patient_name, destination, call_date, call_time.
+// PHC-BR-09 audit trail is written server-side via create_ambulance_log().
+// =========================================================================
+
+/**
+ * Get all ambulance dispatch log entries.
+ * @param {Object} filters - Optional: { date_from, date_to, search }
+ */
+export const getAmbulanceLogs = (filters = {}) => {
+ const params = new URLSearchParams();
+ if (filters.date_from) params.append('date_from', filters.date_from);
+ if (filters.date_to) params.append('date_to', filters.date_to);
+ if (filters.search) params.append('search', filters.search);
+ const qs = params.toString();
+ return api.get(`/compounder/ambulance-log/${qs ? '?' + qs : ''}`);
+};
+
+/**
+ * Create a new ambulance dispatch log entry (PHC-UC-11 M2).
+ * @param {Object} logData - { patient_name, destination, call_date, call_time,
+ * ambulance (optional id), purpose, contact_number }
+ */
+export const createAmbulanceLog = (logData) => {
+ return api.post('/compounder/ambulance-log/', logData);
+};
+
+/**
+ * Delete an ambulance log entry (correction use-case).
+ * @param {number} logId
+ */
+export const deleteAmbulanceLog = (logId) => {
+ return api.delete(`/compounder/ambulance-log/${logId}/`);
+};
+
+// =========================================================================
+// ── COMPOUNDER: HOSPITAL ADMISSIONS - COMMENTED OUT ─────────────────────
+// =========================================================================
+
+// export const getAdmissions = () => {
+// return api.get('/compounder/hospital-admit/');
+// };
+//
+// export const createAdmission = (admissionData) => {
+// return api.post('/compounder/hospital-admit/', admissionData);
+// };
+//
+// export const updateAdmission = (admissionId, admissionData) => {
+// return api.patch(`/compounder/hospital-admit/${admissionId}/`, admissionData);
+// };
+
+// =========================================================================
+// ── COMPOUNDER: PRESCRIPTION CREATION ───────────────────────────────
+// =========================================================================
+
+export const getConsultations = (params = {}) => {
+ const queryParams = new URLSearchParams();
+ if (params.days) queryParams.append('days', params.days);
+ if (params.doctor_id) queryParams.append('doctor_id', params.doctor_id);
+ const query = queryParams.toString();
+ return api.get(`/compounder/consultations/${query ? '?' + query : ''}`);
+};
+
+export const getDoctorsFiltered = (params = {}) => {
+ const queryParams = new URLSearchParams();
+ if (params.active_only !== undefined) queryParams.append('active_only', params.active_only);
+ if (params.specialization) queryParams.append('specialization', params.specialization);
+ const query = queryParams.toString();
+ return api.get(`/compounder/doctors/${query ? '?' + query : ''}`);
+};
+
+export const getUsers = (params = {}) => {
+ const queryParams = new URLSearchParams();
+ if (params.search) queryParams.append('search', params.search);
+ const query = queryParams.toString();
+ return api.get(`/compounder/users/${query ? '?' + query : ''}`);
+};
+
+export const createPrescription = (prescriptionData) => {
+ return api.post('/compounder/prescription/', prescriptionData);
+};
+
+export const getCompounderPrescriptions = (params = {}) => {
+ const queryParams = new URLSearchParams();
+ if (params.doctor_id) queryParams.append('doctor_id', params.doctor_id);
+ if (params.status) queryParams.append('status', params.status);
+ const query = queryParams.toString();
+ return api.get(`/compounder/prescription/${query ? '?' + query : ''}`).then(res => {
+ if (res.data && res.data.results && res.data.count !== undefined) {
+ res.data = res.data.results;
+ }
+ return res;
+ });
+};
+
+// =========================================================================
+// ── COMPOUNDER: COMPLAINT RESPONSE ──────────────────────────────────
+// =========================================================================
+
+export const respondToComplaint = (complaintId, responseData) => {
+ return api.patch(`/compounder/complaint/${complaintId}/respond/`, responseData);
+};
+
+// =========================================================================
+// ── COMPOUNDER: REIMBURSEMENT REVIEW ────────────────────────────────
+// =========================================================================
+
+export const reviewReimbursementClaim = (claimId, reviewData) => {
+ return api.patch(`/compounder/reimbursement-claims/${claimId}/review/`, reviewData);
+};
+
+// =========================================================================
+// ── COMMON: DOCTORS & PATIENTS ──────────────────────────────────────
+// =========================================================================
+
+// NOTE: getPatients() and getMedicines() endpoints don't exist in backend
+// These are fetched from existing models (Stock, Inventory) instead
+
+// =========================================================================
+// ── COMPOUNDER: DOCTOR SCHEDULE EXTENDED ───────────────────────────
+// =========================================================================
+
+export const deleteDoctorSchedule = (scheduleId) => {
+ return api.delete(`/compounder/schedule/${scheduleId}/`);
+};
+
+export const updateDoctorSchedule = (scheduleId, scheduleData) => {
+ return api.patch(`/compounder/schedule/${scheduleId}/`, {
+ doctor: scheduleData.doctor_id,
+ day_of_week: scheduleData.day_of_week,
+ start_time: scheduleData.start_time || '09:00',
+ end_time: scheduleData.end_time || '17:00',
+ room_number: scheduleData.room_number || '',
+ });
+};
+
+// =========================================================================
+// ── COMPOUNDER: ATTENDANCE EXTENDED ─────────────────────────────────
+// =========================================================================
+
+export const getDoctorAttendance = (date = null) => {
+ if (date) {
+ return api.get(`/compounder/attendance/?date=${date}`);
+ }
+ return api.get('/compounder/attendance/');
+};
+
+export const createDoctorAttendance = (attendanceData) => {
+ return api.post('/compounder/attendance/', {
+ doctor: attendanceData.doctor,
+ attendance_date: attendanceData.attendance_date,
+ status: attendanceData.status,
+ notes: attendanceData.notes || '',
+ });
+};
+
+export const updateDoctorAttendanceRecord = (attendanceId, attendanceData) => {
+ return api.patch(`/compounder/attendance/${attendanceId}/`, {
+ doctor: attendanceData.doctor,
+ attendance_date: attendanceData.attendance_date,
+ status: attendanceData.status,
+ notes: attendanceData.notes || '',
+ });
+};
+
+export const deleteDoctorAttendance = (attendanceId) => {
+ return api.delete(`/compounder/attendance/${attendanceId}/`);
+};
+
+// =========================================================================
+// ── COMPOUNDER: DOCTOR DELETE ──────────────────────────────────────
+// =========================================================================
+
+export const deleteDoctor = (doctorId) => {
+ return api.delete(`/compounder/doctors/${doctorId}/`);
+};
+
+// =========================================================================
+// ── COMPOUNDER: CONSULTATIONS (CREATE, READ, DELETE) ────────────────
+// =========================================================================
+
+export const getConsultationsList = (days = 7, doctorId = null) => {
+ const params = new URLSearchParams();
+ params.append('days', days);
+ if (doctorId) {
+ params.append('doctor_id', doctorId);
+ }
+ return api.get(`/compounder/consultations/?${params.toString()}`);
+};
+
+export const createConsultation = (consultationData) => {
+ return api.post('/compounder/consultation/', consultationData);
+};
+
+export const deleteConsultation = (consultationId) => {
+ return api.delete(`/compounder/consultation/${consultationId}/`);
+};
+
+// =========================================================================
+// ── AUDITOR: REIMBURSEMENT CLAIMS APPROVAL ──────────────────────────
+// =========================================================================
+
+export const getAuditorClaims = () => {
+ return api.get('/auditor/reimbursement-claims/');
+};
+
+export const getAuditorClaim = (claimId) => {
+ return api.get(`/auditor/reimbursement-claims/${claimId}/`);
+};
+
+export const approveReimbursementClaim = (claimId, remarks) => {
+ return api.patch(`/auditor/reimbursement-claims/${claimId}/approve/`, {
+ remarks: remarks,
+ });
+};
+
+export const rejectReimbursementClaim = (claimId, remarks) => {
+ return api.patch(`/auditor/reimbursement-claims/${claimId}/reject/`, {
+ remarks: remarks,
+ });
+};
+
+export const downloadClaimDocument = (documentId) => {
+ return api.get(`/auditor/claim-documents/${documentId}/download/`, {
+ responseType: 'blob',
+ });
+};
+
+// =========================================================================
+// ── HEALTH ANNOUNCEMENTS (PHC-UC-12) ─────────────────────────────────────
+// ─────────────────────────────────────────────────────────────────────────
+// All authenticated users can read. PHC staff only can create/deactivate.
+// PHC-UC-17: Portal notification broadcast is triggered server-side on POST.
+// =========================================================================
+
+/** Get all active, non-expired health announcements (all authenticated users). */
+export const getAnnouncements = () => api.get('/announcements/');
+
+/**
+ * Create and broadcast a health announcement (PHC staff only).
+ * @param {Object} data - { title, content, category, priority?, expires_at? }
+ */
+export const createAnnouncement = (data) => api.post('/announcements/', data);
+
+/**
+ * Deactivate (soft-delete) a health announcement (PHC staff only).
+ * @param {number} id - Announcement ID
+ */
+export const deleteAnnouncement = (id) => api.delete(`/announcements/${id}/`);
+
+// =========================================================================
+// ── SYSTEM REPORTS (PHC-UC-13) ───────────────────────────────────────────
+// =========================================================================
+
+/**
+ * Generate utilization reports including demographics and inventory consumption.
+ * @param {string} startDate - Optional YYYY-MM-DD
+ * @param {string} endDate - Optional YYYY-MM-DD
+ */
+export const generateSystemReport = (startDate, endDate) => {
+ const params = new URLSearchParams();
+ if (startDate) params.append('start_date', startDate);
+ if (endDate) params.append('end_date', endDate);
+ return api.get(`/compounder/reports/?${params.toString()}`);
+};
diff --git a/src/Modules/HealthCenter/components/AlertsTab.jsx b/src/Modules/HealthCenter/components/AlertsTab.jsx
new file mode 100644
index 000000000..39f04480f
--- /dev/null
+++ b/src/Modules/HealthCenter/components/AlertsTab.jsx
@@ -0,0 +1,45 @@
+import React from 'react';
+import { Table,
+ ScrollArea, Button, Text } from '@mantine/core';
+
+export default function AlertsTab({ alerts }) {
+ return (
+ <>
+ {Array.isArray(alerts) && alerts.length > 0 ? (
+
+
+
+ Medicine
+ Current Stock
+ Threshold
+ Action
+
+
+
+ {alerts.map((alert) => (
+
+ {alert.medicine_name}
+ {alert.current_stock}
+ {alert.threshold}
+
+
+
+
+ ))}
+
+
+ ) : (
+ No low stock alerts
+ )}
+ >
+ );
+}
diff --git a/src/Modules/HealthCenter/components/AmbulanceLogTab.jsx b/src/Modules/HealthCenter/components/AmbulanceLogTab.jsx
new file mode 100644
index 000000000..ce1455377
--- /dev/null
+++ b/src/Modules/HealthCenter/components/AmbulanceLogTab.jsx
@@ -0,0 +1,278 @@
+import { useState, useEffect } from 'react';
+import {
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Button,
+ Modal,
+ TextInput,
+ ActionIcon,
+ Select,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import { IconPlus, IconTrash } from '@tabler/icons-react';
+import * as api from '../api';
+
+export default function AmbulanceLogTab({ ambulances }) {
+ const [logs, setLogs] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [formData, setFormData] = useState({
+ ambulance: '',
+ patient_name: '',
+ destination: '',
+ call_date: new Date().toISOString().split('T')[0],
+ call_time: new Date().toTimeString().split(' ')[0].substring(0, 5),
+ purpose: '',
+ contact_number: '',
+ });
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ fetchLogs();
+ }, []);
+
+ const fetchLogs = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getAmbulanceLogs();
+ setLogs(Array.isArray(response.data) ? response.data : []);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load ambulance logs',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!formData.patient_name.trim()) newErrors.patient_name = 'Patient name is required';
+ if (!formData.destination.trim()) newErrors.destination = 'Destination is required';
+ if (!formData.call_date) newErrors.call_date = 'Date is required';
+ if (!formData.call_time) newErrors.call_time = 'Time is required';
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ const payload = { ...formData };
+ if (!payload.ambulance) payload.ambulance = null;
+
+ await api.createAmbulanceLog(payload);
+ notifications.show({
+ message: 'Ambulance log added successfully',
+ color: 'green',
+ });
+ setModalOpened(false);
+ resetForm();
+ fetchLogs();
+ } catch (error) {
+
+
+ alert('Backend Error Details:\n\n' + JSON.stringify(error.response?.data, null, 2));
+ notifications.show({
+ message:
+ error.response?.data?.detail ||
+ (error.response?.data && typeof error.response.data === 'object'
+ ? Object.entries(error.response.data)
+ .map(([k, v]) => `${k}: ${v}`)
+ .join(', ')
+ : 'Failed to save ambulance log'),
+ color: 'red',
+ autoClose: 10000,
+ });
+ }
+ };
+
+ const handleDelete = (id) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deletion',
+ children: 'Delete this ambulance log entry? This action will be audited.',
+ labels: { confirm: 'Delete', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteAmbulanceLog(id);
+ notifications.show({
+ message: 'Ambulance log deleted successfully',
+ color: 'green',
+ });
+ fetchLogs();
+ } catch (error) {
+ notifications.show({ message: 'Failed to delete ambulance log', color: 'red' });
+ }
+ },
+ });
+ };
+
+ const resetForm = () => {
+ setFormData({
+ ambulance: '',
+ patient_name: '',
+ destination: '',
+ call_date: new Date().toISOString().split('T')[0],
+ call_time: new Date().toTimeString().split(' ')[0].substring(0, 5),
+ purpose: '',
+ contact_number: '',
+ });
+ setErrors({});
+ };
+
+ const ambulanceOptions = ambulances.map((amb) => ({
+ value: amb.id.toString(),
+ label: `${amb.registration_number} (${amb.vehicle_type})`,
+ }));
+
+ return (
+
+
+
+ Ambulance Usage Log
+
+ }
+ onClick={() => {
+ resetForm();
+ setModalOpened(true);
+ }}
+ >
+ Log Dispatch
+
+
+
+
+ {logs.length === 0 ? (
+
+ No ambulance usage logs found
+
+ ) : (
+
+
+
+ Date & Time
+ Patient
+ Destination
+ Ambulance
+ Logged By
+ Actions
+
+
+
+ {logs.map((log) => (
+
+
+ {log.call_date} {log.call_time}
+
+ {log.patient_name}
+ {log.destination}
+ {log.ambulance_registration || 'N/A'}
+ {log.logged_by_name || 'System'}
+
+ handleDelete(log.id)}
+ >
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title="Log Ambulance Dispatch"
+ size="md"
+ >
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/AmbulanceManagement.jsx b/src/Modules/HealthCenter/components/AmbulanceManagement.jsx
new file mode 100644
index 000000000..9d85b6874
--- /dev/null
+++ b/src/Modules/HealthCenter/components/AmbulanceManagement.jsx
@@ -0,0 +1,549 @@
+/**
+ * Ambulance Management Component
+ * =============================
+ * CRUD for ambulance records
+ * Track ambulance availability and usage
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ TextInput,
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ Badge,
+ ActionIcon,
+ Select,
+ NumberInput,
+ Tabs,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import { IconPlus, IconEdit, IconTrash, IconEye, IconCar, IconList } from '@tabler/icons-react';
+import * as api from '../api';
+import AmbulanceLogTab from './AmbulanceLogTab';
+
+const statusOptions = [
+ { value: 'AVAILABLE', label: 'Available' },
+ { value: 'IN_USE', label: 'In Use' },
+ { value: 'MAINTENANCE', label: 'Maintenance' },
+];
+
+const vehicleTypeOptions = [
+ { value: 'Type A', label: 'Type A - Basic' },
+ { value: 'Type B', label: 'Type B - Standard' },
+ { value: 'Type C', label: 'Type C - Advanced' },
+ { value: 'Advanced', label: 'Advanced - Full Equipment' },
+ { value: 'Basic', label: 'Basic - Transport Only' },
+];
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function AmbulanceManagement() {
+ const [activeTab, setActiveTab] = useState('fleet');
+ const [ambulances, setAmbulances] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [detailsModalOpened, setDetailsModalOpened] = useState(false);
+ const [selectedAmbulance, setSelectedAmbulance] = useState(null);
+ const [editingId, setEditingId] = useState(null);
+ const [formData, setFormData] = useState({
+ vehicle_type: '',
+ registration_number: '',
+ driver_name: '',
+ driver_contact: '',
+ driver_license: '',
+ last_maintenance_date: '',
+ next_maintenance_due: '',
+ notes: '',
+ });
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ fetchAmbulances();
+ }, []);
+
+ const fetchAmbulances = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getAmbulances();
+ setAmbulances(Array.isArray(response.data) ? response.data : []);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load ambulances',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!formData.vehicle_type)
+ newErrors.vehicle_type = 'Vehicle type is required';
+ if (!formData.registration_number.trim())
+ newErrors.registration_number = 'Registration number is required';
+ if (!formData.driver_name.trim())
+ newErrors.driver_name = 'Driver name is required';
+ if (!formData.driver_contact || formData.driver_contact.length < 10)
+ newErrors.driver_contact = 'Valid phone number required (10+ digits)';
+ if (!formData.driver_license.trim())
+ newErrors.driver_license = 'Driver license is required';
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ if (editingId) {
+ // For update, only send fields accepted by update serializer
+ await api.updateAmbulance(editingId, {
+ last_maintenance_date: formData.last_maintenance_date || null,
+ notes: formData.notes || '',
+ });
+ notifications.show({
+ message: 'Ambulance updated successfully',
+ color: 'green',
+ });
+ } else {
+ // For create, send all required fields
+ await api.createAmbulance({
+ vehicle_type: formData.vehicle_type,
+ registration_number: formData.registration_number,
+ driver_name: formData.driver_name,
+ driver_contact: formData.driver_contact,
+ driver_license: formData.driver_license,
+ last_maintenance_date: formData.last_maintenance_date || null,
+ next_maintenance_due: formData.next_maintenance_due || null,
+ notes: formData.notes || '',
+ });
+ notifications.show({
+ message: 'Ambulance added successfully',
+ color: 'green',
+ });
+ }
+ setModalOpened(false);
+ resetForm();
+ await fetchAmbulances();
+ } catch (error) {
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to save ambulance',
+ color: 'red',
+ });
+ }
+ };
+
+ const handleEdit = (ambulance) => {
+ setEditingId(ambulance.id);
+ setFormData({
+ vehicle_type: ambulance.vehicle_type,
+ registration_number: ambulance.registration_number,
+ driver_name: ambulance.driver_name,
+ driver_contact: ambulance.driver_contact,
+ driver_license: ambulance.driver_license || '',
+ last_maintenance_date: ambulance.last_maintenance_date || '',
+ next_maintenance_due: ambulance.next_maintenance_due || '',
+ notes: ambulance.notes || '',
+ });
+ setModalOpened(true);
+ };
+
+ const handleDelete = (id) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deletion',
+ children: 'Delete this ambulance?',
+ labels: { confirm: 'Delete', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteAmbulance(id);
+ notifications.show({
+ message: 'Ambulance deleted successfully',
+ color: 'green',
+ });
+ await fetchAmbulances();
+ } catch (error) {
+ notifications.show({ message: 'Failed to delete ambulance', color: 'red' });
+ }
+ }
+ });
+ };
+
+ const handleViewDetails = (ambulance) => {
+ setSelectedAmbulance(ambulance);
+ setDetailsModalOpened(true);
+ };
+
+ const resetForm = () => {
+ setFormData({
+ vehicle_type: '',
+ registration_number: '',
+ driver_name: '',
+ driver_contact: '',
+ driver_license: '',
+ last_maintenance_date: '',
+ next_maintenance_due: '',
+ notes: '',
+ });
+ setErrors({});
+ setEditingId(null);
+ };
+
+ const getStatusColor = (status) => {
+ const colors = {
+ AVAILABLE: 'green',
+ IN_USE: 'blue',
+ MAINTENANCE: 'yellow',
+ };
+ return colors[status] || 'gray';
+ };
+
+ return (
+
+
+
+ }>
+ Fleet Management
+
+ }>
+ Usage Logs
+
+
+
+
+
+
+
+ Ambulance Fleet
+
+ }
+ onClick={() => {
+ resetForm();
+ setModalOpened(true);
+ }}
+ >
+ Add Ambulance
+
+
+
+
+ {ambulances.length === 0 ? (
+
+ No ambulances registered
+
+ ) : (
+
+
+
+ Type
+ Registration
+ Driver
+ Contact
+ Status
+ Actions
+
+
+
+ {ambulances.map((ambulance) => (
+
+ {ambulance.vehicle_type}
+ {ambulance.registration_number}
+ {ambulance.driver_name}
+ {ambulance.driver_contact}
+
+
+ {ambulance.status}
+
+
+
+
+ handleViewDetails(ambulance)}
+ >
+
+
+ handleEdit(ambulance)}
+ >
+
+
+ handleDelete(ambulance.id)}
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title={editingId ? 'Edit Ambulance' : 'Add New Ambulance'}
+ size="md"
+ >
+
+
+
+
+ {/* Details Modal */}
+ {
+ setDetailsModalOpened(false);
+ setSelectedAmbulance(null);
+ }}
+ title="Ambulance Details"
+ size="md"
+ >
+ {selectedAmbulance && (
+
+
+
+ {/* Vehicle Information */}
+
+
+ Vehicle Information
+
+
+ Type: {selectedAmbulance.vehicle_type}
+
+
+ Registration: {selectedAmbulance.registration_number}
+
+
+
+ Status:
+
+
+ {selectedAmbulance.status}
+
+
+
+
+ {/* Driver Information */}
+
+
+ Driver Information
+
+
+ Name: {selectedAmbulance.driver_name}
+
+
+ Contact: {selectedAmbulance.driver_contact}
+
+
+ License: {selectedAmbulance.driver_license}
+
+
+
+ {/* Maintenance Information */}
+
+
+ Maintenance Information
+
+
+ Last Maintenance:{' '}
+ {selectedAmbulance.last_maintenance_date
+ ? new Date(selectedAmbulance.last_maintenance_date).toLocaleDateString()
+ : 'Not recorded'}
+
+
+ Next Due:{' '}
+ {selectedAmbulance.next_maintenance_due
+ ? new Date(selectedAmbulance.next_maintenance_due).toLocaleDateString()
+ : 'Not scheduled'}
+
+
+
+ {/* Notes */}
+ {selectedAmbulance.notes && (
+
+
+ Notes
+
+
+ {selectedAmbulance.notes}
+
+
+ )}
+
+ {/* Status Information */}
+
+
+ System Information
+
+
+ Active: {selectedAmbulance.is_active ? 'Yes' : 'No'}
+
+ {selectedAmbulance.created_at && (
+
+ Created:{' '}
+ {new Date(selectedAmbulance.created_at).toLocaleString()}
+
+ )}
+ {selectedAmbulance.updated_at && (
+
+ Last Updated:{' '}
+ {new Date(selectedAmbulance.updated_at).toLocaleString()}
+
+ )}
+
+
+
+
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/AnnouncementsManagement.jsx b/src/Modules/HealthCenter/components/AnnouncementsManagement.jsx
new file mode 100644
index 000000000..6d44ecb41
--- /dev/null
+++ b/src/Modules/HealthCenter/components/AnnouncementsManagement.jsx
@@ -0,0 +1,308 @@
+/**
+ * AnnouncementsManagement — Compounder Dashboard
+ * ===============================================
+ * PHC-UC-12: Broadcast Health Announcements
+ * PHC-UC-17: Portal notifications triggered server-side on create
+ * PHC-BR-09: Audit trail written via create_announcement() service
+ *
+ * Compounders create / deactivate announcements here.
+ * Patients see them read-only via AnnouncementsView.jsx.
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Stack, Group, Card, Text, Title, Badge, Button, Modal,
+ TextInput, Textarea, Select, NumberInput, ActionIcon,
+ Table,
+ ScrollArea, Loader, Center, Alert,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import {
+ IconPlus, IconTrash, IconAlertCircle, IconBell, IconSpeakerphone,
+} from '@tabler/icons-react';
+import * as api from '../api';
+
+const CATEGORY_OPTIONS = [
+ { value: 'GENERAL', label: 'General' },
+ { value: 'HEALTH_ADVISORY', label: 'Health Advisory' },
+ { value: 'SCHEDULE_CHANGE', label: 'Schedule Change' },
+ { value: 'EMERGENCY', label: 'Emergency' },
+ { value: 'VACCINATION', label: 'Vaccination Drive' },
+];
+
+const CATEGORY_COLORS = {
+ GENERAL: 'blue',
+ HEALTH_ADVISORY: 'orange',
+ SCHEDULE_CHANGE: 'violet',
+ EMERGENCY: 'red',
+ VACCINATION: 'teal',
+};
+
+const emptyForm = {
+ title: '',
+ content: '',
+ category: 'GENERAL',
+ priority: 0,
+ expires_at: '',
+};
+
+export default function AnnouncementsManagement() {
+ const [announcements, setAnnouncements] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpen, setModalOpen] = useState(false);
+ const [formData, setFormData] = useState(emptyForm);
+ const [submitting, setSubmitting] = useState(false);
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => { fetchAnnouncements(); }, []);
+
+ const fetchAnnouncements = async () => {
+ try {
+ setLoading(true);
+ const res = await api.getAnnouncements();
+ setAnnouncements(Array.isArray(res.data) ? res.data : []);
+ } catch {
+ notifications.show({ message: 'Failed to load announcements', color: 'red' });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validate = () => {
+ const errs = {};
+ if (!formData.title.trim() || formData.title.trim().length < 5)
+ errs.title = 'Title must be at least 5 characters';
+ if (!formData.content.trim() || formData.content.trim().length < 10)
+ errs.content = 'Content must be at least 10 characters';
+ if (formData.expires_at) {
+ const exp = new Date(formData.expires_at);
+ if (isNaN(exp) || exp <= new Date())
+ errs.expires_at = 'Expiry must be a future date/time';
+ }
+ setErrors(errs);
+ return Object.keys(errs).length === 0;
+ };
+
+ const handleSubmit = () => {
+ if (!validate()) return;
+
+ const payload = {
+ title: formData.title.trim(),
+ content: formData.content.trim(),
+ category: formData.category,
+ priority: formData.priority ?? 0,
+ };
+ if (formData.expires_at) payload.expires_at = new Date(formData.expires_at).toISOString();
+
+ // Close the modal immediately to unblock the user
+ setModalOpen(false);
+
+ // Show a loading notification
+ notifications.show({
+ id: 'broadcast-announcement',
+ loading: true,
+ title: 'Broadcasting Announcement',
+ message: 'Saving and safely notifying all portal users. This may take a moment...',
+ autoClose: false,
+ withCloseButton: false,
+ });
+
+ // Run the API request asynchronously
+ api.createAnnouncement(payload)
+ .then(() => {
+ notifications.update({
+ id: 'broadcast-announcement',
+ color: 'green',
+ title: 'Announcement Posted!',
+ message: 'All portal users have been notified successfully.',
+ loading: false,
+ autoClose: 5000,
+ });
+ setFormData(emptyForm);
+ setErrors({});
+ fetchAnnouncements();
+ })
+ .catch((err) => {
+ notifications.update({
+ id: 'broadcast-announcement',
+ color: 'red',
+ title: 'Broadcast Failed',
+ message: err.response?.data?.detail || 'Failed to post announcement',
+ loading: false,
+ autoClose: 8000,
+ });
+ // We do not clear the form data here so the user can re-open and try again if needed
+ });
+ };
+
+ const handleDeactivate = (id, title) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deactivation',
+ children: `Deactivate announcement "${title}"? It will no longer be visible to users.`,
+ labels: { confirm: 'Deactivate', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteAnnouncement(id);
+ notifications.show({ message: 'Announcement deactivated', color: 'green' });
+ fetchAnnouncements();
+ } catch {
+ notifications.show({ message: 'Failed to deactivate announcement', color: 'red' });
+ }
+ }
+ });
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Health Announcements
+
+ Post health advisories and notices to all portal users. Each post triggers a portal notification broadcast.
+
+
+ }
+ onClick={() => { setFormData(emptyForm); setErrors({}); setModalOpen(true); }}
+ >
+ New Announcement
+
+
+
+ {/* Info Box */}
+ } color="blue" variant="light">
+ When you post an announcement, all active portal users instantly receive a portal notification.
+ Announcements remain visible until deactivated or until their expiry date passes.
+
+
+ {/* Table */}
+
+ {loading ? (
+
+ ) : announcements.length === 0 ? (
+ No active announcements. Create one above.
+ ) : (
+
+
+
+ Title
+ Category
+ Priority
+ Posted By
+ Posted At
+ Expires
+ Actions
+
+
+
+ {announcements.map((a) => (
+
+ {a.title}
+
+
+ {a.category.replace('_', ' ')}
+
+
+ {a.priority}
+ {a.created_by_name || '—'}
+ {new Date(a.created_at).toLocaleDateString()}
+
+ {a.expires_at
+ ?
+ {new Date(a.expires_at).toLocaleDateString()}
+ {a.is_expired ? ' (expired)' : ''}
+
+ : Never
+ }
+
+
+ handleDeactivate(a.id, a.title)}
+ >
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Create Modal */}
+ { setModalOpen(false); setFormData(emptyForm); setErrors({}); }}
+ title="Post New Health Announcement"
+ size="lg"
+ >
+
+ } color="orange" variant="light">
+ This will immediately send a portal notification to all active users.
+
+
+ setFormData({ ...formData, title: e.currentTarget.value })}
+ error={errors.title}
+ />
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/AnnouncementsView.jsx b/src/Modules/HealthCenter/components/AnnouncementsView.jsx
new file mode 100644
index 000000000..466db7e6b
--- /dev/null
+++ b/src/Modules/HealthCenter/components/AnnouncementsView.jsx
@@ -0,0 +1,161 @@
+/**
+ * AnnouncementsView — Patient Dashboard
+ * ======================================
+ * PHC-UC-12: Read-only board for patients/employees to view health announcements.
+ * Auto-refreshes every 60 seconds to surface new broadcasts.
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Stack, Card, Text, Title, Badge, Group, Loader, Center,
+ Alert, ThemeIcon,
+} from '@mantine/core';
+import { IconBell, IconAlertTriangle, IconInfoCircle, IconCalendar } from '@tabler/icons-react';
+import * as api from '../api';
+
+const CATEGORY_META = {
+ GENERAL: { color: 'blue', label: 'General' },
+ HEALTH_ADVISORY: { color: 'orange', label: 'Health Advisory' },
+ SCHEDULE_CHANGE: { color: 'violet', label: 'Schedule Change' },
+ EMERGENCY: { color: 'red', label: 'Emergency' },
+ VACCINATION: { color: 'teal', label: 'Vaccination Drive' },
+};
+
+function daysUntilExpiry(expiresAt) {
+ if (!expiresAt) return null;
+ const diff = new Date(expiresAt) - new Date();
+ return Math.ceil(diff / (1000 * 60 * 60 * 24));
+}
+
+export default function AnnouncementsView() {
+ const [announcements, setAnnouncements] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const fetchAnnouncements = async () => {
+ try {
+ setError(null);
+ const res = await api.getAnnouncements();
+ setAnnouncements(Array.isArray(res.data) ? res.data : []);
+ } catch {
+ setError('Unable to load announcements. Please try again later.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ fetchAnnouncements();
+ // Auto-refresh every 60 seconds
+ const interval = setInterval(fetchAnnouncements, 60_000);
+ return () => clearInterval(interval);
+ }, []);
+
+ if (loading) return ;
+
+ if (error) return (
+ } color="red" variant="light">
+ {error}
+
+ );
+
+ const emergencies = announcements.filter(a => a.category === 'EMERGENCY');
+ const others = announcements.filter(a => a.category !== 'EMERGENCY');
+
+ return (
+
+
+
+
+
+
+ Health Center Announcements
+
+ Notices and advisories from the PHC staff
+
+
+
+
+ {announcements.length === 0 ? (
+
+
+
+
+
+ No active announcements at this time.
+
+
+ ) : (
+
+ {/* Emergency notices first */}
+ {emergencies.map(a => (
+
+ ))}
+ {others.map(a => (
+
+ ))}
+
+ )}
+
+ );
+}
+
+function AnnouncementCard({ announcement: a }) {
+ const meta = CATEGORY_META[a.category] || { color: 'gray', label: a.category };
+ const daysLeft = daysUntilExpiry(a.expires_at);
+ const isUrgent = a.category === 'EMERGENCY' || a.priority >= 8;
+
+ return (
+
+
+
+
+ {isUrgent && (
+
+
+
+ )}
+ {a.title}
+
+
+ {meta.label}
+
+
+
+
+ {a.content}
+
+
+
+
+
+
+ {new Date(a.created_at).toLocaleDateString('en-IN', {
+ day: 'numeric', month: 'short', year: 'numeric',
+ })}
+
+
+ Posted by: {a.created_by_name}
+ {a.expires_at && (
+
+ {daysLeft === 0
+ ? 'Expires today'
+ : daysLeft === 1
+ ? 'Expires tomorrow'
+ : `Expires in ${daysLeft} days`}
+
+ )}
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/AttendanceForm.jsx b/src/Modules/HealthCenter/components/AttendanceForm.jsx
new file mode 100644
index 000000000..db0e1dbf1
--- /dev/null
+++ b/src/Modules/HealthCenter/components/AttendanceForm.jsx
@@ -0,0 +1,519 @@
+/**
+ * Attendance Form Component
+ * ========================
+ * Mark doctor attendance (present/absent/on-break)
+ * Track doctor status throughout the day
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Select,
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ Badge,
+ ActionIcon,
+ Textarea,
+ Grid,
+ Paper,
+ ThemeIcon,
+ Box,
+ Center,
+ SimpleGrid,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import { IconPlus, IconEdit, IconTrash, IconCalendar, IconChevronLeft, IconChevronRight } from '@tabler/icons-react';
+import * as api from '../api';
+
+const statusOptions = [
+ { value: 'SCHEDULED', label: 'Scheduled' },
+ { value: 'AVAILABLE', label: 'Available' },
+ { value: 'ON_BREAK', label: 'On Break' },
+ { value: 'DEPARTED', label: 'Departed' },
+];
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+const generateCalendarDays = (date) => {
+ const year = date.getFullYear();
+ const month = date.getMonth();
+
+ // Get the first day of the month
+ const firstDay = new Date(year, month, 1);
+ const lastDay = new Date(year, month + 1, 0);
+
+ // Get the day of week for first day (0 = Sunday)
+ const startingDayOfWeek = firstDay.getDay();
+
+ // Create array of days
+ const days = [];
+
+ // Add previous month's days
+ for (let i = startingDayOfWeek - 1; i >= 0; i--) {
+ days.push(new Date(year, month, -i));
+ }
+
+ // Add current month's days
+ for (let i = 1; i <= lastDay.getDate(); i++) {
+ days.push(new Date(year, month, i));
+ }
+
+ // Add next month's days to fill the grid
+ const remainingDays = 42 - days.length; // 6 weeks * 7 days
+ for (let i = 1; i <= remainingDays; i++) {
+ days.push(new Date(year, month + 1, i));
+ }
+
+ return days;
+};
+
+export default function AttendanceForm() {
+ const [attendances, setAttendances] = useState([]);
+ const [doctors, setDoctors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [editingId, setEditingId] = useState(null);
+ const [selectedDate, setSelectedDate] = useState(new Date());
+ const [formData, setFormData] = useState({
+ doctor_id: '',
+ attendance_date: new Date().toISOString().split('T')[0],
+ status: '',
+ notes: '',
+ });
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ fetchData();
+ }, [selectedDate]);
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const dateStr = selectedDate.toISOString().split('T')[0];
+ const [attendRes, docRes] = await Promise.all([
+ api.getDoctorAttendance(dateStr),
+ api.getDoctors(),
+ ]);
+ setAttendances(normalizeArray(attendRes.data));
+ setDoctors(normalizeArray(docRes.data));
+ } catch (error) {
+
+ notifications.show({ message: 'Failed to load attendance', color: 'red' });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!formData.doctor_id) newErrors.doctor_id = 'Doctor is required';
+ if (!formData.status) newErrors.status = 'Status is required';
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ const payload = {
+ doctor: parseInt(formData.doctor_id),
+ attendance_date: formData.attendance_date,
+ status: formData.status,
+ notes: formData.notes,
+ };
+
+ if (editingId) {
+ await api.updateDoctorAttendanceRecord(editingId, payload);
+ notifications.show({
+ message: 'Attendance updated successfully',
+ color: 'green',
+ });
+ } else {
+ await api.createDoctorAttendance(payload);
+ notifications.show({
+ message: 'Attendance recorded successfully',
+ color: 'green',
+ });
+ }
+ setModalOpened(false);
+ resetForm();
+ await fetchData();
+ } catch (error) {
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to save attendance',
+ color: 'red',
+ });
+ }
+ };
+
+ const handleEdit = (attendance) => {
+ setEditingId(attendance.id);
+ setFormData({
+ doctor_id: attendance.doctor.toString(),
+ attendance_date: attendance.attendance_date,
+ status: attendance.status,
+ notes: attendance.notes || '',
+ });
+ setModalOpened(true);
+ };
+
+ const handleDelete = (id) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deletion',
+ children: 'Delete this attendance record?',
+ labels: { confirm: 'Delete', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteDoctorAttendance(id);
+ notifications.show({
+ message: 'Attendance record deleted',
+ color: 'green',
+ });
+ await fetchData();
+ } catch (error) {
+ notifications.show({ message: 'Failed to delete attendance', color: 'red' });
+ }
+ },
+ });
+ };
+
+ const resetForm = () => {
+ setFormData({
+ doctor_id: '',
+ attendance_date: new Date().toISOString().split('T')[0],
+ status: '',
+ notes: '',
+ });
+ setErrors({});
+ setEditingId(null);
+ };
+
+ const getDoctorName = (doctorId) => {
+ const doctor = doctors.find((d) => d.id === doctorId);
+ if (!doctor) return 'Unknown';
+ return `Dr. ${doctor.doctor_name}${doctor.specialization ? ` (${doctor.specialization})` : ''}`;
+ };
+
+ const getStatusColor = (status) => {
+ const colors = {
+ SCHEDULED: 'blue',
+ AVAILABLE: 'green',
+ ON_BREAK: 'yellow',
+ DEPARTED: 'red',
+ };
+ return colors[status] || 'gray';
+ };
+
+ return (
+
+
+
+
+ Doctor Attendance
+
+
+ Date: {selectedDate.toDateString()}
+
+
+ }
+ onClick={() => {
+ resetForm();
+ setModalOpened(true);
+ }}
+ >
+ Record Attendance
+
+
+
+
+
+
+
+
+
+
+
+
+ Select Date
+
+
+ Choose a date to view attendance records
+
+
+
+
+
+ {selectedDate.toLocaleDateString('en-US', {
+ weekday: 'short',
+ month: 'short',
+ day: 'numeric',
+ year: 'numeric',
+ })}
+
+
+
+
+
+
+
+ {/* Month Navigation */}
+
+
+ {selectedDate.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })}
+
+
+ {
+ const newDate = new Date(selectedDate);
+ newDate.setMonth(newDate.getMonth() - 1);
+ setSelectedDate(newDate);
+ }}
+ disabled={new Date(selectedDate.getFullYear(), selectedDate.getMonth() - 1, 1) > new Date()}
+ >
+
+
+ {
+ const newDate = new Date(selectedDate);
+ newDate.setMonth(newDate.getMonth() + 1);
+ if (newDate <= new Date()) {
+ setSelectedDate(newDate);
+ }
+ }}
+ disabled={new Date(selectedDate.getFullYear(), selectedDate.getMonth() + 1, 1) > new Date()}
+ >
+
+
+
+
+
+ {/* Weekday Headers */}
+ div': {
+ textAlign: 'center',
+ },
+ }}
+ >
+ {['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'].map((day) => (
+
+ {day}
+
+ ))}
+
+
+ {/* Calendar Days Grid */}
+
+ {generateCalendarDays(selectedDate).map((day, idx) => {
+ const isCurrentMonth = day.getMonth() === selectedDate.getMonth();
+ const isToday =
+ day.toDateString() === new Date().toDateString();
+ const isSelected =
+ day.toDateString() === selectedDate.toDateString();
+ const isFuture = day > new Date();
+
+ return (
+ {
+ if (isCurrentMonth && !isFuture) {
+ setSelectedDate(day);
+ }
+ }}
+ >
+ ({
+ width: '100%',
+ height: '45px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: isSelected
+ ? theme.colors.blue[6]
+ : isToday
+ ? theme.colors.orange[0]
+ : 'transparent',
+ color: isSelected ? 'white' : isCurrentMonth ? 'inherit' : theme.colors.gray[4],
+ fontWeight: isSelected || isToday ? 700 : 500,
+ border:
+ isToday && !isSelected
+ ? `2px solid ${theme.colors.orange[5]}`
+ : isSelected
+ ? `2px solid ${theme.colors.blue[8]}`
+ : '2px solid transparent',
+ transition: 'all 0.2s ease',
+ '&:hover': {
+ backgroundColor: isCurrentMonth && !isFuture
+ ? isSelected ? undefined : theme.colors.gray[1]
+ : 'transparent',
+ transform: isCurrentMonth && !isFuture ? 'scale(1.05)' : 'scale(1)',
+ },
+ })}
+ >
+
+ {day.getDate()}
+
+
+
+ {/* Badge for Today or Selected */}
+ {isToday && !isSelected && (
+
+ Today
+
+ )}
+ {isSelected && (
+
+ Selected
+
+ )}
+
+ );
+ })}
+
+
+
+
+
+
+ {attendances.length === 0 ? (
+
+ No attendance records for this date
+
+ ) : (
+
+
+
+ Doctor
+ Status
+ Notes
+ Recorded At
+ Actions
+
+
+
+ {attendances.map((attendance) => (
+
+ {getDoctorName(attendance.doctor)}
+
+
+ {attendance.status}
+
+
+ {attendance.notes || '-'}
+
+ {new Date(attendance.marked_at).toLocaleTimeString()}
+
+
+
+ handleEdit(attendance)}
+ >
+
+
+ handleDelete(attendance.id)}
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title={editingId ? 'Edit Attendance' : 'Record Attendance'}
+ size="md"
+ >
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ClaimsTab.jsx b/src/Modules/HealthCenter/components/ClaimsTab.jsx
new file mode 100644
index 000000000..7c556de39
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ClaimsTab.jsx
@@ -0,0 +1,36 @@
+import React from 'react';
+import { Stack, Card, Group, Button, Text } from '@mantine/core';
+
+export default function ClaimsTab({ claims }) {
+ return (
+ <>
+ {Array.isArray(claims) && claims.length > 0 ? (
+
+ {claims.map((claim) => (
+
+
+
+ Claim #{claim.id}
+
+ Amount: ₹{claim.claim_amount} | Status: {claim.status}
+
+
+
+
+
+ ))}
+
+ ) : (
+ No pending claims
+ )}
+ >
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ComplaintForm.jsx b/src/Modules/HealthCenter/components/ComplaintForm.jsx
new file mode 100644
index 000000000..4fa80ffa8
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ComplaintForm.jsx
@@ -0,0 +1,194 @@
+/**
+ * Complaint Form Component
+ * ========================
+ * Submit new complaint form for patients
+ * File grievances, medical issues, or facility concerns
+ *
+ * PHC-UC-03: File Complaint
+ */
+
+import { useState } from 'react';
+import {
+ Card,
+ Stack,
+ TextInput,
+ Textarea,
+ Button,
+ Group,
+ Alert,
+ Title,
+ Text,
+ Select,
+} from '@mantine/core';
+import { IconAlertCircle, IconCheck } from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import * as api from '../api';
+
+const COMPLAINT_CATEGORIES = [
+ { value: 'medical', label: 'Medical Care' },
+ { value: 'facility', label: 'Facility Issue' },
+ { value: 'staff', label: 'Staff Conduct' },
+ { value: 'hygiene', label: 'Hygiene/Cleanliness' },
+ { value: 'billing', label: 'Billing/Charges' },
+ { value: 'other', label: 'Other' },
+];
+
+const PRIORITY_LEVELS = [
+ { value: 'LOW', label: 'Low' },
+ { value: 'MEDIUM', label: 'Medium' },
+ { value: 'HIGH', label: 'High' },
+ { value: 'URGENT', label: 'Urgent' },
+];
+
+export default function ComplaintForm() {
+ const [formData, setFormData] = useState({
+ title: '',
+ category: 'other',
+ description: '',
+ priority: 'MEDIUM',
+ });
+ const [loading, setLoading] = useState(false);
+ const [submitted, setSubmitted] = useState(false);
+
+ const handleChange = (field, value) => {
+ setFormData((prev) => ({
+ ...prev,
+ [field]: value,
+ }));
+ };
+
+ const handleSubmit = async () => {
+ // Validation
+ if (!formData.title.trim()) {
+ notifications.show({
+ message: 'Please enter a title for your complaint',
+ color: 'orange',
+ });
+ return;
+ }
+
+ if (!formData.description.trim()) {
+ notifications.show({
+ message: 'Please describe your complaint',
+ color: 'orange',
+ });
+ return;
+ }
+
+ try {
+ setLoading(true);
+ await api.createComplaint({
+ title: formData.title,
+ category: formData.category,
+ description: formData.description,
+ priority: formData.priority,
+ });
+
+ notifications.show({
+ message: 'Complaint submitted successfully',
+ color: 'green',
+ icon: ,
+ });
+
+ // Reset form and show success message
+ setFormData({
+ title: '',
+ category: 'other',
+ description: '',
+ priority: 'MEDIUM',
+ });
+ setSubmitted(true);
+ setTimeout(() => setSubmitted(false), 5000);
+ } catch (error) {
+
+ notifications.show({
+ message:
+ error.response?.data?.message ||
+ 'Failed to submit complaint. Please try again.',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
File a Complaint
+
+ We value your feedback. Please share any concerns or issues you've
+ experienced.
+
+
+
+ {submitted && (
+ } title="Complaint Submitted">
+ Your complaint has been recorded. We will review it shortly and get
+ back to you.
+
+ )}
+
+ handleChange('title', e.currentTarget.value)}
+ required
+ disabled={loading}
+ />
+
+ handleChange('category', value)}
+ data={COMPLAINT_CATEGORIES}
+ required
+ disabled={loading}
+ />
+
+ handleChange('priority', value)}
+ data={PRIORITY_LEVELS}
+ required
+ disabled={loading}
+ />
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ComplaintHistory.jsx b/src/Modules/HealthCenter/components/ComplaintHistory.jsx
new file mode 100644
index 000000000..b924705d8
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ComplaintHistory.jsx
@@ -0,0 +1,316 @@
+/**
+ * Complaint History Component
+ * ============================
+ * View own complaints with status tracking
+ * Shows complaint timeline and compounder responses
+ *
+ * PHC-UC-03: Track Complaint Status
+ */
+
+import { useEffect, useState } from 'react';
+import {
+ Card,
+ Stack,
+ Title,
+ Text,
+ Badge,
+ Loader,
+ Group,
+ Alert,
+ Paper,
+ Timeline,
+ ThemeIcon,
+ Button,
+ Modal,
+ SimpleGrid,
+} from '@mantine/core';
+import {
+ IconAlertCircle,
+ IconCheck,
+ IconClock,
+ IconMessageCircle,
+} from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import * as api from '../api';
+
+export default function ComplaintHistory() {
+ const [complaints, setComplaints] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [expandedId, setExpandedId] = useState(null);
+ const [responseModal, setResponseModal] = useState(false);
+ const [selectedComplaint, setSelectedComplaint] = useState(null);
+
+ useEffect(() => {
+ fetchComplaints();
+ }, []);
+
+ const fetchComplaints = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getComplaints();
+ const data = Array.isArray(response.data)
+ ? response.data
+ : response.data?.results || [];
+ setComplaints(data);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load complaints',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getStatusColor = (status) => {
+ switch (status?.toLowerCase()) {
+ case 'resolved':
+ return 'green';
+ case 'in_progress':
+ return 'blue';
+ case 'pending':
+ return 'yellow';
+ case 'rejected':
+ return 'red';
+ default:
+ return 'gray';
+ }
+ };
+
+ const getStatusIcon = (status) => {
+ switch (status?.toLowerCase()) {
+ case 'resolved':
+ return ;
+ case 'in_progress':
+ return ;
+ case 'pending':
+ return ;
+ case 'rejected':
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const getPriorityColor = (priority) => {
+ switch (priority?.toUpperCase()) {
+ case 'URGENT':
+ return 'red';
+ case 'HIGH':
+ return 'orange';
+ case 'MEDIUM':
+ return 'yellow';
+ case 'LOW':
+ return 'green';
+ default:
+ return 'gray';
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (complaints.length === 0) {
+ return (
+ }>
+ You haven't filed any complaints yet.
+
+ );
+ }
+
+ return (
+
+
+
+ Total Complaints: {complaints.length}
+
+
+
+ {complaints.map((complaint) => (
+
+ setExpandedId(expandedId === complaint.id ? null : complaint.id)
+ }
+ >
+
+ {/* Complaint Header */}
+
+
+
+ {complaint.title}
+
+ {complaint.priority || 'MEDIUM'}
+
+
+
+ Category: {complaint.category || 'N/A'} • Filed:{' '}
+ {new Date(complaint.created_at).toLocaleDateString()}
+
+
+
+ {complaint.status?.replace('_', ' ').toUpperCase() || 'PENDING'}
+
+
+
+ {/* Complaint Details - Expandable */}
+ {expandedId === complaint.id && (
+ <>
+
+
+ Description
+
+ {complaint.description}
+
+
+ {complaint.response_note && (
+
+
+
+
+ Compounder Response
+
+
+ {complaint.response_note}
+ {complaint.responded_at && (
+
+ Responded on:{' '}
+ {new Date(complaint.responded_at).toLocaleDateString()}
+
+ )}
+
+ )}
+
+ {complaint.status?.toLowerCase() === 'resolved' && (
+ } title="Resolved">
+ This complaint has been successfully resolved.
+
+ )}
+
+ {complaint.status?.toLowerCase() === 'pending' && (
+ } title="Pending">
+ Your complaint is being reviewed. We will respond shortly.
+
+ )}
+
+
+ {complaint.status?.toLowerCase() === 'resolved' && (
+
+ )}
+
+ >
+ )}
+
+ {/* Summary Row */}
+
+ Click to {expandedId === complaint.id ? 'collapse' : 'expand'}{' '}
+ details
+
+
+
+ ))}
+
+ {/* Detail Modal */}
+ {
+ setResponseModal(false);
+ setSelectedComplaint(null);
+ }}
+ title="Complaint Details"
+ size="lg"
+ >
+ {selectedComplaint && (
+
+
+
+ STATUS
+
+
+ {selectedComplaint.status?.replace('_', ' ').toUpperCase()}
+
+
+
+
+
+
+ CATEGORY
+
+ {selectedComplaint.category || 'N/A'}
+
+
+
+ PRIORITY
+
+
+ {selectedComplaint.priority || 'MEDIUM'}
+
+
+
+
+ FILED ON
+
+
+ {new Date(selectedComplaint.created_at).toLocaleDateString()}
+
+
+ {selectedComplaint.responded_at && (
+
+
+ RESOLVED ON
+
+
+ {new Date(
+ selectedComplaint.responded_at
+ ).toLocaleDateString()}
+
+
+ )}
+
+
+
+
+ DESCRIPTION
+
+ {selectedComplaint.description}
+
+
+ {selectedComplaint.response_note && (
+
+
+ COMPOUNDER RESPONSE
+
+ {selectedComplaint.response_note}
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ComplaintResponse.jsx b/src/Modules/HealthCenter/components/ComplaintResponse.jsx
new file mode 100644
index 000000000..d32a5e2b0
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ComplaintResponse.jsx
@@ -0,0 +1,271 @@
+/**
+ * Complaint Response Component
+ * ===========================
+ * Compounder responds to patient complaints
+ * View and manage complaint status updates
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ Text,
+ Modal,
+ Badge,
+ ActionIcon,
+ Textarea,
+ Paper,
+ Select,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { IconMessageReply, IconCheck } from '@tabler/icons-react';
+import * as api from '../api';
+
+const statusOptions = [
+ { value: 'PENDING', label: 'Pending' },
+ { value: 'IN_PROGRESS', label: 'In Progress' },
+ { value: 'RESOLVED', label: 'Resolved' },
+ { value: 'REJECTED', label: 'Rejected' },
+];
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function ComplaintResponse() {
+ const [complaints, setComplaints] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [selectedComplaint, setSelectedComplaint] = useState(null);
+ const [responseData, setResponseData] = useState({
+ status: '',
+ resolution_notes: '',
+ });
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ fetchComplaints();
+ }, []);
+
+ const fetchComplaints = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getComplaints();
+ setComplaints(Array.isArray(response.data) ? response.data : []);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load complaints',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!responseData.status) newErrors.status = 'Status is required';
+ if (responseData.status === 'RESOLVED' && !responseData.resolution_notes.trim()) {
+ newErrors.resolution_notes = 'Resolution notes required when closing complaint';
+ }
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ const payload = {
+ status: responseData.status,
+ resolution_notes: responseData.resolution_notes,
+ };
+
+ await api.respondToComplaint(selectedComplaint.id, payload);
+ notifications.show({
+ message: 'Complaint response recorded successfully',
+ color: 'green',
+ });
+ setModalOpened(false);
+ resetForm();
+ await fetchComplaints();
+ } catch (error) {
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to respond',
+ color: 'red',
+ });
+ }
+ };
+
+ const resetForm = () => {
+ setResponseData({ status: '', resolution_notes: '' });
+ setErrors({});
+ setSelectedComplaint(null);
+ };
+
+ const handleRespond = (complaint) => {
+ setSelectedComplaint(complaint);
+ setResponseData({
+ status: complaint.status,
+ resolution_notes: complaint.resolution_notes || '',
+ });
+ setModalOpened(true);
+ };
+
+ const getStatusColor = (status) => {
+ const colors = {
+ PENDING: 'yellow',
+ IN_PROGRESS: 'blue',
+ RESOLVED: 'green',
+ REJECTED: 'red',
+ };
+ return colors[status] || 'gray';
+ };
+
+ const getCategoryColor = (category) => {
+ const colors = {
+ 'Medical Care': 'red',
+ 'Facility': 'blue',
+ 'Staff': 'purple',
+ 'Hygiene': 'orange',
+ 'Billing': 'green',
+ 'Other': 'gray',
+ };
+ return colors[category] || 'gray';
+ };
+
+ return (
+
+
+ Complaint Management
+
+
+
+ {complaints.length === 0 ? (
+
+ No complaints
+
+ ) : (
+
+ {complaints.map((complaint) => (
+
+
+
+ {complaint.title}
+
+ From: {complaint.patient_name}
+
+
+
+
+ {complaint.category}
+
+
+ {complaint.status}
+
+
+
+
+
+ {complaint.description}
+
+
+ {complaint.resolution_notes && (
+
+
+ Resolution Notes:
+
+ {complaint.resolution_notes}
+
+ )}
+
+
+
+ Submitted: {new Date(complaint.created_at).toLocaleString()}
+
+ }
+ onClick={() => handleRespond(complaint)}
+ >
+ Respond
+
+
+
+ ))}
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title={`Update Complaint: ${selectedComplaint?.title}`}
+ size="md"
+ >
+
+
+
+ Complaint Details
+
+ {selectedComplaint?.description}
+
+
+
+ setResponseData({ ...responseData, status: value })
+ }
+ error={errors.status}
+ />
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ConsultationForm.jsx b/src/Modules/HealthCenter/components/ConsultationForm.jsx
new file mode 100644
index 000000000..cd550ca7d
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ConsultationForm.jsx
@@ -0,0 +1,809 @@
+/**
+ * Consultation Form Component
+ * ============================
+ * Create new consultations for patients with doctors
+ * Record chief complaints, vitals, and clinical findings
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ Badge,
+ ActionIcon,
+ Textarea,
+ NumberInput,
+ Paper,
+ Select,
+ SimpleGrid,
+ TextInput,
+ Loader,
+ Center,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import { IconPlus, IconCheck, IconTrash, IconPill, IconEye } from '@tabler/icons-react';
+import * as api from '../api';
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function ConsultationForm({ onCreatePrescription }) {
+ const [consultations, setConsultations] = useState([]);
+ const [users, setUsers] = useState([]);
+ const [doctors, setDoctors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [consultationsLoading, setConsultationsLoading] = useState(false);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [detailsModalOpened, setDetailsModalOpened] = useState(false);
+ const [selectedConsultation, setSelectedConsultation] = useState(null);
+ const [formData, setFormData] = useState({
+ user_id: '',
+ doctor_id: '',
+ chief_complaint: '',
+ history_of_present_illness: '',
+ examination_findings: '',
+ provisional_diagnosis: '',
+ final_diagnosis: '',
+ treatment_plan: '',
+ advice: '',
+ blood_pressure_systolic: '',
+ blood_pressure_diastolic: '',
+ pulse_rate: '',
+ temperature: '',
+ oxygen_saturation: '',
+ weight: '',
+ follow_up_date: '',
+ ambulance_requested: 'no',
+ });
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ fetchInitialData();
+ }, []);
+
+ const fetchInitialData = async () => {
+ try {
+ setLoading(true);
+ const [doctorsRes, usersRes, consultationsRes] = await Promise.all([
+ api.getDoctorsFiltered({ active_only: true }),
+ api.getUsers(),
+ api.getConsultationsList(),
+ ]);
+
+ // Load doctors
+ const doctorList = Array.isArray(doctorsRes.data) ? doctorsRes.data : [];
+ const doctorOptions = doctorList.map((doc) => ({
+ value: doc.id.toString(),
+ label: `Dr. ${doc.doctor_name} - ${doc.specialization || 'General'}`,
+ ...doc,
+ }));
+ setDoctors(doctorOptions);
+
+ // Load users
+ const userList = normalizeArray(usersRes.data);
+ setUsers(
+ userList.map((user) => ({
+ value: user.value || user.id.toString(),
+ label: user.label || `${user.username} - ${user.full_name}`,
+ ...user,
+ }))
+ );
+
+ // Load consultations
+ const consultationList = normalizeArray(consultationsRes.data);
+ setConsultations(consultationList);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load data: ' + (error?.response?.data?.detail || error.message),
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ if (!formData.user_id) newErrors.user_id = 'Patient (User) is required';
+ if (!formData.doctor_id) newErrors.doctor_id = 'Doctor is required';
+ if (!formData.chief_complaint) newErrors.chief_complaint = 'Chief complaint is required';
+
+ // Validate vitals if provided
+ if (formData.blood_pressure_systolic && (formData.blood_pressure_systolic < 0 || formData.blood_pressure_systolic > 300)) {
+ newErrors.blood_pressure_systolic = 'Systolic BP should be 0-300';
+ }
+ if (formData.blood_pressure_diastolic && (formData.blood_pressure_diastolic < 0 || formData.blood_pressure_diastolic > 300)) {
+ newErrors.blood_pressure_diastolic = 'Diastolic BP should be 0-300';
+ }
+ if (formData.pulse_rate && (formData.pulse_rate < 0 || formData.pulse_rate > 300)) {
+ newErrors.pulse_rate = 'Pulse rate should be 0-300';
+ }
+ if (formData.temperature && (formData.temperature < 30 || formData.temperature > 45)) {
+ newErrors.temperature = 'Temperature should be 30-45°C';
+ }
+ if (formData.oxygen_saturation && (formData.oxygen_saturation < 0 || formData.oxygen_saturation > 100)) {
+ newErrors.oxygen_saturation = 'O2 saturation should be 0-100%';
+ }
+ if (formData.weight && (formData.weight < 0 || formData.weight > 300)) {
+ newErrors.weight = 'Weight should be 0-300 kg';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ const payload = {
+ user_id: parseInt(formData.user_id),
+ doctor_id: parseInt(formData.doctor_id),
+ chief_complaint: formData.chief_complaint,
+ history_of_present_illness: formData.history_of_present_illness,
+ examination_findings: formData.examination_findings,
+ provisional_diagnosis: formData.provisional_diagnosis,
+ final_diagnosis: formData.final_diagnosis,
+ treatment_plan: formData.treatment_plan,
+ advice: formData.advice,
+ blood_pressure_systolic: formData.blood_pressure_systolic ? parseInt(formData.blood_pressure_systolic) : null,
+ blood_pressure_diastolic: formData.blood_pressure_diastolic ? parseInt(formData.blood_pressure_diastolic) : null,
+ pulse_rate: formData.pulse_rate ? parseInt(formData.pulse_rate) : null,
+ temperature: formData.temperature ? parseFloat(formData.temperature) : null,
+ oxygen_saturation: formData.oxygen_saturation ? parseInt(formData.oxygen_saturation) : null,
+ weight: formData.weight ? parseFloat(formData.weight) : null,
+ follow_up_date: formData.follow_up_date || null,
+ ambulance_requested: formData.ambulance_requested,
+ };
+
+ // Remove null values
+ Object.keys(payload).forEach(key => {
+ if (payload[key] === null || payload[key] === '') {
+ delete payload[key];
+ }
+ });
+
+ await api.createConsultation(payload);
+ notifications.show({
+ message: 'Consultation created successfully',
+ color: 'green',
+ });
+ resetForm();
+ setModalOpened(false);
+ await fetchInitialData();
+ } catch (error) {
+
+
+
+ const errorData = error.response?.data;
+ let errorMessage = 'Failed to create consultation';
+
+ if (errorData) {
+ errorMessage =
+ errorData.detail ||
+ errorData.error ||
+ JSON.stringify(errorData) ||
+ error.message ||
+ 'Failed to create consultation';
+ }
+
+ notifications.show({
+ message: errorMessage,
+ color: 'red',
+ });
+ }
+ };
+
+ const resetForm = () => {
+ setFormData({
+ user_id: '',
+ doctor_id: '',
+ chief_complaint: '',
+ history_of_present_illness: '',
+ examination_findings: '',
+ provisional_diagnosis: '',
+ final_diagnosis: '',
+ treatment_plan: '',
+ advice: '',
+ blood_pressure_systolic: '',
+ blood_pressure_diastolic: '',
+ pulse_rate: '',
+ temperature: '',
+ oxygen_saturation: '',
+ weight: '',
+ follow_up_date: '',
+ ambulance_requested: 'no',
+ });
+ setErrors({});
+ };
+
+ const handleDeleteConsultation = (consultationId) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deletion',
+ children: 'Are you sure you want to delete this consultation?',
+ labels: { confirm: 'Delete', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteConsultation(consultationId);
+ notifications.show({
+ message: 'Consultation deleted successfully',
+ color: 'green',
+ });
+ setConsultations(consultations.filter(c => c.id !== consultationId));
+ } catch (error) {
+ notifications.show({
+ message: 'Failed to delete consultation: ' + (error?.response?.data?.detail || error.message),
+ color: 'red',
+ });
+ }
+ }
+ });
+ };
+
+ const handleCreatePrescription = (consultation) => {
+ if (onCreatePrescription) {
+ onCreatePrescription(consultation);
+ }
+ };
+
+ const handleViewDetails = (consultation) => {
+ setSelectedConsultation(consultation);
+ setDetailsModalOpened(true);
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Create New Consultation
+
+ }
+ onClick={() => setModalOpened(true)}
+ >
+ New Consultation
+
+
+
+ {/* Consultations Table */}
+
+
+ Recent Consultations
+ {consultations.length === 0 ? (
+
+ No consultations yet
+
+ ) : (
+
+
+
+
+ Patient
+ Doctor
+ Chief Complaint
+ Date
+ Actions
+
+
+
+ {consultations.map((consultation) => (
+
+
+ {consultation.patient_username || 'N/A'}
+
+
+ {consultation.doctor_name || 'N/A'}
+
+
+
+ {consultation.chief_complaint || 'N/A'}
+
+
+
+
+ {consultation.consultation_date
+ ? new Date(consultation.consultation_date).toLocaleDateString()
+ : 'N/A'}
+
+
+
+
+ handleViewDetails(consultation)}
+ >
+
+
+ handleCreatePrescription(consultation)}
+ >
+
+
+ handleDeleteConsultation(consultation.id)}
+ >
+
+
+
+
+
+ ))}
+
+
+
+ )}
+
+
+
+ {/* Consultation Details Modal */}
+ {
+ setSelectedConsultation(null);
+ setDetailsModalOpened(false);
+ }}
+ title="Consultation Details"
+ size="xl"
+ scrollAreaComponent={Paper}
+ >
+ {selectedConsultation && (
+
+ {/* Header: Patient, Doctor, Date */}
+
+
+
+ Patient
+ {selectedConsultation.patient_username || 'N/A'}
+
+
+ Doctor
+ {selectedConsultation.doctor_name || 'N/A'}
+
+
+ Consultation Date
+
+ {selectedConsultation.consultation_date
+ ? new Date(selectedConsultation.consultation_date).toLocaleDateString()
+ : 'N/A'}
+
+
+
+
+
+ {/* Section 1: Chief Complaint (Always show) */}
+
+ Chief Complaint *
+
+ {selectedConsultation.chief_complaint || 'N/A'}
+
+
+
+ {/* Section 2: Clinical Information */}
+
+
Clinical Information
+
+
+ History of Present Illness
+
+ {selectedConsultation.history_of_present_illness || 'N/A'}
+
+
+
+ Examination Findings
+
+ {selectedConsultation.examination_findings || 'N/A'}
+
+
+
+
+
+ {/* Section 3: Vitals */}
+
+
Vitals
+
+
+
+ Blood Pressure (Systolic)
+
+ {selectedConsultation.blood_pressure_systolic || 'N/A'} mmHg
+
+
+
+ Blood Pressure (Diastolic)
+
+ {selectedConsultation.blood_pressure_diastolic || 'N/A'} mmHg
+
+
+
+ Pulse Rate
+
+ {selectedConsultation.pulse_rate || 'N/A'} bpm
+
+
+
+ Temperature
+
+ {selectedConsultation.temperature || 'N/A'}°C
+
+
+
+ O2 Saturation
+
+ {selectedConsultation.oxygen_saturation || 'N/A'}%
+
+
+
+ Weight
+
+ {selectedConsultation.weight || 'N/A'} kg
+
+
+
+
+
+
+ {/* Section 4: Diagnosis */}
+
+
Diagnosis
+
+
+ Provisional Diagnosis
+
+ {selectedConsultation.provisional_diagnosis || 'N/A'}
+
+
+
+ Final Diagnosis
+
+ {selectedConsultation.final_diagnosis || 'N/A'}
+
+
+
+
+
+ {/* Section 5: Treatment and Advice */}
+
+
Treatment & Advice
+
+
+ Treatment Plan
+
+ {selectedConsultation.treatment_plan || 'N/A'}
+
+
+
+ Advice for Patient
+
+ {selectedConsultation.advice || 'N/A'}
+
+
+
+
+
+ {/* Section 6: Follow-up */}
+
+ Follow-up
+
+ Follow-up Date
+
+ {selectedConsultation.follow_up_date
+ ? new Date(selectedConsultation.follow_up_date).toLocaleDateString()
+ : 'N/A'}
+
+
+
+
+ {/* Section 7: Ambulance Requested */}
+
+ Ambulance
+
+ Ambulance Requested
+
+ {selectedConsultation.ambulance_requested === 'yes' ||
+ selectedConsultation.ambulance_requested === true
+ ? 'Yes'
+ : 'No'}
+
+
+
+
+ {/* Close Button */}
+
+
+
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title="Create Consultation"
+ size="xl"
+ scrollAreaComponent={Paper}
+ >
+
+ {/* Patient and Doctor Selection */}
+
+
+ setFormData({ ...formData, user_id: value })
+ }
+ error={errors.user_id}
+ searchable
+ />
+
+ setFormData({ ...formData, doctor_id: value })
+ }
+ error={errors.doctor_id}
+ searchable
+ />
+
+
+ {/* Chief Complaint */}
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/DoctorForm.jsx b/src/Modules/HealthCenter/components/DoctorForm.jsx
new file mode 100644
index 000000000..cde81c7c9
--- /dev/null
+++ b/src/Modules/HealthCenter/components/DoctorForm.jsx
@@ -0,0 +1,370 @@
+/**
+ * Doctor Form Component
+ * ====================
+ * Add/Edit doctor information
+ * Used in CompoundDashboard for doctor management
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Paper,
+ TextInput,
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ Badge,
+ ActionIcon,
+ Select,
+ Checkbox,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import { IconEdit, IconTrash, IconPlus, IconToggleLeft, IconToggleRight } from '@tabler/icons-react';
+import * as api from '../api';
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function DoctorForm() {
+ const [doctors, setDoctors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [editingId, setEditingId] = useState(null);
+ const [showInactive, setShowInactive] = useState(false);
+ const [formData, setFormData] = useState({
+ doctor_name: '',
+ specialization: '',
+ doctor_phone: '',
+ email: '',
+ is_active: true,
+ });
+ const [errors, setErrors] = useState({});
+
+ const specializations = [
+ 'Cardiology',
+ 'Neurology',
+ 'General Medicine',
+ 'Pediatrics',
+ 'Orthopedics',
+ 'Surgery',
+ 'Ophthalmology',
+ 'ENT',
+ 'Dermatology',
+ 'Psychiatry',
+ ];
+
+ useEffect(() => {
+ fetchDoctors();
+ }, [showInactive]);
+
+ const fetchDoctors = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getDoctors(!showInactive);
+ const doctorsArray = Array.isArray(response.data) ? response.data : [];
+ setDoctors(doctorsArray);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load doctors',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!formData.doctor_name.trim()) newErrors.doctor_name = 'Doctor name is required';
+ if (!formData.specialization) newErrors.specialization = 'Specialization is required';
+ if (!formData.doctor_phone || formData.doctor_phone.length < 10)
+ newErrors.doctor_phone = 'Valid phone number required (10+ digits)';
+ if (!formData.email || !formData.email.includes('@'))
+ newErrors.email = 'Valid email required';
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ if (editingId) {
+ await api.updateDoctor(editingId, formData);
+ notifications.show({
+ message: 'Doctor updated successfully',
+ color: 'green',
+ });
+ } else {
+ await api.createDoctor(formData);
+ notifications.show({
+ message: 'Doctor created successfully',
+ color: 'green',
+ });
+ }
+ setModalOpened(false);
+ setFormData({
+ doctor_name: '',
+ specialization: '',
+ doctor_phone: '',
+ email: '',
+ is_active: true,
+ });
+ await fetchDoctors();
+ } catch (error) {
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to save doctor',
+ color: 'red',
+ });
+ }
+ };
+
+ const handleEdit = (doctor) => {
+ setEditingId(doctor.id);
+ setFormData({
+ doctor_name: doctor.doctor_name,
+ specialization: doctor.specialization,
+ doctor_phone: doctor.doctor_phone,
+ email: doctor.email,
+ is_active: doctor.is_active !== undefined ? doctor.is_active : true,
+ });
+ setModalOpened(true);
+ };
+
+ const handleDelete = (id) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deletion',
+ children: (
+
+ Are you sure you want to delete this doctor? This action cannot be undone.
+
+ ),
+ labels: { confirm: 'Delete Doctor', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteDoctor(id);
+ notifications.show({
+ message: 'Doctor deleted successfully',
+ color: 'green',
+ });
+ await fetchDoctors();
+ } catch (error) {
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to delete doctor',
+ color: 'red',
+ });
+ }
+ },
+ });
+ };
+
+ const handleToggleStatus = async (doctor) => {
+ try {
+ const updatedData = {
+ doctor_name: doctor.doctor_name,
+ specialization: doctor.specialization,
+ doctor_phone: doctor.doctor_phone,
+ email: doctor.email,
+ is_active: !doctor.is_active,
+ };
+ await api.updateDoctor(doctor.id, updatedData);
+ notifications.show({
+ message: `Doctor ${!doctor.is_active ? 'activated' : 'deactivated'} successfully`,
+ color: 'green',
+ });
+ await fetchDoctors();
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to change doctor status',
+ color: 'red',
+ });
+ }
+ };
+
+ const handleModalClose = () => {
+ setModalOpened(false);
+ setEditingId(null);
+ setFormData({
+ doctor_name: '',
+ specialization: '',
+ doctor_phone: '',
+ email: '',
+ is_active: true,
+ });
+ setErrors({});
+ };
+
+ return (
+
+ {/* Header */}
+
+
+ Doctor Management
+
+
+
+ setShowInactive(e.currentTarget.checked)}
+ />
+
+ }
+ onClick={() => setModalOpened(true)}
+ >
+ Add Doctor
+
+
+
+
+ {/* Doctor List Table */}
+
+ {doctors.length === 0 ? (
+
+ No doctors found
+
+ ) : (
+
+
+
+ Name
+ Specialization
+ Phone
+ Email
+ Status
+ Actions
+
+
+
+ {doctors.map((doctor) => (
+
+ Dr. {doctor.doctor_name}
+ {doctor.specialization}
+ {doctor.doctor_phone}
+ {doctor.email}
+
+
+ {doctor.is_active ? 'Active' : 'Inactive'}
+
+
+
+
+ handleToggleStatus(doctor)}
+ >
+ {doctor.is_active ? : }
+
+ handleEdit(doctor)}
+ >
+
+
+ handleDelete(doctor.id)}
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/* Modal Form */}
+
+
+
+ setFormData({ ...formData, doctor_name: e.currentTarget.value })
+ }
+ error={errors.doctor_name}
+ />
+
+
+ setFormData({ ...formData, specialization: value })
+ }
+ error={errors.specialization}
+ />
+
+
+ setFormData({ ...formData, doctor_phone: e.currentTarget.value })
+ }
+ error={errors.doctor_phone}
+ />
+
+
+ setFormData({ ...formData, email: e.currentTarget.value })
+ }
+ error={errors.email}
+ />
+
+
+ setFormData({ ...formData, is_active: e.currentTarget.checked })
+ }
+ description="Check to activate doctor, uncheck to deactivate"
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/DoctorScheduleTab.jsx b/src/Modules/HealthCenter/components/DoctorScheduleTab.jsx
new file mode 100644
index 000000000..e447a8f7e
--- /dev/null
+++ b/src/Modules/HealthCenter/components/DoctorScheduleTab.jsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import { Card, Stack, Select, TextInput, Button, Title, Text, Table } from '@mantine/core';
+
+const DAYS_OF_WEEK = [
+ { value: 'MONDAY', label: 'Monday' },
+ { value: 'TUESDAY', label: 'Tuesday' },
+ { value: 'WEDNESDAY', label: 'Wednesday' },
+ { value: 'THURSDAY', label: 'Thursday' },
+ { value: 'FRIDAY', label: 'Friday' },
+ { value: 'SATURDAY', label: 'Saturday' },
+];
+
+export default function DoctorScheduleTab({
+ scheduleForm,
+ setScheduleForm,
+ doctors,
+ doctorSchedules,
+ onAddSchedule,
+ loading,
+}) {
+ return (
+
+
+
+
+
Add Doctor Schedule
+
+ Set weekly schedules for doctors
+
+
+
+ setScheduleForm({ ...scheduleForm, doctor_id: value })
+ }
+ data={
+ Array.isArray(doctors)
+ ? doctors.map((doc) => ({
+ value: doc.id.toString(),
+ label: doc.doctor_name,
+ }))
+ : []
+ }
+ required
+ />
+
+ setScheduleForm({ ...scheduleForm, day_of_week: value })
+ }
+ data={DAYS_OF_WEEK}
+ required
+ />
+
+ setScheduleForm({
+ ...scheduleForm,
+ start_time: e.currentTarget.value,
+ })
+ }
+ />
+
+ setScheduleForm({
+ ...scheduleForm,
+ end_time: e.currentTarget.value,
+ })
+ }
+ />
+
+ setScheduleForm({
+ ...scheduleForm,
+ room_number: e.currentTarget.value,
+ })
+ }
+ />
+
+
+
+
+ {/* Current Schedules */}
+ {Array.isArray(doctorSchedules) && doctorSchedules.length > 0 ? (
+
+
+ Current Schedules
+
+
+
+
+ Doctor
+ Day
+ Time
+ Room
+
+
+
+ {doctorSchedules.map((schedule) => (
+
+ {schedule.doctor_name}
+ {schedule.day_of_week}
+
+ {schedule.start_time} - {schedule.end_time}
+
+ {schedule.room_number}
+
+ ))}
+
+
+
+ ) : null}
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/DoctorSchedulesView.jsx b/src/Modules/HealthCenter/components/DoctorSchedulesView.jsx
new file mode 100644
index 000000000..1d1074d4a
--- /dev/null
+++ b/src/Modules/HealthCenter/components/DoctorSchedulesView.jsx
@@ -0,0 +1,158 @@
+/**
+ * Doctor Schedules View Component
+ * ===============================
+ * Display all doctor schedules in a read-only table format
+ * Patient-facing view without any action buttons
+ *
+ * PHC-UC-01: View Doctor Schedules
+ */
+
+import { useEffect, useState } from 'react';
+import {
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Loader,
+ Stack,
+ Group,
+ Alert,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { IconAlertCircle } from '@tabler/icons-react';
+import * as api from '../api';
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function DoctorSchedulesView() {
+ const [schedules, setSchedules] = useState([]);
+ const [doctors, setDoctors] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ // Use patient-accessible endpoint getDoctorAvailability instead of compounder endpoint
+ const response = await api.getDoctorAvailability();
+
+ const doctorsList = normalizeArray(response.data);
+
+ // Extract schedules from all doctors
+ const allSchedules = [];
+ const doctorsMap = {};
+
+ doctorsList.forEach(item => {
+ const doctorData = item.doctor || item;
+ const schedules = item.schedule || item.schedules || [];
+
+ // Store doctor info for lookup
+ doctorsMap[doctorData.id] = doctorData;
+
+ // Add each schedule with doctor info
+ schedules.forEach(schedule => {
+ allSchedules.push({
+ ...schedule,
+ doctor: doctorData.id,
+ doctor_name: doctorData.doctor_name,
+ specialization: doctorData.specialization
+ });
+ });
+ });
+
+ setSchedules(allSchedules);
+ setDoctors(Object.values(doctorsMap));
+ } catch (error) {
+
+ const errorMessage = error.response?.data?.detail || error.message || 'Failed to load schedules';
+
+
+
+ // Show warning but don't break the UI
+ notifications.show({
+ message: 'Could not load doctor schedules',
+ color: 'yellow',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getDoctorName = (schedule) => {
+ return schedule.doctor_name ? `Dr. ${schedule.doctor_name}` : 'N/A';
+ };
+
+ const getDoctorSpecialization = (schedule) => {
+ return schedule.specialization || 'N/A';
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+
+ Doctor Schedules
+
+
+ View all doctor schedules at the health center
+
+
+
+
+ {schedules.length === 0 ? (
+ }>
+ No doctor schedules are available at the moment.
+
+ ) : (
+
+
+
+
+ Doctor
+ Specialization
+ Day
+ Time
+ Room
+
+
+
+ {schedules.map((schedule) => (
+
+
+ {getDoctorName(schedule)}
+
+
+ {getDoctorSpecialization(schedule)}
+
+ {schedule.day_of_week}
+
+ {schedule.start_time} - {schedule.end_time}
+
+
+ {schedule.room_number}
+
+
+ ))}
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ErrorBoundary.jsx b/src/Modules/HealthCenter/components/ErrorBoundary.jsx
new file mode 100644
index 000000000..70e43008d
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ErrorBoundary.jsx
@@ -0,0 +1,47 @@
+import React, { Component } from 'react';
+import { Title, Text, Button, Container, Group } from '@mantine/core';
+import { IconAlertTriangle } from '@tabler/icons-react';
+
+export default class ErrorBoundary extends Component {
+ constructor(props) {
+ super(props);
+ this.state = { hasError: false, error: null };
+ }
+
+ static getDerivedStateFromError(error) {
+ return { hasError: true, error };
+ }
+
+ componentDidCatch(error, errorInfo) {
+ // We could log this to a structured error service in production
+ console.warn('React Error Boundary Caught:', error, errorInfo);
+ }
+
+ render() {
+ if (this.state.hasError) {
+ return (
+
+
+ Something went wrong in the Interface
+
+ An unexpected error occurred while rendering the dashboard. Our technical team has been notified.
+ You can try reloading the page to resolve this issue.
+
+
+
+
+
+ );
+ }
+
+ return this.props.children;
+ }
+}
diff --git a/src/Modules/HealthCenter/components/ExpiryBatchReturn.jsx b/src/Modules/HealthCenter/components/ExpiryBatchReturn.jsx
new file mode 100644
index 000000000..510b008b4
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ExpiryBatchReturn.jsx
@@ -0,0 +1,327 @@
+/**
+ * Expiry Batch Return Component
+ * ============================
+ * Mark medicine batches as expired/returned
+ * Track batch returns and expiry management
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Select,
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ Badge,
+ ActionIcon,
+ NumberInput,
+ Textarea,
+ TextInput,
+} from '@mantine/core';
+import { DatePicker } from '@mantine/dates';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import { IconPlus, IconTrash, IconCheck } from '@tabler/icons-react';
+import * as api from '../api';
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function ExpiryBatchReturn() {
+ const [batches, setBatches] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [formData, setFormData] = useState({
+ batch_id: '',
+ medicine_name: '',
+ quantity: '',
+ reason: '',
+ returned_qty: '',
+ });
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getExpiryBatches();
+ const batchList = normalizeArray(response.data);
+ setBatches(batchList);
+ } catch (error) {
+
+ notifications.show({ message: 'Failed to load data', color: 'red' });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!formData.batch_id) newErrors.batch_id = 'Batch is required';
+ if (!formData.quantity || formData.quantity <= 0)
+ newErrors.quantity = 'Quantity must be > 0';
+ if (!formData.reason || formData.reason.trim() === '')
+ newErrors.reason = 'Reason is required';
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ const payload = {
+ returned_qty: parseInt(formData.quantity),
+ return_reason: formData.reason,
+ };
+
+ await api.markBatchAsReturned(formData.batch_id, payload);
+ notifications.show({
+ message: `Batch marked as returned - Reason: ${formData.reason}`,
+ color: 'green',
+ });
+ setModalOpened(false);
+ resetForm();
+ await fetchData();
+ } catch (error) {
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to mark batch as returned',
+ color: 'red',
+ });
+ }
+ };
+
+ const handleDelete = (id) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deletion',
+ children: 'Delete this batch?',
+ labels: { confirm: 'Delete', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteBatch(id);
+ notifications.show({
+ message: 'Batch deleted',
+ color: 'green',
+ });
+ await fetchData();
+ } catch (error) {
+ notifications.show({ message: 'Failed to delete batch', color: 'red' });
+ }
+ },
+ });
+ };
+
+ const resetForm = () => {
+ setFormData({
+ batch_id: '',
+ medicine_name: '',
+ quantity: '',
+ reason: '',
+ returned_qty: '',
+ });
+ setErrors({});
+ };
+
+ const getStatusColor = (batch) => {
+ if (batch.is_returned) return 'red';
+ const today = new Date().toISOString().split('T')[0];
+ if (batch.expiry_date <= today) return 'orange';
+ return 'blue';
+ };
+
+ const getStatusBadge = (batch) => {
+ if (batch.is_returned) return 'Returned';
+ const today = new Date().toISOString().split('T')[0];
+ if (batch.expiry_date <= today) return 'Expired';
+ return 'Active';
+ };
+
+ return (
+
+
+
+ Expiry & Batch Returns
+
+ }
+ onClick={() => {
+ resetForm();
+ setModalOpened(true);
+ }}
+ >
+ Mark as Returned
+
+
+
+
+ {batches.length === 0 ? (
+
+ No batches found
+
+ ) : (
+
+
+
+ Medicine
+ Batch No
+ Quantity
+ Expiry Date
+ Status
+ Reason
+ Actions
+
+
+
+ {batches.map((batch) => {
+ const medicine = batch.stock?.medicine_detail || {};
+ return (
+
+ {medicine.medicine_name || 'N/A'}
+ {batch.batch_no || '-'}
+ {batch.qty}
+
+ {new Date(batch.expiry_date).toLocaleDateString()}
+
+
+
+ {getStatusBadge(batch)}
+
+
+ {batch.return_reason || '-'}
+
+
+ {!batch.is_returned && (
+ {
+ const medicineDetail = batch.stock?.medicine_detail || {};
+ setFormData({
+ batch_id: batch.id.toString(),
+ medicine_name: medicineDetail.medicine_name || '',
+ quantity: batch.qty.toString(),
+ reason: '',
+ returned_qty: batch.qty.toString(),
+ });
+ setModalOpened(true);
+ }}
+ >
+
+
+ )}
+ handleDelete(batch.id)}
+ >
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title="Mark Batch as Returned"
+ size="md"
+ >
+
+ !b.is_returned)
+ .map((batch) => {
+ const medicine = batch.stock?.medicine_detail || {};
+ return {
+ value: batch.id.toString(),
+ label: `${medicine.medicine_name} - Batch ${batch.batch_no} (Exp: ${new Date(batch.expiry_date).toLocaleDateString()})`,
+ };
+ })}
+ value={formData.batch_id}
+ onChange={(value) => {
+ const batch = batches.find((b) => b.id.toString() === value);
+ const medicineDetail = batch?.stock?.medicine_detail || {};
+ setFormData({
+ batch_id: value || '',
+ medicine_name: medicineDetail.medicine_name || '',
+ quantity: batch ? batch.qty.toString() : '',
+ reason: '',
+ returned_qty: batch ? batch.qty.toString() : '',
+ });
+ }}
+ error={errors.batch_id}
+ searchable
+ />
+
+
+
+
+ setFormData({ ...formData, quantity: value === undefined ? '' : String(value) })
+ }
+ error={errors.quantity}
+ min={1}
+ />
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/HospitalAdmissionForm.jsx b/src/Modules/HealthCenter/components/HospitalAdmissionForm.jsx
new file mode 100644
index 000000000..3224d8352
--- /dev/null
+++ b/src/Modules/HealthCenter/components/HospitalAdmissionForm.jsx
@@ -0,0 +1,561 @@
+/**
+ * Hospital Admission Form Component
+ * ================================
+ * Manage patient hospital admissions and discharges
+ * Track admission/discharge records
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ TextInput,
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ Badge,
+ ActionIcon,
+ Select,
+ Textarea,
+ SimpleGrid,
+} from '@mantine/core';
+import { DatePicker } from '@mantine/dates';
+import { notifications } from '@mantine/notifications';
+import { IconPlus, IconEdit, IconCheck, IconEye } from '@tabler/icons-react';
+import * as api from '../api';
+
+const admissionReasonOptions = [
+ { value: 'EMERGENCY', label: 'Emergency' },
+ { value: 'PLANNED', label: 'Planned' },
+ { value: 'ICU', label: 'ICU Required' },
+];
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function HospitalAdmissionForm() {
+ const [admissions, setAdmissions] = useState([]);
+ const [users, setUsers] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [detailsModalOpened, setDetailsModalOpened] = useState(false);
+ const [selectedAdmission, setSelectedAdmission] = useState(null);
+ const [editingId, setEditingId] = useState(null);
+ const [formData, setFormData] = useState({
+ patient_id: '',
+ patient_name: '',
+ admission_date: new Date(),
+ discharge_date: null,
+ ward_number: '',
+ bed_number: '',
+ admission_reason: '',
+ medical_notes: '',
+ discharge_notes: '',
+ });
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ fetchAdmissions();
+ fetchUsers();
+ }, []);
+
+ const fetchAdmissions = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getAdmissions();
+ setAdmissions(Array.isArray(response.data) ? response.data : []);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load admissions',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const fetchUsers = async () => {
+ try {
+ const response = await api.getUsers();
+ const userList = normalizeArray(response.data);
+ setUsers(
+ userList.map((user) => ({
+ value: user.value || user.id.toString(),
+ label: user.label || `${user.username} - ${user.full_name}`,
+ ...user,
+ }))
+ );
+ } catch (error) {
+
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!formData.patient_id)
+ newErrors.patient_id = 'Patient is required';
+ if (!formData.ward_number.trim())
+ newErrors.ward_number = 'Ward number is required';
+ if (!formData.bed_number.trim()) newErrors.bed_number = 'Bed number is required';
+ if (!formData.admission_reason) newErrors.admission_reason = 'Reason is required';
+ if (!formData.admission_date) newErrors.admission_date = 'Admission date is required';
+
+ // Validate ward and bed numbers are numeric
+ if (formData.ward_number && isNaN(parseInt(formData.ward_number))) {
+ newErrors.ward_number = 'Ward number must be numeric';
+ }
+ if (formData.bed_number && isNaN(parseInt(formData.bed_number))) {
+ newErrors.bed_number = 'Bed number must be numeric';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ const wardNum = parseInt(formData.ward_number);
+ const bedNum = parseInt(formData.bed_number);
+
+ if (isNaN(wardNum) || isNaN(bedNum)) {
+ notifications.show({
+ message: 'Ward and bed numbers must be valid numbers',
+ color: 'red',
+ });
+ return;
+ }
+
+ const admissionDate = formData.admission_date instanceof Date
+ ? formData.admission_date
+ : new Date(formData.admission_date);
+ const dischargeDate = formData.discharge_date
+ ? (formData.discharge_date instanceof Date
+ ? formData.discharge_date
+ : new Date(formData.discharge_date))
+ : null;
+
+ const payload = {
+ patient_id: parseInt(formData.patient_id),
+ admission_date: admissionDate
+ .toISOString()
+ .split('T')[0],
+ discharge_date: dischargeDate
+ ? dischargeDate.toISOString().split('T')[0]
+ : null,
+ ward_number: wardNum,
+ bed_number: bedNum,
+ admission_reason: formData.admission_reason,
+ medical_notes: formData.medical_notes,
+ discharge_notes: formData.discharge_notes,
+ };
+
+ if (editingId) {
+ await api.updateAdmission(editingId, payload);
+ notifications.show({
+ message: 'Admission updated successfully',
+ color: 'green',
+ });
+ } else {
+ await api.createAdmission(payload);
+ notifications.show({
+ message: 'Patient admitted successfully',
+ color: 'green',
+ });
+ }
+ setModalOpened(false);
+ resetForm();
+ await fetchAdmissions();
+ } catch (error) {
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to save admission',
+ color: 'red',
+ });
+ }
+ };
+
+ const handleEdit = (admission) => {
+ setEditingId(admission.id);
+ const admissionDate = admission.admission_date
+ ? (admission.admission_date instanceof Date ? admission.admission_date : new Date(admission.admission_date))
+ : new Date();
+ const dischargeDate = admission.discharge_date
+ ? (admission.discharge_date instanceof Date ? admission.discharge_date : new Date(admission.discharge_date))
+ : null;
+
+ setFormData({
+ patient_id: admission.patient_id?.toString() || '',
+ patient_name: admission.patient_name,
+ admission_date: admissionDate,
+ discharge_date: dischargeDate,
+ ward_number: admission.ward_number.toString(),
+ bed_number: admission.bed_number.toString(),
+ admission_reason: admission.admission_reason,
+ medical_notes: admission.medical_notes || '',
+ discharge_notes: admission.discharge_notes || '',
+ });
+ setModalOpened(true);
+ };
+
+ const resetForm = () => {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ setFormData({
+ patient_id: '',
+ patient_name: '',
+ admission_date: today,
+ discharge_date: null,
+ ward_number: '',
+ bed_number: '',
+ admission_reason: '',
+ medical_notes: '',
+ discharge_notes: '',
+ });
+ setErrors({});
+ setEditingId(null);
+ };
+
+ const getStatusBadge = (discharge_date) => {
+ return discharge_date ? (
+ Discharged
+ ) : (
+ Admitted
+ );
+ };
+
+ return (
+
+
+
+ Hospital Admissions
+
+ }
+ onClick={() => {
+ resetForm();
+ setModalOpened(true);
+ }}
+ >
+ New Admission
+
+
+
+
+ {admissions.length === 0 ? (
+
+ No admissions
+
+ ) : (
+
+
+
+ Patient
+ Ward/Bed
+ Admission Date
+ Discharge Date
+ Status
+ Actions
+
+
+
+ {admissions.map((admission) => (
+
+ {admission.patient_name}
+
+ Ward {admission.ward_number}, Bed {admission.bed_number}
+
+
+ {new Date(admission.admission_date).toLocaleDateString()}
+
+
+ {admission.discharge_date
+ ? new Date(admission.discharge_date).toLocaleDateString()
+ : 'Active'}
+
+ {getStatusBadge(admission.discharge_date)}
+
+
+ {
+ setSelectedAdmission(admission);
+ setDetailsModalOpened(true);
+ }}
+ >
+
+
+ handleEdit(admission)}
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title={editingId ? 'Edit Admission' : 'New Hospital Admission'}
+ size="lg"
+ >
+
+ {/* Patient Selection */}
+ {
+ const selectedUser = users.find(u => u.value === value);
+ setFormData({
+ ...formData,
+ patient_id: value,
+ patient_name: selectedUser ? selectedUser.label : '',
+ });
+ }}
+ error={errors.patient_id}
+ searchable
+ />
+
+ {/* Ward & Bed Selection - Side by Side */}
+
+
+ setFormData({ ...formData, ward_number: e.currentTarget.value })
+ }
+ error={errors.ward_number}
+ />
+
+ setFormData({ ...formData, bed_number: e.currentTarget.value })
+ }
+ error={errors.bed_number}
+ />
+
+
+ {/* Admission Date - Full Width */}
+ {
+ const dateStr = e.currentTarget.value;
+ setFormData({
+ ...formData,
+ admission_date: dateStr ? new Date(dateStr + 'T00:00:00') : new Date(),
+ });
+ }}
+ error={errors.admission_date}
+ placeholder="Select admission date"
+ />
+
+ {/* Discharge Date - Full Width */}
+ {
+ const dateStr = e.currentTarget.value;
+ setFormData({
+ ...formData,
+ discharge_date: dateStr ? new Date(dateStr + 'T00:00:00') : null,
+ });
+ }}
+ placeholder="Select discharge date (optional)"
+ description="Optional - leave empty if still admitted"
+ />
+
+ {/* Admission Reason */}
+
+ setFormData({ ...formData, admission_reason: value })
+ }
+ error={errors.admission_reason}
+ searchable
+ />
+
+ {/* Medical Notes - Full Width */}
+
+
+
+ {/* View Details Modal */}
+ {
+ setDetailsModalOpened(false);
+ setSelectedAdmission(null);
+ }}
+ title="Admission Details"
+ size="lg"
+ >
+ {selectedAdmission && (
+
+
+
+
+
+ Patient Name
+ {selectedAdmission.patient_name}
+
+
+ Status
+ {getStatusBadge(selectedAdmission.discharge_date)}
+
+
+
+
+
+
+
+
+ Ward/Bed
+ Ward {selectedAdmission.ward_number}, Bed {selectedAdmission.bed_number}
+
+
+
+
+ Admission Reason
+ {selectedAdmission.admission_reason}
+
+
+
+
+
+
+
+ Admission Date
+ {new Date(selectedAdmission.admission_date).toLocaleDateString()}
+
+
+
+
+ Discharge Date
+
+ {selectedAdmission.discharge_date
+ ? new Date(selectedAdmission.discharge_date).toLocaleDateString()
+ : 'Still Admitted'}
+
+
+
+
+
+ {selectedAdmission.medical_notes && (
+
+
+ Medical Notes
+ {selectedAdmission.medical_notes}
+
+
+ )}
+
+ {selectedAdmission.discharge_notes && (
+
+
+ Discharge Notes
+ {selectedAdmission.discharge_notes}
+
+
+ )}
+
+
+
+
+
+ )}
+
+
+ );
+}
+
diff --git a/src/Modules/HealthCenter/components/InventoryRequisitions.jsx b/src/Modules/HealthCenter/components/InventoryRequisitions.jsx
new file mode 100644
index 000000000..78dbfb5cc
--- /dev/null
+++ b/src/Modules/HealthCenter/components/InventoryRequisitions.jsx
@@ -0,0 +1,469 @@
+/**
+ * Inventory Requisition Component
+ * =======================
+ * PHC-UC-10: Create Inventory Requisition (compounder submits stock request)
+ * PHC-UC-14: Mark Requisition as Fulfilled (compounder closes on receipt of stock)
+ * PHC-WF-02: Full workflow visibility — CREATED → SUBMITTED → APPROVED/REJECTED → FULFILLED
+ *
+ * PHC-UC-16 (Approve Inventory Requisition) is handled by the Approving Authority.
+ * Its backend endpoint (AuthorityInventoryRequisitionView) is implemented but commented
+ * out in views.py pending institute-level role definition (cross-module boundary).
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ NumberInput,
+ Select,
+ Badge,
+ ActionIcon,
+ Tooltip,
+ Textarea,
+ Alert,
+ Divider,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import {
+ IconSend,
+ IconRefresh,
+ IconCheck,
+ IconInfoCircle,
+ IconAlertCircle,
+} from '@tabler/icons-react';
+import * as api from '../api';
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+const STATUS_COLOR = {
+ CREATED: 'gray',
+ SUBMITTED: 'blue',
+ APPROVED: 'green',
+ REJECTED: 'red',
+ FULFILLED: 'teal',
+};
+
+const STATUS_LABEL = {
+ CREATED: 'Created',
+ SUBMITTED: 'Pending Approval',
+ APPROVED: 'Approved',
+ REJECTED: 'Rejected',
+ FULFILLED: 'Fulfilled',
+};
+
+export default function InventoryRequisition() {
+ const [requisitions, setRequisitions] = useState([]);
+ const [medicineOptions, setMedicineOptions] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ // ── New Requisition modal
+ const [modalOpened, setModalOpened] = useState(false);
+ const [formData, setFormData] = useState({ medicine_id: '', quantity: '' });
+ const [formErrors, setFormErrors] = useState({});
+
+ // ── Fulfill modal (PHC-UC-14)
+ const [fulfillModalOpened, setFulfillModalOpened] = useState(false);
+ const [fulfillTarget, setFulfillTarget] = useState(null);
+ const [quantityFulfilled, setQuantityFulfilled] = useState('');
+ const [fulfillError, setFulfillError] = useState('');
+ const [fulfillLoading, setFulfillLoading] = useState(false);
+
+ // ── Details popover
+ const [detailModalOpened, setDetailModalOpened] = useState(false);
+ const [detailReq, setDetailReq] = useState(null);
+
+ useEffect(() => { loadData(); }, []);
+
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ await Promise.all([loadMedicineOptions(), loadRequisitions()]);
+ } catch {
+ notifications.show({ message: 'Failed to load data', color: 'red' });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const loadMedicineOptions = async () => {
+ try {
+ const res = await api.getMedicines();
+ setMedicineOptions(
+ normalizeArray(res.data).map((med) => ({
+ value: String(med.id),
+ label: med.pack_size_label
+ ? `${med.medicine_name} (${med.pack_size_label})`
+ : med.medicine_name,
+ }))
+ );
+ } catch { /* silently fail */ }
+ };
+
+ const loadRequisitions = async () => {
+ try {
+ const res = await api.getCompounderRequisitions();
+ setRequisitions(normalizeArray(res.data));
+ } catch { /* silently fail */ }
+ };
+
+ // ─────────────────────────────────────────────────────────────────────────
+ // Create Requisition (PHC-UC-10)
+ // ─────────────────────────────────────────────────────────────────────────
+
+ const validateForm = () => {
+ const errs = {};
+ if (!formData.medicine_id) errs.medicine_id = 'Medicine is required';
+ if (!formData.quantity || Number(formData.quantity) <= 0)
+ errs.quantity = 'Quantity must be > 0';
+ setFormErrors(errs);
+ return Object.keys(errs).length === 0;
+ };
+
+ const handleSubmitRequisition = async () => {
+ if (!validateForm()) return;
+ try {
+ await api.createCompounderRequisition(
+ Number(formData.medicine_id),
+ Number(formData.quantity)
+ );
+ notifications.show({
+ message: 'Requisition request sent to authority for approval',
+ color: 'green',
+ });
+ setModalOpened(false);
+ resetForm();
+ await loadRequisitions();
+ } catch {
+ notifications.show({ message: 'Failed to send requisition request', color: 'red' });
+ }
+ };
+
+ const resetForm = () => { setFormData({ medicine_id: '', quantity: '' }); setFormErrors({}); };
+
+ // ─────────────────────────────────────────────────────────────────────────
+ // Mark as Fulfilled (PHC-UC-14)
+ // ─────────────────────────────────────────────────────────────────────────
+
+ const openFulfillModal = (req) => {
+ setFulfillTarget({
+ id: req.id,
+ medicine_name: req.medicine_detail?.medicine_name || 'N/A',
+ quantity_requested: req.quantity_requested,
+ approval_remarks: req.approval_remarks || '',
+ });
+ setQuantityFulfilled(String(req.quantity_requested));
+ setFulfillError('');
+ setFulfillModalOpened(true);
+ };
+
+ const handleFulfill = async () => {
+ const qty = Number(quantityFulfilled);
+ if (!qty || qty < 1) { setFulfillError('Quantity fulfilled must be at least 1'); return; }
+ setFulfillLoading(true);
+ try {
+ await api.fulfillCompounderRequisition(fulfillTarget.id, qty);
+ notifications.show({
+ message: `Requisition #${fulfillTarget.id} marked as fulfilled`,
+ color: 'teal',
+ });
+ setFulfillModalOpened(false);
+ setFulfillTarget(null);
+ await loadRequisitions();
+ } catch (error) {
+ const msg = error?.response?.data?.detail || 'Failed to fulfill requisition';
+ notifications.show({ message: msg, color: 'red' });
+ } finally {
+ setFulfillLoading(false);
+ }
+ };
+
+ // ─────────────────────────────────────────────────────────────────────────
+ // Render
+ // ─────────────────────────────────────────────────────────────────────────
+
+ const pendingApproval = requisitions.filter((r) => r.status === 'SUBMITTED').length;
+ const approved = requisitions.filter((r) => r.status === 'APPROVED').length;
+
+ return (
+
+ {/* Header */}
+
+
+ Inventory Requisition
+
+ Submit stock requests to the Approving Authority. Mark them as fulfilled once supplies arrive.
+
+
+ {pendingApproval > 0 && (
+ {pendingApproval} pending approval
+ )}
+ {approved > 0 && (
+ {approved} approved — awaiting fulfillment
+ )}
+
+
+
+ } onClick={loadData} loading={loading}>
+ Refresh
+
+ } onClick={() => { resetForm(); setModalOpened(true); }}>
+ New Request
+
+
+
+
+ {/* Requisitions Table */}
+
+ {requisitions.length === 0 ? (
+ No requisitions found.
+ ) : (
+
+
+
+ ID
+ Medicine
+ Qty Requested
+ Qty Fulfilled
+ Submitted On
+ Status
+ Actions
+
+
+
+ {requisitions.map((req) => (
+
+ #{req.id}
+ {req.medicine_detail?.medicine_name || 'N/A'}
+ {req.quantity_requested}
+
+ {req.status === 'FULFILLED'
+ ? {req.quantity_fulfilled}
+ : —}
+
+ {new Date(req.created_date).toLocaleDateString()}
+
+
+ {STATUS_LABEL[req.status] || req.status}
+
+
+
+
+ {/* Info — always available */}
+
+ { setDetailReq(req); setDetailModalOpened(true); }}
+ >
+
+
+
+
+ {/* Mark as Fulfilled — only for APPROVED (PHC-UC-14) */}
+ {req.status === 'APPROVED' && (
+
+ openFulfillModal(req)}
+ >
+
+
+
+ )}
+
+
+
+ ))}
+
+
+ )}
+
+
+ {/* ─── New Requisition Modal (PHC-UC-10) ──────────────────────────────── */}
+ { resetForm(); setModalOpened(false); }}
+ title="New Requisition Request"
+ size="md"
+ >
+
+ } color="blue" variant="light">
+ This request will be forwarded to the Approving Authority for review.
+ Once approved, you can mark it as fulfilled when the stock arrives.
+
+
+ setFormData({ ...formData, medicine_id: v || '' })}
+ error={formErrors.medicine_id}
+ />
+
+ setFormData({ ...formData, quantity: v === undefined ? '' : String(v) })}
+ error={formErrors.quantity}
+ min={1}
+ />
+
+
+
+ } onClick={handleSubmitRequisition}>
+ Submit Request
+
+
+
+
+
+ {/* ─── Mark as Fulfilled Modal (PHC-UC-14) ────────────────────────────── */}
+ { setFulfillModalOpened(false); setFulfillTarget(null); }}
+ title="Mark Requisition as Fulfilled"
+ size="md"
+ >
+ {fulfillTarget && (
+
+ } color="teal" variant="light">
+ Confirm that the ordered supplies have been physically received.
+
+
+
+
+ Requisition #{fulfillTarget.id}
+ Approved
+
+ Medicine: {fulfillTarget.medicine_name}
+ Quantity Requested: {fulfillTarget.quantity_requested}
+ {fulfillTarget.approval_remarks && (
+
+ Authority Remarks: {fulfillTarget.approval_remarks}
+
+ )}
+
+
+ {
+ setQuantityFulfilled(v === undefined ? '' : String(v));
+ setFulfillError('');
+ }}
+ error={fulfillError}
+ min={1}
+ />
+
+
+
+ }
+ onClick={handleFulfill}
+ loading={fulfillLoading}
+ >
+ Confirm Fulfillment
+
+
+
+ )}
+
+
+ {/* ─── Details Modal ───────────────────────────────────────────────────── */}
+ { setDetailModalOpened(false); setDetailReq(null); }}
+ title={`Requisition #${detailReq?.id} — Details`}
+ size="md"
+ >
+ {detailReq && (
+
+
+ Medicine
+ {detailReq.medicine_detail?.medicine_name || 'N/A'}
+
+
+ Quantity Requested
+ {detailReq.quantity_requested}
+
+
+ Status
+
+ {STATUS_LABEL[detailReq.status] || detailReq.status}
+
+
+
+ Submitted On
+ {new Date(detailReq.created_date).toLocaleDateString()}
+
+
+
+ {/* Approval details */}
+ {detailReq.approved_date && (
+
+ Approved On
+ {new Date(detailReq.approved_date).toLocaleDateString()}
+
+ )}
+ {detailReq.approval_remarks && (
+
+ Authority Remarks
+ {detailReq.approval_remarks}
+
+ )}
+
+ {/* Rejection details */}
+ {detailReq.status === 'REJECTED' && detailReq.rejection_reason && (
+ } color="red" variant="light" mt="xs">
+ Rejected
+ {detailReq.rejection_reason}
+
+ )}
+
+ {/* Fulfillment details */}
+ {detailReq.status === 'FULFILLED' && (
+ <>
+
+
+ Quantity Received
+ {detailReq.quantity_fulfilled}
+
+ {detailReq.fulfilled_date && (
+
+ Fulfilled On
+ {new Date(detailReq.fulfilled_date).toLocaleDateString()}
+
+ )}
+ >
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/InventoryTab.jsx b/src/Modules/HealthCenter/components/InventoryTab.jsx
new file mode 100644
index 000000000..13de20ae6
--- /dev/null
+++ b/src/Modules/HealthCenter/components/InventoryTab.jsx
@@ -0,0 +1,20 @@
+import React from 'react';
+import { Stack, Card, Button, Text } from '@mantine/core';
+
+export default function InventoryTab() {
+ return (
+
+
+
+ Access full inventory management with stock updates and requisitions
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/MedicalHistory.jsx b/src/Modules/HealthCenter/components/MedicalHistory.jsx
new file mode 100644
index 000000000..6f3d77630
--- /dev/null
+++ b/src/Modules/HealthCenter/components/MedicalHistory.jsx
@@ -0,0 +1,349 @@
+/**
+ * Medical History Page
+ * =====================
+ * Displays patient's medical records including:
+ * - Consultations/visits
+ * - Prescriptions
+ * - Clinical notes
+ *
+ * PHC-UC-02: View Medical History
+ */
+
+import { useEffect, useState } from 'react';
+import {
+ Container,
+ Title,
+ Text,
+ Loader,
+ Card,
+ Stack,
+ Group,
+ Badge,
+ Tabs,
+ Timeline,
+ ThemeIcon,
+ Button,
+ Modal,
+ SimpleGrid,
+ Table,
+ ScrollArea,
+} from '@mantine/core';
+import {
+ IconCalendar,
+ IconFileText,
+ IconPill,
+ IconClock,
+ IconUserMd,
+ IconClipboard,
+} from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import * as api from '../api';
+
+export default function MedicalHistory() {
+ const [medicalData, setMedicalData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [selectedConsultation, setSelectedConsultation] = useState(null);
+ const [consultationModal, setConsultationModal] = useState(false);
+
+ useEffect(() => {
+ fetchMedicalHistory();
+ }, []);
+
+ const fetchMedicalHistory = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getMedicalHistory();
+ setMedicalData(response.data);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load medical history',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const consultations = medicalData?.consultations || [];
+ const prescriptions = medicalData?.prescriptions || [];
+
+ return (
+
+ {/* Header */}
+
+ Medical History
+ Your complete medical records and prescriptions
+
+
+ {/* Tabs */}
+
+
+ }
+ >
+ Consultations ({consultations.length})
+
+ }>
+ Prescriptions ({prescriptions.length})
+
+
+
+ {/* Consultations Tab */}
+
+ {consultations.length > 0 ? (
+
+ {consultations.map((consultation, idx) => (
+ }
+ title={`Dr. ${consultation.doctor_name}`}
+ >
+
+
+ {/* Date and Time */}
+
+
+ 📅 {consultation.consultation_date}
+
+ {consultation.consultation_time && (
+
+ 🕐 {consultation.consultation_time}
+
+ )}
+
+
+ {/* Vitals */}
+ {(consultation.blood_pressure ||
+ consultation.pulse ||
+ consultation.temperature) && (
+
+
+ Vitals
+
+
+ {consultation.blood_pressure && (
+
+
+ BP
+
+
+ {consultation.blood_pressure}
+
+
+ )}
+ {consultation.pulse && (
+
+
+ Pulse
+
+
+ {consultation.pulse} bpm
+
+
+ )}
+ {consultation.temperature && (
+
+
+ Temp
+
+
+ {consultation.temperature}°C
+
+
+ )}
+ {consultation.oxygen_saturation && (
+
+
+ O₂
+
+
+ {consultation.oxygen_saturation}%
+
+
+ )}
+
+
+ )}
+
+ {/* Clinical Notes */}
+ {consultation.clinical_notes && (
+
+
+ Clinical Notes
+
+ {consultation.clinical_notes}
+
+ )}
+
+ {/* Diagnosis */}
+ {consultation.diagnosis && (
+
+
+ Diagnosis
+
+ {consultation.diagnosis}
+
+ )}
+
+ {/* Action Button */}
+ {consultation.prescription_id && (
+
+ )}
+
+
+
+ ))}
+
+ ) : (
+ No consultations recorded yet
+ )}
+
+
+ {/* Prescriptions Tab */}
+
+ {prescriptions.length > 0 ? (
+
+ {prescriptions.map((prescription) => (
+
+
+ {/* Header */}
+
+
+ Prescription #{prescription.id}
+
+ Date: {prescription.prescription_date}
+
+
+ {prescription.status || 'Active'}
+
+
+ {/* Medicines Table */}
+
+
+ Medicines
+
+
+
+
+ Medicine
+ Dosage
+ Duration
+
+
+
+ {prescription.medicines?.map((medicine, idx) => (
+
+ {medicine.medicine_name}
+
+ {medicine.quantity} {medicine.unit} ×{' '}
+ {medicine.times_per_day}
+ times/day
+
+ {medicine.days} days
+
+ ))}
+
+
+
+
+ {/* Instructions */}
+ {prescription.follow_up_instructions && (
+
+
+ Follow-up Instructions
+
+ {prescription.follow_up_instructions}
+
+ )}
+
+
+ ))}
+
+ ) : (
+ No prescriptions on record
+ )}
+
+
+
+ {/* Prescription Modal */}
+ {
+ setConsultationModal(false);
+ setSelectedConsultation(null);
+ }}
+ title="Prescription Details"
+ size="lg"
+ >
+ {selectedConsultation && (
+
+
+
+ Medicines
+
+
+
+
+ Medicine
+ Dosage
+ Duration
+
+
+
+ {selectedConsultation.medicines?.map((medicine, idx) => (
+
+ {medicine.medicine_name}
+
+ {medicine.quantity} {medicine.unit} ×{' '}
+ {medicine.times_per_day}
+ times/day
+
+ {medicine.days} days
+
+ ))}
+
+
+
+
+ {selectedConsultation.follow_up_instructions && (
+
+
+ Follow-up Instructions
+
+
+ {selectedConsultation.follow_up_instructions}
+
+
+ )}
+
+ )}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/PrescriptionCreation.jsx b/src/Modules/HealthCenter/components/PrescriptionCreation.jsx
new file mode 100644
index 000000000..1c373beac
--- /dev/null
+++ b/src/Modules/HealthCenter/components/PrescriptionCreation.jsx
@@ -0,0 +1,963 @@
+/**
+ * Prescription Creation Component
+ * ===============================
+ * Create prescriptions with FIFO stock deduction preview
+ * Add multiple medicines, check stock availability, preview batch deduction
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ ActionIcon,
+ Textarea,
+ NumberInput,
+ Paper,
+ Select,
+ SimpleGrid,
+ TextInput,
+ Badge,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { IconPlus, IconTrash, IconCheck, IconEye } from '@tabler/icons-react';
+import * as api from '../api';
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function PrescriptionCreation({ selectedConsultation, onPrescriptionCreated }) {
+ const [prescriptions, setPrescriptions] = useState([]);
+ const [users, setUsers] = useState([]);
+ const [doctors, setDoctors] = useState([]);
+ const [medicines, setMedicines] = useState([]);
+ const [stock, setStock] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [detailsModalOpened, setDetailsModalOpened] = useState(false);
+ const [selectedDetailsPrescription, setSelectedDetailsPrescription] = useState(null);
+ const [medicineLines, setMedicineLines] = useState([]);
+ const [formData, setFormData] = useState({
+ user_id: '',
+ doctor_id: '',
+ details: '',
+ special_instructions: '',
+ test_recommended: '',
+ follow_up_suggestions: '',
+ is_for_dependent: false,
+ dependent_name: '',
+ dependent_relation: '',
+ });
+ const [errors, setErrors] = useState({});
+ const [fifoPreview, setFifoPreview] = useState({});
+
+ useEffect(() => {
+ fetchInitialData();
+ }, []);
+
+ useEffect(() => {
+ if (selectedConsultation) {
+ // Extract patient_id and doctor_id - ensure we get numeric IDs
+ let patientId = selectedConsultation.patient_id;
+ let doctorId = selectedConsultation.doctor_id;
+
+ // If patient_id is an object, try to get the id property
+ if (typeof patientId === 'object' && patientId !== null) {
+ patientId = patientId.id || patientId.value;
+ }
+
+ // If doctor_id is an object, try to get the id property
+ if (typeof doctorId === 'object' && doctorId !== null) {
+ doctorId = doctorId.id || doctorId.value;
+ }
+
+ // If patientId looks like a username (not numeric), try to find matching user
+ if (patientId && isNaN(parseInt(patientId))) {
+
+ // Search users array for matching username
+ const matchingUser = users.find(u => u.label?.includes(patientId) || u.id === patientId);
+ if (matchingUser) {
+ patientId = matchingUser.value;
+ }
+ }
+
+ // If doctorId looks like a username (not numeric), try to find matching doctor
+ if (doctorId && isNaN(parseInt(doctorId))) {
+
+ const matchingDoctor = doctors.find(d => d.label?.includes(doctorId) || d.id === doctorId);
+ if (matchingDoctor) {
+ doctorId = matchingDoctor.value;
+ }
+ }
+
+ setFormData(prev => ({
+ ...prev,
+ user_id: patientId ? patientId.toString() : '',
+ doctor_id: doctorId ? doctorId.toString() : '',
+ }));
+ setModalOpened(true);
+ }
+ }, [selectedConsultation, users, doctors]);
+
+ const fetchInitialData = async () => {
+ try {
+ setLoading(true);
+ const [stockRes, doctorsRes, usersRes, prescriptionsRes] = await Promise.all([
+ api.getStock(),
+ api.getDoctorsFiltered({ active_only: true }),
+ api.getUsers(),
+ api.getCompounderPrescriptions(),
+ ]);
+
+ // Extract medicines from stock
+ const stockArray = Array.isArray(stockRes.data) ? stockRes.data : [];
+ setStock(stockArray);
+
+ // Create medicines list from stock (unique medicines)
+ const medicineMap = new Map();
+ for (const item of stockArray) {
+ if (item.medicine_detail && !medicineMap.has(item.medicine_detail.id)) {
+ medicineMap.set(item.medicine_detail.id, {
+ value: item.medicine_detail.id.toString(),
+ label: `${item.medicine_detail.medicine_name} (${item.medicine_detail.strength || 'N/A'})`,
+ ...item.medicine_detail,
+ });
+ }
+ }
+ setMedicines(Array.from(medicineMap.values()));
+
+ // Load doctors with enhanced info
+ const doctorList = Array.isArray(doctorsRes.data) ? doctorsRes.data : [];
+ const doctorOptions = doctorList.map((doc) => ({
+ value: doc.id.toString(),
+ label: `Dr. ${doc.doctor_name} - ${doc.specialization || 'General'}`,
+ ...doc,
+ }));
+ setDoctors(doctorOptions);
+
+ // Load users with enhanced info
+ const userList = normalizeArray(usersRes.data);
+ setUsers(
+ userList.map((user) => ({
+ value: user.value || user.id.toString(),
+ label: user.label || `${user.username} - ${user.full_name}`,
+ ...user,
+ }))
+ );
+
+ // Load prescriptions
+ const prescriptionList = normalizeArray(prescriptionsRes.data);
+ setPrescriptions(prescriptionList);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load data: ' + (error?.response?.data?.detail || error.message),
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const calculateFifoPreview = (medicineId, quantity) => {
+ // Get all batches for this medicine from stock
+ const medicineStock = [];
+
+ for (const stockItem of stock) {
+ if (stockItem.medicine_detail && stockItem.medicine_detail.id === parseInt(medicineId)) {
+ // Get non-returned batches and sort by expiry date (FIFO)
+ if (stockItem.expiry_batches) {
+ const activeBatches = stockItem.expiry_batches
+ .filter((batch) => !batch.is_returned)
+ .sort((a, b) => new Date(a.expiry_date) - new Date(b.expiry_date));
+ medicineStock.push(...activeBatches);
+ }
+ }
+ }
+
+ const preview = [];
+ let remainingQty = quantity;
+
+ for (const batch of medicineStock) {
+ if (remainingQty <= 0) break;
+
+ const availableQty = batch.qty - (batch.returned_qty || 0);
+ const qtyToDeduct = Math.min(availableQty, remainingQty);
+
+ preview.push({
+ batch_no: batch.batch_no,
+ expiry_date: batch.expiry_date,
+ qty_available: availableQty,
+ qty_deduct: qtyToDeduct,
+ });
+
+ remainingQty -= qtyToDeduct;
+ }
+
+ return {
+ preview,
+ canFulfill: remainingQty === 0,
+ remainingUnfulfilled: Math.max(0, remainingQty),
+ totalAvailable: medicineStock.reduce((sum, b) => sum + (b.qty - (b.returned_qty || 0)), 0),
+ };
+ };
+
+ const addMedicineLine = () => {
+ setMedicineLines([
+ ...medicineLines,
+ {
+ id: Date.now(),
+ medicine: '',
+ qty_prescribed: 0,
+ days: 0,
+ times_per_day: 1,
+ instructions: '',
+ notes: '',
+ },
+ ]);
+ };
+
+ const removeMedicineLine = (lineId) => {
+ setMedicineLines(medicineLines.filter((line) => line.id !== lineId));
+ const newPreview = { ...fifoPreview };
+ delete newPreview[lineId];
+ setFifoPreview(newPreview);
+ };
+
+ const updateMedicineLine = (lineId, field, value) => {
+ const updatedLines = medicineLines.map((line) => {
+ if (line.id === lineId) {
+ const updated = { ...line, [field]: value };
+
+ // Calculate FIFO preview when quantity or medicine changes
+ if ((field === 'qty_prescribed' || field === 'medicine') && updated.medicine && updated.qty_prescribed > 0) {
+ const preview = calculateFifoPreview(updated.medicine, updated.qty_prescribed);
+ setFifoPreview((prev) => ({ ...prev, [lineId]: preview }));
+ }
+
+ return updated;
+ }
+ return line;
+ });
+ setMedicineLines(updatedLines);
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+
+ // Ensure user_id and doctor_id are not empty strings
+ if (!formData.user_id || formData.user_id.trim() === '') {
+ newErrors.user_id = 'User is required';
+ }
+ if (!formData.doctor_id || formData.doctor_id.trim() === '') {
+ newErrors.doctor_id = 'Doctor is required';
+ }
+
+ // Validate each medicine line if medicines are added
+ medicineLines.forEach((line, idx) => {
+ if (!line.medicine) newErrors[`medicine_${idx}`] = 'Medicine required';
+ if (line.qty_prescribed <= 0) newErrors[`qty_${idx}`] = 'Quantity must be > 0';
+ if (line.days <= 0) newErrors[`days_${idx}`] = 'Days must be > 0';
+ if (line.times_per_day < 1 || line.times_per_day > 12) newErrors[`times_${idx}`] = 'Times per day 1-12';
+ });
+
+ // Check if all medicines can be fulfilled (only if medicines exist)
+ if (medicineLines.length > 0) {
+ let allFulfillable = true;
+ medicineLines.forEach((line) => {
+ const preview = fifoPreview[line.id];
+ if (preview && !preview.canFulfill) {
+ allFulfillable = false;
+ }
+ });
+
+ if (!allFulfillable) {
+ newErrors.stock = 'Insufficient stock for some medicines';
+ }
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ // Ensure user_id and doctor_id are valid integers
+ const userId = parseInt(formData.user_id);
+ const doctorId = parseInt(formData.doctor_id);
+
+ if (isNaN(userId)) {
+
+ const availableUserValues = users.map(u => u.value).join(', ');
+ notifications.show({
+ message: `Invalid user selection: "${formData.user_id}". Expected numeric ID from: [${availableUserValues}]. Please select a user from the dropdown.`,
+ color: 'red',
+ });
+ return;
+ }
+
+ if (isNaN(doctorId)) {
+
+ const availableDoctorValues = doctors.map(d => d.value).join(', ');
+ notifications.show({
+ message: `Invalid doctor selection: "${formData.doctor_id}". Expected numeric ID from: [${availableDoctorValues}]. Please select a doctor from the dropdown.`,
+ color: 'red',
+ });
+ return;
+ }
+
+ const payload = {
+ user_id: userId,
+ doctor_id: doctorId,
+ medicines: medicineLines.map((line) => ({
+ medicine: parseInt(line.medicine),
+ qty_prescribed: parseInt(line.qty_prescribed),
+ days: parseInt(line.days),
+ times_per_day: parseInt(line.times_per_day),
+ instructions: line.instructions,
+ notes: line.notes,
+ })),
+ details: formData.details,
+ special_instructions: formData.special_instructions,
+ test_recommended: formData.test_recommended,
+ follow_up_suggestions: formData.follow_up_suggestions,
+ is_for_dependent: formData.is_for_dependent,
+ dependent_name: formData.dependent_name,
+ dependent_relation: formData.dependent_relation,
+ };
+
+ await api.createPrescription(payload);
+ notifications.show({
+ message: 'Prescription created successfully',
+ color: 'green',
+ });
+ resetForm();
+ setModalOpened(false);
+ await fetchInitialData();
+
+ // Invoke callback if prescription was created from consultation
+ if (onPrescriptionCreated) {
+ onPrescriptionCreated();
+ }
+ } catch (error) {
+
+
+
+ // Show the most relevant error message
+ const errorData = error.response?.data;
+ let errorMessage = 'Failed to create prescription';
+
+ if (errorData) {
+ // Check various error response formats
+ errorMessage =
+ errorData.detail ||
+ errorData.error ||
+ JSON.stringify(errorData) ||
+ error.message ||
+ 'Failed to create prescription';
+ }
+
+ notifications.show({
+ message: errorMessage,
+ color: 'red',
+ });
+ }
+ };
+
+ const resetForm = () => {
+ setFormData({
+ user_id: '',
+ doctor_id: '',
+ details: '',
+ special_instructions: '',
+ test_recommended: '',
+ follow_up_suggestions: '',
+ is_for_dependent: false,
+ dependent_name: '',
+ dependent_relation: '',
+ });
+ setMedicineLines([]);
+ setFifoPreview({});
+ setErrors({});
+ };
+
+ const getMedicineName = (medicineId) => {
+ const med = medicines.find((m) => m.value === medicineId);
+ return med?.label || 'Unknown';
+ };
+
+ return (
+
+ {selectedConsultation && (
+
+ Selected Consultation:
+
+
+ Patient
+ {selectedConsultation.patient_username || 'N/A'}
+
+
+ Doctor
+ {selectedConsultation.doctor_name || 'N/A'}
+
+
+ Chief Complaint
+ {selectedConsultation.chief_complaint || 'N/A'}
+
+
+
+ )}
+
+
+
+ Prescriptions
+
+ {/* Commented out: New Prescription button - functionality preserved in state */}
+ {/* {!selectedConsultation && (
+ }
+ onClick={() => setModalOpened(true)}
+ >
+ New Prescription
+
+ )} */}
+
+
+
+ {prescriptions.length === 0 ? (
+
+ No prescriptions yet
+
+ ) : (
+
+
+
+ Patient
+ Doctor
+ Chief Complaint
+ Date
+ Actions
+
+
+
+ {prescriptions.map((presc) => (
+
+
+ {presc.patient_name && presc.patient_name !== 'N/A'
+ ? presc.patient_name
+ : (presc.patient ? `Patient ${presc.patient}` : 'N/A')}
+
+ {presc.doctor_name || 'N/A'}
+
+ {presc.details || 'N/A'}
+
+
+ {presc.created_at ? new Date(presc.created_at).toLocaleDateString() : 'N/A'}
+
+
+
+ {
+ setSelectedDetailsPrescription(presc);
+ setDetailsModalOpened(true);
+ }}
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title="Create Prescription"
+ size="xl"
+ >
+
+ {/* User and Doctor Selection */}
+
+
+ setFormData({ ...formData, user_id: value })
+ }
+ error={errors.user_id}
+ searchable
+ disabled={!!selectedConsultation}
+ description={selectedConsultation ? 'Auto-selected from consultation' : ''}
+ />
+
+ setFormData({ ...formData, doctor_id: value })
+ }
+ error={errors.doctor_id}
+ searchable
+ disabled={!!selectedConsultation}
+ description={selectedConsultation ? 'Auto-selected from consultation' : ''}
+ />
+
+
+ {/* Medicines Section */}
+
+
+ Medicines (Optional)
+ }
+ onClick={addMedicineLine}
+ >
+ Add Medicine
+
+
+
+ {errors.stock && (
+
+ {errors.stock}
+
+ )}
+
+
+ {medicineLines.map((line, idx) => (
+
+
+ {/* Medicine Line Header */}
+
+
+ Medicine {idx + 1}
+
+ removeMedicineLine(line.id)}
+ >
+
+
+
+
+ {/* Medicine Selection & Quantity */}
+
+
+ updateMedicineLine(line.id, 'medicine', value)
+ }
+ error={errors[`medicine_${idx}`]}
+ searchable
+ />
+
+ updateMedicineLine(line.id, 'qty_prescribed', value)
+ }
+ error={errors[`qty_${idx}`]}
+ />
+
+
+ {/* Days & Times Per Day */}
+
+
+ updateMedicineLine(line.id, 'days', value)
+ }
+ error={errors[`days_${idx}`]}
+ />
+
+ updateMedicineLine(line.id, 'times_per_day', value)
+ }
+ error={errors[`times_${idx}`]}
+ />
+
+
+ {/* Instructions & Notes */}
+
+
+ updateMedicineLine(
+ line.id,
+ 'instructions',
+ e.currentTarget.value
+ )
+ }
+ minRows={2}
+ />
+
+ updateMedicineLine(line.id, 'notes', e.currentTarget.value)
+ }
+ minRows={2}
+ />
+
+
+ {/* FIFO Preview */}
+ {fifoPreview[line.id] && (
+
+
+
+ Stock Preview (FIFO)
+
+ {fifoPreview[line.id].canFulfill ? (
+ }
+ >
+ Can Fulfill
+
+ ) : (
+
+ Insufficient (Need{' '}
+ {fifoPreview[line.id].remainingUnfulfilled} more)
+
+ )}
+
+
+
+
+
+ Batch No
+ Expiry
+ Qty Deduct
+
+
+
+ {fifoPreview[line.id].preview.map((batch, i) => (
+
+ {batch.batch_no}
+
+ {new Date(batch.expiry_date).toLocaleDateString()}
+
+
+ {batch.qty_deduct} units
+
+
+ ))}
+
+
+
+ )}
+
+
+ ))}
+
+
+
+ {/* Prescription Details */}
+
+ setFormData({ ...formData, details: e.currentTarget.value })
+ }
+ minRows={2}
+ />
+
+ {/* Special Instructions */}
+
+ setFormData({ ...formData, special_instructions: e.currentTarget.value })
+ }
+ minRows={2}
+ />
+
+ {/* Test Recommended */}
+
+ setFormData({ ...formData, test_recommended: e.currentTarget.value })
+ }
+ />
+
+ {/* Follow Up Suggestions */}
+
+ setFormData({ ...formData, follow_up_suggestions: e.currentTarget.value })
+ }
+ minRows={2}
+ />
+
+ {/* Dependent Information (Optional) */}
+
+
+
+
+ Prescription for Dependent (Optional)
+
+
+
+
+
+
+
+
+
+ {formData.is_for_dependent && (
+
+
+ setFormData({
+ ...formData,
+ dependent_name: e.currentTarget.value,
+ })
+ }
+ />
+
+ setFormData({
+ ...formData,
+ dependent_relation: e.currentTarget.value,
+ })
+ }
+ />
+
+ )}
+
+
+
+ {/* Form Actions */}
+
+
+ } onClick={handleSubmit}>
+ Create Prescription
+
+
+
+
+
+ {/* View Details Modal */}
+ {
+ setDetailsModalOpened(false);
+ setSelectedDetailsPrescription(null);
+ }}
+ title="Prescription Details"
+ size="lg"
+ >
+ {selectedDetailsPrescription && (
+
+ {/* Header Info */}
+
+
+
+
+ Prescription ID
+ {selectedDetailsPrescription.id}
+
+
+ Date
+
+ {selectedDetailsPrescription.created_at
+ ? new Date(selectedDetailsPrescription.created_at).toLocaleDateString('en-IN')
+ : 'N/A'}
+
+
+
+
+
+
+ {/* Patient & Doctor Info */}
+
+
+
+ Patient
+
+ {selectedDetailsPrescription.patient_name && selectedDetailsPrescription.patient_name !== 'N/A'
+ ? selectedDetailsPrescription.patient_name
+ : (selectedDetailsPrescription.patient ? `Patient ${selectedDetailsPrescription.patient}` : 'N/A')}
+
+ {selectedDetailsPrescription.is_for_dependent && (
+ <>
+ Dependent: {selectedDetailsPrescription.dependent_name}
+ Relation: {selectedDetailsPrescription.dependent_relation}
+ >
+ )}
+
+
+
+
+ Doctor
+ {selectedDetailsPrescription.doctor_name || 'N/A'}
+
+
+
+
+ {/* Chief Complaint / Details */}
+ {selectedDetailsPrescription.details && (
+
+
+ Chief Complaint
+ {selectedDetailsPrescription.details}
+
+
+ )}
+
+ {/* Special Instructions */}
+ {selectedDetailsPrescription.special_instructions && (
+
+
+ Special Instructions
+ {selectedDetailsPrescription.special_instructions}
+
+
+ )}
+
+ {/* Test Recommended */}
+ {selectedDetailsPrescription.test_recommended && (
+
+
+ Test Recommended
+ {selectedDetailsPrescription.test_recommended}
+
+
+ )}
+
+ {/* Follow Up Suggestions */}
+ {selectedDetailsPrescription.follow_up_suggestions && (
+
+
+ Follow-up Suggestions
+ {selectedDetailsPrescription.follow_up_suggestions}
+
+
+ )}
+
+ {/* Prescribed Medicines */}
+ {selectedDetailsPrescription.prescribed_medicines && selectedDetailsPrescription.prescribed_medicines.length > 0 && (
+
+
+ Prescribed Medicines
+
+
+
+ Medicine
+ Quantity
+ Days
+ Times/Day
+ Instructions
+
+
+
+ {selectedDetailsPrescription.prescribed_medicines.map((med, idx) => (
+
+ {med.medicine_detail?.medicine_name || 'N/A'}
+ {med.qty_prescribed}
+ {med.days}
+ {med.times_per_day}
+ {med.instructions || '-'}
+
+ ))}
+
+
+
+
+ )}
+
+ {/* Status */}
+
+
+ Status
+ {selectedDetailsPrescription.status_display || selectedDetailsPrescription.status || 'Active'}
+
+
+
+ {/* Close Button */}
+
+
+
+
+ )}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/PrescriptionList.jsx b/src/Modules/HealthCenter/components/PrescriptionList.jsx
new file mode 100644
index 000000000..c1c2cdf1e
--- /dev/null
+++ b/src/Modules/HealthCenter/components/PrescriptionList.jsx
@@ -0,0 +1,535 @@
+/**
+ * Prescription List Component
+ * ============================
+ * Display patient's prescriptions (read-only list)
+ * Shows prescription details, medicines, and status
+ *
+ * PHC-UC-02: View Prescriptions
+ */
+
+import { useEffect, useState } from 'react';
+import jsPDF from 'jspdf';
+import autoTable from 'jspdf-autotable';
+import {
+ Container,
+ Card,
+ Stack,
+ Title,
+ Text,
+ Badge,
+ Loader,
+ Group,
+ Table,
+ ScrollArea,
+ SimpleGrid,
+ Alert,
+ Paper,
+ ActionIcon,
+ Tooltip,
+} from '@mantine/core';
+import { IconAlertCircle, IconCheck, IconClock, IconDownload } from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import * as api from '../api';
+import instiLogo from '../../../assets/iiitdmj_logo.png';
+
+export default function PrescriptionList() {
+ const [prescriptions, setPrescriptions] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [expandedId, setExpandedId] = useState(null);
+
+ useEffect(() => {
+ fetchPrescriptions();
+ }, []);
+
+ const fetchPrescriptions = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getPrescriptions();
+
+
+
+
+
+
+ let data = [];
+ if (Array.isArray(response.data)) {
+ data = response.data;
+ } else if (response.data?.results && Array.isArray(response.data.results)) {
+ data = response.data.results;
+ } else if (response.data) {
+ // If it's an object but not an array, try to extract the data
+
+ data = [];
+ }
+
+
+
+
+ setPrescriptions(data);
+ } catch (error) {
+
+ const errorMessage = error.response?.data?.detail || error.message || 'Failed to load prescriptions';
+ const statusCode = error.response?.status;
+ const fullError = error.response?.data;
+
+
+
+ // Log detailed error information if available
+ if (fullError?.error_type) {
+
+
+
+ if (fullError.traceback) {
+
+ }
+ }
+
+ // Show error notification for user feedback
+ if (statusCode === 400) {
+
+ notifications.show({
+ message: 'Please complete your profile setup to view prescriptions',
+ color: 'yellow',
+ });
+ setPrescriptions([]);
+ } else if (statusCode === 403) {
+ notifications.show({
+ message: 'You do not have permission to view prescriptions',
+ color: 'red',
+ });
+ setPrescriptions([]);
+ } else if (statusCode === 404) {
+
+ setPrescriptions([]);
+ } else {
+ notifications.show({
+ message: errorMessage,
+ color: 'red',
+ });
+ setPrescriptions([]);
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const getStatusColor = (status) => {
+ switch (status?.toLowerCase()) {
+ case 'active':
+ return 'green';
+ case 'expired':
+ return 'red';
+ case 'pending':
+ return 'yellow';
+ default:
+ return 'gray';
+ }
+ };
+
+ const getStatusIcon = (status) => {
+ switch (status?.toLowerCase()) {
+ case 'active':
+ return ;
+ case 'expired':
+ return ;
+ case 'pending':
+ return ;
+ default:
+ return null;
+ }
+ };
+
+ const downloadPDF = async (prescription, e) => {
+ e.stopPropagation(); // prevent card expand/collapse
+
+ const logoImg = new Image();
+ logoImg.src = instiLogo;
+ await new Promise((resolve) => {
+ logoImg.onload = resolve;
+ logoImg.onerror = resolve; // Continue even if error
+ });
+
+ const doc = new jsPDF();
+ const pageWidth = doc.internal.pageSize.getWidth();
+ let currentY = 8; // Start Y position
+
+ // ── Header bar ──────────────────────────────────────────────────────────
+ if (logoImg.complete && logoImg.naturalHeight !== 0) {
+ const aspect = logoImg.naturalWidth / logoImg.naturalHeight;
+ const maxWidth = pageWidth - 28; // 14px padding on both left and right
+
+ let imgHeight = 16;
+ let imgWidth = imgHeight * aspect;
+
+ // Safely bound the banner width to prevent it spilling outside margins
+ if (imgWidth > maxWidth) {
+ imgWidth = maxWidth;
+ imgHeight = imgWidth / aspect;
+ }
+
+ // Center the image horizontally
+ const imgX = (pageWidth - imgWidth) / 2;
+ doc.addImage(logoImg, 'PNG', imgX, currentY, imgWidth, imgHeight);
+
+ currentY += imgHeight + 6; // Move Y below the image
+ } else {
+ currentY = 20;
+ }
+
+ // Sub-title text directly below the logo
+ doc.setTextColor(100, 100, 100);
+ doc.setFontSize(12);
+ doc.setFont('helvetica', 'normal');
+ doc.text('Health Center – Medical Prescription', pageWidth / 2, currentY, { align: 'center' });
+
+ currentY += 4;
+
+ // Header Divider
+ doc.setDrawColor(30, 136, 229);
+ doc.setLineWidth(0.5);
+ doc.line(14, currentY, pageWidth - 14, currentY);
+
+ let y = currentY + 8; // Update the y tracker for the rest of the document
+ doc.setTextColor(30, 30, 30);
+
+ // ── Prescription meta ────────────────────────────────────────────────────
+ doc.setFontSize(10);
+ doc.setFont('helvetica', 'bold');
+ doc.text(`Prescription #${prescription.id}`, 14, y);
+ doc.setFont('helvetica', 'normal');
+ doc.text(
+ `Date: ${new Date(prescription.issued_date).toLocaleDateString()}`,
+ pageWidth - 14,
+ y,
+ { align: 'right' }
+ );
+ y += 7;
+ doc.text(`Patient: ${prescription.patient_name || 'N/A'}`, 14, y);
+ doc.text(
+ `Roll No: ${prescription.patient_username || 'N/A'}`,
+ pageWidth - 14,
+ y,
+ { align: 'right' }
+ );
+ y += 7;
+
+ if (prescription.is_for_dependent) {
+ doc.text(
+ `Dependent: ${prescription.dependent_name || 'N/A'} (${prescription.dependent_relation || 'N/A'})`,
+ 14,
+ y
+ );
+ y += 7;
+ }
+
+ doc.text(`Doctor: ${prescription.doctor_name || 'N/A'}`, 14, y);
+ doc.text(
+ `Status: ${prescription.status || 'PENDING'}`,
+ pageWidth - 14,
+ y,
+ { align: 'right' }
+ );
+ y += 4;
+
+ // ── Divider ──────────────────────────────────────────────────────────────
+ doc.setDrawColor(200, 200, 200);
+ doc.line(14, y, pageWidth - 14, y);
+ y += 6;
+
+ // ── Helper to add a labelled block ───────────────────────────────────────
+ const addBlock = (label, value) => {
+ if (!value) return;
+ doc.setFontSize(10);
+ doc.setFont('helvetica', 'bold');
+ doc.text(label, 14, y);
+ y += 5;
+ doc.setFont('helvetica', 'normal');
+ const lines = doc.splitTextToSize(String(value), pageWidth - 28);
+ doc.text(lines, 14, y);
+ y += lines.length * 5 + 3;
+ };
+
+ addBlock('Diagnosis / Details:', prescription.details);
+ addBlock("Doctor's Notes:", prescription.notes);
+ addBlock('Special Instructions:', prescription.special_instructions);
+ addBlock('Recommended Tests:', prescription.test_recommended);
+ addBlock('Follow-up Suggestions:', prescription.follow_up_suggestions);
+
+ // ── Medicines table ──────────────────────────────────────────────────────
+ if (prescription.prescribed_medicines?.length > 0) {
+ doc.setFontSize(10);
+ doc.setFont('helvetica', 'bold');
+ doc.text(
+ `Prescribed Medicines (${prescription.prescribed_medicines.length})`,
+ 14,
+ y
+ );
+ y += 4;
+
+ autoTable(doc, {
+ startY: y,
+ head: [['Medicine', 'Dosage', 'Frequency', 'Duration', 'Instructions', 'Notes']],
+ body: prescription.prescribed_medicines.map((m) => [
+ m.medicine_name || 'N/A',
+ m.dosage || 'N/A',
+ m.frequency || 'N/A',
+ m.duration || 'N/A',
+ m.instructions || '-',
+ m.notes || '-',
+ ]),
+ theme: 'striped',
+ headStyles: { fillColor: [30, 136, 229], textColor: 255, fontSize: 9 },
+ bodyStyles: { fontSize: 9 },
+ margin: { left: 14, right: 14 },
+ });
+
+ y = doc.lastAutoTable.finalY + 6;
+ }
+
+ addBlock('Recommended Tests:', prescription.test_recommended);
+ addBlock('Follow-up Suggestions:', prescription.follow_up_suggestions);
+ addBlock('Additional Instructions:', prescription.instructions);
+
+ // ── Footer ───────────────────────────────────────────────────────────────
+ const pageHeight = doc.internal.pageSize.getHeight();
+ doc.setDrawColor(200, 200, 200);
+ doc.line(14, pageHeight - 18, pageWidth - 14, pageHeight - 18);
+ doc.setFontSize(8);
+ doc.setTextColor(150, 150, 150);
+ doc.text('PDPM IIITDM Jabalpur Health Center – Confidential Medical Document', pageWidth / 2, pageHeight - 12, { align: 'center' });
+ doc.text(`Generated: ${new Date().toLocaleString()}`, pageWidth / 2, pageHeight - 7, { align: 'center' });
+
+ doc.save(`Prescription_${prescription.id}_${prescription.doctor_name || 'Doctor'}.pdf`);
+
+ notifications.show({
+ title: 'PDF Downloaded',
+ message: `Prescription #${prescription.id} saved as PDF.`,
+ color: 'green',
+ });
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ if (prescriptions.length === 0) {
+ return (
+
+ }>
+
+
+ You don't have any prescriptions yet. Consult with a doctor to get a prescription.
+
+
+ If you recently consulted a doctor, please refresh the page or try again shortly.
+
+
+
+
+ Prescriptions data loaded. Total: {prescriptions.length}
+
+
+ );
+ }
+
+ return (
+
+
+
+ Total Prescriptions: {prescriptions.length}
+
+
+
+ {prescriptions.map((prescription) => (
+
+ setExpandedId(expandedId === prescription.id ? null : prescription.id)
+ }
+ >
+
+ {/* Prescription Header */}
+
+
+
+ Prescription #{prescription.id}
+
+
+ Doctor: {prescription.doctor_name || 'N/A'}
+
+
+ Issued: {new Date(prescription.issued_date).toLocaleDateString()}
+
+
+
+
+ {prescription.status || 'PENDING'}
+
+
+ downloadPDF(prescription, e)}
+ aria-label={`Download prescription ${prescription.id} as PDF`}
+ >
+
+
+
+
+
+
+ {/* Prescription Details - Expandable */}
+ {expandedId === prescription.id && (
+ <>
+
+
+
+ Prescription Date
+
+
+ {new Date(prescription.issued_date).toLocaleDateString()}
+
+
+
+
+
+ Doctor
+
+
+ {prescription.doctor_name || 'N/A'}
+
+
+
+
+ {prescription.is_for_dependent && (
+
+
+
+ Dependent Patient
+
+
+ {prescription.dependent_name || 'N/A'}
+
+
+
+
+ Relationship
+
+
+ {prescription.dependent_relation || 'N/A'}
+
+
+
+ )}
+
+ {prescription.details && (
+
+
+ Diagnosis / Details
+
+ {prescription.details}
+
+ )}
+
+ {prescription.notes && (
+
+
+ Doctor's Notes
+
+ {prescription.notes}
+
+ )}
+
+ {prescription.special_instructions && (
+
+
+ Special Instructions
+
+ {prescription.special_instructions}
+
+ )}
+
+ {/* Prescribed Medicines List */}
+ {prescription.prescribed_medicines &&
+ prescription.prescribed_medicines.length > 0 && (
+
+
+ Prescribed Medicines ({prescription.total_medicines || prescription.prescribed_medicines.length})
+
+
+
+
+ Medicine
+ Dosage
+ Frequency
+ Duration
+ Instructions
+ Notes
+
+
+
+ {prescription.prescribed_medicines.map((med, idx) => (
+
+ {med.medicine_name}
+ {med.dosage || 'N/A'}
+ {med.frequency || 'N/A'}
+ {med.duration || 'N/A'}
+ {med.instructions || '-'}
+ {med.notes || '-'}
+
+ ))}
+
+
+
+ )}
+
+ {prescription.test_recommended && (
+
+
+ Recommended Tests
+
+ {prescription.test_recommended}
+
+ )}
+
+ {prescription.follow_up_suggestions && (
+
+
+ Follow-up Suggestions
+
+ {prescription.follow_up_suggestions}
+
+ )}
+
+ {prescription.instructions && (
+
+
+ Additional Instructions
+
+ {prescription.instructions}
+
+ )}
+ >
+ )}
+
+ {/* Summary Row */}
+
+ Click to {expandedId === prescription.id ? 'collapse' : 'expand'} details
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/RegisterDoctorTab.jsx b/src/Modules/HealthCenter/components/RegisterDoctorTab.jsx
new file mode 100644
index 000000000..888e3ce7e
--- /dev/null
+++ b/src/Modules/HealthCenter/components/RegisterDoctorTab.jsx
@@ -0,0 +1,54 @@
+import React from 'react';
+import { Card, Stack, TextInput, Button, Title, Text } from '@mantine/core';
+
+export default function RegisterDoctorTab({
+ doctorForm,
+ setDoctorForm,
+ onRegister,
+ loading,
+}) {
+ return (
+
+
+
+
Register New Doctor
+
+ Add a new doctor to the health center system
+
+
+
+ setDoctorForm({ ...doctorForm, name: e.currentTarget.value })
+ }
+ required
+ />
+
+ setDoctorForm({
+ ...doctorForm,
+ specialization: e.currentTarget.value,
+ })
+ }
+ required
+ />
+
+ setDoctorForm({ ...doctorForm, phone: e.currentTarget.value })
+ }
+ />
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ReimbursementReview.jsx b/src/Modules/HealthCenter/components/ReimbursementReview.jsx
new file mode 100644
index 000000000..f758dd08b
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ReimbursementReview.jsx
@@ -0,0 +1,525 @@
+/**
+ * Claims Processing Page
+ * =======================
+ * Allows PHC and Accounts staff to:
+ * - Review pending claims
+ * - Approve or reject claims
+ * - Add remarks and verification notes
+ * - Track approval workflow
+ *
+ * PHC-UC-15: Staff process claims
+ * PHC-WF-01: Multi-stage claim approval workflow
+ * PHC-BR-08: Approval threshold logic
+ */
+
+import { useEffect, useState } from 'react';
+import {
+ Container,
+ Title,
+ Text,
+ Loader,
+ Card,
+ Stack,
+ Group,
+ Badge,
+ Button,
+ Modal,
+ Textarea,
+ Table,
+ ScrollArea,
+ Tabs,
+ SimpleGrid,
+ Timeline,
+ ThemeIcon,
+ ActionIcon,
+} from '@mantine/core';
+import { IconCheck, IconX, IconEye } from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import * as api from '../api';
+
+const STATUS_COLORS = {
+ SUBMITTED: 'blue',
+ ACCOUNTS_VERIFICATION: 'yellow',
+ SANCTION_REVIEW: 'orange',
+ FINAL_PAYMENT: 'teal',
+ REIMBURSED: 'green',
+ REJECTED: 'red',
+};
+
+/**
+ * Normalize API response to always return an array
+ * Handles various response formats: array, object with .data, object with .results
+ */
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function ClaimsProcessing() {
+ const [claims, setClaims] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [processing, setProcessing] = useState(false);
+ const [claimDetailModal, setClaimDetailModal] = useState(false);
+ const [selectedClaim, setSelectedClaim] = useState(null);
+ const [decision, setDecision] = useState('');
+ const [remarks, setRemarks] = useState('');
+
+ useEffect(() => {
+ fetchPendingClaims();
+ // Auto-refresh every 30 seconds
+ const interval = setInterval(fetchPendingClaims, 30000);
+ return () => clearInterval(interval);
+ }, []);
+
+ const fetchPendingClaims = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getStaffClaims();
+ // Normalize response to always be an array
+ const claimsData = normalizeArray(response.data);
+ setClaims(claimsData);
+ } catch (error) {
+
+ setClaims([]); // Set to empty array on error
+ notifications.show({
+ message: 'Failed to load claims',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleProcessClaim = async () => {
+ if (!decision || !remarks) {
+ notifications.show({
+ message: 'Please select decision and add remarks',
+ color: 'yellow',
+ });
+ return;
+ }
+
+ try {
+ setProcessing(true);
+ await api.processStaffClaim(selectedClaim.id, decision, remarks);
+
+ notifications.show({
+ message: `Claim ${decision === 'APPROVE' ? 'approved' : 'rejected'} successfully`,
+ color: 'green',
+ });
+
+ setClaimDetailModal(false);
+ setDecision('');
+ setRemarks('');
+ setSelectedClaim(null);
+ await fetchPendingClaims();
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to process claim',
+ color: 'red',
+ });
+ } finally {
+ setProcessing(false);
+ }
+ };
+
+ if (loading && claims.length === 0) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+ Claims Processing
+ Review and process pending reimbursement claims
+
+
+ {/* Statistics */}
+
+
+
+
+
+ Total Pending
+
+
+ {claims.length}
+
+
+
+
+
+
+
+
+
+ Active Claims
+
+
+ {claims.filter((c) => c.status !== 'REJECTED' && c.status !== 'REIMBURSED').length}
+
+
+
+
+
+
+ {/* Claims Table */}
+ {claims.length > 0 ? (
+
+
+ All ({claims.length})
+ c.status === 'SUBMITTED').length})`}
+ >
+ Submitted
+
+ c.status === 'ACCOUNTS_VERIFICATION').length})`}
+ >
+ Accounts Review
+
+ c.status === 'SANCTION_REVIEW').length})`}
+ >
+ Sanction Review
+
+ c.status === 'REIMBURSED').length})`}
+ >
+ Approved
+
+ c.status === 'REJECTED').length})`}
+ >
+ Rejected
+
+
+
+
+ {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ c.status === 'SUBMITTED')}
+ onViewClaim={(claim) => {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ c.status === 'ACCOUNTS_VERIFICATION'
+ )}
+ onViewClaim={(claim) => {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ c.status === 'SANCTION_REVIEW')}
+ onViewClaim={(claim) => {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ c.status === 'REIMBURSED')}
+ onViewClaim={(claim) => {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ c.status === 'REJECTED')}
+ onViewClaim={(claim) => {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+ ) : (
+
+
+ No pending claims to process
+
+
+ )}
+
+ {/* Claim Detail Modal */}
+ {
+ setClaimDetailModal(false);
+ setSelectedClaim(null);
+ setDecision('');
+ setRemarks('');
+ }}
+ title={selectedClaim ? `Claim #${selectedClaim.id} - Review & Process` : 'Claim Details'}
+ size="lg"
+ >
+ {selectedClaim && (
+
+ {/* Claim Info */}
+
+
+
+ Amount
+
+
+ ₹{selectedClaim.claim_amount}
+
+
+
+
+ Status
+
+
+ {selectedClaim.status}
+
+
+
+
+ Submitted
+
+ {selectedClaim.submission_date}
+
+
+
+ Expense Date
+
+ {selectedClaim.expense_date}
+
+
+
+ {/* Documents */}
+ {selectedClaim.documents && selectedClaim.documents.length > 0 && (
+
+
+ Supporting Documents
+
+
+
+
+ Type
+ Verified
+
+
+
+ {selectedClaim.documents.map((doc) => (
+
+ {doc.document_type}
+
+ {doc.verified ? (
+
+ Verified
+
+ ) : (
+
+ Pending
+
+ )}
+
+
+ ))}
+
+
+
+ )}
+
+ {/* Approval History */}
+ {selectedClaim.approval_history &&
+ selectedClaim.approval_history.length > 0 && (
+
+
+ Approval History
+
+
+ {selectedClaim.approval_history.map((history, idx) => (
+
+ ) : (
+
+ )
+ }
+ title={`${history.reviewed_by} - ${history.action}`}
+ >
+
+ {history.remarks}
+
+
+ {history.review_date}
+
+
+ ))}
+
+
+ )}
+
+ {/* Processing Section */}
+ {selectedClaim.status === 'SUBMITTED' && (
+
+
+ Your Review
+
+
+ {/* Decision Selection */}
+
+
+ Decision:
+
+
+
+
+
+ {/* Remarks */}
+ setRemarks(e.currentTarget.value)}
+ minRows={3}
+ mb="md"
+ />
+
+ {/* Submit Button */}
+
+
+
+
+
+ )}
+
+ )}
+
+
+ );
+}
+
+/**
+ * Claims Table Component
+ */
+function ClaimsTable({ claims, onViewClaim }) {
+ if (claims.length === 0) {
+ return (
+
+
+ No claims in this status
+
+
+ );
+ }
+
+ return (
+
+
+
+ Claim ID
+ Patient
+ Amount
+ Status
+ Submitted
+ Action
+
+
+
+ {claims.map((claim) => (
+
+ #{claim.id}
+
+ {claim.patient_name}
+ {claim.patient_id}
+
+ ₹{claim.claim_amount}
+
+
+ {claim.status}
+
+
+ {claim.submission_date}
+
+ onViewClaim(claim)}
+ >
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ReimbursementSection.jsx b/src/Modules/HealthCenter/components/ReimbursementSection.jsx
new file mode 100644
index 000000000..ab7724275
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ReimbursementSection.jsx
@@ -0,0 +1,772 @@
+/**
+ * Reimbursement Dashboard Page
+ * =============================
+ * Allows employees to:
+ * - Submit medical expense claims
+ * - Upload supporting documents
+ * - Track claim status
+ * - View approval history
+ *
+ * PHC-UC-04: Apply for Reimbursement
+ * PHC-UC-05: Track Reimbursement Status
+ * PHC-WF-01: Multi-stage claim approval workflow
+ */
+
+import { useEffect, useState } from 'react';
+import {
+ Container,
+ Grid,
+ Title,
+ Text,
+ Loader,
+ Card,
+ Stack,
+ Group,
+ Badge,
+ Button,
+ Modal,
+ Textarea,
+ NumberInput,
+ FileInput,
+ Tabs,
+ Timeline,
+ ThemeIcon,
+ SimpleGrid,
+ TextInput,
+ Select,
+ ActionIcon,
+ Table,
+} from '@mantine/core';
+import {
+ IconCalendar,
+ IconCurrencyDollar,
+ IconFileText,
+ IconTrash,
+ IconCheck,
+ IconX,
+ IconClock,
+ IconUpload,
+ IconEye,
+} from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import * as api from '../api';
+
+const STATUSES = {
+ SUBMITTED: { label: 'Submitted', color: 'blue' },
+ ACCOUNTS_VERIFICATION: { label: 'Accounts Review', color: 'yellow' },
+ SANCTION_REVIEW: { label: 'Sanction Review', color: 'orange' },
+ FINAL_PAYMENT: { label: 'Final Payment', color: 'teal' },
+ REIMBURSED: { label: 'Reimbursed', color: 'green' },
+ REJECTED: { label: 'Rejected', color: 'red' },
+};
+
+export default function ReimbursementDashboard() {
+ const [claims, setClaims] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [submitModal, setSubmitModal] = useState(false);
+ const [claimDetailModal, setClaimDetailModal] = useState(false);
+ const [selectedClaim, setSelectedClaim] = useState(null);
+ const [submittingClaim, setSubmittingClaim] = useState(false);
+ const [uploadingDoc, setUploadingDoc] = useState(false);
+
+ // Form state
+ const [reason, setReason] = useState('');
+ const [claimAmount, setClaimAmount] = useState('');
+ const [expenseDate, setExpenseDate] = useState('');
+ const [remarks, setRemarks] = useState('');
+ const [documents, setDocuments] = useState([]);
+ const [selectedDocumentFile, setSelectedDocumentFile] = useState(null);
+ const [documentType, setDocumentType] = useState('');
+ const [prescriptions, setPrescriptions] = useState([]);
+ const [selectedPrescription, setSelectedPrescription] = useState(null);
+ const [prescriptionsLoading, setPrescriptionsLoading] = useState(false);
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ // Fetch prescriptions when modal opens
+ useEffect(() => {
+ if (submitModal) {
+ fetchPrescriptions();
+ }
+ }, [submitModal]);
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const claimsRes = await api.getReimbursementClaims();
+ const data = claimsRes.data;
+ setClaims(Array.isArray(data) ? data : Array.isArray(data?.results) ? data.results : []);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load claims data',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const resetForm = () => {
+ setReason('');
+ setClaimAmount('');
+ setExpenseDate('');
+ setRemarks('');
+ setDocuments([]);
+ setSelectedDocumentFile(null);
+ setDocumentType('');
+ setSelectedPrescription(null);
+ };
+
+ const fetchPrescriptions = async () => {
+ try {
+ setPrescriptionsLoading(true);
+
+
+
+ const response = await api.getPrescriptions();
+
+
+ const prescriptionOptions = (response.data || []).map((prescription) => ({
+ value: prescription.id.toString(),
+ label: `Prescription #${prescription.id} - ${prescription.doctor_name || 'Unknown Doctor'} (${prescription.issued_date || 'N/A'})`,
+ }));
+
+
+ setPrescriptions(prescriptionOptions);
+
+ if (prescriptionOptions.length === 0) {
+
+ notifications.show({
+ message: 'No prescriptions found. A valid prescription is required to submit a claim.',
+ color: 'blue',
+ autoClose: 3000,
+ });
+ }
+ } catch (error) {
+
+
+
+ // Show specific error messages
+ if (error.response?.status === 401 || error.response?.status === 403) {
+
+ notifications.show({
+ message: '⚠️ Not authenticated for prescriptions. Please login again.',
+ color: 'orange',
+ autoClose: false,
+ });
+ } else {
+ notifications.show({
+ message: 'Failed to load prescriptions. A valid prescription is required to submit a claim.',
+ color: 'red',
+ autoClose: 3000,
+ });
+ }
+ } finally {
+ setPrescriptionsLoading(false);
+ }
+ };
+
+ const handleSubmitClaim = async () => {
+ if (!reason || !claimAmount || !expenseDate || !selectedPrescription) {
+ notifications.show({
+ message: 'Please fill all required fields, including selecting a prescription',
+ color: 'yellow',
+ });
+ return;
+ }
+
+ try {
+ setSubmittingClaim(true);
+ const claimPayload = {
+ description: reason,
+ claim_amount: parseFloat(claimAmount),
+ expense_date: expenseDate,
+ prescription: parseInt(selectedPrescription),
+ };
+
+
+
+ const response = await api.submitReimbursementClaim(claimPayload);
+ const newClaim = response.data;
+
+ // Upload attached documents (if any were added by the user)
+ if (documents.length > 0) {
+ for (const doc of documents) {
+ await api.uploadClaimDocument(newClaim.id, doc.file, doc.type);
+ }
+ }
+
+ notifications.show({
+ message: documents.length > 0
+ ? 'Claim and documents submitted successfully'
+ : 'Claim submitted successfully',
+ color: 'green',
+ });
+
+ // Reset form and close modal
+ resetForm();
+ setSubmitModal(false);
+
+ // Refresh claims list
+ await fetchData();
+ } catch (error) {
+
+
+
+
+ // Log detailed error information
+ if (error.response?.data) {
+ const errorData = error.response.data;
+
+
+
+
+ // For validation errors, show specific field errors
+ if (typeof errorData === 'object' && !errorData.detail) {
+ const fieldErrors = Object.entries(errorData)
+ .map(([field, errors]) => `${field}: ${Array.isArray(errors) ? errors.join(', ') : errors}`)
+ .join('\n');
+
+ notifications.show({
+ message: 'Validation Error:\n' + fieldErrors,
+ color: 'red',
+ });
+ return;
+ }
+ }
+
+ // Specific message if document upload failed due to storage
+ if (error.response?.status === 503) {
+ notifications.show({
+ title: 'Document upload failed',
+ message: error.response.data?.detail || 'File storage is currently unavailable. Please try submitting without attachments or contact the administrator.',
+ color: 'red',
+ autoClose: 8000,
+ });
+ return;
+ }
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to submit claim',
+ color: 'red',
+ });
+ } finally {
+ setSubmittingClaim(false);
+ }
+ };
+
+ const handleUploadDocument = async (claimId) => {
+ if (!selectedDocumentFile || !documentType) {
+ notifications.show({
+ message: 'Please select document and type',
+ color: 'yellow',
+ });
+ return;
+ }
+
+ try {
+ setUploadingDoc(true);
+ const formData = new FormData();
+ formData.append('file', selectedDocumentFile);
+ formData.append('document_type', documentType);
+
+ await api.uploadClaimDocument(claimId, selectedDocumentFile, documentType);
+
+ notifications.show({
+ message: 'Document uploaded successfully',
+ color: 'green',
+ });
+
+ setSelectedDocumentFile(null);
+ setDocumentType('');
+ await fetchData();
+ } catch (error) {
+
+ const statusCode = error.response?.status;
+ const detail = error.response?.data?.detail;
+
+ if (statusCode === 503) {
+ // Storage unavailable
+ notifications.show({
+ title: 'Storage Unavailable',
+ message: detail || 'File storage is not configured. Please contact the administrator.',
+ color: 'orange',
+ autoClose: 8000,
+ });
+ } else {
+ notifications.show({
+ message: detail || 'Failed to upload document',
+ color: 'red',
+ });
+ }
+ } finally {
+ setUploadingDoc(false);
+ }
+ };
+
+ const handleAddDocument = () => {
+ if (selectedDocumentFile && documentType) {
+ setDocuments([
+ ...documents,
+ {
+ file: selectedDocumentFile,
+ type: documentType,
+ },
+ ]);
+ setSelectedDocumentFile(null);
+ setDocumentType('');
+ }
+ };
+
+ const removeDocument = (index) => {
+ setDocuments(documents.filter((_, i) => i !== index));
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
Reimbursement Claims
+
+ Submit and track your medical expense claims
+
+
+
+
+
+
+ {/* Claims Summary */}
+
+
+
+
+
+ Total Claims
+
+
+ {claims.length}
+
+
+
+
+
+
+
+
+
+
+ Pending Claims
+
+
+ {claims.filter((c) => ['DRAFT', 'SUBMITTED', 'PHC_REVIEW', 'ACCOUNTS_VERIFICATION', 'SANCTION_REVIEW'].includes(c.status))
+ .length}
+
+
+
+
+
+
+
+
+
+
+ Total Reimbursed
+
+
+ ₹{claims
+ .filter((c) => c.status === 'REIMBURSED')
+ .reduce((sum, c) => sum + (c.claim_amount || 0), 0)
+ .toFixed(2)}
+
+
+
+
+
+
+
+ {/* Claims List */}
+
+
+ All Claims
+ Pending
+ Approved
+ Rejected
+
+
+
+ {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ ['DRAFT', 'SUBMITTED', 'PHC_REVIEW', 'ACCOUNTS_VERIFICATION', 'SANCTION_REVIEW'].includes(c.status)
+ )}
+ onViewDetail={(claim) => {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ ['SANCTION_APPROVED', 'FINAL_PAYMENT', 'REIMBURSED'].includes(c.status))}
+ onViewDetail={(claim) => {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ c.status === 'REJECTED')}
+ onViewDetail={(claim) => {
+ setSelectedClaim(claim);
+ setClaimDetailModal(true);
+ }}
+ />
+
+
+
+ {/* Submit Claim Modal */}
+ {
+ setSubmitModal(false);
+ resetForm();
+ }}
+ title="Submit Reimbursement Claim"
+ size="lg"
+ >
+
+ {/* Reason for Claim */}
+ setReason(e.currentTarget.value)}
+ minRows={3}
+ required
+ />
+
+ {/* Prescription Selection */}
+
+
+ {/* Claim Amount */}
+
+
+ {/* Expense Date */}
+ setExpenseDate(e.currentTarget.value)}
+ type="date"
+ />
+
+ {/* Remarks */}
+ setRemarks(e.currentTarget.value)}
+ minRows={3}
+ />
+
+ {/* Document Attachment */}
+
+
+ Supporting Documents (Optional)
+
+
+ Upload PDF bills, receipts, or prescriptions. Documents can also be uploaded later.
+
+
+
+
+
+
+
+ {/* Documents List */}
+ {documents.length > 0 && (
+
+ {documents.map((doc, idx) => (
+
+ {doc.file.name}
+ removeDocument(idx)}
+ >
+
+
+
+ ))}
+
+ )}
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+
+ {/* Claim Detail Modal */}
+ {
+ setClaimDetailModal(false);
+ setSelectedClaim(null);
+ }}
+ title={selectedClaim ? `Claim #${selectedClaim.id}` : 'Claim Details'}
+ size="lg"
+ >
+ {selectedClaim && (
+
+ {/* Claim Status */}
+
+
+ Status
+
+
+ {STATUSES[selectedClaim.status]?.label || selectedClaim.status}
+
+
+
+ {/* Claim Info */}
+
+
+
+ Amount
+
+ ₹{selectedClaim.claim_amount}
+
+
+
+ Submission Date
+
+ {selectedClaim.submission_date}
+
+
+
+ {/* Uploaded Documents */}
+ {selectedClaim.documents && selectedClaim.documents.length > 0 ? (
+
+
+ Uploaded Documents
+
+
+ {selectedClaim.documents.map((doc) => (
+
+
+
+
+ {doc.document_name || 'Document'}
+ {doc.document_type} • Uploaded {new Date(doc.uploaded_at).toLocaleDateString()}
+
+
+
+ {doc.verified && (
+
+ Verified
+
+ )}
+ }
+ onClick={() => window.open(doc.document_file, '_blank')}
+ >
+ View
+
+
+
+ ))}
+
+
+ ) : (
+
+
+ Documents
+
+ No documents attached to this claim.
+
+ )}
+
+ {/* Approval History */}
+ {selectedClaim.approval_history && (
+
+
+ Approval History
+
+
+ {selectedClaim.approval_history.map((history, idx) => (
+
+ ) : (
+
+ )
+ }
+ title={`${history.reviewed_by} - ${history.action}`}
+ >
+
+ {history.remarks}
+
+
+ {history.review_date}
+
+
+ ))}
+
+
+ )}
+
+ )}
+
+
+ );
+}
+
+/**
+ * Claims List Component
+ */
+function ClaimsList({ claims, onViewDetail }) {
+ if (claims.length === 0) {
+ return (
+
+ No claims to display
+
+ );
+ }
+
+ return (
+
+ {claims.map((claim) => (
+
+
+
+
+ Claim #{claim.id}
+
+ {STATUSES[claim.status]?.label || claim.status}
+
+
+
+ Amount: ₹{claim.claim_amount} | Submitted:{' '}
+ {claim.submission_date}
+
+
+
+
+
+ ))}
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ReportsTab.jsx b/src/Modules/HealthCenter/components/ReportsTab.jsx
new file mode 100644
index 000000000..8223f309b
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ReportsTab.jsx
@@ -0,0 +1,237 @@
+/**
+ * ReportsTab.jsx
+ * =========================================================================
+ * PHC-UC-13: Generate System Reports
+ * Displays visualizations and aggregated tables for:
+ * 1. Patient Demographics
+ * 2. Consultations volume over time
+ * 3. Inventory consumption
+ * 4. Disease Patterns
+ * =========================================================================
+ */
+import { useState, useEffect } from 'react';
+import {
+ Stack, Group, Card, Text, Title, Badge, NumberInput, Select,
+ Loader, Center, Alert, RingProgress, Table,
+ ScrollArea, SimpleGrid, Grid,
+ TextInput, Button, Divider
+} from '@mantine/core';
+import {
+ IconChartBar, IconAlertTriangle, IconUser, IconPill,
+ IconStethoscope, IconCalendar, IconFilter
+} from '@tabler/icons-react';
+import * as api from '../api';
+
+export default function ReportsTab() {
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [data, setData] = useState(null);
+
+ // Default to past 30 days
+ const [startDate, setStartDate] = useState(() => {
+ const d = new Date();
+ d.setDate(d.getDate() - 30);
+ return d.toISOString().split('T')[0];
+ });
+
+ const [endDate, setEndDate] = useState(() => {
+ return new Date().toISOString().split('T')[0];
+ });
+
+ const generateReport = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const res = await api.generateSystemReport(startDate, endDate);
+ setData(res.data);
+ } catch (err) {
+ setError(err.response?.data?.detail || 'Failed to fetch report data.');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ useEffect(() => {
+ generateReport();
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ if (loading && !data) return ;
+ if (error) return } color="red">{error};
+ if (!data) return null;
+
+ // Render Helpers
+ const demographicsColors = ['blue', 'teal', 'grape', 'orange', 'cyan', 'pink'];
+ const totalDemo = Object.values(data.metrics.demographics).reduce((a, b) => a + b, 0);
+
+ const rings = Object.entries(data.metrics.demographics).map(([key, val], idx) => ({
+ value: totalDemo === 0 ? 0 : (val / totalDemo) * 100,
+ color: demographicsColors[idx % demographicsColors.length],
+ tooltip: `${key}: ${val}`,
+ label: key
+ }));
+
+ return (
+
+
+
+ System Utilization Reports
+
+ Aggregated metrics for health center engagement and stock consumption.
+
+
+
+
+ setStartDate(e.currentTarget.value)}
+ />
+ setEndDate(e.currentTarget.value)}
+ />
+ }
+ onClick={generateReport}
+ loading={loading}
+ style={{ alignSelf: 'flex-end' }}
+ >
+ Apply Dates
+
+
+
+
+
+
+
+
+
+ Total Consultations
+ {data.metrics.total_visits}
+
+
+
+
+
+
+
+
+ Report Period
+ {data.period.days} Days
+
+
+
+
+
+
+
+
+ Unique Meds Dispensed
+ {data.inventory_consumption.length}
+
+
+
+
+
+
+ {/* Demographics */}
+
+
+ Patient Demographics
+ {totalDemo > 0 ? (
+
+
+ ({ value, color, tooltip }))}
+ label={
+
+ {totalDemo}
Visits
+
+ }
+ />
+
+ {rings.map((r, i) => (
+
+ {r.label}
+
+ ))}
+
+
+
+ ) : (
+ No data for selected period
+ )}
+
+
+
+ {/* Disease Patterns */}
+
+
+ Common Diagnoses
+ {data.disease_patterns.length > 0 ? (
+
+
+
+ Disease / Diagnosis
+ Occurrences
+
+
+
+ {data.disease_patterns.map((item, idx) => (
+
+ {item.disease}
+ {item.count}
+
+ ))}
+
+
+ ) : (
+ No diagnosis data
+ )}
+
+
+
+
+ {/* Inventory Consumption */}
+
+ Top Inventory Consumption
+ {data.inventory_consumption.length > 0 ? (
+
+
+
+ Medicine Name
+ Total Units Dispensed
+
+
+
+ {data.inventory_consumption.map((item, idx) => (
+
+
+
+
+ {item.medicine_name}
+
+
+
+
+ {item.total_dispensed}
+
+
+
+ ))}
+
+
+ ) : (
+ No consumption data for this period.
+ )}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ScheduleForm.jsx b/src/Modules/HealthCenter/components/ScheduleForm.jsx
new file mode 100644
index 000000000..048445379
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ScheduleForm.jsx
@@ -0,0 +1,344 @@
+/**
+ * Schedule Form Component
+ * ======================
+ * Create/edit doctor schedules
+ * Allows compounder to assign schedules to doctors
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ Paper,
+ Select,
+ TextInput,
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ Badge,
+ ActionIcon,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import { IconEdit, IconTrash, IconPlus, IconClock } from '@tabler/icons-react';
+import * as api from '../api';
+
+const daysOfWeek = [
+ { value: 'MONDAY', label: 'Monday' },
+ { value: 'TUESDAY', label: 'Tuesday' },
+ { value: 'WEDNESDAY', label: 'Wednesday' },
+ { value: 'THURSDAY', label: 'Thursday' },
+ { value: 'FRIDAY', label: 'Friday' },
+ { value: 'SATURDAY', label: 'Saturday' },
+ { value: 'SUNDAY', label: 'Sunday' },
+];
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function ScheduleForm() {
+ const [schedules, setSchedules] = useState([]);
+ const [doctors, setDoctors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [modalOpened, setModalOpened] = useState(false);
+ const [editingId, setEditingId] = useState(null);
+ const [formData, setFormData] = useState({
+ doctor_id: '',
+ day_of_week: '',
+ start_time: '09:00',
+ end_time: '17:00',
+ room_number: '',
+ });
+ const [errors, setErrors] = useState({});
+
+ useEffect(() => {
+ fetchData();
+ }, []);
+
+ const fetchData = async () => {
+ try {
+ setLoading(true);
+ const [schedulesRes, doctorsRes] = await Promise.all([
+ api.getDoctorSchedules(),
+ api.getDoctors(),
+ ]);
+ setSchedules(Array.isArray(schedulesRes.data) ? schedulesRes.data : []);
+ setDoctors(Array.isArray(doctorsRes.data) ? doctorsRes.data : []);
+ } catch (error) {
+
+ notifications.show({ message: 'Failed to load schedules', color: 'red' });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const validateForm = () => {
+ const newErrors = {};
+ if (!formData.doctor_id) newErrors.doctor_id = 'Doctor is required';
+ if (!formData.day_of_week) newErrors.day_of_week = 'Day is required';
+ if (!formData.start_time) newErrors.start_time = 'Start time is required';
+ if (!formData.end_time) newErrors.end_time = 'End time is required';
+ if (formData.start_time && formData.end_time && formData.start_time >= formData.end_time)
+ newErrors.end_time = 'End time must be after start time';
+ if (!formData.room_number || formData.room_number < 1)
+ newErrors.room_number = 'Valid room number required';
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleSubmit = async () => {
+ if (!validateForm()) return;
+
+ try {
+ const payload = {
+ doctor_id: parseInt(formData.doctor_id),
+ day_of_week: formData.day_of_week,
+ start_time: formData.start_time,
+ end_time: formData.end_time,
+ room_number: formData.room_number || '',
+ };
+
+ if (editingId) {
+ await api.updateDoctorSchedule(editingId, payload);
+ notifications.show({
+ message: 'Schedule updated successfully',
+ color: 'green',
+ });
+ } else {
+ await api.createDoctorSchedule(payload);
+ notifications.show({
+ message: 'Schedule created successfully',
+ color: 'green',
+ });
+ }
+ setModalOpened(false);
+ resetForm();
+ await fetchData();
+ } catch (error) {
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to save schedule',
+ color: 'red',
+ });
+ }
+ };
+
+ const convertTimeStringToDate = (timeString) => {
+ if (!timeString) return null;
+ if (timeString instanceof Date) return timeString;
+
+ const [hours, minutes, seconds] = timeString.split(':').map(Number);
+ const date = new Date();
+ date.setHours(hours, minutes, seconds || 0, 0);
+ return date;
+ };
+
+ const handleEdit = (schedule) => {
+ setEditingId(schedule.id);
+ setFormData({
+ doctor_id: schedule.doctor.toString(),
+ day_of_week: schedule.day_of_week,
+ start_time: schedule.start_time || '09:00',
+ end_time: schedule.end_time || '17:00',
+ room_number: schedule.room_number.toString(),
+ });
+ setModalOpened(true);
+ };
+
+ const handleDelete = (id) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deletion',
+ children: 'Delete this schedule?',
+ labels: { confirm: 'Delete', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteDoctorSchedule(id);
+ notifications.show({
+ message: 'Schedule deleted successfully',
+ color: 'green',
+ });
+ await fetchData();
+ } catch (error) {
+ notifications.show({ message: 'Failed to delete schedule', color: 'red' });
+ }
+ }
+ });
+ };
+
+ const resetForm = () => {
+ setFormData({
+ doctor_id: '',
+ day_of_week: '',
+ start_time: '09:00',
+ end_time: '17:00',
+ room_number: '',
+ });
+ setErrors({});
+ setEditingId(null);
+ };
+
+ const getDoctorName = (doctorId) => {
+ const doctor = doctors.find((d) => d.id === doctorId);
+ return doctor ? `Dr. ${doctor.doctor_name}` : 'Unknown';
+ };
+
+ return (
+
+
+
+ Schedule Management
+
+ }
+ onClick={() => {
+ resetForm();
+ setModalOpened(true);
+ }}
+ >
+ Add Schedule
+
+
+
+
+ {schedules.length === 0 ? (
+
+ No schedules found
+
+ ) : (
+
+
+
+ Doctor
+ Day
+ Time
+ Room
+ Actions
+
+
+
+ {schedules.map((schedule) => (
+
+ {getDoctorName(schedule.doctor)}
+ {schedule.day_of_week}
+ {`${schedule.start_time} - ${schedule.end_time}`}
+ {schedule.room_number}
+
+
+ handleEdit(schedule)}
+ >
+
+
+ handleDelete(schedule.id)}
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+ {
+ resetForm();
+ setModalOpened(false);
+ }}
+ title={editingId ? 'Edit Schedule' : 'Add New Schedule'}
+ size="md"
+ >
+
+ ({
+ value: d.id.toString(),
+ label: `Dr. ${d.doctor_name} (${d.specialization})`,
+ }))}
+ value={formData.doctor_id}
+ onChange={(value) => setFormData({ ...formData, doctor_id: value })}
+ error={errors.doctor_id}
+ searchable
+ />
+
+
+ setFormData({ ...formData, day_of_week: value })
+ }
+ error={errors.day_of_week}
+ />
+
+ }
+ value={formData.start_time}
+ onChange={(e) =>
+ setFormData({ ...formData, start_time: e.currentTarget.value })
+ }
+ error={errors.start_time}
+ />
+
+ }
+ value={formData.end_time}
+ onChange={(e) =>
+ setFormData({ ...formData, end_time: e.currentTarget.value })
+ }
+ error={errors.end_time}
+ />
+
+
+ setFormData({ ...formData, room_number: e.currentTarget.value })
+ }
+ error={errors.room_number}
+ />
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/ScheduleViewer.jsx b/src/Modules/HealthCenter/components/ScheduleViewer.jsx
new file mode 100644
index 000000000..442daedd6
--- /dev/null
+++ b/src/Modules/HealthCenter/components/ScheduleViewer.jsx
@@ -0,0 +1,407 @@
+/**
+ * Doctor Availability Page
+ * =========================
+ * Displays all available doctors with schedules and status
+ * Allows patients to book appointments
+ *
+ * PHC-UC-01: View Doctor Availability
+ * PHC-UC-04: Apply for Appointment
+ */
+
+import { useEffect, useState } from 'react';
+import {
+ Container,
+ Grid,
+ Paper,
+ Title,
+ Text,
+ Button,
+ Loader,
+ Card,
+ SimpleGrid,
+ Stack,
+ Group,
+ Badge,
+ Modal,
+ Select,
+ Input,
+ Textarea,
+ NumberInput,
+ Checkbox,
+ TextInput,
+} from '@mantine/core';
+import { DatePicker } from '@mantine/dates';
+import { notifications } from '@mantine/notifications';
+import { IconCalendar, IconClock } from '@tabler/icons-react';
+import * as api from '../api';
+
+export default function ScheduleViewer() {
+ const [doctors, setDoctors] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [bookingModal, setBookingModal] = useState(false);
+ const [selectedDoctor, setSelectedDoctor] = useState(null);
+ const [bookingData, setBookingData] = useState({
+ appointment_date: null,
+ appointment_time: '10:00',
+ reason_for_visit: '',
+ symptoms: '',
+ });
+ const [submitting, setSubmitting] = useState(false);
+
+ useEffect(() => {
+ fetchDoctors();
+ }, []);
+
+ const fetchDoctors = async () => {
+ try {
+ setLoading(true);
+ const response = await api.getDoctorAvailability();
+ const doctorsArray = Array.isArray(response.data)
+ ? response.data
+ : [response.data];
+ setDoctors(doctorsArray);
+ } catch (error) {
+
+ const statusCode = error.response?.status;
+ const errorMessage = error.response?.data?.detail || error.message || 'Failed to load doctor availability';
+
+
+
+ // Handle 400 errors gracefully - likely due to missing user profile data
+ if (statusCode === 400) {
+
+ setDoctors([]);
+ notifications.show({
+ message: 'Please complete your profile setup to view doctor availability',
+ color: 'yellow',
+ });
+ } else if (statusCode === 403) {
+ notifications.show({
+ message: 'You do not have permission to view doctor availability',
+ color: 'red',
+ });
+ } else {
+ notifications.show({
+ message: errorMessage,
+ color: 'red',
+ });
+ }
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const convertTimeStringToDate = (timeString) => {
+ if (!timeString) return null;
+ if (timeString instanceof Date) return timeString;
+
+ const [hours, minutes, seconds] = timeString.split(':').map(Number);
+ const date = new Date();
+ date.setHours(hours, minutes, seconds || 0, 0);
+ return date;
+ };
+
+ const handleBooking = async () => {
+ if (!selectedDoctor || !bookingData.appointment_date || !bookingData.appointment_time) {
+ notifications.show({
+ message: 'Please fill all required fields',
+ color: 'yellow',
+ });
+ return;
+ }
+
+ try {
+ setSubmitting(true);
+ const appointmentPayload = {
+ doctor_id: selectedDoctor.id,
+ appointment_date: bookingData.appointment_date.toISOString().split('T')[0],
+ appointment_time: bookingData.appointment_time,
+ reason_for_visit: bookingData.reason_for_visit,
+ symptoms: bookingData.symptoms,
+ };
+
+ await api.createAppointment(appointmentPayload);
+
+ notifications.show({
+ message: `Appointment booked with Dr. ${selectedDoctor.doctor_name}`,
+ color: 'green',
+ });
+
+ setBookingModal(false);
+ setSelectedDoctor(null);
+ setBookingData({
+ appointment_date: null,
+ appointment_time: '10:00',
+ reason_for_visit: '',
+ symptoms: '',
+ });
+
+ // Refresh appointments list
+ await fetchDoctors();
+ } catch (error) {
+
+ notifications.show({
+ message: error.response?.data?.detail || 'Failed to book appointment',
+ color: 'red',
+ });
+ } finally {
+ setSubmitting(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Header */}
+
+ Doctor Availability
+ Select a doctor to view schedule and book an appointment
+
+
+ {/* Doctors Grid */}
+
+ {doctors.length === 0 ? (
+
+ No doctors available at the moment
+
+ ) : (
+ doctors.map((item) => {
+ // Handle both direct doctor objects and nested response structure
+ const doctorData = item.doctor || item;
+ const doctorId = doctorData.id || item.id;
+ const doctorName = doctorData.doctor_name || item.doctor_name;
+ const specialization = doctorData.specialization || item.specialization;
+ const qualifications = doctorData.qualifications || item.qualifications;
+ const phone = doctorData.phone || item.phone;
+ const email = doctorData.email || item.email;
+ const schedule = item.schedule || item.schedules || [];
+ const todaysStatus = item.todays_status || item.todays_attendance;
+
+ return (
+
+
+ {/* Doctor Info */}
+
+
+ Dr. {doctorName}
+
+
+ {specialization}
+
+ {qualifications && (
+
+ {qualifications}
+
+ )}
+
+
+ {/* Contact Info */}
+
+ {phone && (
+
+ 📞 {phone}
+
+ )}
+ {email && (
+
+ ✉️ {email}
+
+ )}
+
+
+ {/* Divider */}
+ {todaysStatus &&
}
+
+ {/* Today's Schedule Status Section */}
+ {todaysStatus && (
+
+
+ 📅 Today's Status
+
+
+
+ {todaysStatus.status}
+
+ {todaysStatus.available_until && (
+
+ Until {todaysStatus.available_until}
+
+ )}
+
+
+ )}
+
+ {/* Weekly Schedule Section */}
+ {schedule && schedule.length > 0 && (
+
+
+ 📆 Weekly Schedule
+
+
+ {schedule.map((slot) => (
+
+
+ {slot.day_of_week}
+
+
+ {slot.start_time} - {slot.end_time} (Room {slot.room_number})
+
+
+ ))}
+
+
+ )}
+
+ {/* Action Button */}
+
+
+
+ );
+ })
+ )}
+
+
+ {/* Booking Modal */}
+ {
+ setBookingModal(false);
+ setSelectedDoctor(null);
+ }}
+ title={selectedDoctor ? `Book Appointment - Dr. ${selectedDoctor.doctor_name}` : 'Book Appointment'}
+ size="lg"
+ >
+
+ {/* Doctor Info Summary */}
+ {selectedDoctor && (
+
+
+ Doctor: Dr. {selectedDoctor.doctor_name}
+
+
+ Specialization: {selectedDoctor.specialization}
+
+
+ )}
+
+ {/* Date Selection */}
+
+
+ Preferred Date *
+
+
+ setBookingData({ ...bookingData, appointment_date: date })
+ }
+ minDate={new Date()}
+ />
+
+
+ {/* Time Selection */}
+
+
+ Preferred Time *
+
+
+ setBookingData({ ...bookingData, appointment_time: e.currentTarget.value })
+ }
+ />
+
+
+ {/* Reason for Visit */}
+
+
+ Reason for Visit
+
+
+ setBookingData({
+ ...bookingData,
+ reason_for_visit: e.currentTarget.value,
+ })
+ }
+ />
+
+
+ {/* Symptoms */}
+
+
+ Symptoms
+
+
+ setBookingData({ ...bookingData, symptoms: e.currentTarget.value })
+ }
+ />
+
+
+ {/* Action Buttons */}
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/StatisticsCards.jsx b/src/Modules/HealthCenter/components/StatisticsCards.jsx
new file mode 100644
index 000000000..4533f2f0a
--- /dev/null
+++ b/src/Modules/HealthCenter/components/StatisticsCards.jsx
@@ -0,0 +1,55 @@
+import React from 'react';
+import { SimpleGrid, Card, Group, Text } from '@mantine/core';
+import {
+ IconFileText,
+ IconAlertTriangle,
+ IconPackage,
+} from '@tabler/icons-react';
+
+export default function StatisticsCards({ claims, alerts, requisitions }) {
+ return (
+
+
+
+
+
+ Pending Claims
+
+
+ {Array.isArray(claims) ? claims.length : 0}
+
+
+
+
+
+
+
+
+
+
+ Low Stock Alerts
+
+
+ {Array.isArray(alerts) ? alerts.length : 0}
+
+
+
+
+
+
+
+
+
+
+ Pending Requisitions
+
+
+ {Array.isArray(requisitions) ? requisitions.length : 0}
+
+
+
+
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/StockForm.jsx b/src/Modules/HealthCenter/components/StockForm.jsx
new file mode 100644
index 000000000..93156cab8
--- /dev/null
+++ b/src/Modules/HealthCenter/components/StockForm.jsx
@@ -0,0 +1,514 @@
+/**
+ * Stock Form Component
+ * ===================
+ * Add/manage medicine stock
+ * Track inventory and manage medicine availability
+ */
+
+import { useState, useEffect } from 'react';
+import {
+ TextInput,
+ Button,
+ Stack,
+ Group,
+ Card,
+ Table,
+ ScrollArea,
+ Text,
+ Modal,
+ ActionIcon,
+ NumberInput,
+ Textarea,
+ Select,
+} from '@mantine/core';
+import { notifications } from '@mantine/notifications';
+import { modals } from '@mantine/modals';
+import { IconPlus, IconEdit, IconTrash } from '@tabler/icons-react';
+import * as api from '../api';
+
+const normalizeArray = (data) => {
+ if (Array.isArray(data)) return data;
+ if (data?.results && Array.isArray(data.results)) return data.results;
+ if (data?.data && Array.isArray(data.data)) return data.data;
+ return [];
+};
+
+export default function StockForm() {
+ const [medicineOptions, setMedicineOptions] = useState([]);
+ const [stockList, setStockList] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [stockModalOpened, setStockModalOpened] = useState(false);
+ const [medicineModalOpened, setMedicineModalOpened] = useState(false);
+ const [editingId, setEditingId] = useState(null);
+ const [stockFormData, setStockFormData] = useState({
+ medicine_id: '',
+ total_qty: '',
+ expiry_date: new Date().toISOString().split('T')[0],
+ batch_no: '',
+ });
+ const [medicineFormData, setMedicineFormData] = useState({
+ medicine_name: '',
+ brand_name: '',
+ generic_name: '',
+ manufacturer_name: '',
+ unit: 'tablets',
+ pack_size_label: '',
+ reorder_threshold: 10,
+ });
+ const [stockErrors, setStockErrors] = useState({});
+ const [medicineErrors, setMedicineErrors] = useState({});
+
+ useEffect(() => {
+ loadData();
+ }, []);
+
+ const loadData = async () => {
+ try {
+ setLoading(true);
+ await Promise.all([loadMedicineOptions(), loadStockList()]);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load data',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const loadMedicineOptions = async () => {
+ try {
+ const response = await api.getMedicines();
+ const options = normalizeArray(response.data).map(med => ({
+ value: String(med.id),
+ label: med.pack_size_label ? `${med.medicine_name} (${med.pack_size_label})` : med.medicine_name,
+ }));
+ setMedicineOptions(options);
+ } catch (error) {
+
+ }
+ };
+
+ const loadStockList = async () => {
+ try {
+ const response = await api.getStock();
+
+ setStockList(normalizeArray(response.data));
+ } catch (error) {
+
+ }
+ };
+
+ const validateStockForm = () => {
+ const newErrors = {};
+ if (!stockFormData.medicine_id || stockFormData.medicine_id === '') newErrors.medicine_id = 'Medicine is required';
+ if (!stockFormData.total_qty || stockFormData.total_qty === '' || Number(stockFormData.total_qty) <= 0) newErrors.total_qty = 'Quantity must be > 0';
+ if (!stockFormData.expiry_date) newErrors.expiry_date = 'Expiry date is required';
+ if (!stockFormData.batch_no.trim()) newErrors.batch_no = 'Batch number is required';
+ setStockErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const validateMedicineForm = () => {
+ const newErrors = {};
+ if (!medicineFormData.medicine_name.trim()) newErrors.medicine_name = 'Medicine name is required';
+ if (!medicineFormData.generic_name.trim()) newErrors.generic_name = 'Generic name is required';
+ if (medicineFormData.reorder_threshold <= 0) newErrors.reorder_threshold = 'Reorder threshold must be > 0';
+ setMedicineErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleStockSubmit = async () => {
+ if (!validateStockForm()) return;
+
+ try {
+ const payload = {
+ medicine_id: Number(stockFormData.medicine_id),
+ total_qty: Number(stockFormData.total_qty),
+ expiry_date: stockFormData.expiry_date,
+ batch_no: stockFormData.batch_no.trim(),
+ };
+
+
+
+ if (editingId) {
+ await api.updateStock(editingId, payload);
+ notifications.show({
+ message: 'Stock updated successfully',
+ color: 'green',
+ });
+ } else {
+ await api.addStock(payload);
+ notifications.show({
+ message: 'Stock added successfully',
+ color: 'green',
+ });
+ }
+ setStockModalOpened(false);
+ resetStockForm();
+ await loadStockList();
+ } catch (error) {
+
+ const errorMsg = error.response?.data?.errors?.detail || error.response?.data?.detail || error.response?.data?.batch_no?.[0] || error.response?.data?.medicine_id?.[0] || error.response?.data?.qty?.[0] || error.response?.data?.expiry_date?.[0] || 'Failed to save stock';
+ notifications.show({
+ message: errorMsg,
+ color: 'red',
+ });
+ }
+ };
+
+ const handleMedicineSubmit = async () => {
+ if (!validateMedicineForm()) return;
+
+ try {
+ const payload = {
+ medicine_name: medicineFormData.medicine_name.trim(),
+ brand_name: medicineFormData.brand_name.trim(),
+ generic_name: medicineFormData.generic_name.trim(),
+ manufacturer_name: medicineFormData.manufacturer_name.trim(),
+ unit: medicineFormData.unit,
+ pack_size_label: medicineFormData.pack_size_label.trim(),
+ reorder_threshold: Number(medicineFormData.reorder_threshold),
+ };
+
+
+
+ const response = await api.addMedicine(payload);
+ notifications.show({
+ message: 'Medicine added successfully',
+ color: 'green',
+ });
+
+ setMedicineModalOpened(false);
+ resetMedicineForm();
+
+ // Reload medicines and set the newly added medicine as selected
+ await loadMedicineOptions();
+ const newMedicineId = response.data.id || response.data.medicine_id;
+ setStockFormData(prev => ({ ...prev, medicine_id: String(newMedicineId) }));
+ } catch (error) {
+
+ const errorMsg = error.response?.data?.errors?.detail || error.response?.data?.detail || error.response?.data?.medicine_name?.[0] || error.response?.data?.generic_name?.[0] || 'Failed to save medicine';
+ notifications.show({
+ message: errorMsg,
+ color: 'red',
+ });
+ }
+ };
+
+ const handleEdit = (stock) => {
+ setEditingId(stock.id);
+ setStockFormData({
+ medicine_id: stock.medicine_id || stock.medicine,
+ total_qty: stock.qty || stock.total_qty,
+ expiry_date: stock.expiry_date || new Date().toISOString().split('T')[0],
+ batch_no: stock.batch_no || '',
+ });
+ setStockModalOpened(true);
+ };
+
+ const handleDelete = (id) => {
+ modals.openConfirmModal({
+ title: 'Confirm Deletion',
+ children: 'Delete this medicine from stock?',
+ labels: { confirm: 'Delete', cancel: 'Cancel' },
+ confirmProps: { color: 'red' },
+ onConfirm: async () => {
+ try {
+ await api.deleteStock(id);
+ notifications.show({
+ message: 'Medicine deleted successfully',
+ color: 'green',
+ });
+ await loadStockList();
+ } catch (error) {
+ notifications.show({ message: 'Failed to delete medicine', color: 'red' });
+ }
+ }
+ });
+ };
+
+ const resetStockForm = () => {
+ setStockFormData({
+ medicine_id: '',
+ total_qty: '',
+ expiry_date: new Date().toISOString().split('T')[0],
+ batch_no: '',
+ });
+ setStockErrors({});
+ setEditingId(null);
+ };
+
+ const resetMedicineForm = () => {
+ setMedicineFormData({
+ medicine_name: '',
+ brand_name: '',
+ generic_name: '',
+ manufacturer_name: '',
+ unit: 'tablets',
+ pack_size_label: '',
+ reorder_threshold: 10,
+ });
+ setMedicineErrors({});
+ };
+
+ return (
+
+
+
+ Stock Management
+
+
+ }
+ variant="light"
+ onClick={() => {
+ resetMedicineForm();
+ setMedicineModalOpened(true);
+ }}
+ >
+ Add Medicine
+
+ }
+ onClick={() => {
+ resetStockForm();
+ setStockModalOpened(true);
+ }}
+ >
+ Add Stock
+
+
+
+
+
+ {stockList.length === 0 ? (
+
+ No medicines in stock
+
+ ) : (
+
+
+
+ Medicine
+ Generic Name
+ Pack Size
+ Quantity
+ Batches
+ Actions
+
+
+
+ {stockList.map((stock) => {
+ const med = stock.medicine_detail || {};
+ return (
+
+ {med.medicine_name || 'N/A'}
+ {med.generic_name || 'N/A'}
+ {med.pack_size_label || 'N/A'}
+
+ {stock.total_qty || 0} {med.unit || 'units'}
+
+
+ {stock.expiry_batches && stock.expiry_batches.length > 0
+ ? `${stock.expiry_batches.length} batch(es)`
+ : 'No batches'}
+
+
+
+ handleEdit(stock)}
+ >
+
+
+ handleDelete(stock.id)}
+ >
+
+
+
+
+
+ );
+ })}
+
+
+ )}
+
+
+ {
+ resetStockForm();
+ setStockModalOpened(false);
+ }}
+ title={editingId ? 'Edit Stock' : 'Add Stock'}
+ size="md"
+ >
+
+ {
+ setStockFormData({ ...stockFormData, medicine_id: value || '' });
+ }}
+ error={stockErrors.medicine_id}
+ />
+
+ {
+ setStockFormData({ ...stockFormData, total_qty: value === undefined ? '' : String(value) });
+ }}
+ error={stockErrors.total_qty}
+ min={1}
+ />
+
+ {
+ setStockFormData({ ...stockFormData, expiry_date: e.currentTarget.value });
+ }}
+ error={stockErrors.expiry_date}
+ />
+
+ {
+ setStockFormData({ ...stockFormData, batch_no: e.currentTarget.value });
+ }}
+ error={stockErrors.batch_no}
+ />
+
+
+
+
+
+
+
+
+ {
+ resetMedicineForm();
+ setMedicineModalOpened(false);
+ }}
+ title="Add New Medicine"
+ size="md"
+ >
+
+ {
+ setMedicineFormData({ ...medicineFormData, medicine_name: e.currentTarget.value });
+ }}
+ error={medicineErrors.medicine_name}
+ />
+
+ {
+ setMedicineFormData({ ...medicineFormData, brand_name: e.currentTarget.value });
+ }}
+ />
+
+ {
+ setMedicineFormData({ ...medicineFormData, generic_name: e.currentTarget.value });
+ }}
+ error={medicineErrors.generic_name}
+ />
+
+ {
+ setMedicineFormData({ ...medicineFormData, manufacturer_name: e.currentTarget.value });
+ }}
+ />
+
+ {
+ setMedicineFormData({ ...medicineFormData, unit: value || 'tablets' });
+ }}
+ />
+
+ {
+ setMedicineFormData({ ...medicineFormData, pack_size_label: e.currentTarget.value });
+ }}
+ />
+
+ {
+ setMedicineFormData({ ...medicineFormData, reorder_threshold: value || 10 });
+ }}
+ error={medicineErrors.reorder_threshold}
+ min={1}
+ />
+
+
+
+
+
+
+
+
+ );
+}
+
diff --git a/src/Modules/HealthCenter/components/StockManagement.jsx b/src/Modules/HealthCenter/components/StockManagement.jsx
new file mode 100644
index 000000000..90231419c
--- /dev/null
+++ b/src/Modules/HealthCenter/components/StockManagement.jsx
@@ -0,0 +1,552 @@
+/**
+ * Inventory Management Page
+ * ==========================
+ * Allows PHC staff to:
+ * - View current medicinie inventory
+ * - Update stock quantities
+ * - View expiring medicines
+ * - Track low stock alerts
+ *
+ * PHC-UC-09: Manage Inventory
+ * PHC-UC-18: View Low Stock Alerts
+ * PHC-BR-07: Low stock threshold logic
+ */
+
+import { useEffect, useState } from 'react';
+import {
+ Container,
+ Title,
+ Text,
+ Loader,
+ Card,
+ Stack,
+ Group,
+ Badge,
+ Button,
+ Modal,
+ NumberInput,
+ Select,
+ Table,
+ ScrollArea,
+ Tabs,
+ SimpleGrid,
+ TextInput,
+ ActionIcon,
+ Tooltip,
+} from '@mantine/core';
+import {
+ IconAlertTriangle,
+ IconPackage,
+ IconClock,
+ IconEdit,
+ IconSearch,
+} from '@tabler/icons-react';
+import { notifications } from '@mantine/notifications';
+import * as api from '../api';
+
+export default function InventoryManagement() {
+ const [inventory, setInventory] = useState([]);
+ const [lowStockAlerts, setLowStockAlerts] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [searchQuery, setSearchQuery] = useState('');
+ const [filterExpired, setFilterExpired] = useState('all'); // all, expiring, expired
+
+ // Stock update modal
+ const [stockModal, setStockModal] = useState(false);
+ const [selectedMedicine, setSelectedMedicine] = useState(null);
+ const [quantityChange, setQuantityChange] = useState('');
+ const [updateReason, setUpdateReason] = useState('');
+ const [updatingStock, setUpdatingStock] = useState(false);
+
+ useEffect(() => {
+ fetchInventoryData();
+ }, []);
+
+ const fetchInventoryData = async () => {
+ try {
+ setLoading(true);
+ const [inventoryRes, alertsRes] = await Promise.all([
+ api.getInventory(),
+ api.getLowStockAlerts(),
+ ]);
+
+ setInventory(inventoryRes.data || []);
+ setLowStockAlerts(alertsRes.data || []);
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to load inventory data',
+ color: 'red',
+ });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleStockUpdate = async () => {
+ if (!quantityChange || !updateReason) {
+ notifications.show({
+ message: 'Please fill all required fields',
+ color: 'yellow',
+ });
+ return;
+ }
+
+ try {
+ setUpdatingStock(true);
+ await api.updateInventoryStock(
+ selectedMedicine.id,
+ parseInt(quantityChange),
+ updateReason
+ );
+
+ notifications.show({
+ message: 'Stock updated successfully',
+ color: 'green',
+ });
+
+ setStockModal(false);
+ setSelectedMedicine(null);
+ setQuantityChange('');
+ setUpdateReason('');
+ await fetchInventoryData();
+ } catch (error) {
+
+ notifications.show({
+ message: 'Failed to update stock',
+ color: 'red',
+ });
+ } finally {
+ setUpdatingStock(false);
+ }
+ };
+
+ // Filter inventory based on search and expiry filter
+ const getFilteredInventory = () => {
+ let filtered = inventory;
+
+ // Search filter
+ if (searchQuery) {
+ filtered = filtered.filter((item) =>
+ item.medicine_name
+ .toLowerCase()
+ .includes(searchQuery.toLowerCase())
+ );
+ }
+
+ // Expiry filter
+ if (filterExpired === 'expired') {
+ const today = new Date();
+ filtered = filtered.filter((item) => {
+ if (!item.expiry_date) return false;
+ return new Date(item.expiry_date) < today;
+ });
+ } else if (filterExpired === 'expiring') {
+ const today = new Date();
+ const thirtyDaysLater = new Date(today.getTime() + 30 * 24 * 60 * 60 * 1000);
+ filtered = filtered.filter((item) => {
+ if (!item.expiry_date) return false;
+ const expiry = new Date(item.expiry_date);
+ return expiry >= today && expiry <= thirtyDaysLater;
+ });
+ }
+
+ return filtered;
+ };
+
+ if (loading) {
+ return (
+
+
+
+
+
+ );
+ }
+
+ const filteredInventory = getFilteredInventory();
+
+ return (
+
+ {/* Header */}
+
+ Inventory Management
+ Manage medicine stock and track alerts
+
+
+ {/* Statistics */}
+
+
+
+
+
+ Total Medicines
+
+
+ {inventory.length}
+
+
+
+
+
+
+
+
+
+
+ Low Stock Alerts
+
+
+ {lowStockAlerts.length}
+
+
+
+
+
+
+
+
+
+
+ Expiring Soon
+
+
+ {inventory.filter((item) => {
+ if (!item.expiry_date) return false;
+ const today = new Date();
+ const thirtyDaysLater = new Date(
+ today.getTime() + 30 * 24 * 60 * 60 * 1000
+ );
+ const expiry = new Date(item.expiry_date);
+ return expiry >= today && expiry <= thirtyDaysLater;
+ }).length}
+
+
+
+
+
+
+
+ {/* Tabs */}
+
+
+ All Inventory
+ Low Stock ({lowStockAlerts.length})
+
+ Expired
+
+
+ {/* All Inventory Tab */}
+
+
+ }
+ value={searchQuery}
+ onChange={(e) => setSearchQuery(e.currentTarget.value)}
+ />
+
+
+ {
+ setSelectedMedicine(medicine);
+ setStockModal(true);
+ }}
+ />
+
+
+ {/* Low Stock Tab */}
+
+ {lowStockAlerts.length > 0 ? (
+ {
+ setSelectedMedicine({
+ id: medicine.medicine_id,
+ medicine_name: medicine.medicine_name,
+ current_stock: medicine.current_stock,
+ reorder_threshold: medicine.threshold,
+ });
+ setStockModal(true);
+ }}
+ />
+ ) : (
+
+
+ No low stock alerts
+
+
+ )}
+
+
+ {/* Expiring Tab */}
+
+ {(() => {
+ const today = new Date();
+ const thirtyDaysLater = new Date(
+ today.getTime() + 30 * 24 * 60 * 60 * 1000
+ );
+ const expiringItems = inventory.filter((item) => {
+ if (!item.expiry_date) return false;
+ const expiry = new Date(item.expiry_date);
+ return expiry >= today && expiry <= thirtyDaysLater;
+ });
+
+ return expiringItems.length > 0 ? (
+ {}} />
+ ) : (
+
+
+ No medicines expiring soon
+
+
+ );
+ })()}
+
+
+ {/* Expired Tab */}
+
+ {(() => {
+ const today = new Date();
+ const expiredItems = inventory.filter((item) => {
+ if (!item.expiry_date) return false;
+ return new Date(item.expiry_date) < today;
+ });
+
+ return expiredItems.length > 0 ? (
+ {}} />
+ ) : (
+
+
+ No expired medicines
+
+
+ );
+ })()}
+
+
+
+ {/* Stock Update Modal */}
+ {
+ setStockModal(false);
+ setSelectedMedicine(null);
+ setQuantityChange('');
+ setUpdateReason('');
+ }}
+ title={
+ selectedMedicine
+ ? `Update Stock - ${selectedMedicine.medicine_name}`
+ : 'Update Stock'
+ }
+ size="md"
+ >
+
+ {selectedMedicine && (
+
+
+
+
+ Current Stock
+
+
+ {selectedMedicine.current_stock} units
+
+
+
+
+ Reorder Threshold
+
+
+ {selectedMedicine.reorder_threshold} units
+
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+/**
+ * Inventory Table Component
+ */
+function InventoryTable({ data, onUpdateStock }) {
+ if (data.length === 0) {
+ return (
+
+
+ No items to display
+
+
+ );
+ }
+
+ return (
+
+
+
+ Medicine Name
+ Current Stock
+ Batch Number
+ Expiry Date
+ Supplier
+ Action
+
+
+
+ {data.map((item) => {
+ const isExpired =
+ item.expiry_date &&
+ new Date(item.expiry_date) < new Date();
+ const isExpiringSoon =
+ item.expiry_date &&
+ new Date(item.expiry_date) <
+ new Date(new Date().getTime() + 30 * 24 * 60 * 60 * 1000) &&
+ new Date(item.expiry_date) >= new Date();
+
+ return (
+
+ {item.medicine_name}
+
+
+ {item.quantity_remaining}
+
+
+ {item.batch_number || '-'}
+
+ {item.expiry_date && (
+
+ {item.expiry_date}
+
+ )}
+
+ {item.supplier || '-'}
+
+
+ onUpdateStock(item)}
+ >
+
+
+
+
+
+ );
+ })}
+
+
+ );
+}
+
+/**
+ * Low Stock Alerts Table Component
+ */
+function LowStockTable({ alerts, onUpdateStock }) {
+ return (
+
+
+
+ Medicine
+ Current Stock
+ Threshold
+ Status
+ Action
+
+
+
+ {alerts.map((alert) => (
+
+ {alert.medicine_name}
+
+ {alert.current_stock}
+
+ {alert.threshold}
+
+ Critical
+
+
+
+ onUpdateStock(alert)}
+ >
+
+
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/TodaysScheduleTab.jsx b/src/Modules/HealthCenter/components/TodaysScheduleTab.jsx
new file mode 100644
index 000000000..c00e983f6
--- /dev/null
+++ b/src/Modules/HealthCenter/components/TodaysScheduleTab.jsx
@@ -0,0 +1,221 @@
+import React, { useState } from 'react';
+import { Card, Alert, Table,
+ ScrollArea, Title, Badge, Stack, Text, Group, Button } from '@mantine/core';
+import { IconClock, IconUserCheck } from '@tabler/icons-react';
+
+export default function TodaysScheduleTab({ todaysSchedule, allDoctors = [] }) {
+ const [viewMode, setViewMode] = useState('fixed');
+
+ const getStatusColor = (status) => {
+ const colorMap = {
+ AVAILABLE: 'green',
+ ON_BREAK: 'yellow',
+ SCHEDULED: 'blue',
+ DEPARTED: 'red',
+ };
+ return colorMap[status] || 'gray';
+ };
+
+ const formatDateTime = (dateTimeString) => {
+ if (!dateTimeString) return 'Not recorded';
+ try {
+ const date = new Date(dateTimeString);
+ const time = date.toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit',
+ hour12: true,
+ });
+ return time;
+ } catch {
+ return dateTimeString;
+ }
+ };
+
+ // For attendance view: combine scheduled doctors with any additional doctors with attendance records
+ const getAttendanceList = () => {
+ const attendanceMap = new Map();
+
+ // First, add all scheduled doctors
+ todaysSchedule.forEach(schedule => {
+ const key = `${schedule.doctor_id}`;
+ if (!attendanceMap.has(key)) {
+ attendanceMap.set(key, {
+ doctor_id: schedule.doctor_id,
+ doctor_name: schedule.doctor_name,
+ specialization: schedule.specialization,
+ todays_status: schedule.todays_status,
+ in_fixed_schedule: true,
+ });
+ }
+ });
+
+ // Then, add any additional doctors with attendance records (not in fixed schedule)
+ allDoctors.forEach(doctorItem => {
+ const doctorData = doctorItem.doctor || doctorItem;
+ const key = `${doctorData.id}`;
+ const hasAttendance = doctorItem.todays_status && (
+ doctorItem.todays_status.status_label ||
+ doctorItem.todays_status.marked_at
+ );
+
+ // Only add if has attendance and not already added
+ if (hasAttendance && !attendanceMap.has(key)) {
+ attendanceMap.set(key, {
+ doctor_id: doctorData.id,
+ doctor_name: doctorData.doctor_name,
+ specialization: doctorData.specialization,
+ todays_status: doctorItem.todays_status,
+ in_fixed_schedule: false,
+ });
+ }
+ });
+
+ return Array.from(attendanceMap.values());
+ };
+
+ return (
+
+ {Array.isArray(todaysSchedule) && todaysSchedule.length > 0 ? (
+ <>
+ {/* View Toggle Buttons */}
+
+ }
+ variant={viewMode === 'fixed' ? 'filled' : 'light'}
+ onClick={() => setViewMode('fixed')}
+ >
+ Fixed Schedule
+
+ }
+ variant={viewMode === 'attendance' ? 'filled' : 'light'}
+ onClick={() => setViewMode('attendance')}
+ >
+ Today's Attendance
+
+
+
+ {/* ═══════════════════════════════════════════ */}
+ {/* VIEW 1: Fixed Schedule */}
+ {/* ═══════════════════════════════════════════ */}
+ {viewMode === 'fixed' && (
+
+
+ Today's Doctor Schedule ({todaysSchedule.length} doctor{todaysSchedule.length !== 1 ? 's' : ''})
+
+
+
+
+ Doctor
+ Specialization
+ Time Slot
+ Room
+
+
+
+ {todaysSchedule.map((schedule) => (
+
+
+ Dr. {schedule.doctor_name}
+
+ {schedule.specialization || 'N/A'}
+
+
+
+
+ {schedule.start_time} - {schedule.end_time}
+
+
+
+
+
+ {schedule.room_number || 'N/A'}
+
+
+
+ ))}
+
+
+
+ )}
+
+ {/* ═══════════════════════════════════════════ */}
+ {/* VIEW 2: Today's Attendance */}
+ {/* ═══════════════════════════════════════════ */}
+ {viewMode === 'attendance' && (
+
+
+
+ Today's Doctor Attendance
+
+ {getAttendanceList().length > 0 ? (
+
+
+
+ Doctor
+ Status
+ Time Recorded
+ Notes
+ Type
+
+
+
+ {getAttendanceList().map((doctor) => {
+ const attendance = doctor.todays_status;
+ return (
+
+
+ Dr. {doctor.doctor_name}
+ {doctor.specialization || 'N/A'}
+
+
+ {attendance && attendance.status_label ? (
+
+ {attendance.status_label}
+
+ ) : (
+ Not Recorded
+ )}
+
+
+ {attendance && attendance.marked_at ? (
+ {formatDateTime(attendance.marked_at)}
+ ) : (
+ N/A
+ )}
+
+
+
+ {attendance && attendance.notes ? attendance.notes : '-'}
+
+
+
+
+ {doctor.in_fixed_schedule ? 'Scheduled' : 'Extra'}
+
+
+
+ );
+ })}
+
+
+ ) : (
+
+ No doctor attendance has been recorded today.
+
+ )}
+
+
+ )}
+ >
+ ) : (
+
+ No doctors are scheduled to work today. Check back tomorrow!
+
+ )}
+
+ );
+}
diff --git a/src/Modules/HealthCenter/components/UpdateStatusTab.jsx b/src/Modules/HealthCenter/components/UpdateStatusTab.jsx
new file mode 100644
index 000000000..87e2590cf
--- /dev/null
+++ b/src/Modules/HealthCenter/components/UpdateStatusTab.jsx
@@ -0,0 +1,104 @@
+import React from 'react';
+import { Card, Stack, Select, Button, Title, Text, SimpleGrid, Group, Badge } from '@mantine/core';
+
+const STATUS_OPTIONS = [
+ { value: 'SCHEDULED', label: 'Scheduled' },
+ { value: 'AVAILABLE', label: 'Available' },
+ { value: 'ON_BREAK', label: 'On Break' },
+ { value: 'DEPARTED', label: 'Departed' },
+];
+
+export default function UpdateStatusTab({
+ attendanceForm,
+ setAttendanceForm,
+ doctors,
+ todaysSchedule,
+ onUpdateAttendance,
+ loading,
+}) {
+ const getStatusColor = (status) => {
+ switch (status) {
+ case 'AVAILABLE':
+ return 'green';
+ case 'ON_BREAK':
+ return 'yellow';
+ default:
+ return 'red';
+ }
+ };
+
+ return (
+
+
+
+
+
Update Doctor Status
+
+ Mark doctor attendance for today
+
+
+
+ setAttendanceForm({ ...attendanceForm, doctor_id: value })
+ }
+ data={
+ Array.isArray(doctors)
+ ? doctors.map((doc) => ({
+ value: doc.id.toString(),
+ label: doc.doctor_name,
+ }))
+ : []
+ }
+ required
+ />
+
+ setAttendanceForm({ ...attendanceForm, status: value })
+ }
+ data={STATUS_OPTIONS}
+ required
+ />
+
+
+
+
+ {/* Current Status Display */}
+ {Array.isArray(todaysSchedule) && todaysSchedule.length > 0 ? (
+
+
+ Today's Doctor Status
+
+
+ {todaysSchedule.map((schedule) => (
+
+
+
+ {schedule.doctor_name}
+
+ {schedule.specialization}
+
+
+
+ {schedule.status}
+
+
+
+ ))}
+
+
+ ) : null}
+
+ );
+}
diff --git a/src/Modules/HealthCenter/config.js b/src/Modules/HealthCenter/config.js
new file mode 100644
index 000000000..52a4f66df
--- /dev/null
+++ b/src/Modules/HealthCenter/config.js
@@ -0,0 +1,340 @@
+/**
+ * Health Center Module Configuration
+ * ====================================
+ *
+ * Centralized configuration for the Health Center module
+ * Adjust these values to customize module behavior
+ */
+
+// ============================================================================
+// API CONFIGURATION
+// ============================================================================
+
+export const API_CONFIG = {
+ // Base URL for all API requests
+ BASE_URL: import.meta.env.VITE_API_BASE_URL || '/api/phc',
+
+ // Request timeout in milliseconds
+ TIMEOUT: 30000,
+
+ // Enable request/response logging
+ DEBUG_MODE: import.meta.env.MODE === 'development',
+
+ // Retry configuration
+ RETRY: {
+ ENABLED: true,
+ MAX_ATTEMPTS: 3,
+ DELAY_MS: 1000,
+ },
+};
+
+// ============================================================================
+// FEATURE FLAGS
+// ============================================================================
+
+export const FEATURES = {
+ // Enable health center module
+ ENABLED: import.meta.env.VITE_ENABLE_HEALTH_CENTER !== 'false',
+
+ // Enable patient features
+ PATIENT_FEATURES: {
+ BOOK_APPOINTMENTS: true,
+ VIEW_MEDICAL_HISTORY: true,
+ SUBMIT_CLAIMS: true,
+ MANAGE_HEALTH_PROFILE: true,
+ },
+
+ // Enable staff features
+ STAFF_FEATURES: {
+ PROCESS_CLAIMS: true,
+ MANAGE_INVENTORY: true,
+ VIEW_ALERTS: true,
+ MARK_DOCTOR_ATTENDANCE: true,
+ },
+
+ // Advanced features
+ ADVANCED: {
+ ENABLE_PRESCRIPTION_RENEWAL: false,
+ ENABLE_DOCTOR_NOTES: true,
+ ENABLE_TELEHEALTH: false,
+ ENABLE_ANALYTICS: false,
+ },
+};
+
+// ============================================================================
+// STATUS CONFIGURATION
+// ============================================================================
+
+export const STATUS_CONFIG = {
+ // Appointment statuses
+ APPOINTMENT_STATUS: {
+ SCHEDULED: { label: 'Scheduled', color: 'blue', icon: 'calendar-plus' },
+ COMPLETED: { label: 'Completed', color: 'green', icon: 'check' },
+ CANCELLED: { label: 'Cancelled', color: 'red', icon: 'x' },
+ CHECKED_IN: { label: 'Checked In', color: 'teal', icon: 'clock' },
+ },
+
+ // Claim statuses with workflow stages
+ CLAIM_STATUS: {
+ SUBMITTED: { label: 'Submitted', color: 'blue', stage: 1 },
+ ACCOUNTS_VERIFICATION: { label: 'Accounts Review', color: 'yellow', stage: 2 },
+ SANCTION_REVIEW: { label: 'Sanction Review', color: 'orange', stage: 3 },
+ FINAL_PAYMENT: { label: 'Final Payment', color: 'teal', stage: 4 },
+ REIMBURSED: { label: 'Reimbursed', color: 'green', stage: 5 },
+ REJECTED: { label: 'Rejected', color: 'red', stage: 0 },
+ },
+
+ // Doctor availability statuses
+ DOCTOR_STATUS: {
+ AVAILABLE: { label: 'Available', color: 'green', icon: 'check-circle' },
+ ON_BREAK: { label: 'On Break', color: 'yellow', icon: 'pause-circle' },
+ DEPARTED: { label: 'Departed', color: 'red', icon: 'x-circle' },
+ SCHEDULED: { label: 'Scheduled', color: 'blue', icon: 'calendar' },
+ },
+
+ // Stock level statuses
+ STOCK_STATUS: {
+ IN_STOCK: { label: 'In Stock', color: 'green' },
+ LOW_STOCK: { label: 'Low Stock', color: 'yellow' },
+ OUT_OF_STOCK: { label: 'Out of Stock', color: 'red' },
+ EXPIRING: { label: 'Expiring Soon', color: 'orange' },
+ EXPIRED: { label: 'Expired', color: 'red' },
+ },
+};
+
+// ============================================================================
+// VALIDATION RULES
+// ============================================================================
+
+export const VALIDATION_RULES = {
+ // Appointment booking rules
+ APPOINTMENT: {
+ MIN_DAYS_IN_ADVANCE: 1,
+ MAX_DAYS_IN_ADVANCE: 30,
+ CANCEL_WITHIN_HOURS: 24,
+ },
+
+ // Claim submission rules
+ CLAIM: {
+ MAX_SUBMISSION_DAYS: 30, // PHC-BR-06: Within 30 days of expense
+ MIN_AMOUNT: 100,
+ MAX_AMOUNT: 100000,
+ REQUIRED_DOCUMENTS: 1,
+ },
+
+ // Document upload rules
+ DOCUMENT: {
+ MAX_FILE_SIZE_MB: 5,
+ ALLOWED_TYPES: ['application/pdf', 'image/jpeg', 'image/png'],
+ ALLOWED_EXTENSIONS: ['.pdf', '.jpg', '.jpeg', '.png'],
+ },
+
+ // Inventory rules
+ INVENTORY: {
+ MIN_QUANTITY: 0,
+ REORDER_THRESHOLD_DEFAULT: 50,
+ EXPIRY_WARNING_DAYS: 30,
+ },
+};
+
+// ============================================================================
+// PAGINATION AND TABLE CONFIGURATION
+// ============================================================================
+
+export const TABLE_CONFIG = {
+ // Default page size for tables
+ PAGE_SIZE: 10,
+
+ // Available page sizes
+ PAGE_SIZE_OPTIONS: [5, 10, 25, 50, 100],
+
+ // Enable/disable features
+ ENABLE_SEARCH: true,
+ ENABLE_SORT: true,
+ ENABLE_FILTER: true,
+ ENABLE_EXPORT: false,
+};
+
+// ============================================================================
+// AUTO-REFRESH CONFIGURATION
+// ============================================================================
+
+export const REFRESH_CONFIG = {
+ // Auto-refresh interval (milliseconds)
+ STAFF_DASHBOARD_INTERVAL: 30000, // 30 seconds
+ CLAIMS_LIST_INTERVAL: 30000,
+ INVENTORY_INTERVAL: 60000, // 60 seconds
+
+ // Enable auto-refresh
+ ENABLE_AUTO_REFRESH: true,
+
+ // Show refresh indicator
+ SHOW_REFRESH_INDICATOR: true,
+};
+
+// ============================================================================
+// NOTIFICATION CONFIGURATION
+// ============================================================================
+
+export const NOTIFICATION_CONFIG = {
+ // Notification position
+ POSITION: 'top-right',
+
+ // Auto-hide duration (milliseconds)
+ AUTO_CLOSE_DURATION: 4000,
+
+ // Enable notifications
+ ENABLED: true,
+
+ // Notification types
+ TYPES: {
+ SUCCESS: { color: 'green', icon: 'check' },
+ ERROR: { color: 'red', icon: 'x' },
+ WARNING: { color: 'yellow', icon: 'alert' },
+ INFO: { color: 'blue', icon: 'info' },
+ },
+};
+
+// ============================================================================
+// ACCESSIBILITY CONFIGURATION
+// ============================================================================
+
+export const A11Y_CONFIG = {
+ // Enable keyboard navigation
+ KEYBOARD_NAVIGATION: true,
+
+ // Enable screen reader support
+ SCREEN_READER_SUPPORT: true,
+
+ // Minimum button size (in pixels)
+ MIN_BUTTON_SIZE: 44,
+
+ // High contrast mode
+ HIGH_CONTRAST_MODE: false,
+
+ // Reduce motion for animations
+ REDUCE_MOTION: false,
+};
+
+// ============================================================================
+// DATE AND TIME FORMATTING
+// ============================================================================
+
+export const DATE_TIME_CONFIG = {
+ // Date format (using standard pattern)
+ DATE_FORMAT: 'YYYY-MM-DD',
+
+ // Time format
+ TIME_FORMAT: 'HH:mm',
+
+ // Full datetime format
+ DATETIME_FORMAT: 'YYYY-MM-DD HH:mm',
+
+ // Display format for users
+ DISPLAY_DATE_FORMAT: 'MMM DD, YYYY',
+ DISPLAY_TIME_FORMAT: 'h:mm A',
+ DISPLAY_DATETIME_FORMAT: 'MMM DD, YYYY h:mm A',
+
+ // Time zone handling
+ USE_TIMEZONE: false,
+ DEFAULT_TIMEZONE: 'UTC',
+};
+
+// ============================================================================
+// ROLE-BASED CONFIGURATION
+// ============================================================================
+
+export const ROLE_CONFIG = {
+ // Available roles in the module
+ ROLES: {
+ PATIENT: 'patient',
+ STAFF: 'staff',
+ PHC_STAFF: 'phc_staff',
+ ACCOUNTS_STAFF: 'accounts_staff',
+ DOCTOR: 'doctor',
+ ADMIN: 'admin',
+ },
+
+ // Feature access by role
+ ROLE_FEATURES: {
+ patient: ['book_appointment', 'view_medical_history', 'submit_claim', 'track_claim'],
+ staff: ['book_appointment', 'view_medical_history', 'submit_claim', 'track_claim'],
+ phc_staff: [
+ 'process_claims',
+ 'manage_inventory',
+ 'view_alerts',
+ 'mark_attendance',
+ 'view_reports',
+ ],
+ accounts_staff: ['process_claims', 'generate_reports', 'audit_claims'],
+ doctor: ['manage_patients', 'prescribe_medicine', 'mark_attendance'],
+ admin: ['all_features'],
+ },
+};
+
+// ============================================================================
+// EXPORT UTILITY FUNCTIONS
+// ============================================================================
+
+/**
+ * Get status configuration by key
+ * @param {string} statusType - Status type (e.g., 'CLAIM_STATUS')
+ * @param {string} statusValue - Status value (e.g., 'SUBMITTED')
+ * @returns {object} Status configuration
+ */
+export function getStatusConfig(statusType, statusValue) {
+ const config = STATUS_CONFIG[statusType];
+ return config ? config[statusValue] : null;
+}
+
+/**
+ * Check if feature is enabled
+ * @param {string} featurePath - Dot notation path (e.g., 'PATIENT_FEATURES.BOOK_APPOINTMENTS')
+ * @returns {boolean} Feature enabled
+ */
+export function isFeatureEnabled(featurePath) {
+ const keys = featurePath.split('.');
+ let value = FEATURES;
+ for (const key of keys) {
+ value = value[key];
+ if (value === undefined) return false;
+ }
+ return value === true;
+}
+
+/**
+ * Get user role permissions
+ * @param {string} role - User role
+ * @returns {array} List of permissions
+ */
+export function getRolePermissions(role) {
+ return ROLE_CONFIG.ROLE_FEATURES[role] || [];
+}
+
+/**
+ * Check if user has permission
+ * @param {string} role - User role
+ * @param {string} permission - Required permission
+ * @returns {boolean} Has permission
+ */
+export function hasPermission(role, permission) {
+ const permissions = getRolePermissions(role);
+ return permissions.includes(permission) || permissions.includes('all_features');
+}
+
+// ============================================================================
+// EXPORT ALL CONFIGURATION
+// ============================================================================
+
+export default {
+ API_CONFIG,
+ FEATURES,
+ STATUS_CONFIG,
+ VALIDATION_RULES,
+ TABLE_CONFIG,
+ REFRESH_CONFIG,
+ NOTIFICATION_CONFIG,
+ A11Y_CONFIG,
+ DATE_TIME_CONFIG,
+ ROLE_CONFIG,
+};
diff --git a/src/Modules/HealthCenter/index.jsx b/src/Modules/HealthCenter/index.jsx
new file mode 100644
index 000000000..e82c8e4cb
--- /dev/null
+++ b/src/Modules/HealthCenter/index.jsx
@@ -0,0 +1,168 @@
+/**
+ * Health Center Module Routing
+ * =============================
+ *
+ * Configures all routes for the Health Center module
+ * Routes are split by user role (patient/staff) for proper access control
+ */
+
+import { lazy } from 'react';
+import ProtectedRoute from '../../routes/ProtectedRoute';
+import ErrorBoundary from './components/ErrorBoundary';
+
+// Lazy load pages for code splitting
+const PatientDashboard = lazy(() => import('./PatientDashboard'));
+const CompoundDashboard = lazy(() => import('./CompoundDashboard'));
+// AuditorDashboard removed — auditor review is not part of PHC module scope.
+// Backend API endpoints for auditor actions remain in views.py.
+
+/**
+ * Get Health Center routes
+ * Format: { path, element, requiresAuth, roles }
+ *
+ * Routes are consolidated into dashboard views with tabs:
+ * - PatientDashboard: Doctor Schedules, Prescriptions, Complaints, Reimbursement
+ * - CompoundDashboard: Statistics, Claims, Alerts, Doctor Management, Inventory
+ */
+export const getHealthCenterRoutes = () => [
+ // Patient routes - all consolidated into PatientDashboard tabs
+ {
+ path: '/health_center/dashboard',
+ element: ,
+ requiresAuth: true,
+ roles: ['patient', 'staff'],
+ label: 'Dashboard',
+ },
+ {
+ path: '/health_center/doctor-availability',
+ element: ,
+ requiresAuth: true,
+ roles: ['patient', 'staff'],
+ label: 'Browse Doctors',
+ },
+ {
+ path: '/health_center/medical-history',
+ element: ,
+ requiresAuth: true,
+ roles: ['patient', 'staff'],
+ label: 'Medical History',
+ },
+ {
+ path: '/health_center/reimbursement',
+ element: ,
+ requiresAuth: true,
+ roles: ['patient', 'staff'],
+ label: 'Reimbursement Claims',
+ },
+
+ // Staff routes - all consolidated into CompoundDashboard tabs
+ {
+ path: '/health_center/staff/dashboard',
+ element: ,
+ requiresAuth: true,
+ roles: ['phc_staff', 'accounts_staff', 'auditor'],
+ label: 'Staff Dashboard',
+ },
+ {
+ path: '/health_center/claims-processing',
+ element: ,
+ requiresAuth: true,
+ roles: ['phc_staff', 'accounts_staff', 'auditor'],
+ label: 'Claims Processing',
+ },
+ {
+ path: '/health_center/claims-processing/:claimId',
+ element: ,
+ requiresAuth: true,
+ roles: ['phc_staff', 'accounts_staff', 'auditor'],
+ },
+ {
+ path: '/health_center/inventory',
+ element: ,
+ requiresAuth: true,
+ roles: ['phc_staff'],
+ label: 'Inventory Management',
+ },
+ // Auditor routes removed — not part of PHC module scope.
+ // Backend endpoints remain available at /api/phc/auditor/*
+];
+
+/**
+ * Generate protected routes for React Router
+ * Usage: getHealthCenterRoutes().map(route => generateRoute(route))
+ */
+export const generateRoutes = (routes) => {
+ return routes.map((route) => ({
+ path: route.path,
+ element: route.requiresAuth ? (
+
+ {route.element}
+
+ ) : (
+ route.element
+ ),
+ }));
+};
+
+/**
+ * Navigation menu items (for sidebar/navigation)
+ */
+export const getHealthCenterNavItems = () => {
+ return [
+ {
+ section: 'PATIENT',
+ items: [
+ {
+ label: 'Dashboard',
+ href: '/health_center/dashboard',
+ icon: 'IconHome',
+ roles: ['patient', 'staff'],
+ },
+ {
+ label: 'Book Appointment',
+ href: '/health_center/doctor-availability',
+ icon: 'IconCalendar',
+ roles: ['patient', 'staff'],
+ },
+ {
+ label: 'Medical Records',
+ href: '/health_center/medical-history',
+ icon: 'IconFileText',
+ roles: ['patient', 'staff'],
+ },
+ {
+ label: 'Reimbursement',
+ href: '/health_center/reimbursement',
+ icon: 'IconDollar',
+ roles: ['patient', 'staff'],
+ },
+ ],
+ },
+ {
+ section: 'STAFF',
+ items: [
+ {
+ label: 'Staff Dashboard',
+ href: '/health_center/staff/dashboard',
+ icon: 'IconLayoutDashboard',
+ roles: ['phc_staff', 'accounts_staff', 'auditor'],
+ },
+ {
+ label: 'Process Claims',
+ href: '/health_center/claims-processing',
+ icon: 'IconClipboardCheck',
+ roles: ['phc_staff', 'accounts_staff', 'auditor'],
+ },
+ {
+ label: 'Inventory',
+ href: '/health_center/inventory',
+ icon: 'IconPackage',
+ roles: ['phc_staff'],
+ },
+ // Auditor Dashboard nav item removed — not part of PHC scope
+ ],
+ },
+ ];
+};
+
+export default getHealthCenterRoutes;
diff --git a/src/Modules/HealthCenter/utils.js b/src/Modules/HealthCenter/utils.js
new file mode 100644
index 000000000..43a801dfc
--- /dev/null
+++ b/src/Modules/HealthCenter/utils.js
@@ -0,0 +1,586 @@
+/**
+ * Health Center Module Utilities
+ * ===============================
+ *
+ * Shared utility functions used across all pages
+ * Includes formatting, validation, and helper functions
+ */
+
+import {
+ STATUS_CONFIG,
+ VALIDATION_RULES,
+ DATE_TIME_CONFIG,
+ ROLE_CONFIG,
+} from './config';
+
+// ============================================================================
+// DATE AND TIME UTILITIES
+// ============================================================================
+
+/**
+ * Format date to YYYY-MM-DD
+ * @param {Date | string} date - Date to format
+ * @returns {string} Formatted date
+ */
+export function formatDate(date) {
+ if (!date) return '';
+ const d = new Date(date);
+ const month = String(d.getMonth() + 1).padStart(2, '0');
+ const day = String(d.getDate()).padStart(2, '0');
+ return `${d.getFullYear()}-${month}-${day}`;
+}
+
+/**
+ * Format time to HH:MM
+ * @param {string} time - Time string (HH:MM:SS format)
+ * @returns {string} Formatted time (HH:MM)
+ */
+export function formatTime(time) {
+ if (!time) return '';
+ const [hours, minutes] = time.split(':');
+ return `${hours}:${minutes}`;
+}
+
+/**
+ * Format date for display (e.g., "Jan 15, 2024")
+ * @param {Date | string} date - Date to format
+ * @returns {string} Display formatted date
+ */
+export function formatDateForDisplay(date) {
+ if (!date) return '';
+ return new Date(date).toLocaleDateString('en-US', {
+ year: 'numeric',
+ month: 'short',
+ day: 'numeric',
+ });
+}
+
+/**
+ * Check if date is in the past
+ * @param {string | Date} date - Date to check
+ * @returns {boolean} True if date is in past
+ */
+export function isPastDate(date) {
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const checkDate = new Date(date);
+ checkDate.setHours(0, 0, 0, 0);
+ return checkDate < today;
+}
+
+/**
+ * Check if date is within N days (for expiry warnings)
+ * @param {string} expiryDate - Expiry date
+ * @param {number} days - Number of days to check within
+ * @returns {boolean} True if expiring within N days
+ */
+export function isExpiringWithin(expiryDate, days = 30) {
+ if (!expiryDate) return false;
+ const today = new Date();
+ const expiry = new Date(expiryDate);
+ const daysUntilExpiry = Math.floor(
+ (expiry - today) / (1000 * 60 * 60 * 24)
+ );
+ return daysUntilExpiry <= days && daysUntilExpiry >= 0;
+}
+
+/**
+ * Check if date is expired
+ * @param {string} expiryDate - Expiry date
+ * @returns {boolean} True if expired
+ */
+export function isExpired(expiryDate) {
+ if (!expiryDate) return false;
+ const today = new Date();
+ const expiry = new Date(expiryDate);
+ return expiry < today;
+}
+
+/**
+ * Get days until expiry
+ * @param {string} expiryDate - Expiry date
+ * @returns {number} Number of days until expiry (-1 if expired)
+ */
+export function daysUntilExpiry(expiryDate) {
+ if (!expiryDate) return null;
+ const today = new Date();
+ const expiry = new Date(expiryDate);
+ const days = Math.floor(
+ (expiry - today) / (1000 * 60 * 60 * 24)
+ );
+ return days;
+}
+
+// ============================================================================
+// STATUS AND BADGE UTILITIES
+// ============================================================================
+
+/**
+ * Get badge color for claim status
+ * @param {string} status - Claim status
+ * @returns {string} Badge color
+ */
+export function getClaimStatusColor(status) {
+ const config = STATUS_CONFIG.CLAIM_STATUS[status];
+ return config ? config.color : 'gray';
+}
+
+/**
+ * Get label for claim status
+ * @param {string} status - Claim status
+ * @returns {string} Status label
+ */
+export function getClaimStatusLabel(status) {
+ const config = STATUS_CONFIG.CLAIM_STATUS[status];
+ return config ? config.label : status;
+}
+
+/**
+ * Get badge color for appointment status
+ * @param {string} status - Appointment status
+ * @returns {string} Badge color
+ */
+export function getAppointmentStatusColor(status) {
+ const config = STATUS_CONFIG.APPOINTMENT_STATUS[status];
+ return config ? config.color : 'gray';
+}
+
+/**
+ * Get badge color for doctor availability status
+ * @param {string} status - Doctor status
+ * @returns {string} Badge color
+ */
+export function getDoctorStatusColor(status) {
+ const config = STATUS_CONFIG.DOCTOR_STATUS[status];
+ return config ? config.color : 'gray';
+}
+
+/**
+ * Get badge color for stock status
+ * @param {number} currentStock - Current stock quantity
+ * @param {number} threshold - Reorder threshold
+ * @returns {string} Badge color
+ */
+export function getStockStatusColor(currentStock, threshold) {
+ if (currentStock === 0) return 'red'; // Out of stock
+ if (currentStock < threshold) return 'yellow'; // Low stock
+ return 'green'; // In stock
+}
+
+/**
+ * Get stock level label
+ * @param {number} currentStock - Current stock quantity
+ * @param {number} threshold - Reorder threshold
+ * @returns {string} Stock label
+ */
+export function getStockStatusLabel(currentStock, threshold) {
+ if (currentStock === 0) return 'Out of Stock';
+ if (currentStock < threshold) return 'Low Stock';
+ return 'In Stock';
+}
+
+// ============================================================================
+// VALIDATION UTILITIES
+// ============================================================================
+
+/**
+ * Validate claim amount
+ * @param {number} amount - Claim amount
+ * @returns {object} Validation result {valid, error}
+ */
+export function validateClaimAmount(amount) {
+ if (!amount || amount <= 0) {
+ return { valid: false, error: 'Amount must be greater than 0' };
+ }
+ if (amount < VALIDATION_RULES.CLAIM.MIN_AMOUNT) {
+ return {
+ valid: false,
+ error: `Minimum claim amount is ₹${VALIDATION_RULES.CLAIM.MIN_AMOUNT}`,
+ };
+ }
+ if (amount > VALIDATION_RULES.CLAIM.MAX_AMOUNT) {
+ return {
+ valid: false,
+ error: `Maximum claim amount is ₹${VALIDATION_RULES.CLAIM.MAX_AMOUNT}`,
+ };
+ }
+ return { valid: true };
+}
+
+/**
+ * Validate claim submission date (within 30 days)
+ * @param {string} expenseDate - Expense date
+ * @returns {object} Validation result {valid, error}
+ */
+export function validateClaimDate(expenseDate) {
+ if (!expenseDate) {
+ return { valid: false, error: 'Please select an expense date' };
+ }
+
+ const expenseTime = new Date(expenseDate).getTime();
+ const nowTime = new Date().getTime();
+ const daysDiff = Math.floor((nowTime - expenseTime) / (1000 * 60 * 60 * 24));
+
+ if (daysDiff < 0) {
+ return { valid: false, error: 'Expense date cannot be in the future' };
+ }
+
+ if (daysDiff > VALIDATION_RULES.CLAIM.MAX_SUBMISSION_DAYS) {
+ return {
+ valid: false,
+ error: `Claims must be submitted within ${VALIDATION_RULES.CLAIM.MAX_SUBMISSION_DAYS} days of expense`,
+ };
+ }
+
+ return { valid: true };
+}
+
+/**
+ * Validate appointment date
+ * @param {Date} appointmentDate - Appointment date
+ * @returns {object} Validation result {valid, error}
+ */
+export function validateAppointmentDate(appointmentDate) {
+ if (!appointmentDate) {
+ return { valid: false, error: 'Please select an appointment date' };
+ }
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const apptDate = new Date(appointmentDate);
+ apptDate.setHours(0, 0, 0, 0);
+
+ const minDate = new Date(today);
+ minDate.setDate(minDate.getDate() + VALIDATION_RULES.APPOINTMENT.MIN_DAYS_IN_ADVANCE);
+
+ const maxDate = new Date(today);
+ maxDate.setDate(maxDate.getDate() + VALIDATION_RULES.APPOINTMENT.MAX_DAYS_IN_ADVANCE);
+
+ if (apptDate < minDate) {
+ return {
+ valid: false,
+ error: `Appointments can be booked ${VALIDATION_RULES.APPOINTMENT.MIN_DAYS_IN_ADVANCE} day(s) in advance`,
+ };
+ }
+
+ if (apptDate > maxDate) {
+ return {
+ valid: false,
+ error: `Appointments can only be booked up to ${VALIDATION_RULES.APPOINTMENT.MAX_DAYS_IN_ADVANCE} days in advance`,
+ };
+ }
+
+ return { valid: true };
+}
+
+/**
+ * Validate file upload
+ * @param {File} file - File to validate
+ * @returns {object} Validation result {valid, error}
+ */
+export function validateFileUpload(file) {
+ if (!file) {
+ return { valid: false, error: 'Please select a file' };
+ }
+
+ const maxSizeBytes = VALIDATION_RULES.DOCUMENT.MAX_FILE_SIZE_MB * 1024 * 1024;
+ if (file.size > maxSizeBytes) {
+ return {
+ valid: false,
+ error: `File size must be less than ${VALIDATION_RULES.DOCUMENT.MAX_FILE_SIZE_MB}MB`,
+ };
+ }
+
+ if (!VALIDATION_RULES.DOCUMENT.ALLOWED_TYPES.includes(file.type)) {
+ return {
+ valid: false,
+ error: `File type not allowed. Allowed types: ${VALIDATION_RULES.DOCUMENT.ALLOWED_TYPES.join(', ')}`,
+ };
+ }
+
+ return { valid: true };
+}
+
+// ============================================================================
+// PERMISSION UTILITIES
+// ============================================================================
+
+/**
+ * Check if user role can access feature
+ * @param {string} userRole - User's role
+ * @param {string} requiredPermission - Required permission
+ * @returns {boolean} Can access feature
+ */
+export function canAccessFeature(userRole, requiredPermission) {
+ const permissions = ROLE_CONFIG.ROLE_FEATURES[userRole] || [];
+ return (
+ permissions.includes(requiredPermission) ||
+ permissions.includes('all_features')
+ );
+}
+
+/**
+ * Check if user can book appointments
+ * @param {string} userRole - User's role
+ * @returns {boolean} Can book
+ */
+export function canBookAppointment(userRole) {
+ return canAccessFeature(userRole, 'book_appointment');
+}
+
+/**
+ * Check if user can submit claims
+ * @param {string} userRole - User's role
+ * @returns {boolean} Can submit
+ */
+export function canSubmitClaim(userRole) {
+ return canAccessFeature(userRole, 'submit_claim');
+}
+
+/**
+ * Check if user can process claims
+ * @param {string} userRole - User's role
+ * @returns {boolean} Can process
+ */
+export function canProcessClaim(userRole) {
+ return canAccessFeature(userRole, 'process_claims');
+}
+
+/**
+ * Check if user can manage inventory
+ * @param {string} userRole - User's role
+ * @returns {boolean} Can manage
+ */
+export function canManageInventory(userRole) {
+ return canAccessFeature(userRole, 'manage_inventory');
+}
+
+// ============================================================================
+// FORMATTING UTILITIES
+// ============================================================================
+
+/**
+ * Format currency (Indian Rupees)
+ * @param {number} amount - Amount to format
+ * @returns {string} Formatted currency
+ */
+export function formatCurrency(amount) {
+ if (!amount && amount !== 0) return '₹0';
+ return `₹${amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`;
+}
+
+/**
+ * Format percentage
+ * @param {number} value - Value to format
+ * @param {number} decimals - Decimal places
+ * @returns {string} Formatted percentage
+ */
+export function formatPercentage(value, decimals = 0) {
+ if (!value && value !== 0) return '0%';
+ return `${value.toFixed(decimals)}%`;
+}
+
+/**
+ * Format large numbers with abbreviation
+ * @param {number} num - Number to format
+ * @returns {string} Formatted number
+ */
+export function formatNumber(num) {
+ if (num < 1000) return num.toString();
+ if (num < 1000000) return `${(num / 1000).toFixed(1)}K`;
+ if (num < 1000000000) return `${(num / 1000000).toFixed(1)}M`;
+ return `${(num / 1000000000).toFixed(1)}B`;
+}
+
+/**
+ * Truncate text to specified length
+ * @param {string} text - Text to truncate
+ * @param {number} length - Maximum length
+ * @returns {string} Truncated text
+ */
+export function truncateText(text, length = 50) {
+ if (!text || text.length <= length) return text;
+ return `${text.substring(0, length)}...`;
+}
+
+// ============================================================================
+// WORKFLOW UTILITIES
+// ============================================================================
+
+/**
+ * Get workflow stage number from claim status
+ * @param {string} status - Claim status
+ * @returns {number} Stage number
+ */
+export function getClaimWorkflowStage(status) {
+ const config = STATUS_CONFIG.CLAIM_STATUS[status];
+ return config ? config.stage : 0;
+}
+
+/**
+ * Get next workflow stage
+ * @param {string} currentStatus - Current claim status
+ * @returns {string} Next status (if approved)
+ */
+export function getNextClaimStatus(currentStatus) {
+ const stageMap = {
+ SUBMITTED: 'ACCOUNTS_VERIFICATION',
+ ACCOUNTS_VERIFICATION: 'SANCTION_REVIEW',
+ SANCTION_REVIEW: 'FINAL_PAYMENT',
+ FINAL_PAYMENT: 'REIMBURSED',
+ };
+ return stageMap[currentStatus] || currentStatus;
+}
+
+/**
+ * Get workflow progress percentage
+ * @param {string} status - Claim status
+ * @returns {number} Progress 0-100
+ */
+export function getClaimWorkflowProgress(status) {
+ const stage = getClaimWorkflowStage(status);
+ const totalStages = 5;
+ return status === 'REJECTED' ? 0 : (stage / totalStages) * 100;
+}
+
+// ============================================================================
+// COMMON CALCULATIONS
+// ============================================================================
+
+/**
+ * Calculate days between two dates
+ * @param {Date} startDate - Start date
+ * @param {Date} endDate - End date
+ * @returns {number} Number of days
+ */
+export function daysBetween(startDate, endDate) {
+ const start = new Date(startDate);
+ const end = new Date(endDate);
+ const diffTime = Math.abs(end - start);
+ return Math.ceil(diffTime / (1000 * 60 * 60 * 24));
+}
+
+/**
+ * Calculate age from date of birth
+ * @param {string | Date} dob - Date of birth
+ * @returns {number} Age in years
+ */
+export function calculateAge(dob) {
+ const birthDate = new Date(dob);
+ const today = new Date();
+ let age = today.getFullYear() - birthDate.getFullYear();
+ const monthDiff = today.getMonth() - birthDate.getMonth();
+
+ if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
+ age--;
+ }
+
+ return age;
+}
+
+/**
+ * Calculate average of numbers
+ * @param {array} numbers - Array of numbers
+ * @returns {number} Average
+ */
+export function calculateAverage(numbers) {
+ if (!numbers || numbers.length === 0) return 0;
+ const sum = numbers.reduce((a, b) => a + b, 0);
+ return sum / numbers.length;
+}
+
+// ============================================================================
+// ERROR HANDLING UTILITIES
+// ============================================================================
+
+/**
+ * Extract error message from API response
+ * @param {object} error - Axios error object
+ * @returns {string} Error message
+ */
+export function getErrorMessage(error) {
+ if (!error) return 'An unknown error occurred';
+
+ // Axios error
+ if (error.response) {
+ const data = error.response.data;
+ if (typeof data === 'string') return data;
+ if (data.detail) return data.detail;
+ if (data.message) return data.message;
+ if (data.error) return data.error;
+ }
+
+ // Network error
+ if (error.message) return error.message;
+
+ return 'An unknown error occurred';
+}
+
+/**
+ * Format error for display
+ * @param {object} error - Error object
+ * @returns {object} Formatted error {title, message}
+ */
+export function formatError(error) {
+ const message = getErrorMessage(error);
+ return {
+ title: 'Error',
+ message: truncateText(message, 200),
+ };
+}
+
+// ============================================================================
+// EXPORT ALL UTILITIES
+// ============================================================================
+
+export default {
+ // Date utilities
+ formatDate,
+ formatTime,
+ formatDateForDisplay,
+ isPastDate,
+ isExpiringWithin,
+ isExpired,
+ daysUntilExpiry,
+
+ // Status utilities
+ getClaimStatusColor,
+ getClaimStatusLabel,
+ getAppointmentStatusColor,
+ getDoctorStatusColor,
+ getStockStatusColor,
+ getStockStatusLabel,
+
+ // Validation utilities
+ validateClaimAmount,
+ validateClaimDate,
+ validateAppointmentDate,
+ validateFileUpload,
+
+ // Permission utilities
+ canAccessFeature,
+ canBookAppointment,
+ canSubmitClaim,
+ canProcessClaim,
+ canManageInventory,
+
+ // Formatting utilities
+ formatCurrency,
+ formatPercentage,
+ formatNumber,
+ truncateText,
+
+ // Workflow utilities
+ getClaimWorkflowStage,
+ getNextClaimStatus,
+ getClaimWorkflowProgress,
+
+ // Calculations
+ daysBetween,
+ calculateAge,
+ calculateAverage,
+
+ // Error handling
+ getErrorMessage,
+ formatError,
+};
diff --git a/src/components/sidebarContent.jsx b/src/components/sidebarContent.jsx
index b77581b39..d47b1dbb4 100644
--- a/src/components/sidebarContent.jsx
+++ b/src/components/sidebarContent.jsx
@@ -30,6 +30,7 @@ import {
Question as HelpIcon,
User as ProfileIcon,
Gear as SettingsIcon,
+ ShieldCheck as AuditorIcon,
CaretRight,
CaretLeft,
} from "@phosphor-icons/react";
@@ -84,7 +85,7 @@ function SidebarContent({ isCollapsed, toggleSidebar }) {
label: "HealthCare Center",
id: "phc",
icon: ,
- url: "/",
+ url: "/health-center",
},
{
label: "File Tracking",
@@ -193,7 +194,9 @@ function SidebarContent({ isCollapsed, toggleSidebar }) {
const navigate = useNavigate();
useEffect(() => {
- const filterModules = Modules.filter(
+ let updatedModules = [...Modules];
+
+ const filterModules = updatedModules.filter(
(module) => accessibleModules[module.id] || module.id === "home",
);
setFilteredModules(filterModules);
diff --git a/src/routes/healthCenterRoutes/index.jsx b/src/routes/healthCenterRoutes/index.jsx
new file mode 100644
index 000000000..5c0fd6c74
--- /dev/null
+++ b/src/routes/healthCenterRoutes/index.jsx
@@ -0,0 +1,41 @@
+/**
+ * HealthCenter Routes
+ * ====================
+ * Defines the module-level routing for the PHC frontend module.
+ *
+ * Mounted in App.jsx as:
+ * } />
+ *
+ * Available routes:
+ * /health-center/patient → PatientDashboard
+ * /health-center/compounder → CompoundDashboard
+ * /health-center/ → Routes to appropriate dashboard based on role
+ */
+
+import { Route, Routes, Navigate } from "react-router-dom";
+import { useSelector } from "react-redux";
+import { Layout } from "../../components/layout";
+import PatientDashboard from "../../Modules/HealthCenter/PatientDashboard";
+import CompoundDashboard from "../../Modules/HealthCenter/CompoundDashboard";
+
+const CompounderRoles = ["Health Center Doctor", "Health Center Pathologist", "Doctor", "Pathologist", "compounder"];
+
+export default function HealthCenterRoutes() {
+ const roles = useSelector((state) => state.user.roles || []);
+
+ // Determine user role
+ const isCompounder = roles.some((role) => CompounderRoles.includes(role));
+
+ // Route based on priority: compounder > patient
+ const defaultPath = isCompounder ? "compounder" : "patient";
+
+ return (
+
+
+ } />
+ } />
+ } />
+
+
+ );
+}