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,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;
|