create-jinmankn-app 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/index.js +76 -0
- package/package.json +20 -0
- package/templates/blueprint/BLUEPRINT_REPRODUCTION_PROMPT.md +996 -0
- package/templates/blueprint/HOW_IT_WORKS.md +286 -0
- package/templates/blueprint/README.md +123 -0
- package/templates/blueprint/backend/config/db.js +12 -0
- package/templates/blueprint/backend/controllers/authController.js +90 -0
- package/templates/blueprint/backend/controllers/itemController.js +74 -0
- package/templates/blueprint/backend/middleware/auth.js +32 -0
- package/templates/blueprint/backend/middleware/errorHandler.js +23 -0
- package/templates/blueprint/backend/models/Item.js +26 -0
- package/templates/blueprint/backend/models/User.js +28 -0
- package/templates/blueprint/backend/package-lock.json +2190 -0
- package/templates/blueprint/backend/package.json +23 -0
- package/templates/blueprint/backend/routes/authRoutes.js +11 -0
- package/templates/blueprint/backend/routes/healthRoutes.js +9 -0
- package/templates/blueprint/backend/routes/itemRoutes.js +21 -0
- package/templates/blueprint/backend/server.js +29 -0
- package/templates/blueprint/frontend/.env.example +1 -0
- package/templates/blueprint/frontend/index.html +13 -0
- package/templates/blueprint/frontend/package-lock.json +2844 -0
- package/templates/blueprint/frontend/package.json +23 -0
- package/templates/blueprint/frontend/public/favicon.svg +4 -0
- package/templates/blueprint/frontend/src/App.jsx +78 -0
- package/templates/blueprint/frontend/src/assets/logo.svg +4 -0
- package/templates/blueprint/frontend/src/components/DashboardLayout.jsx +103 -0
- package/templates/blueprint/frontend/src/components/ProtectedRoute.jsx +18 -0
- package/templates/blueprint/frontend/src/index.css +1 -0
- package/templates/blueprint/frontend/src/main.jsx +13 -0
- package/templates/blueprint/frontend/src/pages/DashboardHome.jsx +74 -0
- package/templates/blueprint/frontend/src/pages/Items.jsx +243 -0
- package/templates/blueprint/frontend/src/pages/Login.jsx +101 -0
- package/templates/blueprint/frontend/src/pages/Profile.jsx +79 -0
- package/templates/blueprint/frontend/src/pages/Register.jsx +122 -0
- package/templates/blueprint/frontend/src/pages/Report.jsx +124 -0
- package/templates/blueprint/frontend/vite.config.js +10 -0
- package/templates/blueprint/package.json +13 -0
- package/templates/blueprint/scripts/pack-blueprint.ps1 +18 -0
- package/templates/chom/Backend/app.js +25 -0
- package/templates/chom/Backend/package-lock.json +1551 -0
- package/templates/chom/Backend/package.json +23 -0
- package/templates/chom/Backend/seedAdmin.js +21 -0
- package/templates/chom/Backend/src/controllers/payment.c.js +57 -0
- package/templates/chom/Backend/src/controllers/students.c.js +58 -0
- package/templates/chom/Backend/src/controllers/users.c.js +62 -0
- package/templates/chom/Backend/src/middleware/authentication.js +18 -0
- package/templates/chom/Backend/src/models/payment.m.js +13 -0
- package/templates/chom/Backend/src/models/students.m.js +10 -0
- package/templates/chom/Backend/src/models/users.m.js +11 -0
- package/templates/chom/Backend/src/routes/users.r.js +21 -0
- package/templates/chom/Frontend/README.md +16 -0
- package/templates/chom/Frontend/eslint.config.js +21 -0
- package/templates/chom/Frontend/index.html +13 -0
- package/templates/chom/Frontend/package-lock.json +3075 -0
- package/templates/chom/Frontend/package.json +31 -0
- package/templates/chom/Frontend/public/favicon.svg +1 -0
- package/templates/chom/Frontend/public/icons.svg +24 -0
- package/templates/chom/Frontend/src/App.css +189 -0
- package/templates/chom/Frontend/src/App.jsx +28 -0
- package/templates/chom/Frontend/src/api/api.jsx +27 -0
- package/templates/chom/Frontend/src/assets/hero.png +0 -0
- package/templates/chom/Frontend/src/assets/react.svg +1 -0
- package/templates/chom/Frontend/src/assets/vite.svg +1 -0
- package/templates/chom/Frontend/src/components/Navbar.jsx +21 -0
- package/templates/chom/Frontend/src/index.css +8 -0
- package/templates/chom/Frontend/src/main.jsx +10 -0
- package/templates/chom/Frontend/src/pages/Dashboard.jsx +21 -0
- package/templates/chom/Frontend/src/pages/Landing.jsx +39 -0
- package/templates/chom/Frontend/src/pages/Login.jsx +49 -0
- package/templates/chom/Frontend/src/pages/Overview.jsx +42 -0
- package/templates/chom/Frontend/src/pages/Register.jsx +76 -0
- package/templates/chom/Frontend/src/pages/Students.jsx +14 -0
- package/templates/chom/Frontend/vite.config.js +8 -0
- package/templates/chom/package.json +13 -0
- package/templates/hospital-faisal/backend/.env.example +9 -0
- package/templates/hospital-faisal/backend/config/db.js +96 -0
- package/templates/hospital-faisal/backend/controllers/appointmentController.js +164 -0
- package/templates/hospital-faisal/backend/controllers/authController.js +106 -0
- package/templates/hospital-faisal/backend/controllers/hospitalReportController.js +72 -0
- package/templates/hospital-faisal/backend/controllers/medicalReportController.js +105 -0
- package/templates/hospital-faisal/backend/controllers/patientController.js +98 -0
- package/templates/hospital-faisal/backend/database/schema.sql +47 -0
- package/templates/hospital-faisal/backend/middleware/auth.js +30 -0
- package/templates/hospital-faisal/backend/middleware/errorHandler.js +23 -0
- package/templates/hospital-faisal/backend/middleware/role.js +6 -0
- package/templates/hospital-faisal/backend/package-lock.json +2092 -0
- package/templates/hospital-faisal/backend/package.json +23 -0
- package/templates/hospital-faisal/backend/routes/appointmentRoutes.js +25 -0
- package/templates/hospital-faisal/backend/routes/authRoutes.js +12 -0
- package/templates/hospital-faisal/backend/routes/healthRoutes.js +9 -0
- package/templates/hospital-faisal/backend/routes/hospitalReportRoutes.js +10 -0
- package/templates/hospital-faisal/backend/routes/medicalReportRoutes.js +16 -0
- package/templates/hospital-faisal/backend/routes/patientRoutes.js +22 -0
- package/templates/hospital-faisal/backend/server.js +46 -0
- package/templates/hospital-faisal/frontend/.env.example +1 -0
- package/templates/hospital-faisal/frontend/index.html +10 -0
- package/templates/hospital-faisal/frontend/package-lock.json +2844 -0
- package/templates/hospital-faisal/frontend/package.json +23 -0
- package/templates/hospital-faisal/frontend/public/favicon.svg +4 -0
- package/templates/hospital-faisal/frontend/src/App.jsx +56 -0
- package/templates/hospital-faisal/frontend/src/api.js +20 -0
- package/templates/hospital-faisal/frontend/src/assets/logo.svg +4 -0
- package/templates/hospital-faisal/frontend/src/components/DashboardLayout.jsx +114 -0
- package/templates/hospital-faisal/frontend/src/components/ProtectedRoute.jsx +18 -0
- package/templates/hospital-faisal/frontend/src/components/RoleRoute.jsx +14 -0
- package/templates/hospital-faisal/frontend/src/index.css +1 -0
- package/templates/hospital-faisal/frontend/src/main.jsx +13 -0
- package/templates/hospital-faisal/frontend/src/pages/Appointments.jsx +305 -0
- package/templates/hospital-faisal/frontend/src/pages/DashboardHome.jsx +105 -0
- package/templates/hospital-faisal/frontend/src/pages/Login.jsx +98 -0
- package/templates/hospital-faisal/frontend/src/pages/MedicalReports.jsx +182 -0
- package/templates/hospital-faisal/frontend/src/pages/Patients.jsx +237 -0
- package/templates/hospital-faisal/frontend/src/pages/Profile.jsx +78 -0
- package/templates/hospital-faisal/frontend/src/pages/Register.jsx +133 -0
- package/templates/hospital-faisal/frontend/src/pages/Report.jsx +167 -0
- package/templates/hospital-faisal/frontend/vite.config.js +10 -0
- package/templates/hospital-faisal/package.json +13 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blueprint-frontend",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"axios": "^1.7.9",
|
|
13
|
+
"react": "^18.3.1",
|
|
14
|
+
"react-dom": "^18.3.1",
|
|
15
|
+
"react-router-dom": "^6.28.1"
|
|
16
|
+
},
|
|
17
|
+
"devDependencies": {
|
|
18
|
+
"@tailwindcss/vite": "^4.1.8",
|
|
19
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
20
|
+
"tailwindcss": "^4.1.8",
|
|
21
|
+
"vite": "^6.0.6"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Routes, Route, Navigate } from 'react-router-dom';
|
|
2
|
+
import ProtectedRoute from './components/ProtectedRoute';
|
|
3
|
+
import RoleRoute from './components/RoleRoute';
|
|
4
|
+
import DashboardLayout from './components/DashboardLayout';
|
|
5
|
+
import Login from './pages/Login';
|
|
6
|
+
import Register from './pages/Register';
|
|
7
|
+
import DashboardHome from './pages/DashboardHome';
|
|
8
|
+
import Patients from './pages/Patients';
|
|
9
|
+
import Appointments from './pages/Appointments';
|
|
10
|
+
import MedicalReports from './pages/MedicalReports';
|
|
11
|
+
import Report from './pages/Report';
|
|
12
|
+
import Profile from './pages/Profile';
|
|
13
|
+
|
|
14
|
+
function App() {
|
|
15
|
+
return (
|
|
16
|
+
<Routes>
|
|
17
|
+
<Route path="/login" element={<Login />} />
|
|
18
|
+
<Route path="/register" element={<Register />} />
|
|
19
|
+
|
|
20
|
+
<Route
|
|
21
|
+
path="/dashboard"
|
|
22
|
+
element={
|
|
23
|
+
<ProtectedRoute>
|
|
24
|
+
<DashboardLayout />
|
|
25
|
+
</ProtectedRoute>
|
|
26
|
+
}
|
|
27
|
+
>
|
|
28
|
+
<Route index element={<DashboardHome />} />
|
|
29
|
+
<Route
|
|
30
|
+
path="patients"
|
|
31
|
+
element={
|
|
32
|
+
<RoleRoute allowedRoles={['receptionist']}>
|
|
33
|
+
<Patients />
|
|
34
|
+
</RoleRoute>
|
|
35
|
+
}
|
|
36
|
+
/>
|
|
37
|
+
<Route path="appointments" element={<Appointments />} />
|
|
38
|
+
<Route
|
|
39
|
+
path="medical-reports"
|
|
40
|
+
element={
|
|
41
|
+
<RoleRoute allowedRoles={['doctor']}>
|
|
42
|
+
<MedicalReports />
|
|
43
|
+
</RoleRoute>
|
|
44
|
+
}
|
|
45
|
+
/>
|
|
46
|
+
<Route path="report" element={<Report />} />
|
|
47
|
+
<Route path="profile" element={<Profile />} />
|
|
48
|
+
</Route>
|
|
49
|
+
|
|
50
|
+
<Route path="/" element={<Navigate to="/dashboard" replace />} />
|
|
51
|
+
<Route path="*" element={<Navigate to="/dashboard" replace />} />
|
|
52
|
+
</Routes>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export default App;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
|
|
3
|
+
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:5000/api';
|
|
4
|
+
|
|
5
|
+
export const api = axios.create({
|
|
6
|
+
baseURL: API_URL,
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
api.interceptors.request.use((config) => {
|
|
10
|
+
const token = localStorage.getItem('token');
|
|
11
|
+
if (token) {
|
|
12
|
+
config.headers.Authorization = `Bearer ${token}`;
|
|
13
|
+
}
|
|
14
|
+
return config;
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export function getStoredUser() {
|
|
18
|
+
const raw = localStorage.getItem('user');
|
|
19
|
+
return raw ? JSON.parse(raw) : null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Link, Outlet, useNavigate } from 'react-router-dom';
|
|
3
|
+
import { api, getStoredUser } from '../api';
|
|
4
|
+
|
|
5
|
+
function DashboardLayout() {
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
const [user, setUser] = useState(getStoredUser());
|
|
8
|
+
const [error, setError] = useState('');
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const loadUser = async () => {
|
|
12
|
+
try {
|
|
13
|
+
const response = await api.get('/auth/me');
|
|
14
|
+
setUser(response.data.user);
|
|
15
|
+
localStorage.setItem('user', JSON.stringify(response.data.user));
|
|
16
|
+
} catch (err) {
|
|
17
|
+
if (!getStoredUser()) {
|
|
18
|
+
setError(err.response?.data?.message || 'Could not load user');
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
loadUser();
|
|
23
|
+
}, []);
|
|
24
|
+
|
|
25
|
+
const handleLogout = () => {
|
|
26
|
+
localStorage.removeItem('token');
|
|
27
|
+
localStorage.removeItem('user');
|
|
28
|
+
navigate('/login');
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const isReceptionist = user?.role === 'receptionist';
|
|
32
|
+
const isDoctor = user?.role === 'doctor';
|
|
33
|
+
|
|
34
|
+
const linkClass =
|
|
35
|
+
'block rounded-lg px-3 py-2.5 text-sm font-medium text-slate-300 hover:bg-slate-800 hover:text-white transition-colors';
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="min-h-screen bg-slate-100">
|
|
39
|
+
<aside className="fixed inset-y-0 left-0 w-64 bg-slate-900 text-slate-100 flex flex-col">
|
|
40
|
+
<div className="px-5 py-6 border-b border-slate-700">
|
|
41
|
+
<p className="text-xs font-semibold uppercase tracking-wider text-teal-400">
|
|
42
|
+
King Faisal Hospital
|
|
43
|
+
</p>
|
|
44
|
+
<h1 className="text-lg font-bold text-white mt-1">Rwanda</h1>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<nav className="flex-1 px-3 py-4 space-y-1">
|
|
48
|
+
<Link to="/dashboard" className={linkClass}>
|
|
49
|
+
Dashboard
|
|
50
|
+
</Link>
|
|
51
|
+
{isReceptionist && (
|
|
52
|
+
<>
|
|
53
|
+
<Link to="/dashboard/patients" className={linkClass}>
|
|
54
|
+
Patients
|
|
55
|
+
</Link>
|
|
56
|
+
<Link to="/dashboard/appointments" className={linkClass}>
|
|
57
|
+
Appointments
|
|
58
|
+
</Link>
|
|
59
|
+
</>
|
|
60
|
+
)}
|
|
61
|
+
{isDoctor && (
|
|
62
|
+
<>
|
|
63
|
+
<Link to="/dashboard/appointments" className={linkClass}>
|
|
64
|
+
My appointments
|
|
65
|
+
</Link>
|
|
66
|
+
<Link to="/dashboard/medical-reports" className={linkClass}>
|
|
67
|
+
Medical reports
|
|
68
|
+
</Link>
|
|
69
|
+
</>
|
|
70
|
+
)}
|
|
71
|
+
<Link to="/dashboard/report" className={linkClass}>
|
|
72
|
+
Reports
|
|
73
|
+
</Link>
|
|
74
|
+
<Link to="/dashboard/profile" className={linkClass}>
|
|
75
|
+
Profile
|
|
76
|
+
</Link>
|
|
77
|
+
</nav>
|
|
78
|
+
|
|
79
|
+
<div className="px-4 py-4 border-t border-slate-700">
|
|
80
|
+
{error && <p className="text-xs text-red-400 mb-2">{error}</p>}
|
|
81
|
+
{user && (
|
|
82
|
+
<>
|
|
83
|
+
<p className="text-sm font-medium text-white truncate">{user.name}</p>
|
|
84
|
+
<p className="text-xs text-teal-400 capitalize mt-0.5">{user.role}</p>
|
|
85
|
+
<p className="text-xs text-slate-400 truncate mt-0.5">{user.email}</p>
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
<button
|
|
89
|
+
type="button"
|
|
90
|
+
onClick={handleLogout}
|
|
91
|
+
className="mt-3 w-full text-sm font-medium text-slate-300 hover:text-white border border-slate-600 rounded-lg px-3 py-2 hover:bg-slate-800 transition-colors"
|
|
92
|
+
>
|
|
93
|
+
Log out
|
|
94
|
+
</button>
|
|
95
|
+
</div>
|
|
96
|
+
</aside>
|
|
97
|
+
|
|
98
|
+
<div className="ml-64 flex min-h-screen flex-col">
|
|
99
|
+
<header className="bg-white border-b border-slate-200 px-6 py-4">
|
|
100
|
+
<p className="text-sm text-slate-500">Hospital management system</p>
|
|
101
|
+
<h2 className="text-xl font-semibold text-slate-800">
|
|
102
|
+
{user ? `Welcome, ${user.name}` : 'Dashboard'}
|
|
103
|
+
</h2>
|
|
104
|
+
</header>
|
|
105
|
+
|
|
106
|
+
<main className="flex-1 p-6 overflow-auto">
|
|
107
|
+
<Outlet />
|
|
108
|
+
</main>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export default DashboardLayout;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// this component is helping us to protect our routes so that only authenticated users can access the routes
|
|
2
|
+
import { Navigate } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
// this function is firstly checking if the user is authenticated by checking the token in the localStorage
|
|
5
|
+
// if the user is not authenticated, it will redirect the user to the login page
|
|
6
|
+
// if the user is authenticated, it will return the children components
|
|
7
|
+
//and the children component is the component that we want to protect
|
|
8
|
+
function ProtectedRoute({ children }) {
|
|
9
|
+
const token = localStorage.getItem('token');
|
|
10
|
+
|
|
11
|
+
if (!token) {
|
|
12
|
+
return <Navigate to="/login" replace />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
return children;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export default ProtectedRoute;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Navigate } from 'react-router-dom';
|
|
2
|
+
import { getStoredUser } from '../api';
|
|
3
|
+
|
|
4
|
+
function RoleRoute({ children, allowedRoles }) {
|
|
5
|
+
const user = getStoredUser();
|
|
6
|
+
|
|
7
|
+
if (!user || !allowedRoles.includes(user.role)) {
|
|
8
|
+
return <Navigate to="/dashboard" replace />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
return children;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export default RoleRoute;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import 'tailwindcss';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
+
import App from './App';
|
|
5
|
+
import './index.css';
|
|
6
|
+
|
|
7
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
8
|
+
<React.StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</React.StrictMode>
|
|
13
|
+
);
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { api, getStoredUser } from '../api';
|
|
3
|
+
|
|
4
|
+
const emptyForm = {
|
|
5
|
+
patientId: '',
|
|
6
|
+
doctorId: '',
|
|
7
|
+
appointmentDate: '',
|
|
8
|
+
appointmentTime: '',
|
|
9
|
+
status: 'scheduled',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function Appointments() {
|
|
13
|
+
const user = getStoredUser();
|
|
14
|
+
const isReceptionist = user?.role === 'receptionist';
|
|
15
|
+
|
|
16
|
+
const [appointments, setAppointments] = useState([]);
|
|
17
|
+
const [patients, setPatients] = useState([]);
|
|
18
|
+
const [doctors, setDoctors] = useState([]);
|
|
19
|
+
const [form, setForm] = useState(emptyForm);
|
|
20
|
+
const [error, setError] = useState('');
|
|
21
|
+
const [message, setMessage] = useState('');
|
|
22
|
+
const [editId, setEditId] = useState(null);
|
|
23
|
+
const [loading, setLoading] = useState(true);
|
|
24
|
+
const [submitting, setSubmitting] = useState(false);
|
|
25
|
+
|
|
26
|
+
const loadData = async () => {
|
|
27
|
+
try {
|
|
28
|
+
const requests = [api.get('/appointments')];
|
|
29
|
+
if (isReceptionist) {
|
|
30
|
+
requests.push(api.get('/patients'), api.get('/auth/doctors'));
|
|
31
|
+
}
|
|
32
|
+
const results = await Promise.all(requests);
|
|
33
|
+
setAppointments(results[0].data.appointments);
|
|
34
|
+
if (isReceptionist) {
|
|
35
|
+
setPatients(results[1].data.patients);
|
|
36
|
+
setDoctors(results[2].data.doctors);
|
|
37
|
+
}
|
|
38
|
+
} catch (err) {
|
|
39
|
+
setError(err.response?.data?.message || 'Failed to load appointments');
|
|
40
|
+
} finally {
|
|
41
|
+
setLoading(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
useEffect(() => {
|
|
46
|
+
loadData();
|
|
47
|
+
}, [isReceptionist]);
|
|
48
|
+
|
|
49
|
+
const handleChange = (e) => {
|
|
50
|
+
setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleEdit = (appt) => {
|
|
54
|
+
setEditId(appt.id);
|
|
55
|
+
setForm({
|
|
56
|
+
patientId: String(appt.patientId),
|
|
57
|
+
doctorId: String(appt.doctorId),
|
|
58
|
+
appointmentDate: appt.appointmentDate,
|
|
59
|
+
appointmentTime: appt.appointmentTime?.slice(0, 5) || appt.appointmentTime,
|
|
60
|
+
status: appt.status,
|
|
61
|
+
});
|
|
62
|
+
setError('');
|
|
63
|
+
setMessage('');
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const handleCancelEdit = () => {
|
|
67
|
+
setEditId(null);
|
|
68
|
+
setForm(emptyForm);
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const handleCancelAppointment = async (id) => {
|
|
72
|
+
if (!window.confirm('Cancel this appointment?')) return;
|
|
73
|
+
try {
|
|
74
|
+
await api.patch(`/appointments/${id}/cancel`);
|
|
75
|
+
setMessage('Appointment cancelled');
|
|
76
|
+
loadData();
|
|
77
|
+
} catch (err) {
|
|
78
|
+
setError(err.response?.data?.message || 'Cancel failed');
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const handleDelete = async (id) => {
|
|
83
|
+
if (!window.confirm('Delete this appointment permanently?')) return;
|
|
84
|
+
try {
|
|
85
|
+
await api.delete(`/appointments/${id}`);
|
|
86
|
+
setMessage('Appointment deleted');
|
|
87
|
+
if (editId === id) handleCancelEdit();
|
|
88
|
+
loadData();
|
|
89
|
+
} catch (err) {
|
|
90
|
+
setError(err.response?.data?.message || 'Delete failed');
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleSubmit = async (e) => {
|
|
95
|
+
e.preventDefault();
|
|
96
|
+
setSubmitting(true);
|
|
97
|
+
setError('');
|
|
98
|
+
setMessage('');
|
|
99
|
+
|
|
100
|
+
const payload = {
|
|
101
|
+
patientId: Number(form.patientId),
|
|
102
|
+
doctorId: Number(form.doctorId),
|
|
103
|
+
appointmentDate: form.appointmentDate,
|
|
104
|
+
appointmentTime: form.appointmentTime,
|
|
105
|
+
status: form.status,
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
if (editId) {
|
|
110
|
+
await api.put(`/appointments/${editId}`, payload);
|
|
111
|
+
setMessage('Appointment updated');
|
|
112
|
+
} else {
|
|
113
|
+
await api.post('/appointments', payload);
|
|
114
|
+
setMessage('Appointment created');
|
|
115
|
+
}
|
|
116
|
+
setForm(emptyForm);
|
|
117
|
+
setEditId(null);
|
|
118
|
+
loadData();
|
|
119
|
+
} catch (err) {
|
|
120
|
+
setError(err.response?.data?.message || 'Save failed');
|
|
121
|
+
} finally {
|
|
122
|
+
setSubmitting(false);
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const statusClass = (status) => {
|
|
127
|
+
if (status === 'completed') return 'text-green-700 bg-green-50';
|
|
128
|
+
if (status === 'cancelled') return 'text-red-700 bg-red-50';
|
|
129
|
+
return 'text-amber-700 bg-amber-50';
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
return (
|
|
133
|
+
<div className="max-w-6xl">
|
|
134
|
+
<h1 className="text-2xl font-bold text-slate-800 mb-6">
|
|
135
|
+
{isReceptionist ? 'Appointment Management' : 'My Appointments'}
|
|
136
|
+
</h1>
|
|
137
|
+
|
|
138
|
+
{message && (
|
|
139
|
+
<p className="mb-4 text-sm text-green-700 bg-green-50 border border-green-200 rounded px-3 py-2">
|
|
140
|
+
{message}
|
|
141
|
+
</p>
|
|
142
|
+
)}
|
|
143
|
+
{error && (
|
|
144
|
+
<p className="mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
|
|
145
|
+
{error}
|
|
146
|
+
</p>
|
|
147
|
+
)}
|
|
148
|
+
|
|
149
|
+
{isReceptionist && (
|
|
150
|
+
<section className="bg-white rounded-lg shadow-md p-6 mb-8">
|
|
151
|
+
<h2 className="text-lg font-semibold text-slate-800 mb-4">
|
|
152
|
+
{editId ? 'Edit appointment' : 'Book appointment'}
|
|
153
|
+
</h2>
|
|
154
|
+
<form onSubmit={handleSubmit} className="grid gap-4 sm:grid-cols-2">
|
|
155
|
+
<div>
|
|
156
|
+
<label className="block text-sm font-medium text-slate-700 mb-1">Patient</label>
|
|
157
|
+
<select
|
|
158
|
+
name="patientId"
|
|
159
|
+
required
|
|
160
|
+
value={form.patientId}
|
|
161
|
+
onChange={handleChange}
|
|
162
|
+
className="w-full border border-slate-300 rounded px-3 py-2"
|
|
163
|
+
>
|
|
164
|
+
<option value="">Select patient</option>
|
|
165
|
+
{patients.map((p) => (
|
|
166
|
+
<option key={p.id} value={p.id}>
|
|
167
|
+
{p.fullName}
|
|
168
|
+
</option>
|
|
169
|
+
))}
|
|
170
|
+
</select>
|
|
171
|
+
</div>
|
|
172
|
+
<div>
|
|
173
|
+
<label className="block text-sm font-medium text-slate-700 mb-1">Doctor</label>
|
|
174
|
+
<select
|
|
175
|
+
name="doctorId"
|
|
176
|
+
required
|
|
177
|
+
value={form.doctorId}
|
|
178
|
+
onChange={handleChange}
|
|
179
|
+
className="w-full border border-slate-300 rounded px-3 py-2"
|
|
180
|
+
>
|
|
181
|
+
<option value="">Select doctor</option>
|
|
182
|
+
{doctors.map((d) => (
|
|
183
|
+
<option key={d.id} value={d.id}>
|
|
184
|
+
{d.name}
|
|
185
|
+
</option>
|
|
186
|
+
))}
|
|
187
|
+
</select>
|
|
188
|
+
</div>
|
|
189
|
+
<div>
|
|
190
|
+
<label className="block text-sm font-medium text-slate-700 mb-1">Date</label>
|
|
191
|
+
<input
|
|
192
|
+
name="appointmentDate"
|
|
193
|
+
type="date"
|
|
194
|
+
required
|
|
195
|
+
value={form.appointmentDate}
|
|
196
|
+
onChange={handleChange}
|
|
197
|
+
className="w-full border border-slate-300 rounded px-3 py-2"
|
|
198
|
+
/>
|
|
199
|
+
</div>
|
|
200
|
+
<div>
|
|
201
|
+
<label className="block text-sm font-medium text-slate-700 mb-1">Time</label>
|
|
202
|
+
<input
|
|
203
|
+
name="appointmentTime"
|
|
204
|
+
type="time"
|
|
205
|
+
required
|
|
206
|
+
value={form.appointmentTime}
|
|
207
|
+
onChange={handleChange}
|
|
208
|
+
className="w-full border border-slate-300 rounded px-3 py-2"
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
<div>
|
|
212
|
+
<label className="block text-sm font-medium text-slate-700 mb-1">Status</label>
|
|
213
|
+
<select
|
|
214
|
+
name="status"
|
|
215
|
+
value={form.status}
|
|
216
|
+
onChange={handleChange}
|
|
217
|
+
className="w-full border border-slate-300 rounded px-3 py-2"
|
|
218
|
+
>
|
|
219
|
+
<option value="scheduled">Scheduled</option>
|
|
220
|
+
<option value="completed">Completed</option>
|
|
221
|
+
<option value="cancelled">Cancelled</option>
|
|
222
|
+
</select>
|
|
223
|
+
</div>
|
|
224
|
+
<div className="sm:col-span-2 flex gap-2">
|
|
225
|
+
<button
|
|
226
|
+
type="submit"
|
|
227
|
+
disabled={submitting}
|
|
228
|
+
className="bg-teal-600 text-white font-medium px-4 py-2 rounded hover:bg-teal-700 disabled:opacity-50"
|
|
229
|
+
>
|
|
230
|
+
{submitting ? 'Saving...' : editId ? 'Update' : 'Create'}
|
|
231
|
+
</button>
|
|
232
|
+
{editId && (
|
|
233
|
+
<button type="button" onClick={handleCancelEdit} className="border border-slate-300 px-4 py-2 rounded">
|
|
234
|
+
Cancel
|
|
235
|
+
</button>
|
|
236
|
+
)}
|
|
237
|
+
</div>
|
|
238
|
+
</form>
|
|
239
|
+
</section>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
<section>
|
|
243
|
+
<h2 className="text-lg font-semibold text-slate-800 mb-4">
|
|
244
|
+
{isReceptionist ? 'All appointments' : 'Assigned to you'}
|
|
245
|
+
</h2>
|
|
246
|
+
{loading ? (
|
|
247
|
+
<p className="text-slate-500">Loading...</p>
|
|
248
|
+
) : appointments.length === 0 ? (
|
|
249
|
+
<p className="text-slate-500">No appointments yet.</p>
|
|
250
|
+
) : (
|
|
251
|
+
<div className="overflow-x-auto bg-white rounded-lg border border-slate-200">
|
|
252
|
+
<table className="w-full text-sm text-left">
|
|
253
|
+
<thead className="bg-slate-50 text-slate-600">
|
|
254
|
+
<tr>
|
|
255
|
+
<th className="px-4 py-2">Patient</th>
|
|
256
|
+
<th className="px-4 py-2">Doctor</th>
|
|
257
|
+
<th className="px-4 py-2">Date</th>
|
|
258
|
+
<th className="px-4 py-2">Time</th>
|
|
259
|
+
<th className="px-4 py-2">Status</th>
|
|
260
|
+
{isReceptionist && <th className="px-4 py-2">Actions</th>}
|
|
261
|
+
</tr>
|
|
262
|
+
</thead>
|
|
263
|
+
<tbody className="divide-y divide-slate-200">
|
|
264
|
+
{appointments.map((a) => (
|
|
265
|
+
<tr key={a.id}>
|
|
266
|
+
<td className="px-4 py-2">{a.patientName}</td>
|
|
267
|
+
<td className="px-4 py-2">{a.doctorName}</td>
|
|
268
|
+
<td className="px-4 py-2">{a.appointmentDate}</td>
|
|
269
|
+
<td className="px-4 py-2">{a.appointmentTime?.slice(0, 5)}</td>
|
|
270
|
+
<td className="px-4 py-2">
|
|
271
|
+
<span className={`px-2 py-0.5 rounded text-xs font-medium capitalize ${statusClass(a.status)}`}>
|
|
272
|
+
{a.status}
|
|
273
|
+
</span>
|
|
274
|
+
</td>
|
|
275
|
+
{isReceptionist && (
|
|
276
|
+
<td className="px-4 py-2 space-x-2 whitespace-nowrap">
|
|
277
|
+
<button type="button" onClick={() => handleEdit(a)} className="text-teal-600 hover:underline">
|
|
278
|
+
Edit
|
|
279
|
+
</button>
|
|
280
|
+
{a.status !== 'cancelled' && (
|
|
281
|
+
<button
|
|
282
|
+
type="button"
|
|
283
|
+
onClick={() => handleCancelAppointment(a.id)}
|
|
284
|
+
className="text-amber-600 hover:underline"
|
|
285
|
+
>
|
|
286
|
+
Cancel
|
|
287
|
+
</button>
|
|
288
|
+
)}
|
|
289
|
+
<button type="button" onClick={() => handleDelete(a.id)} className="text-red-600 hover:underline">
|
|
290
|
+
Delete
|
|
291
|
+
</button>
|
|
292
|
+
</td>
|
|
293
|
+
)}
|
|
294
|
+
</tr>
|
|
295
|
+
))}
|
|
296
|
+
</tbody>
|
|
297
|
+
</table>
|
|
298
|
+
</div>
|
|
299
|
+
)}
|
|
300
|
+
</section>
|
|
301
|
+
</div>
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export default Appointments;
|