fix(files): list a hotel booking's files and files pinned to a stop on the place card

A hotel booking reaches its place through the stay, not its own place_id, so
the card listed the booking but not its confirmation. Links to a stop, which
MCP's link_trip_file and plugins can make, were never returned by the file
list at all; it now carries linked_assignment_ids. Without files the row no
longer opens to nothing.
This commit is contained in:
Maurice
2026-10-14 10:45:22 +02:00
committed by Maurice
parent 7c6b7622fc
commit 10a987fec1
11 changed files with 63 additions and 10 deletions
@@ -25,6 +25,7 @@ export interface FileLinkWithReservationRow extends FileLinkRow {
export interface FileLinkTargetRow {
file_id: number;
reservation_id: number | null;
assignment_id: number | null;
place_id: number | null;
budget_item_id: number | null;
}
@@ -64,7 +65,7 @@ export class FileLinksRepository extends TrekRepository<FileLinks> {
if (file_ids.length === 0) return [];
return await this.kysely<FileLinksKyselyDB>()
.selectFrom('file_links')
.select(['file_id', 'reservation_id', 'place_id', 'budget_item_id'])
.select(['file_id', 'reservation_id', 'assignment_id', 'place_id', 'budget_item_id'])
.where('file_id', 'in', file_ids)
.execute();
}
+2
View File
@@ -349,6 +349,8 @@ export class FilesService {
...formatFile(f),
linked_reservation_ids: fileLinks.filter((l) => l.reservation_id).map((l) => l.reservation_id),
linked_place_ids: fileLinks.filter((l) => l.place_id).map((l) => l.place_id),
// MCP's link_trip_file and plugins can pin a file to one stop of the plan.
linked_assignment_ids: fileLinks.filter((l) => l.assignment_id).map((l) => l.assignment_id),
linked_budget_item_ids: fileLinks.filter((l) => l.budget_item_id).map((l) => l.budget_item_id),
};
});
@@ -225,6 +225,7 @@ export const PLUGIN_ENTITY_CONTRACT = {
'url',
'linked_reservation_ids',
'linked_place_ids',
'linked_assignment_ids',
'linked_budget_item_ids',
],
},
@@ -354,6 +354,19 @@ describe('listFiles', () => {
const empty = createTrip(testDb, user.id);
expect(await svc.listFiles(empty.id, false)).toEqual([]);
});
it('FILE-SVC-014b: lists the stops a file is pinned to, which MCP and plugins can link', async () => {
const { user, trip } = seedTrip();
const day = createDay(testDb, trip.id);
const stop = createDayAssignment(testDb, day.id, createPlace(testDb, trip.id).id);
const pinned = await makeFile(trip.id, user.id);
const bare = await makeFile(trip.id, user.id);
await svc.createFileLink(pinned.id, { assignment_id: stop.id });
const files = (await svc.listFiles(trip.id, false)) as Record<string, unknown>[];
expect(files.find((f) => f.id === pinned.id).linked_assignment_ids).toEqual([stop.id]);
expect(files.find((f) => f.id === bare.id).linked_assignment_ids).toEqual([]);
});
});
// ── receipts on an expense ────────────────────────────────────────────────────