import { Injectable } from '@nestjs/common';
import type { StationKind } from '../application/kitchen-station.service';
import { DatabaseService } from '../../../infrastructure/database/database.service';
@Injectable()
export class KitchenBoardService {
constructor(private readonly database: DatabaseService) {}
async get(input: {
organizationId: string;
locationId: string;
stationId?: string;
stationKind?: StationKind;
}) {
if (!input || typeof input !== 'object' || !this.isBoundedText(input.organizationId, 200) || !this.isBoundedText(input.locationId, 200)) {
throw new Error('kitchen.scope_invalid');
}
if (input.stationId !== undefined && !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(input.stationId)) {
throw new Error('kitchen.station_id_invalid');
}
if (input.stationKind !== undefined && !(['kitchen', 'bar', 'pass', 'expedite'] as const).includes(input.stationKind)) {
throw new Error('kitchen.station_kind_invalid');
}
return this.database.query<any>(
`select
kt.id,
kt.station_id,
ks.station_kind,
ks.display_name as station_name,
kt.order_id,
kt.service_session_id,
kt.course_number,
kt.status,
kt.version,
kt.priority,
kt.created_at,
kt.fired_at,
kt.started_at,
kt.ready_at,
extract(epoch from (
now() - coalesce(kt.fired_at, kt.created_at)
))::int as elapsed_seconds,
ks.target_seconds,
coalesce(
jsonb_agg(
jsonb_build_object(
'id', kti.id,
'orderItemId', kti.order_item_id,
'productName', kti.product_name_snapshot,
'quantity', kti.quantity,
'seatNumber', kti.seat_number,
'notes', kti.notes,
'modifiers', kti.modifiers_snapshot,
'allergens', kti.allergens_snapshot,
'status', kti.status
)
order by kti.created_at
) filter(where kti.id is not null),
'[]'::jsonb
) as items
from kitchen_tickets kt
join kitchen_stations ks on ks.id=kt.station_id
left join kitchen_ticket_items kti on kti.kitchen_ticket_id=kt.id
where kt.organization_id=$1
and kt.location_id=$2
and ($3::uuid is null or kt.station_id=$3)
and ($4::text is null or ks.station_kind=$4)
and kt.status in ('queued','held','fired','in_progress','ready','recalled')
group by kt.id, ks.id
order by
case kt.status
when 'ready' then 0
when 'in_progress' then 1
when 'fired' then 2
when 'recalled' then 3
else 4
end,
kt.priority desc,
coalesce(kt.fired_at, kt.created_at)`,
[input.organizationId, input.locationId, input.stationId ?? null, input.stationKind ?? null],
);
}
private isBoundedText(value: unknown, max: number): value is string {
return typeof value === 'string' && value.trim().length > 0 && value.length <= max;
}
}
