project-startup 1.0.2 → 1.1.1
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/package.json +1 -1
- package/template/Logics/Booking/controllers/bookingController.js +74 -133
- package/template/Logics/Booking/controllers/busController.js +30 -78
- package/template/Logics/Stocks/controllers/sparePartController.js +72 -0
- package/template/Logics/Stocks/controllers/stockInController.js +96 -0
- package/template/Logics/Stocks/controllers/stockOutController.js +128 -0
- package/template/Logics/Stocks/schema.sql +53 -0
package/package.json
CHANGED
|
@@ -1,41 +1,23 @@
|
|
|
1
1
|
const db = require("../config/db");
|
|
2
2
|
|
|
3
3
|
// GET /api/bookings
|
|
4
|
-
// Manager → all bookings with customer + bus details
|
|
5
|
-
// Customer → only their own bookings
|
|
6
4
|
exports.getBookings = async (req, res) => {
|
|
7
5
|
if (req.user.role === "manager") {
|
|
8
6
|
const [bookings] = await db.query(
|
|
9
|
-
`SELECT
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
b.plate_number,
|
|
17
|
-
b.destination,
|
|
18
|
-
b.departure_time,
|
|
19
|
-
b.price_rwf
|
|
20
|
-
FROM bookings bk
|
|
21
|
-
JOIN users u ON u.id = bk.customer_id
|
|
22
|
-
JOIN buses b ON b.id = bk.bus_id
|
|
23
|
-
ORDER BY bk.booked_at DESC`
|
|
7
|
+
`SELECT bk.id, bk.seats, bk.total_rwf, bk.booked_at,
|
|
8
|
+
u.name AS customer_name, u.email AS customer_email,
|
|
9
|
+
b.plate_number, b.destination, b.departure_time, b.price_rwf
|
|
10
|
+
FROM bookings bk
|
|
11
|
+
JOIN users u ON u.id = bk.customer_id
|
|
12
|
+
JOIN buses b ON b.id = bk.bus_id
|
|
13
|
+
ORDER BY bk.booked_at DESC`
|
|
24
14
|
);
|
|
25
15
|
return res.json(bookings);
|
|
26
16
|
}
|
|
27
17
|
|
|
28
|
-
// Customer: only their rows
|
|
29
18
|
const [bookings] = await db.query(
|
|
30
|
-
`SELECT
|
|
31
|
-
|
|
32
|
-
bk.seats,
|
|
33
|
-
bk.total_rwf,
|
|
34
|
-
bk.booked_at,
|
|
35
|
-
b.plate_number,
|
|
36
|
-
b.destination,
|
|
37
|
-
b.departure_time,
|
|
38
|
-
b.price_rwf
|
|
19
|
+
`SELECT bk.id, bk.seats, bk.total_rwf, bk.booked_at,
|
|
20
|
+
b.plate_number, b.destination, b.departure_time, b.price_rwf
|
|
39
21
|
FROM bookings bk
|
|
40
22
|
JOIN buses b ON b.id = bk.bus_id
|
|
41
23
|
WHERE bk.customer_id = ?
|
|
@@ -46,131 +28,90 @@ exports.getBookings = async (req, res) => {
|
|
|
46
28
|
res.json(bookings);
|
|
47
29
|
};
|
|
48
30
|
|
|
49
|
-
// POST /api/bookings
|
|
31
|
+
// POST /api/bookings (customer only) — SIMPLIFIED
|
|
50
32
|
exports.createBooking = async (req, res) => {
|
|
51
33
|
const { busId, seats } = req.body;
|
|
52
34
|
|
|
53
35
|
if (!busId || !seats || seats < 1) {
|
|
54
|
-
return res.status(400).json({ error: "busId and seats
|
|
36
|
+
return res.status(400).json({ error: "busId and seats required" });
|
|
55
37
|
}
|
|
56
38
|
|
|
57
|
-
//
|
|
58
|
-
|
|
59
|
-
|
|
39
|
+
// 1. Get the bus
|
|
40
|
+
const [buses] = await db.query(
|
|
41
|
+
"SELECT * FROM buses WHERE id = ?",
|
|
42
|
+
[busId]
|
|
43
|
+
);
|
|
60
44
|
|
|
61
|
-
|
|
62
|
-
|
|
45
|
+
if (buses.length === 0) {
|
|
46
|
+
return res.status(404).json({ error: "Bus not found" });
|
|
47
|
+
}
|
|
63
48
|
|
|
64
|
-
|
|
65
|
-
// This prevents another request from reading stale available_seats
|
|
66
|
-
const [rows] = await conn.query(
|
|
67
|
-
"SELECT * FROM buses WHERE id = ? FOR UPDATE",
|
|
68
|
-
[busId]
|
|
69
|
-
);
|
|
49
|
+
const bus = buses[0];
|
|
70
50
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const bus = rows[0];
|
|
77
|
-
|
|
78
|
-
if (new Date(bus.departure_time) <= new Date()) {
|
|
79
|
-
await conn.rollback();
|
|
80
|
-
return res.status(400).json({ error: "This bus has already departed." });
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
if (seats > bus.available_seats) {
|
|
84
|
-
await conn.rollback();
|
|
85
|
-
return res.status(400).json({
|
|
86
|
-
error: `Only ${bus.available_seats} seat(s) available.`,
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Decrement available_seats
|
|
91
|
-
await conn.query(
|
|
92
|
-
"UPDATE buses SET available_seats = available_seats - ? WHERE id = ?",
|
|
93
|
-
[seats, busId]
|
|
94
|
-
);
|
|
51
|
+
// 2. Check departure time
|
|
52
|
+
if (new Date(bus.departure_time) <= new Date()) {
|
|
53
|
+
return res.status(400).json({ error: "Bus already departed" });
|
|
54
|
+
}
|
|
95
55
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
56
|
+
// 3. Check seats (simple check, no lock)
|
|
57
|
+
if (seats > bus.available_seats) {
|
|
58
|
+
return res.status(400).json({
|
|
59
|
+
error: `Only ${bus.available_seats} seat(s) available`
|
|
60
|
+
});
|
|
61
|
+
}
|
|
102
62
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
bk.id,
|
|
109
|
-
bk.seats,
|
|
110
|
-
bk.total_rwf,
|
|
111
|
-
bk.booked_at,
|
|
112
|
-
b.plate_number,
|
|
113
|
-
b.destination,
|
|
114
|
-
b.departure_time
|
|
115
|
-
FROM bookings bk
|
|
116
|
-
JOIN buses b ON b.id = bk.bus_id
|
|
117
|
-
WHERE bk.id = ?`,
|
|
118
|
-
[result.insertId]
|
|
119
|
-
);
|
|
63
|
+
// 4. Decrement seats AND insert booking (two separate queries)
|
|
64
|
+
await db.query(
|
|
65
|
+
"UPDATE buses SET available_seats = available_seats - ? WHERE id = ?",
|
|
66
|
+
[seats, busId]
|
|
67
|
+
);
|
|
120
68
|
|
|
121
|
-
|
|
69
|
+
const [result] = await db.query(
|
|
70
|
+
`INSERT INTO bookings (bus_id, customer_id, seats, total_rwf)
|
|
71
|
+
VALUES (?, ?, ?, ?)`,
|
|
72
|
+
[busId, req.user.id, seats, seats * bus.price_rwf]
|
|
73
|
+
);
|
|
122
74
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
75
|
+
// 5. Return the new booking
|
|
76
|
+
res.status(201).json({
|
|
77
|
+
id: result.insertId,
|
|
78
|
+
bus_id: busId,
|
|
79
|
+
customer_id: req.user.id,
|
|
80
|
+
seats,
|
|
81
|
+
total_rwf: seats * bus.price_rwf,
|
|
82
|
+
plate_number: bus.plate_number,
|
|
83
|
+
destination: bus.destination,
|
|
84
|
+
departure_time: bus.departure_time
|
|
85
|
+
});
|
|
129
86
|
};
|
|
130
87
|
|
|
131
|
-
// DELETE /api/bookings/:id
|
|
88
|
+
// DELETE /api/bookings/:id — SIMPLIFIED
|
|
132
89
|
exports.cancelBooking = async (req, res) => {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
[req.params.id, req.user.id]
|
|
142
|
-
);
|
|
90
|
+
// 1. Find booking and check it belongs to user
|
|
91
|
+
const [bookings] = await db.query(
|
|
92
|
+
`SELECT bk.*, b.departure_time
|
|
93
|
+
FROM bookings bk
|
|
94
|
+
JOIN buses b ON b.id = bk.bus_id
|
|
95
|
+
WHERE bk.id = ? AND bk.customer_id = ?`,
|
|
96
|
+
[req.params.id, req.user.id]
|
|
97
|
+
);
|
|
143
98
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
}
|
|
99
|
+
if (bookings.length === 0) {
|
|
100
|
+
return res.status(404).json({ error: "Booking not found" });
|
|
101
|
+
}
|
|
148
102
|
|
|
149
|
-
|
|
103
|
+
const booking = bookings[0];
|
|
150
104
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
105
|
+
// 2. Restore seats only if bus hasn't departed
|
|
106
|
+
if (new Date(booking.departure_time) > new Date()) {
|
|
107
|
+
await db.query(
|
|
108
|
+
"UPDATE buses SET available_seats = available_seats + ? WHERE id = ?",
|
|
109
|
+
[booking.seats, booking.bus_id]
|
|
155
110
|
);
|
|
111
|
+
}
|
|
156
112
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
"UPDATE buses SET available_seats = available_seats + ? WHERE id = ?",
|
|
160
|
-
[booking.seats, booking.bus_id]
|
|
161
|
-
);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
await conn.query("DELETE FROM bookings WHERE id = ?", [booking.id]);
|
|
165
|
-
|
|
166
|
-
await conn.commit();
|
|
167
|
-
|
|
168
|
-
res.json({ message: "Booking cancelled. Seats have been restored." });
|
|
113
|
+
// 3. Delete booking
|
|
114
|
+
await db.query("DELETE FROM bookings WHERE id = ?", [req.params.id]);
|
|
169
115
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
throw err;
|
|
173
|
-
} finally {
|
|
174
|
-
conn.release();
|
|
175
|
-
}
|
|
176
|
-
};
|
|
116
|
+
res.json({ message: "Booking cancelled" });
|
|
117
|
+
};
|
|
@@ -10,124 +10,76 @@ exports.getAllBuses = async (req, res) => {
|
|
|
10
10
|
|
|
11
11
|
// GET /api/buses/:id
|
|
12
12
|
exports.getBusById = async (req, res) => {
|
|
13
|
-
const [
|
|
13
|
+
const [buses] = await db.query(
|
|
14
14
|
"SELECT * FROM buses WHERE id = ?",
|
|
15
15
|
[req.params.id]
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
-
if (
|
|
19
|
-
return res.status(404).json({ error: "Bus not found
|
|
18
|
+
if (buses.length === 0) {
|
|
19
|
+
return res.status(404).json({ error: "Bus not found" });
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
res.json(
|
|
22
|
+
res.json(buses[0]);
|
|
23
23
|
};
|
|
24
24
|
|
|
25
|
-
// POST /api/buses
|
|
25
|
+
// POST /api/buses (manager only) — SIMPLIFIED
|
|
26
26
|
exports.createBus = async (req, res) => {
|
|
27
27
|
const { plateNumber, destination, maxSeats, departureTime, priceRwf } = req.body;
|
|
28
28
|
|
|
29
29
|
if (!plateNumber || !destination || !maxSeats || !departureTime || !priceRwf) {
|
|
30
|
-
return res.status(400).json({ error: "All fields
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (new Date(departureTime) <= new Date()) {
|
|
34
|
-
return res.status(400).json({ error: "Departure time must be in the future." });
|
|
30
|
+
return res.status(400).json({ error: "All fields required" });
|
|
35
31
|
}
|
|
36
32
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"SELECT id FROM buses WHERE plate_number = ?",
|
|
40
|
-
[plateNumber]
|
|
41
|
-
);
|
|
42
|
-
|
|
43
|
-
if (existing.length > 0) {
|
|
44
|
-
return res.status(400).json({ error: "Plate number already exists." });
|
|
33
|
+
if (new Date(deploymentTime) <= new Date()) {
|
|
34
|
+
return res.status(400).json({ error: "Departure time must be in future" });
|
|
45
35
|
}
|
|
46
36
|
|
|
47
37
|
const [result] = await db.query(
|
|
48
|
-
`INSERT INTO buses
|
|
49
|
-
|
|
38
|
+
`INSERT INTO buses
|
|
39
|
+
(plate_number, destination, max_seats, available_seats, departure_time, price_rwf)
|
|
50
40
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
51
41
|
[plateNumber, destination, maxSeats, maxSeats, departureTime, priceRwf]
|
|
52
|
-
// ^ available_seats starts equal to max_seats
|
|
53
42
|
);
|
|
54
43
|
|
|
55
|
-
|
|
56
|
-
const [rows] = await db.query(
|
|
44
|
+
const [buses] = await db.query(
|
|
57
45
|
"SELECT * FROM buses WHERE id = ?",
|
|
58
46
|
[result.insertId]
|
|
59
47
|
);
|
|
60
48
|
|
|
61
|
-
res.status(201).json(
|
|
49
|
+
res.status(201).json(buses[0]);
|
|
62
50
|
};
|
|
63
51
|
|
|
64
|
-
// PUT /api/buses/:id
|
|
52
|
+
// PUT /api/buses/:id (manager only) — SIMPLIFIED
|
|
65
53
|
exports.updateBus = async (req, res) => {
|
|
66
|
-
const { plateNumber, destination,
|
|
67
|
-
|
|
68
|
-
// Fetch current state first so we can calculate available_seats correctly
|
|
69
|
-
const [rows] = await db.query(
|
|
70
|
-
"SELECT * FROM buses WHERE id = ?",
|
|
71
|
-
[req.params.id]
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
if (rows.length === 0) {
|
|
75
|
-
return res.status(404).json({ error: "Bus not found." });
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const bus = rows[0];
|
|
79
|
-
|
|
80
|
-
// If maxSeats is being changed, recalculate available_seats:
|
|
81
|
-
// available = newMax - (oldMax - oldAvailable) ← keeps already-booked seats
|
|
82
|
-
let newAvailable = bus.available_seats;
|
|
83
|
-
|
|
84
|
-
if (maxSeats) {
|
|
85
|
-
const booked = bus.max_seats - bus.available_seats;
|
|
86
|
-
if (parseInt(maxSeats) < booked) {
|
|
87
|
-
return res.status(400).json({
|
|
88
|
-
error: `Cannot reduce max seats below already booked count (${booked}).`,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
newAvailable = parseInt(maxSeats) - booked;
|
|
92
|
-
}
|
|
54
|
+
const { plateNumber, destination, departureTime, priceRwf } = req.body;
|
|
93
55
|
|
|
94
56
|
await db.query(
|
|
95
57
|
`UPDATE buses SET
|
|
96
|
-
plate_number
|
|
97
|
-
destination
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
departure_time = COALESCE(?, departure_time),
|
|
101
|
-
price_rwf = COALESCE(?, price_rwf)
|
|
58
|
+
plate_number = COALESCE(?, plate_number),
|
|
59
|
+
destination = COALESCE(?, destination),
|
|
60
|
+
departure_time = COALESCE(?, departure_time),
|
|
61
|
+
price_rwf = COALESCE(?, price_rwf)
|
|
102
62
|
WHERE id = ?`,
|
|
103
|
-
[
|
|
104
|
-
plateNumber || null,
|
|
105
|
-
destination || null,
|
|
106
|
-
maxSeats || null,
|
|
107
|
-
newAvailable,
|
|
108
|
-
departureTime|| null,
|
|
109
|
-
priceRwf || null,
|
|
110
|
-
req.params.id,
|
|
111
|
-
]
|
|
63
|
+
[plateNumber, destination, departureTime, priceRwf, req.params.id]
|
|
112
64
|
);
|
|
113
65
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
66
|
+
const [buses] = await db.query(
|
|
67
|
+
"SELECT * FROM buses WHERE id = ?",
|
|
68
|
+
[req.params.id]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
res.json(buses[0]);
|
|
117
72
|
};
|
|
118
73
|
|
|
119
|
-
// DELETE /api/buses/:id
|
|
74
|
+
// DELETE /api/buses/:id (manager only)
|
|
120
75
|
exports.deleteBus = async (req, res) => {
|
|
121
|
-
// Check if any bookings exist for this bus
|
|
122
76
|
const [bookings] = await db.query(
|
|
123
77
|
"SELECT id FROM bookings WHERE bus_id = ?",
|
|
124
78
|
[req.params.id]
|
|
125
79
|
);
|
|
126
80
|
|
|
127
81
|
if (bookings.length > 0) {
|
|
128
|
-
return res.status(400).json({
|
|
129
|
-
error: "Cannot delete a bus that has existing bookings.",
|
|
130
|
-
});
|
|
82
|
+
return res.status(400).json({ error: "Cannot delete bus with bookings" });
|
|
131
83
|
}
|
|
132
84
|
|
|
133
85
|
const [result] = await db.query(
|
|
@@ -136,8 +88,8 @@ exports.deleteBus = async (req, res) => {
|
|
|
136
88
|
);
|
|
137
89
|
|
|
138
90
|
if (result.affectedRows === 0) {
|
|
139
|
-
return res.status(404).json({ error: "Bus not found
|
|
91
|
+
return res.status(404).json({ error: "Bus not found" });
|
|
140
92
|
}
|
|
141
93
|
|
|
142
|
-
res.json({ message: "Bus deleted
|
|
143
|
-
};
|
|
94
|
+
res.json({ message: "Bus deleted" });
|
|
95
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const db = require("../db");
|
|
2
|
+
|
|
3
|
+
// GET /api/parts
|
|
4
|
+
exports.getAllParts = async (req, res) => {
|
|
5
|
+
const [parts] = await db.query("SELECT * FROM spare_parts ORDER BY name ASC");
|
|
6
|
+
res.json(parts);
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
// GET /api/parts/:id
|
|
10
|
+
exports.getPartById = async (req, res) => {
|
|
11
|
+
const [parts] = await db.query("SELECT * FROM spare_parts WHERE id = ?", [req.params.id]);
|
|
12
|
+
|
|
13
|
+
if (parts.length === 0) {
|
|
14
|
+
return res.status(404).json({ error: "Spare part not found" });
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
res.json(parts[0]);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// POST /api/parts
|
|
21
|
+
exports.createPart = async (req, res) => {
|
|
22
|
+
const { name, quantity, unitPrice } = req.body;
|
|
23
|
+
|
|
24
|
+
if (!name || quantity == null || !unitPrice) {
|
|
25
|
+
return res.status(400).json({ error: "name, quantity and unitPrice required" });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (quantity < 0) {
|
|
29
|
+
return res.status(400).json({ error: "Quantity cannot be negative" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const [result] = await db.query(
|
|
33
|
+
"INSERT INTO spare_parts (name, quantity, unit_price) VALUES (?, ?, ?)",
|
|
34
|
+
[name.trim(), quantity, unitPrice]
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
const [parts] = await db.query("SELECT * FROM spare_parts WHERE id = ?", [result.insertId]);
|
|
38
|
+
res.status(201).json(parts[0]);
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
// PUT /api/parts/:id
|
|
42
|
+
exports.updatePart = async (req, res) => {
|
|
43
|
+
const [parts] = await db.query("SELECT * FROM spare_parts WHERE id = ?", [req.params.id]);
|
|
44
|
+
|
|
45
|
+
if (parts.length === 0) {
|
|
46
|
+
return res.status(404).json({ error: "Spare part not found" });
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { name, unitPrice } = req.body;
|
|
50
|
+
|
|
51
|
+
await db.query(
|
|
52
|
+
`UPDATE spare_parts SET
|
|
53
|
+
name = COALESCE(?, name),
|
|
54
|
+
unit_price = COALESCE(?, unit_price)
|
|
55
|
+
WHERE id = ?`,
|
|
56
|
+
[name || null, unitPrice || null, req.params.id]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const [updated] = await db.query("SELECT * FROM spare_parts WHERE id = ?", [req.params.id]);
|
|
60
|
+
res.json(updated[0]);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
// DELETE /api/parts/:id — SIMPLIFIED (removed stock-in check)
|
|
64
|
+
exports.deletePart = async (req, res) => {
|
|
65
|
+
const [result] = await db.query("DELETE FROM spare_parts WHERE id = ?", [req.params.id]);
|
|
66
|
+
|
|
67
|
+
if (result.affectedRows === 0) {
|
|
68
|
+
return res.status(404).json({ error: "Spare part not found" });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
res.json({ message: "Spare part deleted" });
|
|
72
|
+
};
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
const db = require("../db");
|
|
2
|
+
|
|
3
|
+
// GET /api/stock-in
|
|
4
|
+
exports.getAllStockIn = async (req, res) => {
|
|
5
|
+
const [rows] = await db.query(
|
|
6
|
+
`SELECT si.id, si.quantity, si.remaining_qty, si.unit_price, si.total_price,
|
|
7
|
+
si.imported_at, sp.id AS spare_part_id, sp.name AS spare_part_name
|
|
8
|
+
FROM stock_in si
|
|
9
|
+
JOIN spare_parts sp ON sp.id = si.spare_part_id
|
|
10
|
+
ORDER BY si.imported_at DESC`
|
|
11
|
+
);
|
|
12
|
+
res.json(rows);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
// GET /api/stock-in/:id
|
|
16
|
+
exports.getStockInById = async (req, res) => {
|
|
17
|
+
const [rows] = await db.query(
|
|
18
|
+
`SELECT si.*, sp.name AS spare_part_name
|
|
19
|
+
FROM stock_in si
|
|
20
|
+
JOIN spare_parts sp ON sp.id = si.spare_part_id
|
|
21
|
+
WHERE si.id = ?`,
|
|
22
|
+
[req.params.id]
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (rows.length === 0) {
|
|
26
|
+
return res.status(404).json({ error: "Stock-in record not found" });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
res.json(rows[0]);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// POST /api/stock-in — SIMPLIFIED (no transaction)
|
|
33
|
+
exports.createStockIn = async (req, res) => {
|
|
34
|
+
const { sparePartId, quantity, unitPrice } = req.body;
|
|
35
|
+
|
|
36
|
+
if (!sparePartId || !quantity || !unitPrice) {
|
|
37
|
+
return res.status(400).json({ error: "sparePartId, quantity and unitPrice required" });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (quantity < 1) {
|
|
41
|
+
return res.status(400).json({ error: "Quantity must be at least 1" });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Verify part exists
|
|
45
|
+
const [parts] = await db.query("SELECT * FROM spare_parts WHERE id = ?", [sparePartId]);
|
|
46
|
+
if (parts.length === 0) {
|
|
47
|
+
return res.status(404).json({ error: "Spare part not found" });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const totalPrice = quantity * unitPrice;
|
|
51
|
+
|
|
52
|
+
// Insert stock-in batch
|
|
53
|
+
const [result] = await db.query(
|
|
54
|
+
`INSERT INTO stock_in (spare_part_id, quantity, remaining_qty, unit_price, total_price)
|
|
55
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
56
|
+
[sparePartId, quantity, quantity, unitPrice, totalPrice]
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
// Increment part's total stock
|
|
60
|
+
await db.query(
|
|
61
|
+
"UPDATE spare_parts SET quantity = quantity + ? WHERE id = ?",
|
|
62
|
+
[quantity, sparePartId]
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// Return full record with part name
|
|
66
|
+
const [rows] = await db.query(
|
|
67
|
+
`SELECT si.*, sp.name AS spare_part_name
|
|
68
|
+
FROM stock_in si
|
|
69
|
+
JOIN spare_parts sp ON sp.id = si.spare_part_id
|
|
70
|
+
WHERE si.id = ?`,
|
|
71
|
+
[result.insertId]
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
res.status(201).json(rows[0]);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// DELETE /api/stock-in/:id — SIMPLIFIED (no transaction, no stock-out check)
|
|
78
|
+
exports.deleteStockIn = async (req, res) => {
|
|
79
|
+
const [rows] = await db.query("SELECT * FROM stock_in WHERE id = ?", [req.params.id]);
|
|
80
|
+
|
|
81
|
+
if (rows.length === 0) {
|
|
82
|
+
return res.status(404).json({ error: "Stock-in record not found" });
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const stockIn = rows[0];
|
|
86
|
+
|
|
87
|
+
// Give quantity back to spare part
|
|
88
|
+
await db.query(
|
|
89
|
+
"UPDATE spare_parts SET quantity = quantity - ? WHERE id = ?",
|
|
90
|
+
[stockIn.quantity, stockIn.spare_part_id]
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
await db.query("DELETE FROM stock_in WHERE id = ?", [req.params.id]);
|
|
94
|
+
|
|
95
|
+
res.json({ message: "Stock-in deleted and quantity reversed" });
|
|
96
|
+
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
const db = require("../db");
|
|
2
|
+
|
|
3
|
+
// GET /api/stock-out
|
|
4
|
+
exports.getAllStockOut = async (req, res) => {
|
|
5
|
+
const [rows] = await db.query(
|
|
6
|
+
`SELECT so.id, so.quantity, so.unit_price, so.total_price, so.sold_at,
|
|
7
|
+
sp.id AS spare_part_id, sp.name AS spare_part_name,
|
|
8
|
+
si.id AS stock_in_id, si.unit_price AS import_unit_price, si.imported_at
|
|
9
|
+
FROM stock_out so
|
|
10
|
+
JOIN stock_in si ON si.id = so.stock_in_id
|
|
11
|
+
JOIN spare_parts sp ON sp.id = so.spare_part_id
|
|
12
|
+
ORDER BY so.sold_at DESC`
|
|
13
|
+
);
|
|
14
|
+
res.json(rows);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// GET /api/stock-out/:id
|
|
18
|
+
exports.getStockOutById = async (req, res) => {
|
|
19
|
+
const [rows] = await db.query(
|
|
20
|
+
`SELECT so.*, sp.name AS spare_part_name, si.remaining_qty, si.unit_price AS import_unit_price
|
|
21
|
+
FROM stock_out so
|
|
22
|
+
JOIN stock_in si ON si.id = so.stock_in_id
|
|
23
|
+
JOIN spare_parts sp ON sp.id = so.spare_part_id
|
|
24
|
+
WHERE so.id = ?`,
|
|
25
|
+
[req.params.id]
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if (rows.length === 0) {
|
|
29
|
+
return res.status(404).json({ error: "Stock-out record not found" });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
res.json(rows[0]);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// POST /api/stock-out — SIMPLIFIED (no transaction, no FOR UPDATE lock)
|
|
36
|
+
exports.createStockOut = async (req, res) => {
|
|
37
|
+
const { stockInId, quantity, unitPrice } = req.body;
|
|
38
|
+
|
|
39
|
+
if (!stockInId || !quantity || !unitPrice) {
|
|
40
|
+
return res.status(400).json({ error: "stockInId, quantity and unitPrice required" });
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (quantity < 1) {
|
|
44
|
+
return res.status(400).json({ error: "Quantity must be at least 1" });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Get the stock-in batch
|
|
48
|
+
const [siRows] = await db.query(
|
|
49
|
+
`SELECT si.*, sp.id AS part_id
|
|
50
|
+
FROM stock_in si
|
|
51
|
+
JOIN spare_parts sp ON sp.id = si.spare_part_id
|
|
52
|
+
WHERE si.id = ?`,
|
|
53
|
+
[stockInId]
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (siRows.length === 0) {
|
|
57
|
+
return res.status(404).json({ error: "Stock-in batch not found" });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const batch = siRows[0];
|
|
61
|
+
|
|
62
|
+
// Check remaining quantity (simple check, no lock)
|
|
63
|
+
if (quantity > batch.remaining_qty) {
|
|
64
|
+
return res.status(400).json({
|
|
65
|
+
error: `Only ${batch.remaining_qty} unit(s) remaining in this batch`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const totalPrice = quantity * unitPrice;
|
|
70
|
+
|
|
71
|
+
// Insert sale record
|
|
72
|
+
const [result] = await db.query(
|
|
73
|
+
`INSERT INTO stock_out (stock_in_id, spare_part_id, quantity, unit_price, total_price)
|
|
74
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
75
|
+
[stockInId, batch.part_id, quantity, unitPrice, totalPrice]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
// Decrement remaining units in batch
|
|
79
|
+
await db.query(
|
|
80
|
+
"UPDATE stock_in SET remaining_qty = remaining_qty - ? WHERE id = ?",
|
|
81
|
+
[quantity, stockInId]
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
// Decrement part's total stock
|
|
85
|
+
await db.query(
|
|
86
|
+
"UPDATE spare_parts SET quantity = quantity - ? WHERE id = ?",
|
|
87
|
+
[quantity, batch.part_id]
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Return full record
|
|
91
|
+
const [rows] = await db.query(
|
|
92
|
+
`SELECT so.*, sp.name AS spare_part_name, si.unit_price AS import_unit_price, si.remaining_qty
|
|
93
|
+
FROM stock_out so
|
|
94
|
+
JOIN stock_in si ON si.id = so.stock_in_id
|
|
95
|
+
JOIN spare_parts sp ON sp.id = so.spare_part_id
|
|
96
|
+
WHERE so.id = ?`,
|
|
97
|
+
[result.insertId]
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
res.status(201).json(rows[0]);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
// DELETE /api/stock-out/:id — SIMPLIFIED (no transaction)
|
|
104
|
+
exports.deleteStockOut = async (req, res) => {
|
|
105
|
+
const [rows] = await db.query("SELECT * FROM stock_out WHERE id = ?", [req.params.id]);
|
|
106
|
+
|
|
107
|
+
if (rows.length === 0) {
|
|
108
|
+
return res.status(404).json({ error: "Stock-out record not found" });
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const sale = rows[0];
|
|
112
|
+
|
|
113
|
+
// Restore batch's remaining quantity
|
|
114
|
+
await db.query(
|
|
115
|
+
"UPDATE stock_in SET remaining_qty = remaining_qty + ? WHERE id = ?",
|
|
116
|
+
[sale.quantity, sale.stock_in_id]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// Restore part's total stock
|
|
120
|
+
await db.query(
|
|
121
|
+
"UPDATE spare_parts SET quantity = quantity + ? WHERE id = ?",
|
|
122
|
+
[sale.quantity, sale.spare_part_id]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
await db.query("DELETE FROM stock_out WHERE id = ?", [sale.id]);
|
|
126
|
+
|
|
127
|
+
res.json({ message: "Stock-out deleted and quantity restored" });
|
|
128
|
+
};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
-- Run once to set up the database:
|
|
2
|
+
-- mysql -u root -p < schema.sql
|
|
3
|
+
|
|
4
|
+
CREATE DATABASE IF NOT EXISTS spareparts_db;
|
|
5
|
+
USE spareparts_db;
|
|
6
|
+
|
|
7
|
+
-- ─── Spare Parts ──────────────────────────────────────────────────────────────
|
|
8
|
+
-- Master catalogue of every part.
|
|
9
|
+
-- `quantity` is the current stock level — updated by stock-in and stock-out.
|
|
10
|
+
CREATE TABLE IF NOT EXISTS spare_parts (
|
|
11
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
12
|
+
name VARCHAR(150) NOT NULL,
|
|
13
|
+
quantity INT NOT NULL DEFAULT 0, -- current total stock on hand
|
|
14
|
+
unit_price DECIMAL(12,2) NOT NULL,
|
|
15
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- ─── Stock In ─────────────────────────────────────────────────────────────────
|
|
19
|
+
-- Each row records one import/purchase transaction.
|
|
20
|
+
-- `remaining_qty` tracks how many units from this specific batch are still
|
|
21
|
+
-- available to sell — decremented by stock-out operations (FIFO model).
|
|
22
|
+
CREATE TABLE IF NOT EXISTS stock_in (
|
|
23
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
24
|
+
spare_part_id INT NOT NULL,
|
|
25
|
+
quantity INT NOT NULL, -- units imported in this batch
|
|
26
|
+
remaining_qty INT NOT NULL, -- units still available from this batch
|
|
27
|
+
unit_price DECIMAL(12,2) NOT NULL, -- price paid per unit at import
|
|
28
|
+
total_price DECIMAL(12,2) NOT NULL, -- quantity × unit_price
|
|
29
|
+
imported_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
30
|
+
FOREIGN KEY (spare_part_id) REFERENCES spare_parts(id) ON DELETE CASCADE
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
-- ─── Stock Out ────────────────────────────────────────────────────────────────
|
|
34
|
+
-- Each row records one sale transaction drawn from a specific stock_in batch.
|
|
35
|
+
CREATE TABLE IF NOT EXISTS stock_out (
|
|
36
|
+
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
37
|
+
stock_in_id INT NOT NULL, -- which batch was sold from
|
|
38
|
+
spare_part_id INT NOT NULL, -- denormalised for easy reporting
|
|
39
|
+
quantity INT NOT NULL, -- units sold
|
|
40
|
+
unit_price DECIMAL(12,2) NOT NULL, -- selling price per unit
|
|
41
|
+
total_price DECIMAL(12,2) NOT NULL, -- quantity × unit_price
|
|
42
|
+
sold_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
43
|
+
FOREIGN KEY (stock_in_id) REFERENCES stock_in(id) ON DELETE CASCADE,
|
|
44
|
+
FOREIGN KEY (spare_part_id) REFERENCES spare_parts(id) ON DELETE CASCADE
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
-- ─── Seed data ────────────────────────────────────────────────────────────────
|
|
48
|
+
INSERT IGNORE INTO spare_parts (name, quantity, unit_price) VALUES
|
|
49
|
+
('Brake Pads', 50, 12000),
|
|
50
|
+
('Oil Filter', 80, 4500),
|
|
51
|
+
('Air Filter', 60, 5800),
|
|
52
|
+
('Spark Plugs', 120, 2200),
|
|
53
|
+
('Timing Belt', 30, 18000);
|