krispdev-business 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/Dev-Business/.vscode/settings.json +3 -0
- package/Dev-Business/backend-project/config/db.js +16 -0
- package/Dev-Business/backend-project/controllers/authController.js +41 -0
- package/Dev-Business/backend-project/controllers/carController.js +62 -0
- package/Dev-Business/backend-project/controllers/packageController.js +49 -0
- package/Dev-Business/backend-project/controllers/paymentController.js +63 -0
- package/Dev-Business/backend-project/controllers/reportController.js +129 -0
- package/Dev-Business/backend-project/controllers/serviceController.js +155 -0
- package/Dev-Business/backend-project/middleware/auth.js +16 -0
- package/Dev-Business/backend-project/package-lock.json +1507 -0
- package/Dev-Business/backend-project/package.json +21 -0
- package/Dev-Business/backend-project/routes/authRoutes.js +10 -0
- package/Dev-Business/backend-project/routes/carRoutes.js +11 -0
- package/Dev-Business/backend-project/routes/packageRoutes.js +11 -0
- package/Dev-Business/backend-project/routes/paymentRoutes.js +10 -0
- package/Dev-Business/backend-project/routes/reportRoutes.js +11 -0
- package/Dev-Business/backend-project/routes/serviceRoutes.js +19 -0
- package/Dev-Business/backend-project/server.js +72 -0
- package/Dev-Business/frontend-project/README.md +18 -0
- package/Dev-Business/frontend-project/eslint.config.js +21 -0
- package/Dev-Business/frontend-project/index.html +14 -0
- package/Dev-Business/frontend-project/package-lock.json +3655 -0
- package/Dev-Business/frontend-project/package.json +34 -0
- package/Dev-Business/frontend-project/postcss.config.js +6 -0
- package/Dev-Business/frontend-project/public/favicon.svg +1 -0
- package/Dev-Business/frontend-project/public/icons.svg +24 -0
- package/Dev-Business/frontend-project/src/App.css +184 -0
- package/Dev-Business/frontend-project/src/App.jsx +84 -0
- package/Dev-Business/frontend-project/src/assets/hero.png +0 -0
- package/Dev-Business/frontend-project/src/assets/react.svg +1 -0
- package/Dev-Business/frontend-project/src/assets/vite.svg +1 -0
- package/Dev-Business/frontend-project/src/components/CarForm.jsx +146 -0
- package/Dev-Business/frontend-project/src/components/DailyReport.jsx +264 -0
- package/Dev-Business/frontend-project/src/components/Dashboard.jsx +153 -0
- package/Dev-Business/frontend-project/src/components/Login.jsx +163 -0
- package/Dev-Business/frontend-project/src/components/Navbar.jsx +99 -0
- package/Dev-Business/frontend-project/src/components/Navibar.jsx +100 -0
- package/Dev-Business/frontend-project/src/components/PaymentForm.jsx +211 -0
- package/Dev-Business/frontend-project/src/components/ServiceRecord.jsx +384 -0
- package/Dev-Business/frontend-project/src/index.css +155 -0
- package/Dev-Business/frontend-project/src/main.jsx +10 -0
- package/Dev-Business/frontend-project/src/services/api.js +23 -0
- package/Dev-Business/frontend-project/tailwind.config.js +31 -0
- package/Dev-Business/frontend-project/vite.config.js +15 -0
- package/package.json +17 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Link, useLocation } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
function Navbar({ user, onLogout }) {
|
|
5
|
+
const location = useLocation();
|
|
6
|
+
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
|
|
7
|
+
|
|
8
|
+
const navItems = [
|
|
9
|
+
{ path: '/', name: 'Dashboard', icon: 'M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6' },
|
|
10
|
+
{ path: '/cars', name: 'Cars', icon: 'M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z' },
|
|
11
|
+
{ path: '/services', name: 'Services', icon: 'M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2' },
|
|
12
|
+
{ path: '/payments', name: 'Payments', icon: 'M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z' },
|
|
13
|
+
{ path: '/reports', name: 'Reports', icon: 'M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z' },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<>
|
|
18
|
+
<nav className="glass-card fixed top-4 left-4 right-4 z-50 px-6 py-3 animate-fade-in">
|
|
19
|
+
<div className="flex justify-between items-center">
|
|
20
|
+
<Link to="/" className="flex items-center space-x-3">
|
|
21
|
+
<div className="w-10 h-10 bg-gradient-to-br from-blue-500 to-purple-600 rounded-xl flex items-center justify-center">
|
|
22
|
+
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
23
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
|
24
|
+
</svg>
|
|
25
|
+
</div>
|
|
26
|
+
<span className="text-white font-bold text-xl hidden md:block">SmartPark</span>
|
|
27
|
+
</Link>
|
|
28
|
+
|
|
29
|
+
{/* Desktop Menu */}
|
|
30
|
+
<div className="hidden md:flex space-x-2">
|
|
31
|
+
{navItems.map((item) => (
|
|
32
|
+
<Link
|
|
33
|
+
key={item.path}
|
|
34
|
+
to={item.path}
|
|
35
|
+
className={`px-4 py-2 rounded-lg transition-all duration-300 flex items-center space-x-2 ${
|
|
36
|
+
location.pathname === item.path
|
|
37
|
+
? 'bg-white/20 text-white'
|
|
38
|
+
: 'text-white/70 hover:bg-white/10 hover:text-white'
|
|
39
|
+
}`}
|
|
40
|
+
>
|
|
41
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
42
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon} />
|
|
43
|
+
</svg>
|
|
44
|
+
<span>{item.name}</span>
|
|
45
|
+
</Link>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
|
|
49
|
+
<div className="flex items-center space-x-3">
|
|
50
|
+
<div className="hidden md:block text-right">
|
|
51
|
+
<div className="text-white text-sm font-medium">{user?.username}</div>
|
|
52
|
+
<div className="text-white/40 text-xs">Administrator</div>
|
|
53
|
+
</div>
|
|
54
|
+
<button
|
|
55
|
+
onClick={onLogout}
|
|
56
|
+
className="glass-button-secondary text-sm px-4 py-2"
|
|
57
|
+
>
|
|
58
|
+
Logout
|
|
59
|
+
</button>
|
|
60
|
+
<button
|
|
61
|
+
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
|
62
|
+
className="md:hidden text-white p-2"
|
|
63
|
+
>
|
|
64
|
+
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
65
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
|
|
66
|
+
</svg>
|
|
67
|
+
</button>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</nav>
|
|
71
|
+
|
|
72
|
+
{/* Mobile Menu */}
|
|
73
|
+
{isMobileMenuOpen && (
|
|
74
|
+
<div className="fixed inset-0 z-40 pt-20 animate-fade-in md:hidden">
|
|
75
|
+
<div className="glass-card mx-4 p-4 space-y-2">
|
|
76
|
+
{navItems.map((item) => (
|
|
77
|
+
<Link
|
|
78
|
+
key={item.path}
|
|
79
|
+
to={item.path}
|
|
80
|
+
onClick={() => setIsMobileMenuOpen(false)}
|
|
81
|
+
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-all duration-300 ${
|
|
82
|
+
location.pathname === item.path
|
|
83
|
+
? 'bg-white/20 text-white'
|
|
84
|
+
: 'text-white/70 hover:bg-white/10'
|
|
85
|
+
}`}
|
|
86
|
+
>
|
|
87
|
+
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
88
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={item.icon} />
|
|
89
|
+
</svg>
|
|
90
|
+
<span>{item.name}</span>
|
|
91
|
+
</Link>
|
|
92
|
+
))}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export default Navbar;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
import api from '../services/api';
|
|
4
|
+
|
|
5
|
+
function PaymentForm() {
|
|
6
|
+
const [searchParams] = useSearchParams();
|
|
7
|
+
const [formData, setFormData] = useState({
|
|
8
|
+
AmountPaid: '',
|
|
9
|
+
PaymentDate: new Date().toISOString().split('T')[0],
|
|
10
|
+
RecordNumber: searchParams.get('record') || ''
|
|
11
|
+
});
|
|
12
|
+
const [services, setServices] = useState([]);
|
|
13
|
+
const [selectedService, setSelectedService] = useState(null);
|
|
14
|
+
const [message, setMessage] = useState(null);
|
|
15
|
+
const [loading, setLoading] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
fetchUnpaidServices();
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (formData.RecordNumber) {
|
|
23
|
+
fetchServiceDetails(formData.RecordNumber);
|
|
24
|
+
}
|
|
25
|
+
}, [formData.RecordNumber]);
|
|
26
|
+
|
|
27
|
+
const fetchUnpaidServices = async () => {
|
|
28
|
+
try {
|
|
29
|
+
const response = await api.get('/services');
|
|
30
|
+
const allServices = response.data.data || [];
|
|
31
|
+
const unpaid = allServices.filter(service => !service.AmountPaid);
|
|
32
|
+
setServices(unpaid);
|
|
33
|
+
} catch (error) {
|
|
34
|
+
console.error('Error fetching services:', error);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const fetchServiceDetails = async (recordNumber) => {
|
|
39
|
+
try {
|
|
40
|
+
const response = await api.get(`/services/${recordNumber}`);
|
|
41
|
+
setSelectedService(response.data.data);
|
|
42
|
+
if (response.data.data.PackagePrice) {
|
|
43
|
+
setFormData(prev => ({
|
|
44
|
+
...prev,
|
|
45
|
+
AmountPaid: response.data.data.PackagePrice
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error('Error fetching service details:', error);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const handleChange = (e) => {
|
|
54
|
+
setFormData({ ...formData, [e.target.name]: e.target.value });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleSubmit = async (e) => {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
setLoading(true);
|
|
60
|
+
setMessage(null);
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
await api.post('/payments', formData);
|
|
64
|
+
setMessage({ type: 'success', text: '✅ Payment recorded successfully!' });
|
|
65
|
+
setFormData({
|
|
66
|
+
AmountPaid: '',
|
|
67
|
+
PaymentDate: new Date().toISOString().split('T')[0],
|
|
68
|
+
RecordNumber: ''
|
|
69
|
+
});
|
|
70
|
+
setSelectedService(null);
|
|
71
|
+
fetchUnpaidServices();
|
|
72
|
+
setTimeout(() => setMessage(null), 3000);
|
|
73
|
+
} catch (err) {
|
|
74
|
+
setMessage({ type: 'error', text: err.response?.data?.error || 'Payment failed' });
|
|
75
|
+
} finally {
|
|
76
|
+
setLoading(false);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
return (
|
|
81
|
+
<div className="max-w-4xl mx-auto animate-fade-in space-y-6">
|
|
82
|
+
<div className="glass-card p-8">
|
|
83
|
+
<div className="flex items-center space-x-3 mb-6">
|
|
84
|
+
<div className="w-12 h-12 bg-gradient-to-br from-green-500 to-emerald-600 rounded-xl flex items-center justify-center">
|
|
85
|
+
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
86
|
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2zm7-5a2 2 0 11-4 0 2 2 0 014 0z" />
|
|
87
|
+
</svg>
|
|
88
|
+
</div>
|
|
89
|
+
<div>
|
|
90
|
+
<h2 className="text-2xl font-bold text-white">Record Payment</h2>
|
|
91
|
+
<p className="text-white/60 text-sm">Process customer payment for services</p>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
|
|
95
|
+
{message && (
|
|
96
|
+
<div className={`mb-6 p-4 rounded-lg backdrop-blur-sm ${
|
|
97
|
+
message.type === 'success'
|
|
98
|
+
? 'bg-green-500/20 border border-green-500/50 text-green-200'
|
|
99
|
+
: 'bg-red-500/20 border border-red-500/50 text-red-200'
|
|
100
|
+
} animate-fade-in`}>
|
|
101
|
+
{message.text}
|
|
102
|
+
</div>
|
|
103
|
+
)}
|
|
104
|
+
|
|
105
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
106
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
107
|
+
<div>
|
|
108
|
+
<label className="text-white/80 text-sm block mb-2">Select Service *</label>
|
|
109
|
+
<select
|
|
110
|
+
name="RecordNumber"
|
|
111
|
+
value={formData.RecordNumber}
|
|
112
|
+
onChange={handleChange}
|
|
113
|
+
className="glass-input w-full"
|
|
114
|
+
required
|
|
115
|
+
>
|
|
116
|
+
<option value="">Select unpaid service</option>
|
|
117
|
+
{services.map(service => (
|
|
118
|
+
<option key={service.RecordNumber} value={service.RecordNumber}>
|
|
119
|
+
{service.PlateNumber} - {service.DriverName} - {new Date(service.ServiceDate).toLocaleDateString()}
|
|
120
|
+
</option>
|
|
121
|
+
))}
|
|
122
|
+
</select>
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<div>
|
|
126
|
+
<label className="text-white/80 text-sm block mb-2">Payment Date *</label>
|
|
127
|
+
<input
|
|
128
|
+
type="date"
|
|
129
|
+
name="PaymentDate"
|
|
130
|
+
value={formData.PaymentDate}
|
|
131
|
+
onChange={handleChange}
|
|
132
|
+
className="glass-input w-full"
|
|
133
|
+
required
|
|
134
|
+
/>
|
|
135
|
+
</div>
|
|
136
|
+
|
|
137
|
+
<div>
|
|
138
|
+
<label className="text-white/80 text-sm block mb-2">Amount Paid (RWF) *</label>
|
|
139
|
+
<input
|
|
140
|
+
type="number"
|
|
141
|
+
name="AmountPaid"
|
|
142
|
+
value={formData.AmountPaid}
|
|
143
|
+
onChange={handleChange}
|
|
144
|
+
placeholder="Enter amount"
|
|
145
|
+
className="glass-input w-full"
|
|
146
|
+
required
|
|
147
|
+
min="0"
|
|
148
|
+
step="100"
|
|
149
|
+
/>
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
|
|
153
|
+
{selectedService && (
|
|
154
|
+
<div className="bg-white/5 rounded-lg p-4 mt-4">
|
|
155
|
+
<h4 className="text-white font-semibold mb-2">Service Details</h4>
|
|
156
|
+
<div className="grid grid-cols-2 gap-2 text-sm">
|
|
157
|
+
<div className="text-white/60">Package:</div>
|
|
158
|
+
<div className="text-white">{selectedService.PackageName}</div>
|
|
159
|
+
<div className="text-white/60">Package Price:</div>
|
|
160
|
+
<div className="text-white">{selectedService.PackagePrice?.toLocaleString()} RWF</div>
|
|
161
|
+
<div className="text-white/60">Driver:</div>
|
|
162
|
+
<div className="text-white">{selectedService.DriverName}</div>
|
|
163
|
+
<div className="text-white/60">Vehicle:</div>
|
|
164
|
+
<div className="text-white">{selectedService.PlateNumber}</div>
|
|
165
|
+
</div>
|
|
166
|
+
{parseFloat(formData.AmountPaid) < selectedService.PackagePrice && (
|
|
167
|
+
<div className="mt-3 text-yellow-400 text-sm">
|
|
168
|
+
⚠️ Amount paid is less than package price ({selectedService.PackagePrice.toLocaleString()} RWF)
|
|
169
|
+
</div>
|
|
170
|
+
)}
|
|
171
|
+
{parseFloat(formData.AmountPaid) > selectedService.PackagePrice && (
|
|
172
|
+
<div className="mt-3 text-green-400 text-sm">
|
|
173
|
+
✓ Change to return: {(formData.AmountPaid - selectedService.PackagePrice).toLocaleString()} RWF
|
|
174
|
+
</div>
|
|
175
|
+
)}
|
|
176
|
+
</div>
|
|
177
|
+
)}
|
|
178
|
+
|
|
179
|
+
<div className="flex justify-end pt-4">
|
|
180
|
+
<button
|
|
181
|
+
type="submit"
|
|
182
|
+
disabled={loading}
|
|
183
|
+
className="glass-button disabled:opacity-50"
|
|
184
|
+
>
|
|
185
|
+
{loading ? 'Processing...' : 'Record Payment'}
|
|
186
|
+
</button>
|
|
187
|
+
</div>
|
|
188
|
+
</form>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
{/* Recent Payments Summary */}
|
|
192
|
+
<div className="glass-card p-8">
|
|
193
|
+
<h3 className="text-xl font-bold text-white mb-4">Payment Summary</h3>
|
|
194
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
195
|
+
<div className="bg-white/5 rounded-lg p-4">
|
|
196
|
+
<div className="text-white/60 text-sm">Unpaid Services</div>
|
|
197
|
+
<div className="text-2xl font-bold text-yellow-400">{services.length}</div>
|
|
198
|
+
</div>
|
|
199
|
+
<div className="bg-white/5 rounded-lg p-4">
|
|
200
|
+
<div className="text-white/60 text-sm">Total Due Amount</div>
|
|
201
|
+
<div className="text-2xl font-bold text-white">
|
|
202
|
+
{services.reduce((sum, s) => sum + (s.PackagePrice || 0), 0).toLocaleString()} RWF
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
</div>
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export default PaymentForm;
|