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 + + + + + + {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" + > + + + setFormData({ ...formData, vehicle_type: value }) + } + error={errors.vehicle_type} + searchable + disabled={editingId !== null} + /> + + + setFormData({ ...formData, registration_number: e.currentTarget.value }) + } + error={errors.registration_number} + disabled={editingId !== null} + /> + + + setFormData({ ...formData, driver_name: e.currentTarget.value }) + } + error={errors.driver_name} + disabled={editingId !== null} + /> + + + setFormData({ ...formData, driver_contact: e.currentTarget.value }) + } + error={errors.driver_contact} + disabled={editingId !== null} + /> + + + setFormData({ ...formData, driver_license: e.currentTarget.value }) + } + error={errors.driver_license} + disabled={editingId !== null} + /> + + + setFormData({ ...formData, last_maintenance_date: e.currentTarget.value }) + } + /> + + + setFormData({ ...formData, next_maintenance_due: e.currentTarget.value }) + } + /> + + + setFormData({ ...formData, notes: e.currentTarget.value }) + } + /> + + + + + + + + + {/* 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. + + + + + + {/* 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} + /> + +