feat(berths): ship Waiting List + Maintenance Log tabs
Both berth-detail surfaces were stubbed/hidden behind a comment in berth-tabs.tsx. Their backing schema already existed; this wires the UI and fills the service gaps. Maintenance Log (was ~60% built: schema/migration/add+get service/route): - new edit + delete: updateMaintenanceLog / deleteMaintenanceLog service (port-scoped tenant guard), PATCH/DELETE at maintenance/[logId], plus updateMaintenanceLogSchema. add schema now accepts null for cost / responsibleParty so the shared add+edit dialog sends one body shape. - BerthMaintenanceTab: list (newest first) + add/edit dialog + delete confirm, realtime invalidation. New berth:maintenanceUpdated/Removed socket events. Waiting List (un-hide the orphaned manager + next-in-line notify): - getWaitingList now left-joins the client so the queue renders names, not raw ids. - WaitingListManager rewritten: ClientPicker instead of free-text id, client names, manage_waiting_list gating on add/reorder/remove, and a "Next in line" marker on position 1. - notifyWaitlistNextInLine: when a berth transitions to available, surface the #1 client to staff who hold berths.manage_waiting_list (mirrors the interest-based notifyNextInLine; dedupeKey-suppressed). Hooked into updateBerthStatus on any -> available transition. Tests: maintenance add/get/update/delete + cross-port guard; waitlist notify recipient-resolution / payload / empty + no-permission no-ops. Verified end-to-end in the browser (create/render/delete for both). Also adds scripts/dev-reset-admin-pw.ts (reset a synthetic user's password via the better-auth hasher after a dev reseed). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
118
tests/integration/berth-maintenance-log.test.ts
Normal file
118
tests/integration/berth-maintenance-log.test.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
/**
|
||||
* Integration test: berth maintenance-log service (add / get / update / delete).
|
||||
*
|
||||
* The schema + add/get already existed; this covers the new update + delete
|
||||
* paths and the tenant guard (an entry can only be reached through its own
|
||||
* berth + port). Runs against the real test DB.
|
||||
*/
|
||||
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { berthMaintenanceLog } from '@/lib/db/schema/berths';
|
||||
import { eq } from 'drizzle-orm';
|
||||
|
||||
let svc: typeof import('@/lib/services/berths.service');
|
||||
let makePort: typeof import('../helpers/factories').makePort;
|
||||
let makeBerth: typeof import('../helpers/factories').makeBerth;
|
||||
let makeAuditMeta: typeof import('../helpers/factories').makeAuditMeta;
|
||||
|
||||
beforeAll(async () => {
|
||||
svc = await import('@/lib/services/berths.service');
|
||||
const f = await import('../helpers/factories');
|
||||
makePort = f.makePort;
|
||||
makeBerth = f.makeBerth;
|
||||
makeAuditMeta = f.makeAuditMeta;
|
||||
});
|
||||
|
||||
describe('berth maintenance log', () => {
|
||||
async function setup() {
|
||||
const port = await makePort();
|
||||
const berth = await makeBerth({ portId: port.id });
|
||||
const meta = makeAuditMeta({ portId: port.id });
|
||||
return { port, berth, meta };
|
||||
}
|
||||
|
||||
it('adds, lists (newest first), updates, and deletes an entry', async () => {
|
||||
const { port, berth, meta } = await setup();
|
||||
|
||||
const created = await svc.addMaintenanceLog(
|
||||
berth.id,
|
||||
port.id,
|
||||
{
|
||||
category: 'repair',
|
||||
description: 'Replaced cleat bolt',
|
||||
cost: 120.5,
|
||||
costCurrency: 'EUR',
|
||||
responsibleParty: 'Dockside Ltd',
|
||||
performedDate: '2026-05-10',
|
||||
},
|
||||
meta,
|
||||
);
|
||||
expect(created.category).toBe('repair');
|
||||
expect(created.cost).toBe('120.5');
|
||||
|
||||
// A second, more recent entry to assert ordering.
|
||||
await svc.addMaintenanceLog(
|
||||
berth.id,
|
||||
port.id,
|
||||
{ category: 'inspection', description: 'Annual check', performedDate: '2026-05-20' },
|
||||
meta,
|
||||
);
|
||||
|
||||
const list = await svc.getMaintenanceLogs(berth.id, port.id);
|
||||
expect(list).toHaveLength(2);
|
||||
// Newest performedDate first.
|
||||
expect(list[0]!.performedDate).toBe('2026-05-20');
|
||||
expect(list[1]!.performedDate).toBe('2026-05-10');
|
||||
|
||||
const updated = await svc.updateMaintenanceLog(
|
||||
berth.id,
|
||||
created.id,
|
||||
port.id,
|
||||
{ description: 'Replaced cleat bolt + washer', cost: null },
|
||||
meta,
|
||||
);
|
||||
expect(updated.description).toBe('Replaced cleat bolt + washer');
|
||||
// cost cleared to null
|
||||
expect(updated.cost).toBeNull();
|
||||
|
||||
await svc.deleteMaintenanceLog(berth.id, created.id, port.id, meta);
|
||||
const afterDelete = await db
|
||||
.select()
|
||||
.from(berthMaintenanceLog)
|
||||
.where(eq(berthMaintenanceLog.id, created.id));
|
||||
expect(afterDelete).toHaveLength(0);
|
||||
|
||||
const remaining = await svc.getMaintenanceLogs(berth.id, port.id);
|
||||
expect(remaining).toHaveLength(1);
|
||||
expect(remaining[0]!.category).toBe('inspection');
|
||||
});
|
||||
|
||||
it('refuses to update or delete an entry through a different port (tenant guard)', async () => {
|
||||
const { port, berth, meta } = await setup();
|
||||
const otherPort = await makePort();
|
||||
|
||||
const entry = await svc.addMaintenanceLog(
|
||||
berth.id,
|
||||
port.id,
|
||||
{ category: 'routine', description: 'Pressure wash', performedDate: '2026-05-15' },
|
||||
meta,
|
||||
);
|
||||
|
||||
await expect(
|
||||
svc.updateMaintenanceLog(berth.id, entry.id, otherPort.id, { description: 'hijack' }, meta),
|
||||
).rejects.toThrow();
|
||||
|
||||
await expect(
|
||||
svc.deleteMaintenanceLog(berth.id, entry.id, otherPort.id, meta),
|
||||
).rejects.toThrow();
|
||||
|
||||
// Untouched.
|
||||
const [row] = await db
|
||||
.select()
|
||||
.from(berthMaintenanceLog)
|
||||
.where(eq(berthMaintenanceLog.id, entry.id));
|
||||
expect(row!.description).toBe('Pressure wash');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user