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,105 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { Link } from 'react-router-dom';
3
+ import { api, getStoredUser } from '../api';
4
+
5
+ function DashboardHome() {
6
+ const user = getStoredUser();
7
+ const [stats, setStats] = useState(null);
8
+ const [error, setError] = useState('');
9
+
10
+ useEffect(() => {
11
+ const load = async () => {
12
+ try {
13
+ if (user?.role === 'receptionist') {
14
+ const [patientsRes, appointmentsRes] = await Promise.all([
15
+ api.get('/patients'),
16
+ api.get('/appointments'),
17
+ ]);
18
+ const appointments = appointmentsRes.data.appointments;
19
+ setStats({
20
+ patients: patientsRes.data.patients.length,
21
+ appointments: appointments.length,
22
+ scheduled: appointments.filter((a) => a.status === 'scheduled').length,
23
+ });
24
+ } else if (user?.role === 'doctor') {
25
+ const [appointmentsRes, reportsRes] = await Promise.all([
26
+ api.get('/appointments'),
27
+ api.get('/medical-reports'),
28
+ ]);
29
+ const appointments = appointmentsRes.data.appointments;
30
+ setStats({
31
+ myAppointments: appointments.length,
32
+ scheduled: appointments.filter((a) => a.status === 'scheduled').length,
33
+ reports: reportsRes.data.reports.length,
34
+ });
35
+ }
36
+ } catch (err) {
37
+ setError(err.response?.data?.message || 'Could not load overview');
38
+ }
39
+ };
40
+ load();
41
+ }, [user?.role]);
42
+
43
+ return (
44
+ <div className="max-w-4xl">
45
+ <h1 className="text-2xl font-bold text-slate-800 mb-2">Overview</h1>
46
+ <p className="text-slate-600 mb-8">
47
+ King Faisal Hospital Rwanda — appointment and medical report management.
48
+ </p>
49
+
50
+ {error && (
51
+ <p className="mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
52
+ {error}
53
+ </p>
54
+ )}
55
+
56
+ {user?.role === 'receptionist' && stats && (
57
+ <div className="grid gap-4 sm:grid-cols-3">
58
+ <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
59
+ <p className="text-sm text-slate-500">Registered patients</p>
60
+ <p className="text-3xl font-bold text-slate-800 mt-2">{stats.patients}</p>
61
+ <Link to="/dashboard/patients" className="text-sm text-teal-600 hover:underline mt-3 inline-block">
62
+ Manage patients →
63
+ </Link>
64
+ </div>
65
+ <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
66
+ <p className="text-sm text-slate-500">Total appointments</p>
67
+ <p className="text-3xl font-bold text-slate-800 mt-2">{stats.appointments}</p>
68
+ <Link to="/dashboard/appointments" className="text-sm text-teal-600 hover:underline mt-3 inline-block">
69
+ Manage appointments →
70
+ </Link>
71
+ </div>
72
+ <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
73
+ <p className="text-sm text-slate-500">Scheduled</p>
74
+ <p className="text-3xl font-bold text-amber-600 mt-2">{stats.scheduled}</p>
75
+ </div>
76
+ </div>
77
+ )}
78
+
79
+ {user?.role === 'doctor' && stats && (
80
+ <div className="grid gap-4 sm:grid-cols-3">
81
+ <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
82
+ <p className="text-sm text-slate-500">My appointments</p>
83
+ <p className="text-3xl font-bold text-slate-800 mt-2">{stats.myAppointments}</p>
84
+ <Link to="/dashboard/appointments" className="text-sm text-teal-600 hover:underline mt-3 inline-block">
85
+ View appointments →
86
+ </Link>
87
+ </div>
88
+ <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
89
+ <p className="text-sm text-slate-500">Scheduled</p>
90
+ <p className="text-3xl font-bold text-amber-600 mt-2">{stats.scheduled}</p>
91
+ </div>
92
+ <div className="bg-white rounded-lg shadow-sm border border-slate-200 p-6">
93
+ <p className="text-sm text-slate-500">Medical reports</p>
94
+ <p className="text-3xl font-bold text-teal-700 mt-2">{stats.reports}</p>
95
+ <Link to="/dashboard/medical-reports" className="text-sm text-teal-600 hover:underline mt-3 inline-block">
96
+ Manage reports →
97
+ </Link>
98
+ </div>
99
+ </div>
100
+ )}
101
+ </div>
102
+ );
103
+ }
104
+
105
+ export default DashboardHome;
@@ -0,0 +1,98 @@
1
+ import { useState } from 'react';
2
+ import { Link, useNavigate } from 'react-router-dom';
3
+ import { api } from '../api';
4
+
5
+ function Login() {
6
+ const navigate = useNavigate();
7
+ const [email, setEmail] = useState('');
8
+ const [password, setPassword] = useState('');
9
+ const [error, setError] = useState('');
10
+ const [message, setMessage] = useState('');
11
+ const [loading, setLoading] = useState(false);
12
+
13
+ const handleSubmit = async (e) => {
14
+ e.preventDefault();
15
+ setError('');
16
+ setMessage('');
17
+ setLoading(true);
18
+
19
+ try {
20
+ const response = await api.post('/auth/login', { email, password });
21
+ localStorage.setItem('token', response.data.token);
22
+ localStorage.setItem('user', JSON.stringify(response.data.user));
23
+ setMessage('Login successful');
24
+ navigate('/dashboard');
25
+ } catch (err) {
26
+ setError(err.response?.data?.message || 'Login failed');
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ return (
33
+ <div className="min-h-screen flex items-center justify-center px-4 bg-slate-100">
34
+ <div className="w-full max-w-md bg-white rounded-lg shadow-md p-8">
35
+ <p className="text-xs font-semibold uppercase tracking-wider text-teal-600">
36
+ King Faisal Hospital Rwanda
37
+ </p>
38
+ <h1 className="text-2xl font-bold text-slate-800 mb-6 mt-1">Staff sign in</h1>
39
+
40
+ {message && (
41
+ <p className="mb-4 text-sm text-green-700 bg-green-50 border border-green-200 rounded px-3 py-2">
42
+ {message}
43
+ </p>
44
+ )}
45
+ {error && (
46
+ <p className="mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
47
+ {error}
48
+ </p>
49
+ )}
50
+
51
+ <form onSubmit={handleSubmit} className="space-y-4">
52
+ <div>
53
+ <label htmlFor="email" className="block text-sm font-medium text-slate-700 mb-1">
54
+ Email
55
+ </label>
56
+ <input
57
+ id="email"
58
+ type="email"
59
+ required
60
+ value={email}
61
+ onChange={(e) => setEmail(e.target.value)}
62
+ className="w-full border border-slate-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-teal-500"
63
+ />
64
+ </div>
65
+ <div>
66
+ <label htmlFor="password" className="block text-sm font-medium text-slate-700 mb-1">
67
+ Password
68
+ </label>
69
+ <input
70
+ id="password"
71
+ type="password"
72
+ required
73
+ value={password}
74
+ onChange={(e) => setPassword(e.target.value)}
75
+ className="w-full border border-slate-300 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-teal-500"
76
+ />
77
+ </div>
78
+ <button
79
+ type="submit"
80
+ disabled={loading}
81
+ className="w-full bg-teal-600 text-white font-medium py-2 rounded hover:bg-teal-700 disabled:opacity-50"
82
+ >
83
+ {loading ? 'Signing in...' : 'Sign in'}
84
+ </button>
85
+ </form>
86
+
87
+ <p className="mt-4 text-sm text-slate-600 text-center">
88
+ No account?{' '}
89
+ <Link to="/register" className="text-teal-600 hover:underline">
90
+ Register
91
+ </Link>
92
+ </p>
93
+ </div>
94
+ </div>
95
+ );
96
+ }
97
+
98
+ export default Login;
@@ -0,0 +1,182 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { api } from '../api';
3
+
4
+ function MedicalReports() {
5
+ const [appointments, setAppointments] = useState([]);
6
+ const [reports, setReports] = useState([]);
7
+ const [form, setForm] = useState({
8
+ appointmentId: '',
9
+ diagnosis: '',
10
+ prescription: '',
11
+ reportDate: new Date().toISOString().slice(0, 10),
12
+ });
13
+ const [error, setError] = useState('');
14
+ const [message, setMessage] = useState('');
15
+ const [loading, setLoading] = useState(true);
16
+ const [submitting, setSubmitting] = useState(false);
17
+
18
+ const loadData = async () => {
19
+ try {
20
+ const [apptRes, reportRes] = await Promise.all([
21
+ api.get('/appointments'),
22
+ api.get('/medical-reports'),
23
+ ]);
24
+ const eligible = apptRes.data.appointments.filter(
25
+ (a) => a.status !== 'cancelled' && !a.hasReport,
26
+ );
27
+ setAppointments(eligible);
28
+ setReports(reportRes.data.reports);
29
+ } catch (err) {
30
+ setError(err.response?.data?.message || 'Failed to load data');
31
+ } finally {
32
+ setLoading(false);
33
+ }
34
+ };
35
+
36
+ useEffect(() => {
37
+ loadData();
38
+ }, []);
39
+
40
+ const handleChange = (e) => {
41
+ setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
42
+ };
43
+
44
+ const handleSubmit = async (e) => {
45
+ e.preventDefault();
46
+ setSubmitting(true);
47
+ setError('');
48
+ setMessage('');
49
+
50
+ try {
51
+ await api.post('/medical-reports', {
52
+ appointmentId: Number(form.appointmentId),
53
+ diagnosis: form.diagnosis,
54
+ prescription: form.prescription,
55
+ reportDate: form.reportDate,
56
+ });
57
+ setMessage('Medical report saved');
58
+ setForm({
59
+ appointmentId: '',
60
+ diagnosis: '',
61
+ prescription: '',
62
+ reportDate: new Date().toISOString().slice(0, 10),
63
+ });
64
+ loadData();
65
+ } catch (err) {
66
+ setError(err.response?.data?.message || 'Failed to save report');
67
+ } finally {
68
+ setSubmitting(false);
69
+ }
70
+ };
71
+
72
+ return (
73
+ <div className="max-w-5xl">
74
+ <h1 className="text-2xl font-bold text-slate-800 mb-6">Medical Report Management</h1>
75
+
76
+ {message && (
77
+ <p className="mb-4 text-sm text-green-700 bg-green-50 border border-green-200 rounded px-3 py-2">
78
+ {message}
79
+ </p>
80
+ )}
81
+ {error && (
82
+ <p className="mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
83
+ {error}
84
+ </p>
85
+ )}
86
+
87
+ <section className="bg-white rounded-lg shadow-md p-6 mb-8">
88
+ <h2 className="text-lg font-semibold text-slate-800 mb-4">Add medical report</h2>
89
+ <form onSubmit={handleSubmit} className="space-y-4">
90
+ <div>
91
+ <label className="block text-sm font-medium text-slate-700 mb-1">Assigned appointment</label>
92
+ <select
93
+ name="appointmentId"
94
+ required
95
+ value={form.appointmentId}
96
+ onChange={handleChange}
97
+ className="w-full border border-slate-300 rounded px-3 py-2"
98
+ >
99
+ <option value="">Select appointment</option>
100
+ {appointments.map((a) => (
101
+ <option key={a.id} value={a.id}>
102
+ {a.patientName} — {a.appointmentDate} {a.appointmentTime?.slice(0, 5)} ({a.status})
103
+ </option>
104
+ ))}
105
+ </select>
106
+ </div>
107
+ <div>
108
+ <label className="block text-sm font-medium text-slate-700 mb-1">Diagnosis</label>
109
+ <textarea
110
+ name="diagnosis"
111
+ rows={3}
112
+ required
113
+ value={form.diagnosis}
114
+ onChange={handleChange}
115
+ className="w-full border border-slate-300 rounded px-3 py-2"
116
+ />
117
+ </div>
118
+ <div>
119
+ <label className="block text-sm font-medium text-slate-700 mb-1">Prescription</label>
120
+ <textarea
121
+ name="prescription"
122
+ rows={3}
123
+ required
124
+ value={form.prescription}
125
+ onChange={handleChange}
126
+ className="w-full border border-slate-300 rounded px-3 py-2"
127
+ />
128
+ </div>
129
+ <div>
130
+ <label className="block text-sm font-medium text-slate-700 mb-1">Report date</label>
131
+ <input
132
+ name="reportDate"
133
+ type="date"
134
+ required
135
+ value={form.reportDate}
136
+ onChange={handleChange}
137
+ className="w-full border border-slate-300 rounded px-3 py-2 max-w-xs"
138
+ />
139
+ </div>
140
+ <button
141
+ type="submit"
142
+ disabled={submitting}
143
+ className="bg-teal-600 text-white font-medium px-4 py-2 rounded hover:bg-teal-700 disabled:opacity-50"
144
+ >
145
+ {submitting ? 'Saving...' : 'Save report'}
146
+ </button>
147
+ </form>
148
+ </section>
149
+
150
+ <section>
151
+ <h2 className="text-lg font-semibold text-slate-800 mb-4">Saved medical reports</h2>
152
+ {loading ? (
153
+ <p className="text-slate-500">Loading...</p>
154
+ ) : reports.length === 0 ? (
155
+ <p className="text-slate-500">No medical reports yet.</p>
156
+ ) : (
157
+ <div className="space-y-4">
158
+ {reports.map((r) => (
159
+ <article key={r.id} className="bg-white rounded-lg border border-slate-200 p-5">
160
+ <div className="flex flex-wrap justify-between gap-2 mb-3">
161
+ <div>
162
+ <p className="font-semibold text-slate-800">{r.patientName}</p>
163
+ <p className="text-sm text-slate-500">Dr. {r.doctorName}</p>
164
+ </div>
165
+ <p className="text-sm text-slate-500">Report date: {r.reportDate}</p>
166
+ </div>
167
+ <p className="text-sm text-slate-700">
168
+ <span className="font-medium">Diagnosis:</span> {r.diagnosis}
169
+ </p>
170
+ <p className="text-sm text-slate-700 mt-2">
171
+ <span className="font-medium">Prescription:</span> {r.prescription}
172
+ </p>
173
+ </article>
174
+ ))}
175
+ </div>
176
+ )}
177
+ </section>
178
+ </div>
179
+ );
180
+ }
181
+
182
+ export default MedicalReports;
@@ -0,0 +1,237 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { api } from '../api';
3
+
4
+ const emptyForm = {
5
+ fullName: '',
6
+ gender: 'Male',
7
+ dateOfBirth: '',
8
+ phone: '',
9
+ address: '',
10
+ };
11
+
12
+ function Patients() {
13
+ const [patients, setPatients] = useState([]);
14
+ const [form, setForm] = useState(emptyForm);
15
+ const [error, setError] = useState('');
16
+ const [message, setMessage] = useState('');
17
+ const [editId, setEditId] = useState(null);
18
+ const [loading, setLoading] = useState(true);
19
+ const [submitting, setSubmitting] = useState(false);
20
+
21
+ const loadPatients = async () => {
22
+ try {
23
+ const response = await api.get('/patients');
24
+ setPatients(response.data.patients);
25
+ } catch (err) {
26
+ setError(err.response?.data?.message || 'Failed to load patients');
27
+ } finally {
28
+ setLoading(false);
29
+ }
30
+ };
31
+
32
+ useEffect(() => {
33
+ loadPatients();
34
+ }, []);
35
+
36
+ const handleChange = (e) => {
37
+ setForm((prev) => ({ ...prev, [e.target.name]: e.target.value }));
38
+ };
39
+
40
+ const handleEdit = (patient) => {
41
+ setEditId(patient.id);
42
+ setForm({
43
+ fullName: patient.fullName,
44
+ gender: patient.gender,
45
+ dateOfBirth: patient.dateOfBirth,
46
+ phone: patient.phone,
47
+ address: patient.address,
48
+ });
49
+ setError('');
50
+ setMessage('');
51
+ };
52
+
53
+ const handleCancelEdit = () => {
54
+ setEditId(null);
55
+ setForm(emptyForm);
56
+ setError('');
57
+ setMessage('');
58
+ };
59
+
60
+ const handleDelete = async (id) => {
61
+ if (!window.confirm('Delete this patient record?')) return;
62
+ setError('');
63
+ try {
64
+ await api.delete(`/patients/${id}`);
65
+ setMessage('Patient deleted successfully');
66
+ if (editId === id) handleCancelEdit();
67
+ loadPatients();
68
+ } catch (err) {
69
+ setError(err.response?.data?.message || 'Delete failed');
70
+ }
71
+ };
72
+
73
+ const handleSubmit = async (e) => {
74
+ e.preventDefault();
75
+ setSubmitting(true);
76
+ setError('');
77
+ setMessage('');
78
+
79
+ try {
80
+ if (editId) {
81
+ await api.put(`/patients/${editId}`, form);
82
+ setMessage('Patient updated successfully');
83
+ } else {
84
+ await api.post('/patients', form);
85
+ setMessage('Patient added successfully');
86
+ }
87
+ setForm(emptyForm);
88
+ setEditId(null);
89
+ loadPatients();
90
+ } catch (err) {
91
+ setError(err.response?.data?.message || 'Save failed');
92
+ } finally {
93
+ setSubmitting(false);
94
+ }
95
+ };
96
+
97
+ return (
98
+ <div className="max-w-5xl">
99
+ <h1 className="text-2xl font-bold text-slate-800 mb-6">Patient Management</h1>
100
+
101
+ {message && (
102
+ <p className="mb-4 text-sm text-green-700 bg-green-50 border border-green-200 rounded px-3 py-2">
103
+ {message}
104
+ </p>
105
+ )}
106
+ {error && (
107
+ <p className="mb-4 text-sm text-red-600 bg-red-50 border border-red-200 rounded px-3 py-2">
108
+ {error}
109
+ </p>
110
+ )}
111
+
112
+ <section className="bg-white rounded-lg shadow-md p-6 mb-8">
113
+ <h2 className="text-lg font-semibold text-slate-800 mb-4">
114
+ {editId ? 'Edit patient' : 'Register new patient'}
115
+ </h2>
116
+ <form onSubmit={handleSubmit} className="grid gap-4 sm:grid-cols-2">
117
+ <div className="sm:col-span-2">
118
+ <label className="block text-sm font-medium text-slate-700 mb-1">Full name</label>
119
+ <input
120
+ name="fullName"
121
+ required
122
+ value={form.fullName}
123
+ onChange={handleChange}
124
+ className="w-full border border-slate-300 rounded px-3 py-2"
125
+ />
126
+ </div>
127
+ <div>
128
+ <label className="block text-sm font-medium text-slate-700 mb-1">Gender</label>
129
+ <select
130
+ name="gender"
131
+ value={form.gender}
132
+ onChange={handleChange}
133
+ className="w-full border border-slate-300 rounded px-3 py-2"
134
+ >
135
+ <option>Male</option>
136
+ <option>Female</option>
137
+ <option>Other</option>
138
+ </select>
139
+ </div>
140
+ <div>
141
+ <label className="block text-sm font-medium text-slate-700 mb-1">Date of birth</label>
142
+ <input
143
+ name="dateOfBirth"
144
+ type="date"
145
+ required
146
+ value={form.dateOfBirth}
147
+ onChange={handleChange}
148
+ className="w-full border border-slate-300 rounded px-3 py-2"
149
+ />
150
+ </div>
151
+ <div>
152
+ <label className="block text-sm font-medium text-slate-700 mb-1">Phone</label>
153
+ <input
154
+ name="phone"
155
+ required
156
+ value={form.phone}
157
+ onChange={handleChange}
158
+ className="w-full border border-slate-300 rounded px-3 py-2"
159
+ />
160
+ </div>
161
+ <div className="sm:col-span-2">
162
+ <label className="block text-sm font-medium text-slate-700 mb-1">Address</label>
163
+ <textarea
164
+ name="address"
165
+ rows={2}
166
+ required
167
+ value={form.address}
168
+ onChange={handleChange}
169
+ className="w-full border border-slate-300 rounded px-3 py-2"
170
+ />
171
+ </div>
172
+ <div className="sm:col-span-2 flex gap-2">
173
+ <button
174
+ type="submit"
175
+ disabled={submitting}
176
+ className="bg-teal-600 text-white font-medium px-4 py-2 rounded hover:bg-teal-700 disabled:opacity-50"
177
+ >
178
+ {submitting ? 'Saving...' : editId ? 'Update' : 'Add patient'}
179
+ </button>
180
+ {editId && (
181
+ <button
182
+ type="button"
183
+ onClick={handleCancelEdit}
184
+ className="border border-slate-300 px-4 py-2 rounded hover:bg-slate-50"
185
+ >
186
+ Cancel
187
+ </button>
188
+ )}
189
+ </div>
190
+ </form>
191
+ </section>
192
+
193
+ <section>
194
+ <h2 className="text-lg font-semibold text-slate-800 mb-4">All patients</h2>
195
+ {loading ? (
196
+ <p className="text-slate-500">Loading...</p>
197
+ ) : patients.length === 0 ? (
198
+ <p className="text-slate-500">No patients registered yet.</p>
199
+ ) : (
200
+ <div className="overflow-x-auto bg-white rounded-lg border border-slate-200">
201
+ <table className="w-full text-sm text-left">
202
+ <thead className="bg-slate-50 text-slate-600">
203
+ <tr>
204
+ <th className="px-4 py-2">Name</th>
205
+ <th className="px-4 py-2">Gender</th>
206
+ <th className="px-4 py-2">DOB</th>
207
+ <th className="px-4 py-2">Phone</th>
208
+ <th className="px-4 py-2">Actions</th>
209
+ </tr>
210
+ </thead>
211
+ <tbody className="divide-y divide-slate-200">
212
+ {patients.map((p) => (
213
+ <tr key={p.id}>
214
+ <td className="px-4 py-2">{p.fullName}</td>
215
+ <td className="px-4 py-2">{p.gender}</td>
216
+ <td className="px-4 py-2">{p.dateOfBirth}</td>
217
+ <td className="px-4 py-2">{p.phone}</td>
218
+ <td className="px-4 py-2 space-x-2">
219
+ <button type="button" onClick={() => handleEdit(p)} className="text-teal-600 hover:underline">
220
+ Edit
221
+ </button>
222
+ <button type="button" onClick={() => handleDelete(p.id)} className="text-red-600 hover:underline">
223
+ Delete
224
+ </button>
225
+ </td>
226
+ </tr>
227
+ ))}
228
+ </tbody>
229
+ </table>
230
+ </div>
231
+ )}
232
+ </section>
233
+ </div>
234
+ );
235
+ }
236
+
237
+ export default Patients;