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.
Files changed (117) hide show
  1. package/bin/index.js +76 -0
  2. package/package.json +20 -0
  3. package/templates/blueprint/BLUEPRINT_REPRODUCTION_PROMPT.md +996 -0
  4. package/templates/blueprint/HOW_IT_WORKS.md +286 -0
  5. package/templates/blueprint/README.md +123 -0
  6. package/templates/blueprint/backend/config/db.js +12 -0
  7. package/templates/blueprint/backend/controllers/authController.js +90 -0
  8. package/templates/blueprint/backend/controllers/itemController.js +74 -0
  9. package/templates/blueprint/backend/middleware/auth.js +32 -0
  10. package/templates/blueprint/backend/middleware/errorHandler.js +23 -0
  11. package/templates/blueprint/backend/models/Item.js +26 -0
  12. package/templates/blueprint/backend/models/User.js +28 -0
  13. package/templates/blueprint/backend/package-lock.json +2190 -0
  14. package/templates/blueprint/backend/package.json +23 -0
  15. package/templates/blueprint/backend/routes/authRoutes.js +11 -0
  16. package/templates/blueprint/backend/routes/healthRoutes.js +9 -0
  17. package/templates/blueprint/backend/routes/itemRoutes.js +21 -0
  18. package/templates/blueprint/backend/server.js +29 -0
  19. package/templates/blueprint/frontend/.env.example +1 -0
  20. package/templates/blueprint/frontend/index.html +13 -0
  21. package/templates/blueprint/frontend/package-lock.json +2844 -0
  22. package/templates/blueprint/frontend/package.json +23 -0
  23. package/templates/blueprint/frontend/public/favicon.svg +4 -0
  24. package/templates/blueprint/frontend/src/App.jsx +78 -0
  25. package/templates/blueprint/frontend/src/assets/logo.svg +4 -0
  26. package/templates/blueprint/frontend/src/components/DashboardLayout.jsx +103 -0
  27. package/templates/blueprint/frontend/src/components/ProtectedRoute.jsx +18 -0
  28. package/templates/blueprint/frontend/src/index.css +1 -0
  29. package/templates/blueprint/frontend/src/main.jsx +13 -0
  30. package/templates/blueprint/frontend/src/pages/DashboardHome.jsx +74 -0
  31. package/templates/blueprint/frontend/src/pages/Items.jsx +243 -0
  32. package/templates/blueprint/frontend/src/pages/Login.jsx +101 -0
  33. package/templates/blueprint/frontend/src/pages/Profile.jsx +79 -0
  34. package/templates/blueprint/frontend/src/pages/Register.jsx +122 -0
  35. package/templates/blueprint/frontend/src/pages/Report.jsx +124 -0
  36. package/templates/blueprint/frontend/vite.config.js +10 -0
  37. package/templates/blueprint/package.json +13 -0
  38. package/templates/blueprint/scripts/pack-blueprint.ps1 +18 -0
  39. package/templates/chom/Backend/app.js +25 -0
  40. package/templates/chom/Backend/package-lock.json +1551 -0
  41. package/templates/chom/Backend/package.json +23 -0
  42. package/templates/chom/Backend/seedAdmin.js +21 -0
  43. package/templates/chom/Backend/src/controllers/payment.c.js +57 -0
  44. package/templates/chom/Backend/src/controllers/students.c.js +58 -0
  45. package/templates/chom/Backend/src/controllers/users.c.js +62 -0
  46. package/templates/chom/Backend/src/middleware/authentication.js +18 -0
  47. package/templates/chom/Backend/src/models/payment.m.js +13 -0
  48. package/templates/chom/Backend/src/models/students.m.js +10 -0
  49. package/templates/chom/Backend/src/models/users.m.js +11 -0
  50. package/templates/chom/Backend/src/routes/users.r.js +21 -0
  51. package/templates/chom/Frontend/README.md +16 -0
  52. package/templates/chom/Frontend/eslint.config.js +21 -0
  53. package/templates/chom/Frontend/index.html +13 -0
  54. package/templates/chom/Frontend/package-lock.json +3075 -0
  55. package/templates/chom/Frontend/package.json +31 -0
  56. package/templates/chom/Frontend/public/favicon.svg +1 -0
  57. package/templates/chom/Frontend/public/icons.svg +24 -0
  58. package/templates/chom/Frontend/src/App.css +189 -0
  59. package/templates/chom/Frontend/src/App.jsx +28 -0
  60. package/templates/chom/Frontend/src/api/api.jsx +27 -0
  61. package/templates/chom/Frontend/src/assets/hero.png +0 -0
  62. package/templates/chom/Frontend/src/assets/react.svg +1 -0
  63. package/templates/chom/Frontend/src/assets/vite.svg +1 -0
  64. package/templates/chom/Frontend/src/components/Navbar.jsx +21 -0
  65. package/templates/chom/Frontend/src/index.css +8 -0
  66. package/templates/chom/Frontend/src/main.jsx +10 -0
  67. package/templates/chom/Frontend/src/pages/Dashboard.jsx +21 -0
  68. package/templates/chom/Frontend/src/pages/Landing.jsx +39 -0
  69. package/templates/chom/Frontend/src/pages/Login.jsx +49 -0
  70. package/templates/chom/Frontend/src/pages/Overview.jsx +42 -0
  71. package/templates/chom/Frontend/src/pages/Register.jsx +76 -0
  72. package/templates/chom/Frontend/src/pages/Students.jsx +14 -0
  73. package/templates/chom/Frontend/vite.config.js +8 -0
  74. package/templates/chom/package.json +13 -0
  75. package/templates/hospital-faisal/backend/.env.example +9 -0
  76. package/templates/hospital-faisal/backend/config/db.js +96 -0
  77. package/templates/hospital-faisal/backend/controllers/appointmentController.js +164 -0
  78. package/templates/hospital-faisal/backend/controllers/authController.js +106 -0
  79. package/templates/hospital-faisal/backend/controllers/hospitalReportController.js +72 -0
  80. package/templates/hospital-faisal/backend/controllers/medicalReportController.js +105 -0
  81. package/templates/hospital-faisal/backend/controllers/patientController.js +98 -0
  82. package/templates/hospital-faisal/backend/database/schema.sql +47 -0
  83. package/templates/hospital-faisal/backend/middleware/auth.js +30 -0
  84. package/templates/hospital-faisal/backend/middleware/errorHandler.js +23 -0
  85. package/templates/hospital-faisal/backend/middleware/role.js +6 -0
  86. package/templates/hospital-faisal/backend/package-lock.json +2092 -0
  87. package/templates/hospital-faisal/backend/package.json +23 -0
  88. package/templates/hospital-faisal/backend/routes/appointmentRoutes.js +25 -0
  89. package/templates/hospital-faisal/backend/routes/authRoutes.js +12 -0
  90. package/templates/hospital-faisal/backend/routes/healthRoutes.js +9 -0
  91. package/templates/hospital-faisal/backend/routes/hospitalReportRoutes.js +10 -0
  92. package/templates/hospital-faisal/backend/routes/medicalReportRoutes.js +16 -0
  93. package/templates/hospital-faisal/backend/routes/patientRoutes.js +22 -0
  94. package/templates/hospital-faisal/backend/server.js +46 -0
  95. package/templates/hospital-faisal/frontend/.env.example +1 -0
  96. package/templates/hospital-faisal/frontend/index.html +10 -0
  97. package/templates/hospital-faisal/frontend/package-lock.json +2844 -0
  98. package/templates/hospital-faisal/frontend/package.json +23 -0
  99. package/templates/hospital-faisal/frontend/public/favicon.svg +4 -0
  100. package/templates/hospital-faisal/frontend/src/App.jsx +56 -0
  101. package/templates/hospital-faisal/frontend/src/api.js +20 -0
  102. package/templates/hospital-faisal/frontend/src/assets/logo.svg +4 -0
  103. package/templates/hospital-faisal/frontend/src/components/DashboardLayout.jsx +114 -0
  104. package/templates/hospital-faisal/frontend/src/components/ProtectedRoute.jsx +18 -0
  105. package/templates/hospital-faisal/frontend/src/components/RoleRoute.jsx +14 -0
  106. package/templates/hospital-faisal/frontend/src/index.css +1 -0
  107. package/templates/hospital-faisal/frontend/src/main.jsx +13 -0
  108. package/templates/hospital-faisal/frontend/src/pages/Appointments.jsx +305 -0
  109. package/templates/hospital-faisal/frontend/src/pages/DashboardHome.jsx +105 -0
  110. package/templates/hospital-faisal/frontend/src/pages/Login.jsx +98 -0
  111. package/templates/hospital-faisal/frontend/src/pages/MedicalReports.jsx +182 -0
  112. package/templates/hospital-faisal/frontend/src/pages/Patients.jsx +237 -0
  113. package/templates/hospital-faisal/frontend/src/pages/Profile.jsx +78 -0
  114. package/templates/hospital-faisal/frontend/src/pages/Register.jsx +133 -0
  115. package/templates/hospital-faisal/frontend/src/pages/Report.jsx +167 -0
  116. package/templates/hospital-faisal/frontend/vite.config.js +10 -0
  117. 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,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
2
+ <rect width="32" height="32" rx="6" fill="#2563eb"/>
3
+ <text x="16" y="22" text-anchor="middle" fill="white" font-size="14" font-family="system-ui,sans-serif" font-weight="700">B</text>
4
+ </svg>
@@ -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,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" fill="none">
2
+ <rect width="40" height="40" rx="8" fill="#1e293b"/>
3
+ <text x="20" y="27" text-anchor="middle" fill="white" font-size="18" font-family="system-ui,sans-serif" font-weight="700">B</text>
4
+ </svg>
@@ -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;