create-steve-rogers 1.0.1 → 1.0.2

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 (42) hide show
  1. package/apps/SFMS/.env +9 -0
  2. package/apps/SFMS/README.md +0 -0
  3. package/apps/SFMS/backend/.env +9 -0
  4. package/apps/SFMS/backend/.env.example +9 -0
  5. package/apps/SFMS/backend/package-lock.json +1580 -0
  6. package/apps/SFMS/backend/package.json +23 -0
  7. package/apps/SFMS/backend/src/config/database.js +7 -0
  8. package/apps/SFMS/backend/src/config/env.js +35 -0
  9. package/apps/SFMS/backend/src/middleware/authMiddleware.js +32 -0
  10. package/apps/SFMS/backend/src/models/Payment.js +12 -0
  11. package/apps/SFMS/backend/src/models/Student.js +12 -0
  12. package/apps/SFMS/backend/src/models/User.js +13 -0
  13. package/apps/SFMS/backend/src/routes/authRoutes.js +93 -0
  14. package/apps/SFMS/backend/src/routes/paymentRoutes.js +117 -0
  15. package/apps/SFMS/backend/src/routes/reportRoutes.js +59 -0
  16. package/apps/SFMS/backend/src/routes/studentRoutes.js +79 -0
  17. package/apps/SFMS/backend/src/server.js +34 -0
  18. package/apps/SFMS/frontend/.env.example +8 -0
  19. package/apps/SFMS/frontend/dist/assets/index-B08X8imN.css +1 -0
  20. package/apps/SFMS/frontend/dist/assets/index-DVO0_wcb.js +67 -0
  21. package/apps/SFMS/frontend/dist/favicon.svg +4 -0
  22. package/apps/SFMS/frontend/dist/index.html +20 -0
  23. package/apps/SFMS/frontend/index.html +19 -0
  24. package/apps/SFMS/frontend/package-lock.json +2667 -0
  25. package/apps/SFMS/frontend/package.json +23 -0
  26. package/apps/SFMS/frontend/postcss.config.js +6 -0
  27. package/apps/SFMS/frontend/public/favicon.svg +4 -0
  28. package/apps/SFMS/frontend/src/App.jsx +41 -0
  29. package/apps/SFMS/frontend/src/api/apiClient.js +41 -0
  30. package/apps/SFMS/frontend/src/components/AppLayout.jsx +60 -0
  31. package/apps/SFMS/frontend/src/context/AuthContext.jsx +79 -0
  32. package/apps/SFMS/frontend/src/index.css +229 -0
  33. package/apps/SFMS/frontend/src/main.jsx +16 -0
  34. package/apps/SFMS/frontend/src/pages/DashboardPage.jsx +82 -0
  35. package/apps/SFMS/frontend/src/pages/LoginPage.jsx +142 -0
  36. package/apps/SFMS/frontend/src/pages/PaymentsPage.jsx +269 -0
  37. package/apps/SFMS/frontend/src/pages/ReportsPage.jsx +114 -0
  38. package/apps/SFMS/frontend/src/pages/StudentsPage.jsx +257 -0
  39. package/apps/SFMS/frontend/tailwind.config.js +21 -0
  40. package/apps/SFMS/frontend/vite.config.js +35 -0
  41. package/apps/config.js +7 -0
  42. package/package.json +1 -1
@@ -0,0 +1,142 @@
1
+ import { useState } from 'react';
2
+ import { Navigate, useNavigate, useLocation } from 'react-router-dom';
3
+ import { useAuth } from '../context/AuthContext.jsx';
4
+
5
+ export function LoginPage() {
6
+ const { isAuthenticated, login, register } = useAuth();
7
+ const navigate = useNavigate();
8
+ const location = useLocation();
9
+ const from = location.state?.from;
10
+ const returnPath = from?.pathname ? `${from.pathname}${from.search || ''}` : '/';
11
+
12
+ const [mode, setMode] = useState('login');
13
+ const [email, setEmail] = useState('');
14
+ const [password, setPassword] = useState('');
15
+ const [name, setName] = useState('');
16
+ const [fieldErrors, setFieldErrors] = useState({});
17
+ const [formError, setFormError] = useState('');
18
+ const [submitting, setSubmitting] = useState(false);
19
+
20
+ if (isAuthenticated) return <Navigate to={returnPath} replace />;
21
+
22
+ function validate() {
23
+ const err = {};
24
+ if (!email.trim()) err.email = 'Email is required';
25
+ else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) err.email = 'Invalid email';
26
+ if (!password || password.length < 6) err.password = 'At least 6 characters';
27
+ if (mode === 'register' && !name.trim()) err.name = 'Name is required';
28
+ setFieldErrors(err);
29
+ return Object.keys(err).length === 0;
30
+ }
31
+
32
+ async function onSubmit(e) {
33
+ e.preventDefault();
34
+ setFormError('');
35
+ if (!validate()) return;
36
+ setSubmitting(true);
37
+ try {
38
+ if (mode === 'login') await login(email.trim(), password);
39
+ else await register(name.trim(), email.trim(), password, 'admin');
40
+ navigate(returnPath, { replace: true });
41
+ } catch (err) {
42
+ setFormError(err.message || 'Something went wrong');
43
+ } finally {
44
+ setSubmitting(false);
45
+ }
46
+ }
47
+
48
+ return (
49
+ <div className="auth-page">
50
+ <div className="auth-card">
51
+ <div className="auth-split">
52
+ <section className="auth-panel">
53
+ <p className="text-sm uppercase tracking-[0.3em] text-slate-500">School fee system</p>
54
+ <h1 className="mt-4 text-4xl font-display font-bold text-slate-900">Access your SFMS dashboard</h1>
55
+ <p className="mt-3 text-slate-600 leading-7">Sign in to track students, payments, and reports in one clean workspace.</p>
56
+
57
+ <div className="auth-switcher mt-8">
58
+ <button
59
+ type="button"
60
+ className={mode === 'login' ? 'active' : ''}
61
+ onClick={() => {
62
+ setMode('login');
63
+ setFieldErrors({});
64
+ setFormError('');
65
+ }}
66
+ >
67
+ Login
68
+ </button>
69
+ <button
70
+ type="button"
71
+ className={mode === 'register' ? 'active' : ''}
72
+ onClick={() => {
73
+ setMode('register');
74
+ setFieldErrors({});
75
+ setFormError('');
76
+ }}
77
+ >
78
+ Register
79
+ </button>
80
+ </div>
81
+
82
+ {formError && (
83
+ <div className="mt-6 rounded-3xl border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">
84
+ {formError}
85
+ </div>
86
+ )}
87
+
88
+ <form onSubmit={onSubmit} className="mt-6 space-y-5">
89
+ {mode === 'register' && (
90
+ <div className="input-group">
91
+ <label className="text-sm font-medium text-slate-700">Full name</label>
92
+ <input
93
+ type="text"
94
+ value={name}
95
+ onChange={(e) => setName(e.target.value)}
96
+ className="input-field"
97
+ autoComplete="name"
98
+ />
99
+ {fieldErrors.name && <p className="text-xs text-red-600">{fieldErrors.name}</p>}
100
+ </div>
101
+ )}
102
+ <div className="input-group">
103
+ <label className="text-sm font-medium text-slate-700">Email</label>
104
+ <input
105
+ type="email"
106
+ value={email}
107
+ onChange={(e) => setEmail(e.target.value)}
108
+ className="input-field"
109
+ autoComplete="email"
110
+ />
111
+ {fieldErrors.email && <p className="text-xs text-red-600">{fieldErrors.email}</p>}
112
+ </div>
113
+ <div className="input-group">
114
+ <label className="text-sm font-medium text-slate-700">Password</label>
115
+ <input
116
+ type="password"
117
+ value={password}
118
+ onChange={(e) => setPassword(e.target.value)}
119
+ className="input-field"
120
+ autoComplete={mode === 'login' ? 'current-password' : 'new-password'}
121
+ />
122
+ {fieldErrors.password && <p className="text-xs text-red-600">{fieldErrors.password}</p>}
123
+ </div>
124
+ <button type="submit" disabled={submitting} className="btn-primary w-full">
125
+ {submitting ? 'Please wait…' : mode === 'login' ? 'Sign in' : 'Create account'}
126
+ </button>
127
+ </form>
128
+ </section>
129
+ <aside className="auth-side">
130
+ <p className="auth-logo">SFMS</p>
131
+ <p className="auth-copy">
132
+ A modern school fee experience with calm colors, spacious forms, and clear navigation.
133
+ </p>
134
+ <p className="text-sm text-slate-100/80">
135
+ Keep student records clean, track every payment, and build reports without clutter.
136
+ </p>
137
+ </aside>
138
+ </div>
139
+ </div>
140
+ </div>
141
+ );
142
+ }
@@ -0,0 +1,269 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { apiRequest } from '../api/apiClient.js';
3
+
4
+ const inputClass = 'input-field';
5
+
6
+ export function PaymentsPage() {
7
+ const [list, setList] = useState([]);
8
+ const [students, setStudents] = useState([]);
9
+ const [submitting, setSubmitting] = useState(false);
10
+ const [error, setError] = useState('');
11
+ const [formTarget, setFormTarget] = useState(null);
12
+
13
+ const [studentId, setStudentId] = useState('');
14
+ const [amount, setAmount] = useState('');
15
+ const [paymentDate, setPaymentDate] = useState('');
16
+ const [fieldErrors, setFieldErrors] = useState({});
17
+
18
+ async function load() {
19
+ setError('');
20
+ try {
21
+ const [pay, stud] = await Promise.all([
22
+ apiRequest('/api/payments'),
23
+ apiRequest('/api/students'),
24
+ ]);
25
+ setList(pay);
26
+ setStudents(stud);
27
+ } catch (e) {
28
+ setError(e.message);
29
+ }
30
+ }
31
+
32
+ useEffect(() => {
33
+ load();
34
+ }, []);
35
+
36
+ function validate() {
37
+ const err = {};
38
+ if (!studentId) err.studentId = 'Select a student';
39
+ if (!amount.trim()) {
40
+ err.amount = 'Amount is required';
41
+ } else if (!/^[0-9]+$/.test(amount)) {
42
+ err.amount = 'Amount must contain only digits';
43
+ } else if (Number(amount) < 0) {
44
+ err.amount = 'Amount must be zero or greater';
45
+ }
46
+ if (!paymentDate) err.paymentDate = 'Pick a date';
47
+ setFieldErrors(err);
48
+ return Object.keys(err).length === 0;
49
+ }
50
+
51
+ function openNew() {
52
+ setFormTarget('new');
53
+ setStudentId(students[0]?.id || '');
54
+ setAmount('');
55
+ setPaymentDate(new Date().toISOString().slice(0, 10));
56
+ setFieldErrors({});
57
+ }
58
+
59
+ function openEdit(row) {
60
+ setFormTarget(row);
61
+ setStudentId(row.student_id || '');
62
+ setAmount(String(row.amount ?? ''));
63
+ setPaymentDate(row.payment_date?.slice(0, 10) || '');
64
+ setFieldErrors({});
65
+ }
66
+
67
+ function closeForm() {
68
+ setFormTarget(null);
69
+ setFieldErrors({});
70
+ }
71
+
72
+ async function save(e) {
73
+ e.preventDefault();
74
+ if (!validate()) return;
75
+ setSubmitting(true);
76
+ setError('');
77
+ try {
78
+ const payload = {
79
+ student_id: studentId,
80
+ amount: Number(amount),
81
+ payment_date: paymentDate,
82
+ };
83
+ if (formTarget !== 'new' && formTarget?.id) {
84
+ await apiRequest(`/api/payments/${formTarget.id}`, {
85
+ method: 'PUT',
86
+ body: JSON.stringify(payload),
87
+ });
88
+ } else {
89
+ await apiRequest('/api/payments', { method: 'POST', body: JSON.stringify(payload) });
90
+ }
91
+ closeForm();
92
+ await load();
93
+ } catch (err) {
94
+ setError(err.message);
95
+ } finally {
96
+ setSubmitting(false);
97
+ }
98
+ }
99
+
100
+ async function remove(id) {
101
+ if (!window.confirm('Delete this payment record?')) return;
102
+ try {
103
+ await apiRequest(`/api/payments/${id}`, { method: 'DELETE' });
104
+ await load();
105
+ } catch (e) {
106
+ setError(e.message);
107
+ }
108
+ }
109
+
110
+ return (
111
+ <div>
112
+ <div className="app-header">
113
+ <p className="text-sm uppercase tracking-[0.3em] text-slate-500">Payments</p>
114
+ <h1 className="page-title">Fee transactions</h1>
115
+ <p className="page-subtitle">Record each payment and keep your financial view tidy.</p>
116
+ </div>
117
+
118
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between mb-6">
119
+ <div className="space-y-1">
120
+ <p className="text-base font-semibold text-slate-900">Payments ledger</p>
121
+ <p className="text-sm text-slate-500">Create new entries or update existing ones quickly.</p>
122
+ </div>
123
+ <button type="button" onClick={openNew} disabled={students.length === 0} className="btn-primary w-full sm:w-auto disabled:opacity-50">
124
+ + New payment
125
+ </button>
126
+ </div>
127
+
128
+ {students.length === 0 && (
129
+ <div className="mb-6 rounded-[28px] border border-amber-100 bg-amber-50 px-4 py-4 text-sm text-amber-800">
130
+ Add at least one student before recording payments.
131
+ </div>
132
+ )}
133
+
134
+ {error && (
135
+ <div className="mb-6 rounded-[28px] border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">
136
+ {error}
137
+ </div>
138
+ )}
139
+
140
+ {formTarget === 'new' && (
141
+ <form onSubmit={save} className="form-card mb-8 max-w-2xl space-y-5">
142
+ <div className="flex items-center justify-between gap-4">
143
+ <h2 className="text-xl font-semibold text-slate-900">New payment</h2>
144
+ <button type="button" onClick={closeForm} className="btn-secondary">
145
+ Close
146
+ </button>
147
+ </div>
148
+ <div className="grid gap-4 md:grid-cols-3">
149
+ <div className="input-group">
150
+ <label className="text-sm font-medium text-slate-700">Student</label>
151
+ <select className={inputClass} value={studentId} onChange={(e) => setStudentId(e.target.value)}>
152
+ {students.map((s) => (
153
+ <option key={s.id} value={s.id}>
154
+ {s.full_name} — {s.class}
155
+ </option>
156
+ ))}
157
+ </select>
158
+ {fieldErrors.studentId && <p className="text-xs text-red-600">{fieldErrors.studentId}</p>}
159
+ </div>
160
+ <div className="input-group">
161
+ <label className="text-sm font-medium text-slate-700">Amount</label>
162
+ <input
163
+ type="text"
164
+ inputMode="numeric"
165
+ className={inputClass}
166
+ value={amount}
167
+ onChange={(e) => setAmount(e.target.value.replace(/\D/g, ''))}
168
+ placeholder="Numbers only"
169
+ />
170
+ {fieldErrors.amount && <p className="text-xs text-red-600">{fieldErrors.amount}</p>}
171
+ </div>
172
+ <div className="input-group">
173
+ <label className="text-sm font-medium text-slate-700">Payment date</label>
174
+ <input type="date" className={inputClass} value={paymentDate} onChange={(e) => setPaymentDate(e.target.value)} />
175
+ {fieldErrors.paymentDate && <p className="text-xs text-red-600">{fieldErrors.paymentDate}</p>}
176
+ </div>
177
+ </div>
178
+ <div className="form-actions">
179
+ <button type="submit" disabled={submitting} className="btn-primary">
180
+ {submitting ? 'Saving…' : 'Save'}
181
+ </button>
182
+ <button type="button" onClick={closeForm} className="btn-secondary">
183
+ Cancel
184
+ </button>
185
+ </div>
186
+ </form>
187
+ )}
188
+
189
+ <div className="table-shell">
190
+ <table className="min-w-full text-sm">
191
+ <thead className="table-head text-left">
192
+ <tr>
193
+ <th className="table-cell font-medium">Student</th>
194
+ <th className="table-cell font-medium">Amount</th>
195
+ <th className="table-cell font-medium">Date</th>
196
+ <th className="table-cell font-medium">Actions</th>
197
+ </tr>
198
+ </thead>
199
+ <tbody>
200
+ {list.map((p) => (
201
+ <tr key={p.id} className="table-row">
202
+ <td className="table-cell">
203
+ {p.student?.full_name || '—'} <span className="text-slate-500">({p.student?.class || '—'})</span>
204
+ </td>
205
+ <td className="table-cell font-semibold">{Number(p.amount).toLocaleString()} FRW</td>
206
+ <td className="table-cell">{p.payment_date}</td>
207
+ <td className="table-cell space-x-2 whitespace-nowrap">
208
+ <button type="button" className="btn-secondary" onClick={() => openEdit(p)}>
209
+ Edit
210
+ </button>
211
+ <button type="button" className="btn-destructive" onClick={() => remove(p.id)}>
212
+ Delete
213
+ </button>
214
+ </td>
215
+ </tr>
216
+ ))}
217
+ </tbody>
218
+ </table>
219
+ {list.length === 0 && <p className="placeholder-card">No payments recorded.</p>}
220
+ </div>
221
+
222
+ {formTarget && formTarget !== 'new' && (
223
+ <div className="fixed inset-0 bg-black/40 flex items-center justify-center p-4 z-50" onClick={(e) => e.target === e.currentTarget && closeForm()} role="presentation">
224
+ <div className="form-card max-w-md w-full" onClick={(e) => e.stopPropagation()} role="dialog" aria-modal="true">
225
+ <div className="flex items-center justify-between gap-4 mb-4">
226
+ <h2 className="text-xl font-semibold text-slate-900">Edit payment</h2>
227
+ <button type="button" onClick={closeForm} className="btn-secondary">
228
+ Close
229
+ </button>
230
+ </div>
231
+ <form onSubmit={save} className="space-y-4">
232
+ <div className="input-group">
233
+ <label className="text-sm font-medium text-slate-700">Student</label>
234
+ <select className={inputClass} value={studentId} onChange={(e) => setStudentId(e.target.value)}>
235
+ {students.map((s) => (
236
+ <option key={s.id} value={s.id}>
237
+ {s.full_name}
238
+ </option>
239
+ ))}
240
+ </select>
241
+ </div>
242
+ <div className="input-group">
243
+ <label className="text-sm font-medium text-slate-700">Amount</label>
244
+ <input
245
+ type="text"
246
+ inputMode="numeric"
247
+ className={inputClass}
248
+ value={amount}
249
+ onChange={(e) => setAmount(e.target.value.replace(/\D/g, ''))}
250
+ placeholder="Numbers only"
251
+ />
252
+ {fieldErrors.amount && <p className="text-xs text-red-600">{fieldErrors.amount}</p>}
253
+ </div>
254
+ <div className="input-group">
255
+ <label className="text-sm font-medium text-slate-700">Date</label>
256
+ <input type="date" className={inputClass} value={paymentDate} onChange={(e) => setPaymentDate(e.target.value)} />
257
+ </div>
258
+ <div className="form-actions">
259
+ <button type="submit" disabled={submitting} className="btn-primary w-full">
260
+ Save
261
+ </button>
262
+ </div>
263
+ </form>
264
+ </div>
265
+ </div>
266
+ )}
267
+ </div>
268
+ );
269
+ }
@@ -0,0 +1,114 @@
1
+ import { useState } from 'react';
2
+ import { apiRequest } from '../api/apiClient.js';
3
+
4
+ export function ReportsPage() {
5
+ const [start, setStart] = useState('');
6
+ const [end, setEnd] = useState('');
7
+ const [report, setReport] = useState(null);
8
+ const [loading, setLoading] = useState(false);
9
+ const [error, setError] = useState('');
10
+ const [fieldErrors, setFieldErrors] = useState({});
11
+
12
+ function validate() {
13
+ const err = {};
14
+ if (!start) err.start = 'Start date is required';
15
+ if (!end) err.end = 'End date is required';
16
+ if (start && end && start > end) err.end = 'End date must be on or after start date';
17
+ setFieldErrors(err);
18
+ return Object.keys(err).length === 0;
19
+ }
20
+
21
+ async function onSubmit(e) {
22
+ e.preventDefault();
23
+ if (!validate()) return;
24
+ setLoading(true);
25
+ setError('');
26
+ setReport(null);
27
+ try {
28
+ const q = new URLSearchParams({ start, end });
29
+ const data = await apiRequest(`/api/reports?${q}`);
30
+ setReport(data);
31
+ } catch (err) {
32
+ setError(err.message);
33
+ } finally {
34
+ setLoading(false);
35
+ }
36
+ }
37
+
38
+ return (
39
+ <div>
40
+ <div className="app-header">
41
+ <p className="text-sm uppercase tracking-[0.3em] text-slate-500">Reports</p>
42
+ <h1 className="page-title">Payment summaries</h1>
43
+ <p className="page-subtitle">Choose a date range and get a neat report of collected fees.</p>
44
+ </div>
45
+
46
+ <form onSubmit={onSubmit} className="form-card grid gap-5 md:grid-cols-[1.1fr_1.1fr_auto] items-end">
47
+ <div className="input-group">
48
+ <label className="text-sm font-medium text-slate-700">Start date</label>
49
+ <input
50
+ type="date"
51
+ value={start}
52
+ onChange={(e) => setStart(e.target.value)}
53
+ className="input-field"
54
+ />
55
+ {fieldErrors.start && <p className="text-xs text-red-600">{fieldErrors.start}</p>}
56
+ </div>
57
+ <div className="input-group">
58
+ <label className="text-sm font-medium text-slate-700">End date</label>
59
+ <input
60
+ type="date"
61
+ value={end}
62
+ onChange={(e) => setEnd(e.target.value)}
63
+ className="input-field"
64
+ />
65
+ {fieldErrors.end && <p className="text-xs text-red-600">{fieldErrors.end}</p>}
66
+ </div>
67
+ <button type="submit" disabled={loading} className="btn-primary h-14 w-full md:w-auto">
68
+ {loading ? 'Loading…' : 'Run report'}
69
+ </button>
70
+ </form>
71
+
72
+ {error && (
73
+ <div className="mt-4 rounded-[26px] border border-red-100 bg-red-50 px-4 py-3 text-sm text-red-700">
74
+ {error}
75
+ </div>
76
+ )}
77
+
78
+ {report && (
79
+ <div className="mt-8 space-y-6">
80
+ <div className="panel-card-alt flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
81
+ <p className="text-slate-600 text-sm">
82
+ Showing results from <strong>{report.start}</strong> to <strong>{report.end}</strong>
83
+ </p>
84
+ <p className="text-lg font-display font-bold text-sfms-ink">
85
+ Total: {Number(report.total_amount_paid).toLocaleString()} FRW
86
+ </p>
87
+ </div>
88
+ <div className="table-shell">
89
+ <table className="min-w-full text-sm">
90
+ <thead className="table-head text-left">
91
+ <tr>
92
+ <th className="table-cell font-medium">Student</th>
93
+ <th className="table-cell font-medium">Class</th>
94
+ <th className="table-cell font-medium">Amount</th>
95
+ <th className="table-cell font-medium">Payment date</th>
96
+ </tr>
97
+ </thead>
98
+ <tbody>
99
+ {report.payments.map((p) => (
100
+ <tr key={p.id} className="table-row">
101
+ <td className="table-cell">{p.student?.full_name || '—'}</td>
102
+ <td className="table-cell">{p.student?.class || '—'}</td>
103
+ <td className="table-cell font-semibold">{Number(p.amount).toLocaleString()} FRW</td>
104
+ <td className="table-cell">{p.payment_date}</td>
105
+ </tr>
106
+ ))}
107
+ </tbody>
108
+ </table>
109
+ </div>
110
+ </div>
111
+ )}
112
+ </div>
113
+ );
114
+ }