- 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>
59 lines
3.3 KiB
TypeScript
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); }
|
|
}
|