Added more functionality

This commit is contained in:
2026-08-26 23:40:31 +03:00
parent d1807a424e
commit c37f7bb6ef
6 changed files with 53 additions and 6 deletions
+27
View File
@@ -6,6 +6,23 @@ const app = express();
const port = Number(process.env.PORT || 3000);
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
function normalizeItems(items) {
if (!Array.isArray(items)) return [];
return items.map((item) => {
const itemType = item.itemType === 'labor' ? 'labor' : 'part';
const description = String(item.description || '').trim();
const quantity = Number(item.quantity || 1);
const unitPrice = Number(item.unitPrice || 0);
const laborTimeMinutes = itemType === 'labor' ? Math.round(Number(item.usedTimeHours || 0) * 60) : null;
if (!description || !Number.isFinite(quantity) || quantity <= 0 || !Number.isFinite(unitPrice) || unitPrice < 0 || (itemType === 'labor' && laborTimeMinutes <= 0)) throw new Error('Each service item needs a description, valid price, and valid quantity or labor time.');
return { itemType, description, partNumber: itemType === 'part' ? String(item.partNumber || '').trim() || null : null, laborTimeMinutes, quantity, unitPrice };
});
}
async function insertItems(client, serviceOrderId, items) {
for (const item of items) await client.query(`INSERT INTO service_order_items (service_order_id, item_type, description, part_number, labor_time_minutes, quantity, unit_price) VALUES ($1, $2, $3, $4, $5, $6, $7)`, [serviceOrderId, item.itemType, item.description, item.partNumber, item.laborTimeMinutes, item.quantity, item.unitPrice]);
}
app.use(express.json());
app.use(express.static(path.join(__dirname, 'public')));
@@ -36,6 +53,8 @@ app.get('/api/service-records', async (_request, response) => {
SELECT so.service_order_id, so.status, so.complaint, so.opened_at, so.service_date,
v.vehicle_id, btrim(v.vin) AS vin, v.registration_number AS license_plate, v.make, v.model, v.model_year, v.engine, v.transmission, v.fuel_type, v.color, v.mileage,
c.first_name || ' ' || c.last_name AS customer_name, c.email AS customer_email, c.phone AS customer_phone,
COALESCE((SELECT SUM(CASE WHEN soi.item_type = 'labor' THEN soi.labor_time_minutes / 60.0 * soi.unit_price ELSE soi.quantity * soi.unit_price END) FROM service_order_items soi WHERE soi.service_order_id = so.service_order_id), 0)::numeric(12,2) AS total,
COALESCE((SELECT json_agg(json_build_object('item_type', soi.item_type, 'description', soi.description, 'part_number', soi.part_number, 'labor_time_minutes', soi.labor_time_minutes, 'quantity', soi.quantity, 'unit_price', soi.unit_price, 'line_total', CASE WHEN soi.item_type = 'labor' THEN soi.labor_time_minutes / 60.0 * soi.unit_price ELSE soi.quantity * soi.unit_price END) ORDER BY soi.created_at) FROM service_order_items soi WHERE soi.service_order_id = so.service_order_id), '[]') AS items,
COALESCE(e.first_name || ' ' || e.last_name, 'Unassigned') AS technician
FROM service_orders so
JOIN vehicles v ON v.vehicle_id = so.vehicle_id
@@ -114,6 +133,8 @@ app.post('/api/vehicles', async (request, response) => {
app.post('/api/service-records', async (request, response) => {
const { customerName, customerEmail, customerPhone, vin, licensePlate, mileage, serviceDate, technicianName, complaint, make, model, modelYear, engine, transmission, fuelType, color } = request.body;
let items;
try { items = normalizeItems(request.body.items); } catch (error) { return response.status(400).json({ error: error.message }); }
const parsedMileage = Number(mileage);
if (!customerName || !vin || !licensePlate || !technicianName || !complaint || !serviceDate || !Number.isInteger(parsedMileage) || parsedMileage < 0) {
return response.status(400).json({ error: 'Customer, VIN, license plate, non-negative mileage, service date, technician, and complaint are required.' });
@@ -156,6 +177,7 @@ app.post('/api/service-records', async (request, response) => {
`INSERT INTO service_orders (vehicle_id, assigned_employee_id, complaint, service_date) VALUES ($1, $2, $3, $4) RETURNING service_order_id`,
[vehicle.rows[0].vehicle_id, technician.rows[0].employee_id, complaint.trim(), serviceDate]
);
await insertItems(client, record.rows[0].service_order_id, items);
await client.query('COMMIT');
response.status(201).json({ service_order_id: record.rows[0].service_order_id });
} catch (error) {
@@ -168,6 +190,8 @@ app.post('/api/service-records', async (request, response) => {
app.patch('/api/service-records/:id', async (request, response) => {
const { customerName, customerEmail, customerPhone, vin, licensePlate, mileage, serviceDate, technicianName, complaint, make, model, modelYear, engine, transmission, fuelType, color } = request.body;
let items;
try { items = normalizeItems(request.body.items); } catch (error) { return response.status(400).json({ error: error.message }); }
const parsedMileage = Number(mileage);
if (!customerName || !vin || !licensePlate || !technicianName || !complaint || !serviceDate || !Number.isInteger(parsedMileage) || parsedMileage < 0) return response.status(400).json({ error: 'Customer, VIN, license plate, non-negative mileage, service date, technician, and complaint are required.' });
const customerParts = customerName.trim().split(/\s+/);
@@ -183,6 +207,8 @@ app.patch('/api/service-records/:id', async (request, response) => {
await client.query('UPDATE employees SET first_name = $1, last_name = $2 WHERE employee_id = $3', [technicianFirst, technicianParts.join(' ') || technicianFirst, current.rows[0].assigned_employee_id]);
await client.query(`UPDATE vehicles SET vin = $1, registration_number = $2, make = NULLIF($3, ''), model = NULLIF($4, ''), model_year = NULLIF($5, '')::integer, engine = NULLIF($6, ''), transmission = NULLIF($7, ''), fuel_type = NULLIF($8, ''), color = NULLIF($9, ''), mileage = $10 WHERE vehicle_id = $11`, [vin.trim(), licensePlate.trim(), make?.trim() || '', model?.trim() || '', modelYear || '', engine?.trim() || '', transmission?.trim() || '', fuelType?.trim() || '', color?.trim() || '', parsedMileage, current.rows[0].vehicle_id]);
await client.query('UPDATE service_orders SET complaint = $1, service_date = $2 WHERE service_order_id = $3', [complaint.trim(), serviceDate, request.params.id]);
await client.query('DELETE FROM service_order_items WHERE service_order_id = $1', [request.params.id]);
await insertItems(client, request.params.id, items);
await client.query('COMMIT');
response.json({ service_order_id: request.params.id });
} catch (error) {
@@ -276,6 +302,7 @@ app.use((_request, response) => response.sendFile(path.join(__dirname, 'public',
async function start() {
await pool.query(`ALTER TABLE vehicles ADD COLUMN IF NOT EXISTS engine TEXT, ADD COLUMN IF NOT EXISTS transmission TEXT, ADD COLUMN IF NOT EXISTS fuel_type TEXT, ADD COLUMN IF NOT EXISTS color TEXT`);
await pool.query(`ALTER TABLE service_order_items ADD COLUMN IF NOT EXISTS part_number TEXT, ADD COLUMN IF NOT EXISTS labor_time_minutes INTEGER`);
await pool.query(`ALTER TABLE service_orders ADD COLUMN IF NOT EXISTS service_date DATE`);
await pool.query(`UPDATE service_orders SET service_date = opened_at::date WHERE service_date IS NULL`);
await pool.query(`ALTER TABLE service_orders ALTER COLUMN service_date SET DEFAULT CURRENT_DATE`);