create-steve-rogers 1.0.0 → 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 (94) 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/SIMS/.env +4 -0
  42. package/apps/SIMS/README.md +138 -0
  43. package/apps/SIMS/backend/.env +4 -0
  44. package/apps/SIMS/backend/.env.example +4 -0
  45. package/apps/SIMS/backend/package-lock.json +1600 -0
  46. package/apps/SIMS/backend/package.json +22 -0
  47. package/apps/SIMS/backend/src/config/db.js +9 -0
  48. package/apps/SIMS/backend/src/controllers/authController.js +93 -0
  49. package/apps/SIMS/backend/src/controllers/simsReportController.js +94 -0
  50. package/apps/SIMS/backend/src/controllers/sparePartController.js +41 -0
  51. package/apps/SIMS/backend/src/controllers/stockInController.js +45 -0
  52. package/apps/SIMS/backend/src/controllers/stockOutController.js +123 -0
  53. package/apps/SIMS/backend/src/middleware/auth.js +8 -0
  54. package/apps/SIMS/backend/src/models/SparePart.js +17 -0
  55. package/apps/SIMS/backend/src/models/StockIn.js +16 -0
  56. package/apps/SIMS/backend/src/models/StockOut.js +18 -0
  57. package/apps/SIMS/backend/src/models/User.js +11 -0
  58. package/apps/SIMS/backend/src/routes/authRoutes.js +12 -0
  59. package/apps/SIMS/backend/src/routes/simsReportRoutes.js +8 -0
  60. package/apps/SIMS/backend/src/routes/sparePartRoutes.js +8 -0
  61. package/apps/SIMS/backend/src/routes/stockInRoutes.js +8 -0
  62. package/apps/SIMS/backend/src/routes/stockOutRoutes.js +10 -0
  63. package/apps/SIMS/backend/src/server.js +62 -0
  64. package/apps/SIMS/backend/src/utils/passwordPolicy.js +10 -0
  65. package/apps/SIMS/backend/src/utils/sparePartHelpers.js +5 -0
  66. package/apps/SIMS/frontend/dist/assets/index-3hv-vGL2.css +2 -0
  67. package/apps/SIMS/frontend/dist/assets/index-T8XT7M6y.js +19 -0
  68. package/apps/SIMS/frontend/dist/index.html +14 -0
  69. package/apps/SIMS/frontend/index.html +13 -0
  70. package/apps/SIMS/frontend/package-lock.json +3053 -0
  71. package/apps/SIMS/frontend/package.json +31 -0
  72. package/apps/SIMS/frontend/src/App.jsx +112 -0
  73. package/apps/SIMS/frontend/src/api/authApi.js +7 -0
  74. package/apps/SIMS/frontend/src/api/client.js +8 -0
  75. package/apps/SIMS/frontend/src/api/simsReportApi.js +5 -0
  76. package/apps/SIMS/frontend/src/api/sparePartsApi.js +4 -0
  77. package/apps/SIMS/frontend/src/api/stockInApi.js +4 -0
  78. package/apps/SIMS/frontend/src/api/stockOutApi.js +6 -0
  79. package/apps/SIMS/frontend/src/api/usersApi.js +3 -0
  80. package/apps/SIMS/frontend/src/components/AppLayout.jsx +60 -0
  81. package/apps/SIMS/frontend/src/index.css +737 -0
  82. package/apps/SIMS/frontend/src/main.jsx +13 -0
  83. package/apps/SIMS/frontend/src/pages/DashboardPage.jsx +179 -0
  84. package/apps/SIMS/frontend/src/pages/LoginPage.jsx +75 -0
  85. package/apps/SIMS/frontend/src/pages/RegisterPage.jsx +78 -0
  86. package/apps/SIMS/frontend/src/pages/ReportsPage.jsx +108 -0
  87. package/apps/SIMS/frontend/src/pages/ResetPasswordPage.jsx +75 -0
  88. package/apps/SIMS/frontend/src/pages/SparePartPage.jsx +128 -0
  89. package/apps/SIMS/frontend/src/pages/StockInPage.jsx +100 -0
  90. package/apps/SIMS/frontend/src/pages/StockOutPage.jsx +206 -0
  91. package/apps/SIMS/frontend/src/utils/passwordPolicy.js +8 -0
  92. package/apps/SIMS/frontend/vite.config.js +8 -0
  93. package/apps/config.js +13 -0
  94. package/package.json +1 -1
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "sfms-backend",
3
+ "version": "1.0.0",
4
+ "description": "SFMS API — Express + MongoDB",
5
+ "type": "module",
6
+ "main": "src/server.js",
7
+ "scripts": {
8
+ "start": "node src/server.js",
9
+ "dev": "nodemon src/server.js"
10
+ },
11
+ "engines": {
12
+ "node": ">=18"
13
+ },
14
+ "dependencies": {
15
+ "bcryptjs": "^2.4.3",
16
+ "cors": "^2.8.5",
17
+ "dotenv": "^16.4.5",
18
+ "express": "^4.21.0",
19
+ "jsonwebtoken": "^9.0.2",
20
+ "mongoose": "^8.7.0",
21
+ "nodemon": "^3.1.14"
22
+ }
23
+ }
@@ -0,0 +1,7 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ export async function connectMongo(uri) {
4
+ mongoose.set('strictQuery', true);
5
+ await mongoose.connect(uri);
6
+ return mongoose.connection;
7
+ }
@@ -0,0 +1,35 @@
1
+ /** Central env with fallbacks (||) so missing vars degrade to safe defaults where possible. */
2
+
3
+ const DEFAULT_BACKEND_PORT = 5000;
4
+
5
+ function parsePort(raw, fallback) {
6
+ const n = Number(raw);
7
+ return Number.isFinite(n) && n > 0 ? n : fallback;
8
+ }
9
+
10
+ export const env = {
11
+ port: parsePort(
12
+ process.env.PORT || process.env.SFMS_BACKEND_PORT,
13
+ DEFAULT_BACKEND_PORT
14
+ ),
15
+ mongodbUri:
16
+ process.env.MONGODB_URI?.trim() ||
17
+ process.env.MONGO_URL?.trim() ||
18
+ process.env.DATABASE_URL?.trim() ||
19
+ '',
20
+ jwtSecret:
21
+ process.env.JWT_SECRET?.trim() ||
22
+ process.env.JWT_INGUFU?.trim() ||
23
+ '',
24
+ nodeEnv: process.env.NODE_ENV || 'development',
25
+ };
26
+
27
+ export function requireMongoUri() {
28
+ if (!env.mongodbUri) {
29
+ console.error(
30
+ 'Missing MongoDB connection string. Set MONGODB_URI (or MONGO_URL / DATABASE_URL) in .env'
31
+ );
32
+ process.exit(1);
33
+ }
34
+ return env.mongodbUri;
35
+ }
@@ -0,0 +1,32 @@
1
+ import jwt from 'jsonwebtoken';
2
+ import { env } from '../config/env.js';
3
+ import { User } from '../models/User.js';
4
+
5
+ function jwtSecret() {
6
+ return env.jwtSecret;
7
+ }
8
+
9
+ /** Validates Bearer JWT and attaches req.user */
10
+ export async function protect(req, res, next) {
11
+ const header = req.headers.authorization;
12
+ const token = header?.startsWith('Bearer ') ? header.slice(7) : null;
13
+
14
+ if (!token) {
15
+ return res.status(401).json({ message: 'No token provided' });
16
+ }
17
+
18
+ try {
19
+ const secret = jwtSecret();
20
+ if (!secret) throw new Error('JWT_SECRET missing');
21
+
22
+ const payload = jwt.verify(token, secret);
23
+ const user = await User.findById(payload.id);
24
+ if (!user) {
25
+ return res.status(401).json({ message: 'User not found' });
26
+ }
27
+ req.user = { id: String(user._id), role: user.role };
28
+ next();
29
+ } catch {
30
+ return res.status(401).json({ message: 'Invalid or expired token' });
31
+ }
32
+ }
@@ -0,0 +1,12 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ const paymentSchema = new mongoose.Schema(
4
+ {
5
+ student: { type: mongoose.Schema.Types.ObjectId, ref: 'Student', required: true },
6
+ amount: { type: Number, required: true, min: 0 },
7
+ paymentDate: { type: Date, required: true },
8
+ },
9
+ { timestamps: { createdAt: 'created_at', updatedAt: false } }
10
+ );
11
+
12
+ export const Payment = mongoose.model('Payment', paymentSchema);
@@ -0,0 +1,12 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ const studentSchema = new mongoose.Schema(
4
+ {
5
+ fullName: { type: String, required: true, trim: true },
6
+ className: { type: String, required: true, trim: true },
7
+ parentPhone: { type: String, required: true, trim: true },
8
+ },
9
+ { timestamps: { createdAt: 'created_at', updatedAt: false } }
10
+ );
11
+
12
+ export const Student = mongoose.model('Student', studentSchema);
@@ -0,0 +1,13 @@
1
+ import mongoose from 'mongoose';
2
+
3
+ const userSchema = new mongoose.Schema(
4
+ {
5
+ name: { type: String, required: true, trim: true },
6
+ email: { type: String, required: true, unique: true, lowercase: true, trim: true },
7
+ password: { type: String, required: true, minlength: 6, select: false },
8
+ role: { type: String, enum: ['admin', 'staff'], default: 'admin' },
9
+ },
10
+ { timestamps: { createdAt: 'created_at', updatedAt: false } }
11
+ );
12
+
13
+ export const User = mongoose.model('User', userSchema);
@@ -0,0 +1,93 @@
1
+ import { Router } from 'express';
2
+ import bcrypt from 'bcryptjs';
3
+ import jwt from 'jsonwebtoken';
4
+ import { env } from '../config/env.js';
5
+ import { User } from '../models/User.js';
6
+
7
+ const router = Router();
8
+
9
+ function jwtSecret() {
10
+ return env.jwtSecret;
11
+ }
12
+
13
+ function signToken(user) {
14
+ const secret = jwtSecret();
15
+ if (!secret) throw new Error('JWT_SECRET');
16
+ return jwt.sign({ id: String(user._id), role: user.role }, secret, { expiresIn: '7d' });
17
+ }
18
+
19
+ router.post('/register', async (req, res) => {
20
+ try {
21
+ const { name, email, password, role } = req.body;
22
+ if (!name || !email || !password) {
23
+ return res.status(400).json({ message: 'Name, email and password are required' });
24
+ }
25
+ if (String(password).length < 6) {
26
+ return res.status(400).json({ message: 'Password must be at least 6 characters' });
27
+ }
28
+
29
+ const exists = await User.findOne({ email: String(email).toLowerCase() });
30
+ if (exists) {
31
+ return res.status(409).json({ message: 'Email already registered' });
32
+ }
33
+
34
+ const hash = await bcrypt.hash(String(password), 12);
35
+ const user = await User.create({
36
+ name: String(name).trim(),
37
+ email: String(email).toLowerCase().trim(),
38
+ password: hash,
39
+ role: role === 'staff' ? 'staff' : 'admin',
40
+ });
41
+
42
+ const token = signToken(user);
43
+ return res.status(201).json({
44
+ token,
45
+ user: {
46
+ id: user._id,
47
+ name: user.name,
48
+ email: user.email,
49
+ role: user.role,
50
+ created_at: user.created_at,
51
+ },
52
+ });
53
+ } catch (err) {
54
+ console.error(err);
55
+ return res.status(500).json({ message: 'Registration failed' });
56
+ }
57
+ });
58
+
59
+ router.post('/login', async (req, res) => {
60
+ try {
61
+ const { email, password } = req.body;
62
+ if (!email || !password) {
63
+ return res.status(400).json({ message: 'Email and password are required' });
64
+ }
65
+
66
+ const user = await User.findOne({ email: String(email).toLowerCase() }).select('+password');
67
+ if (!user) {
68
+ return res.status(401).json({ message: 'Invalid email or password' });
69
+ }
70
+
71
+ const match = await bcrypt.compare(String(password), user.password);
72
+ if (!match) {
73
+ return res.status(401).json({ message: 'Invalid email or password' });
74
+ }
75
+
76
+ const token = signToken(user);
77
+ return res.json({
78
+ token,
79
+ user: {
80
+ id: user._id,
81
+ name: user.name,
82
+ email: user.email,
83
+ role: user.role,
84
+ created_at: user.created_at,
85
+ },
86
+ });
87
+ } catch (err) {
88
+ console.error(err);
89
+ return res.status(500).json({ message: 'Login failed' });
90
+ }
91
+ });
92
+
93
+ export default router;
@@ -0,0 +1,117 @@
1
+ import { Router } from 'express';
2
+ import mongoose from 'mongoose';
3
+ import { Payment } from '../models/Payment.js';
4
+ import { Student } from '../models/Student.js';
5
+ import { protect } from '../middleware/authMiddleware.js';
6
+
7
+ const router = Router();
8
+ router.use(protect);
9
+
10
+ function toApi(doc) {
11
+ if (!doc) return null;
12
+ const o = doc.toObject ? doc.toObject() : doc;
13
+ const st = o.student && typeof o.student === 'object' ? o.student : null;
14
+ return {
15
+ id: o._id,
16
+ student_id: o.student?._id ?? o.student,
17
+ amount: o.amount,
18
+ payment_date: o.paymentDate?.toISOString?.().slice(0, 10) ?? o.paymentDate,
19
+ created_at: o.created_at,
20
+ student: st
21
+ ? {
22
+ id: st._id,
23
+ full_name: st.fullName,
24
+ class: st.className,
25
+ parent_phone: st.parentPhone,
26
+ }
27
+ : undefined,
28
+ };
29
+ }
30
+
31
+ router.get('/', async (req, res) => {
32
+ try {
33
+ const rows = await Payment.find()
34
+ .populate('student', 'fullName className parentPhone')
35
+ .sort({ paymentDate: -1 });
36
+ return res.json(rows.map(toApi));
37
+ } catch (err) {
38
+ console.error(err);
39
+ return res.status(500).json({ message: 'Failed to list payments' });
40
+ }
41
+ });
42
+
43
+ router.post('/', async (req, res) => {
44
+ try {
45
+ const { student_id, amount, payment_date } = req.body;
46
+ if (!student_id || amount === undefined || amount === null || !payment_date) {
47
+ return res.status(400).json({ message: 'Student, amount and payment date are required' });
48
+ }
49
+ if (!mongoose.Types.ObjectId.isValid(student_id)) {
50
+ return res.status(400).json({ message: 'Invalid student id' });
51
+ }
52
+ const student = await Student.findById(student_id);
53
+ if (!student) return res.status(404).json({ message: 'Student not found' });
54
+
55
+ const num = Number(amount);
56
+ if (Number.isNaN(num) || num < 0) {
57
+ return res.status(400).json({ message: 'Amount must be a valid non-negative number' });
58
+ }
59
+
60
+ const payment = await Payment.create({
61
+ student: student_id,
62
+ amount: num,
63
+ paymentDate: new Date(payment_date),
64
+ });
65
+ await payment.populate('student', 'fullName className parentPhone');
66
+ return res.status(201).json(toApi(payment));
67
+ } catch (err) {
68
+ console.error(err);
69
+ return res.status(500).json({ message: 'Failed to create payment' });
70
+ }
71
+ });
72
+
73
+ router.put('/:id', async (req, res) => {
74
+ try {
75
+ const { student_id, amount, payment_date } = req.body;
76
+ const patch = {};
77
+ if (student_id !== undefined) {
78
+ if (!mongoose.Types.ObjectId.isValid(student_id)) {
79
+ return res.status(400).json({ message: 'Invalid student id' });
80
+ }
81
+ const student = await Student.findById(student_id);
82
+ if (!student) return res.status(404).json({ message: 'Student not found' });
83
+ patch.student = student_id;
84
+ }
85
+ if (amount !== undefined) {
86
+ const num = Number(amount);
87
+ if (Number.isNaN(num) || num < 0) {
88
+ return res.status(400).json({ message: 'Amount must be a valid non-negative number' });
89
+ }
90
+ patch.amount = num;
91
+ }
92
+ if (payment_date !== undefined) patch.paymentDate = new Date(payment_date);
93
+
94
+ const payment = await Payment.findByIdAndUpdate(req.params.id, patch, {
95
+ new: true,
96
+ runValidators: true,
97
+ }).populate('student', 'fullName className parentPhone');
98
+ if (!payment) return res.status(404).json({ message: 'Payment not found' });
99
+ return res.json(toApi(payment));
100
+ } catch (err) {
101
+ console.error(err);
102
+ return res.status(500).json({ message: 'Failed to update payment' });
103
+ }
104
+ });
105
+
106
+ router.delete('/:id', async (req, res) => {
107
+ try {
108
+ const payment = await Payment.findByIdAndDelete(req.params.id);
109
+ if (!payment) return res.status(404).json({ message: 'Payment not found' });
110
+ return res.json({ message: 'Deleted', id: req.params.id });
111
+ } catch (err) {
112
+ console.error(err);
113
+ return res.status(500).json({ message: 'Failed to delete' });
114
+ }
115
+ });
116
+
117
+ export default router;
@@ -0,0 +1,59 @@
1
+ import { Router } from 'express';
2
+ import { Payment } from '../models/Payment.js';
3
+ import { protect } from '../middleware/authMiddleware.js';
4
+
5
+ const router = Router();
6
+ router.use(protect);
7
+
8
+ function toApi(doc) {
9
+ const o = doc.toObject ? doc.toObject() : doc;
10
+ const st = o.student && typeof o.student === 'object' ? o.student : null;
11
+ return {
12
+ id: o._id,
13
+ student_id: o.student?._id ?? o.student,
14
+ amount: o.amount,
15
+ payment_date: o.paymentDate?.toISOString?.().slice(0, 10) ?? o.paymentDate,
16
+ created_at: o.created_at,
17
+ student: st
18
+ ? { id: st._id, full_name: st.fullName, class: st.className }
19
+ : undefined,
20
+ };
21
+ }
22
+
23
+ router.get('/', async (req, res) => {
24
+ try {
25
+ const { start, end } = req.query;
26
+ if (!start || !end) {
27
+ return res.status(400).json({ message: 'Query params start and end (yyyy-mm-dd) are required' });
28
+ }
29
+
30
+ const startDate = new Date(String(start));
31
+ const endDate = new Date(String(end));
32
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) {
33
+ return res.status(400).json({ message: 'Invalid date format; use yyyy-mm-dd' });
34
+ }
35
+
36
+ // Iherezo ry'itariki: umunsi wuzura (intera ndangagaciro)
37
+ endDate.setHours(23, 59, 59, 999);
38
+
39
+ const rows = await Payment.find({
40
+ paymentDate: { $gte: startDate, $lte: endDate },
41
+ })
42
+ .populate('student', 'fullName className')
43
+ .sort({ paymentDate: -1 });
44
+
45
+ const total = rows.reduce((sum, row) => sum + (row.amount || 0), 0);
46
+
47
+ return res.json({
48
+ start: String(start),
49
+ end: String(end),
50
+ payments: rows.map(toApi),
51
+ total_amount_paid: total,
52
+ });
53
+ } catch (err) {
54
+ console.error(err);
55
+ return res.status(500).json({ message: 'Failed to build report' });
56
+ }
57
+ });
58
+
59
+ export default router;
@@ -0,0 +1,79 @@
1
+ import { Router } from 'express';
2
+ import { Student } from '../models/Student.js';
3
+ import { protect } from '../middleware/authMiddleware.js';
4
+
5
+ const router = Router();
6
+ router.use(protect);
7
+
8
+ function toApi(doc) {
9
+ if (!doc) return null;
10
+ const o = doc.toObject ? doc.toObject() : doc;
11
+ return {
12
+ id: o._id,
13
+ full_name: o.fullName,
14
+ class: o.className,
15
+ parent_phone: o.parentPhone,
16
+ created_at: o.created_at,
17
+ };
18
+ }
19
+
20
+ router.get('/', async (req, res) => {
21
+ try {
22
+ const rows = await Student.find().sort({ created_at: -1 });
23
+ return res.json(rows.map(toApi));
24
+ } catch (err) {
25
+ console.error(err);
26
+ return res.status(500).json({ message: 'Failed to list students' });
27
+ }
28
+ });
29
+
30
+ router.post('/', async (req, res) => {
31
+ try {
32
+ const { full_name, class: className, parent_phone } = req.body;
33
+ if (!full_name || !className || !parent_phone) {
34
+ return res.status(400).json({ message: 'Full name, class and parent phone are required' });
35
+ }
36
+ const student = await Student.create({
37
+ fullName: String(full_name).trim(),
38
+ className: String(className).trim(),
39
+ parentPhone: String(parent_phone).trim(),
40
+ });
41
+ return res.status(201).json(toApi(student));
42
+ } catch (err) {
43
+ console.error(err);
44
+ return res.status(500).json({ message: 'Failed to create student' });
45
+ }
46
+ });
47
+
48
+ router.put('/:id', async (req, res) => {
49
+ try {
50
+ const { full_name, class: className, parent_phone } = req.body;
51
+ const patch = {};
52
+ if (full_name !== undefined) patch.fullName = String(full_name).trim();
53
+ if (className !== undefined) patch.className = String(className).trim();
54
+ if (parent_phone !== undefined) patch.parentPhone = String(parent_phone).trim();
55
+
56
+ const student = await Student.findByIdAndUpdate(req.params.id, patch, {
57
+ new: true,
58
+ runValidators: true,
59
+ });
60
+ if (!student) return res.status(404).json({ message: 'Student not found' });
61
+ return res.json(toApi(student));
62
+ } catch (err) {
63
+ console.error(err);
64
+ return res.status(500).json({ message: 'Failed to update student' });
65
+ }
66
+ });
67
+
68
+ router.delete('/:id', async (req, res) => {
69
+ try {
70
+ const student = await Student.findByIdAndDelete(req.params.id);
71
+ if (!student) return res.status(404).json({ message: 'Student not found' });
72
+ return res.json({ message: 'Deleted', id: req.params.id });
73
+ } catch (err) {
74
+ console.error(err);
75
+ return res.status(500).json({ message: 'Failed to delete' });
76
+ }
77
+ });
78
+
79
+ export default router;
@@ -0,0 +1,34 @@
1
+ import 'dotenv/config';
2
+ import express from 'express';
3
+ import cors from 'cors';
4
+ import { env, requireMongoUri } from './config/env.js';
5
+ import { connectMongo } from './config/database.js';
6
+ import authRoutes from './routes/authRoutes.js';
7
+ import studentRoutes from './routes/studentRoutes.js';
8
+ import paymentRoutes from './routes/paymentRoutes.js';
9
+ import reportRoutes from './routes/reportRoutes.js';
10
+
11
+ const app = express();
12
+
13
+ app.use(cors({ origin: true, credentials: true }));
14
+ app.use(express.json());
15
+
16
+ app.get('/api/health', (req, res) => {
17
+ res.json({ ok: true, app: 'SFMS' });
18
+ });
19
+
20
+ app.use('/api', authRoutes);
21
+ app.use('/api/students', studentRoutes);
22
+ app.use('/api/payments', paymentRoutes);
23
+ app.use('/api/reports', reportRoutes);
24
+
25
+ app.use((req, res) => {
26
+ res.status(404).json({ message: 'Not found' });
27
+ });
28
+
29
+ await connectMongo(requireMongoUri());
30
+ console.log('MongoDB connected');
31
+
32
+ app.listen(env.port, () => {
33
+ console.log(`Server listening on http://localhost:${env.port}`);
34
+ });
@@ -0,0 +1,8 @@
1
+ # Dev server — fixed port; strictPort fails if busy (no random port)
2
+ VITE_DEV_SERVER_PORT=5173
3
+
4
+ # Where Vite proxies /api in dev (matches backend default)
5
+ VITE_API_PROXY_TARGET=http://localhost:5000
6
+
7
+ # Production or custom API origin (optional; empty = relative /api)
8
+ # VITE_API_BASE_URL=https://api.example.com
@@ -0,0 +1 @@
1
+ *,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:DM Sans,system-ui,sans-serif;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}:root{color-scheme:light;--surface: #f8fafc;--panel: rgba(255,255,255,.94);--border: rgba(148,163,184,.24);--text-title: #0f172a;--text-body: #475569;--accent: #0d9488;--accent-dark: #0f766e;--shadow-soft: 0 24px 80px rgba(15,23,42,.08)}html{scroll-behavior:smooth}body{min-height:100vh;--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1));-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;background-image:radial-gradient(circle at top left,rgba(13,148,136,.18),transparent 24%),radial-gradient(circle at bottom right,rgba(14,165,233,.11),transparent 20%),linear-gradient(180deg,#f8fbfd,#f1f5f9)}*{box-sizing:border-box}.app-shell{display:flex;min-height:100vh;flex-direction:column;background-color:transparent}@media (min-width: 768px){.app-shell{flex-direction:row}}.app-sidebar{width:100%;flex-shrink:0;border-width:1px;--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);--tw-backdrop-blur: blur(24px);-webkit-backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia);backdrop-filter:var(--tw-backdrop-blur) var(--tw-backdrop-brightness) var(--tw-backdrop-contrast) var(--tw-backdrop-grayscale) var(--tw-backdrop-hue-rotate) var(--tw-backdrop-invert) var(--tw-backdrop-opacity) var(--tw-backdrop-saturate) var(--tw-backdrop-sepia)}@media (min-width: 768px){.app-sidebar{width:18rem}}.app-sidebar{border-color:#94a3b8b3;background-color:#ffffffeb}.app-brand{border-bottom-width:1px;padding:1.5rem;border-color:#94a3b8b3}.app-brand-name{font-family:Outfit,system-ui,sans-serif;font-size:1.5rem;line-height:2rem;font-weight:800;letter-spacing:-.025em;--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.app-brand-subtitle{margin-top:.25rem;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.app-nav{display:flex;flex-wrap:wrap;gap:.75rem;padding:1rem}.app-nav-link{border-radius:9999px;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.app-nav-link:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.app-nav-link.active{--tw-bg-opacity: 1;background-color:rgb(15 23 42 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1));--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.app-user-panel{margin-top:auto;border-top-width:1px;padding:1.5rem;border-color:#94a3b8b3}.app-user-name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:.875rem;line-height:1.25rem;--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.app-user-action{margin-top:.75rem;width:100%;border-radius:1rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1));padding:.75rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.app-user-action:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.app-content{flex:1 1 0%;padding:1.25rem 1rem 2rem}@media (min-width: 768px){.app-content{padding-left:2rem;padding-right:2rem;padding-top:2rem}}.app-header{margin-bottom:1.5rem}.page-title{font-family:Outfit,system-ui,sans-serif;font-size:1.875rem;line-height:2.25rem;font-weight:700;letter-spacing:-.025em;--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}@media (min-width: 768px){.page-title{font-size:2.25rem;line-height:2.5rem}}.page-subtitle{margin-top:.75rem;max-width:42rem;line-height:1.75rem;--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.panel-card{border-radius:32px;border-width:1px;padding:1.5rem;--tw-shadow: 0 24px 80px rgba(15,23,42,.08);--tw-shadow-colored: 0 24px 80px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);border-color:#94a3b8cc;background-color:#fffffff2}.panel-card-alt{border-radius:32px;border-width:1px;padding:1.5rem;border-color:#94a3b8b3;background-color:#f8fafce6}.stat-grid{margin-top:2rem;display:grid;gap:1.25rem}@media (min-width: 640px){.stat-grid{grid-template-columns:repeat(3,minmax(0,1fr))}}.stat-card{border-radius:28px;border-width:1px;background-image:linear-gradient(to bottom right,var(--tw-gradient-stops));--tw-gradient-from: #fff var(--tw-gradient-from-position);--tw-gradient-to: rgb(255 255 255 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to);--tw-gradient-to: #f8fafc var(--tw-gradient-to-position);padding:1.5rem;--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);border-color:#94a3b8b3}.stat-label{font-size:.75rem;line-height:1rem;font-weight:600;text-transform:uppercase;letter-spacing:.24em;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.stat-value{margin-top:1rem;font-size:1.875rem;line-height:2.25rem;font-weight:700;--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.stat-link{margin-top:1.25rem;display:inline-flex;font-size:.875rem;line-height:1.25rem;font-weight:500;--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.stat-link:hover{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.form-card{border-radius:32px;border-width:1px;padding:1.5rem;--tw-shadow: 0 20px 55px rgba(15,23,42,.06);--tw-shadow-colored: 0 20px 55px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);border-color:#94a3b8cc;background-color:#fffffff2}.input-field{width:100%;border-radius:1rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1));padding:.75rem 1rem;--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1));outline:2px solid transparent;outline-offset:2px;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.input-field:focus{--tw-border-opacity: 1;border-color:rgb(13 148 136 / var(--tw-border-opacity, 1));--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow, 0 0 #0000);--tw-ring-opacity: 1;--tw-ring-color: rgb(13 148 136 / var(--tw-ring-opacity, 1))}.input-group>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem * var(--tw-space-y-reverse))}.form-actions{display:flex;flex-wrap:wrap;gap:.75rem}.btn-primary{display:inline-flex;align-items:center;justify-content:center;border-radius:1rem;--tw-bg-opacity: 1;background-color:rgb(13 148 136 / var(--tw-bg-opacity, 1));padding:.75rem 1.25rem;font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1));--tw-shadow: 0 10px 15px -3px rgb(0 0 0 / .1), 0 4px 6px -4px rgb(0 0 0 / .1);--tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.btn-primary:hover{--tw-bg-opacity: 1;background-color:rgb(15 118 110 / var(--tw-bg-opacity, 1))}.btn-secondary{display:inline-flex;align-items:center;justify-content:center;border-radius:1rem;border-width:1px;--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));padding:.75rem 1.25rem;font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.btn-secondary:hover{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1))}.btn-destructive{display:inline-flex;align-items:center;justify-content:center;border-radius:1rem;--tw-bg-opacity: 1;background-color:rgb(220 38 38 / var(--tw-bg-opacity, 1));padding:.75rem 1.25rem;font-size:.875rem;line-height:1.25rem;font-weight:600;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.btn-destructive:hover{--tw-bg-opacity: 1;background-color:rgb(185 28 28 / var(--tw-bg-opacity, 1))}.table-shell{overflow-x:auto;border-radius:28px;border-width:1px;--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1));background-color:#fffffff2;--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.table-head{--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.table-row{border-top-width:1px;--tw-border-opacity: 1;border-color:rgb(241 245 249 / var(--tw-border-opacity, 1));transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.table-row:hover{--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1))}.table-cell{padding:1rem}.placeholder-card{border-radius:28px;border-width:1px;border-style:dashed;--tw-border-opacity: 1;border-color:rgb(203 213 225 / var(--tw-border-opacity, 1));--tw-bg-opacity: 1;background-color:rgb(248 250 252 / var(--tw-bg-opacity, 1));padding:3rem 1.5rem;text-align:center;--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.auth-page{display:flex;min-height:100vh;align-items:center;justify-content:center;padding:2.5rem 1rem}.auth-card{width:100%;max-width:56rem;overflow:hidden;border-radius:40px;border-width:1px;--tw-shadow: 0 30px 120px rgba(15,23,42,.12);--tw-shadow-colored: 0 30px 120px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow);border-color:#94a3b8cc;background-color:#fffffff2}.auth-split{display:grid;gap:1.5rem}@media (min-width: 768px){.auth-split{grid-template-columns:1.1fr .9fr}}.auth-side{display:none;flex-direction:column;justify-content:center;gap:1rem;border-top-right-radius:40px;border-bottom-right-radius:40px;--tw-bg-opacity: 1;background-color:rgb(13 148 136 / var(--tw-bg-opacity, 1));padding:3rem 2.5rem;--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}@media (min-width: 768px){.auth-side{display:flex}}.auth-logo{font-family:Outfit,system-ui,sans-serif;font-size:2.25rem;line-height:2.5rem;font-weight:900}.auth-copy{margin-top:1rem;font-size:.875rem;line-height:1.75rem;color:#f1f5f9e6}.auth-panel{padding:2rem}@media (min-width: 640px){.auth-panel{padding:2.5rem}}.auth-switcher{display:flex;gap:.75rem;border-radius:9999px;--tw-bg-opacity: 1;background-color:rgb(241 245 249 / var(--tw-bg-opacity, 1));padding:.25rem}.auth-switcher button{flex:1 1 0%;border-radius:9999px;padding:.5rem 1rem;font-size:.875rem;line-height:1.25rem;font-weight:600;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.2s}.auth-switcher button.active{--tw-bg-opacity: 1;background-color:rgb(255 255 255 / var(--tw-bg-opacity, 1));--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1));--tw-shadow: 0 1px 2px 0 rgb(0 0 0 / .05);--tw-shadow-colored: 0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow, 0 0 #0000),var(--tw-ring-shadow, 0 0 #0000),var(--tw-shadow)}.fixed{position:fixed}.inset-0{top:0;right:0;bottom:0;left:0}.z-50{z-index:50}.mb-4{margin-bottom:1rem}.mb-6{margin-bottom:1.5rem}.mb-8{margin-bottom:2rem}.mt-3{margin-top:.75rem}.mt-4{margin-top:1rem}.mt-6{margin-top:1.5rem}.mt-8{margin-top:2rem}.flex{display:flex}.table{display:table}.table-cell{display:table-cell}.table-row{display:table-row}.grid{display:grid}.h-14{height:3.5rem}.w-full{width:100%}.min-w-full{min-width:100%}.max-w-2xl{max-width:42rem}.max-w-md{max-width:28rem}.flex-col{flex-direction:column}.items-end{align-items:flex-end}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-3{gap:.75rem}.gap-4{gap:1rem}.gap-5{gap:1.25rem}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse: 0;margin-right:calc(.5rem * var(--tw-space-x-reverse));margin-left:calc(.5rem * calc(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem * var(--tw-space-y-reverse))}.space-y-5>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.25rem * var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem * var(--tw-space-y-reverse))}.whitespace-nowrap{white-space:nowrap}.rounded-3xl{border-radius:1.5rem}.rounded-\[26px\]{border-radius:26px}.rounded-\[28px\]{border-radius:28px}.border{border-width:1px}.border-amber-100{--tw-border-opacity: 1;border-color:rgb(254 243 199 / var(--tw-border-opacity, 1))}.border-red-100{--tw-border-opacity: 1;border-color:rgb(254 226 226 / var(--tw-border-opacity, 1))}.border-slate-200{--tw-border-opacity: 1;border-color:rgb(226 232 240 / var(--tw-border-opacity, 1))}.bg-amber-50{--tw-bg-opacity: 1;background-color:rgb(255 251 235 / var(--tw-bg-opacity, 1))}.bg-black\/40{background-color:#0006}.bg-red-50{--tw-bg-opacity: 1;background-color:rgb(254 242 242 / var(--tw-bg-opacity, 1))}.bg-gradient-to-br{background-image:linear-gradient(to bottom right,var(--tw-gradient-stops))}.from-slate-50{--tw-gradient-from: #f8fafc var(--tw-gradient-from-position);--tw-gradient-to: rgb(248 250 252 / 0) var(--tw-gradient-to-position);--tw-gradient-stops: var(--tw-gradient-from), var(--tw-gradient-to)}.to-cyan-50{--tw-gradient-to: #ecfeff var(--tw-gradient-to-position)}.to-slate-100{--tw-gradient-to: #f1f5f9 var(--tw-gradient-to-position)}.to-teal-50{--tw-gradient-to: #f0fdfa var(--tw-gradient-to-position)}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.text-left{text-align:left}.font-display{font-family:Outfit,system-ui,sans-serif}.text-4xl{font-size:2.25rem;line-height:2.5rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.leading-7{line-height:1.75rem}.tracking-\[0\.35em\]{letter-spacing:.35em}.tracking-\[0\.3em\]{letter-spacing:.3em}.text-amber-700{--tw-text-opacity: 1;color:rgb(180 83 9 / var(--tw-text-opacity, 1))}.text-amber-800{--tw-text-opacity: 1;color:rgb(146 64 14 / var(--tw-text-opacity, 1))}.text-red-600{--tw-text-opacity: 1;color:rgb(220 38 38 / var(--tw-text-opacity, 1))}.text-red-700{--tw-text-opacity: 1;color:rgb(185 28 28 / var(--tw-text-opacity, 1))}.text-sfms-ink{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.text-slate-100\/80{color:#f1f5f9cc}.text-slate-500{--tw-text-opacity: 1;color:rgb(100 116 139 / var(--tw-text-opacity, 1))}.text-slate-600{--tw-text-opacity: 1;color:rgb(71 85 105 / var(--tw-text-opacity, 1))}.text-slate-700{--tw-text-opacity: 1;color:rgb(51 65 85 / var(--tw-text-opacity, 1))}.text-slate-900{--tw-text-opacity: 1;color:rgb(15 23 42 / var(--tw-text-opacity, 1))}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.disabled\:opacity-50:disabled{opacity:.5}@media (min-width: 640px){.sm\:w-auto{width:auto}.sm\:flex-row{flex-direction:row}.sm\:items-center{align-items:center}.sm\:justify-between{justify-content:space-between}}@media (min-width: 768px){.md\:w-auto{width:auto}.md\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.md\:grid-cols-\[1\.1fr_1\.1fr_auto\]{grid-template-columns:1.1fr 1.1fr auto}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-between{justify-content:space-between}}