mega-plant/app/api/clock/job-start/route.ts
David 1ec52961ee Cash float card, Office role + Slips tab, job card PDF, manual close/delete
- Cash Floats: ledger card (opening balance, top-ups, slips), manual entries, slip photos
- Office access level; clock app Slips tab (photograph a slip, popup for supplier/cost
  code/comments/amount); only Office sees it; per-role route table in lib/clock-access
- Job cards: Machine Service PDF (with Oil block), JC numbers, manual Close job, Delete
- Hide Expenses > Vehicle Costs and Finance (Bastion) from the menu

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-22 15:09:57 +02:00

59 lines
3.3 KiB
TypeScript

import { revalidatePath } from 'next/cache';
import { clockAccess, clockFailure, clockJson } from '@/lib/clock-api';
import { clockPB } from '@/lib/clock-store';
import { saDay } from '@/lib/oil-log';
// POST /api/clock/job-start { asset_id, mechanic_id }
// A mechanic scanned a vehicle's QR and no job was open on it: open one, clock started now.
export async function POST(request: Request) {
try {
const user = await clockAccess(request);
if (user instanceof Response) return user;
let body: { asset_id?: string; mechanic_id?: string };
try { body = await request.json(); } catch { return Response.json({ error: 'Supply valid JSON.' }, { status: 400 }); }
if (!body.asset_id || !body.mechanic_id) return Response.json({ error: 'Choose the vehicle and who is working on it.' }, { status: 400 });
const pb = await clockPB();
const [asset, mechanic] = await Promise.all([
pb.collection('assets').getOne(body.asset_id).catch(() => null),
pb.collection('mechanics').getOne(body.mechanic_id).catch(() => null),
]);
if (!asset) return Response.json({ error: 'That vehicle is not in the plant register.' }, { status: 404 });
if (!mechanic) return Response.json({ error: 'That mechanic is not on the register.' }, { status: 404 });
const open = await pb.collection('job_cards').getFullList({ filter: pb.filter('asset = {:a} && status = "open"', { a: asset.id }) });
if (open.length) return Response.json({ error: `${asset.code} already has a job open (started by ${open[0].mechanic_name}). Scan it again to finish that job.` }, { status: 409 });
// Next job-card number: highest so far + 1 (deleting the newest card re-uses its number).
const last = await pb.collection('job_cards').getList(1, 1, { sort: '-jc_no', fields: 'jc_no' });
const jcNo = Number(last.items[0]?.jc_no || 0) + 1;
let job;
try {
job = await pb.collection('job_cards').create({
jc_no: jcNo,
asset: asset.id, mechanic_name: mechanic.name, mechanic_code: mechanic.employee_code || '',
started_at: new Date().toISOString(), status: 'open',
started_by: String(user.user.name || user.user.email || user.user.id),
});
} catch {
// The unique "one open job per vehicle" index: someone else started it a moment ago.
return Response.json({ error: `${asset.code} was just started by someone else. Scan it again to finish that job.` }, { status: 409 });
}
// Oil already booked to this vehicle today (before the job existed) joins the new job card.
let oil = 0;
try {
const today = saDay(new Date().toISOString());
const waiting = await pb.collection('oil_stock_movements').getFullList({
filter: pb.filter('asset = {:a} && kind = "used" && job = ""', { a: asset.id }), expand: 'item',
});
for (const m of waiting) {
if (String(m.expand?.item?.code || '').startsWith('FUEL')) continue; // diesel has its own tab
if (saDay(String(m.occurred_on || '')) !== today) continue;
await pb.collection('oil_stock_movements').update(m.id, { job: job.id });
oil++;
}
} catch { /* the job is started either way; the oil stays booked out */ }
revalidatePath('/running/job-cards');
return clockJson({ id: job.id, asset: asset.code, mechanic: mechanic.name, at: job.started_at, oil });
} catch (e) { return clockFailure(e); }
}