refinement phase 4

This commit is contained in:
hamid
2026-07-13 12:29:00 +03:30
parent 314763f764
commit 64f6aa45c9
27 changed files with 308 additions and 243 deletions
+21 -7
View File
@@ -21,7 +21,7 @@ import type {
const API = '/api/v1';
/** Wire `TicketSummaryDto` (camelCase, per api-conventions). */
/** Wire `TicketSummaryDto` (camelCase). REQ-028 (delivered) added `lastMessageAt`/`unreadCount`. */
interface TicketSummaryWire {
id: number;
referenceCode: string;
@@ -31,6 +31,8 @@ interface TicketSummaryWire {
bookingId: number | null;
refundId: number | null;
createdAt: string;
lastMessageAt: string | null;
unreadCount: number;
}
/** Wire `TicketMessageDto`. `isInternal` is present on the DTO but is `false` in the user view (server-stripped). */
@@ -67,6 +69,9 @@ function mapSummary(w: TicketSummaryWire): TicketSummary {
bookingId: w.bookingId,
refundId: w.refundId,
createdAt: w.createdAt,
// REQ-028 (delivered): the inbox unread badge + last-activity sort now come off the wire.
lastMessageAt: w.lastMessageAt,
unreadCount: w.unreadCount,
};
}
@@ -162,9 +167,11 @@ function mapAdminThread(w: TicketThreadWire, viewerUserId?: number): AdminTicket
* - `openTicket` → `POST /tickets`.
* - `postMessage` → `POST /tickets/{id}/messages` (a non-staff caller never sets `isInternal`).
*
* NOT the primary implementation this phase (`USE_TICKETS_MOCK = true`) — see `constants.ts`. The wire
* summary has no `unreadCount`/`lastMessageAt` (REQ-028), so those stay undefined here (the inbox degrades).
* `clientMessageId` is client-only (optimistic reconcile) — not sent (the server has no field for it yet).
* PRIMARY once `USE_TICKETS_MOCK = false` (refinement-phase-4; REQ-028 delivered): the summary now carries
* `unreadCount`/`lastMessageAt` (inbox badge + last-activity sort) and the message post sends the optimistic
* `clientMessageId` (server dedupes + echoes it back). The user list still filters only by `Status`; the
* "jump to the existing coordination ticket" by-booking lookup is a minor follow-up (REQ-028 #3 —
* `GET /tickets?BookingId=` is served, but no client method targets it yet).
*/
export const ticketsClientApi: TicketsApi = {
listMyTickets: async (params: TicketListParams): Promise<Paginated<TicketSummary>> => {
@@ -202,7 +209,9 @@ export const ticketsClientApi: TicketsApi = {
unwrap(
await clientFetch<ApiEnvelope<PostMessageResult>>(`${API}/tickets/${ticketId}/messages`, {
method: 'POST',
body: JSON.stringify({ body: body.body }),
// REQ-028 (delivered): send the optimistic `clientMessageId` so the server dedupes a retried send
// and echoes it back on `PostMessageResult` for reconciliation.
body: JSON.stringify({ body: body.body, clientMessageId: body.clientMessageId }),
}),
),
@@ -233,12 +242,17 @@ export const ticketsClientApi: TicketsApi = {
return mapAdminThread(wire, viewerUserId);
},
// Staff post — may set `isInternal` (the one caller allowed to). `clientMessageId` stays client-only.
// Staff post — may set `isInternal` (the one caller allowed to). REQ-028: send `clientMessageId` too.
postAdminMessage: async (ticketId: number, body: PostAdminMessageRequest): Promise<PostMessageResult> =>
unwrap(
await clientFetch<ApiEnvelope<PostMessageResult>>(`${API}/tickets/${ticketId}/messages`, {
method: 'POST',
body: JSON.stringify({ body: body.body, isInternal: body.isInternal }),
body: JSON.stringify({
body: body.body,
isInternal: body.isInternal,
clientMessageId: body.clientMessageId,
}),
}),
),
};
+1 -1
View File
@@ -12,7 +12,7 @@
* ticket reachable. Flip to `false` once the upstreams are real — no hook/component change (only the seam
* selection in `apis/index.ts`).
*/
export const USE_TICKETS_MOCK = true;
export const USE_TICKETS_MOCK = false;
/** Inbox page size (api-conventions `pageSize`, default 50 / max 100). */
export const TICKETS_PAGE_SIZE = 20;