Merge feature/inbox-1: Anfragen-Posteingang UI (list/triage/assign/draft/send) [god-QA pending]

# Conflicts:
#	gerbil-manager-web/e2e/mock-data.ts
#	gerbil-manager-web/src/App.tsx
This commit is contained in:
2026-06-06 11:29:21 +02:00
11 changed files with 845 additions and 1 deletions

View File

@@ -0,0 +1,87 @@
/**
* INBOX-1: Anfragen-Posteingang (/api/requests, Backend INBOX-0/2/3).
*
* Triage-Hinweis: PUT setzt assignedContactId UNBEDINGT aus dem Input
* (Backend überschreibt mit null, wenn das Feld fehlt) — beim reinen
* Status-Wechsel also IMMER die aktuelle Zuordnung mitsenden.
*/
import { ApiError, api } from './client'
import { toQueryString, type GridifyQuery } from './gridify'
import type { Paged } from './types'
/** C#-Enum RequestStatus — Stringnamen auf dem Draht. */
export type RequestStatus = 'New' | 'InProgress' | 'Assigned' | 'Answered' | 'Abandoned'
export const REQUEST_STATUSES: RequestStatus[] = [
'New',
'InProgress',
'Assigned',
'Answered',
'Abandoned',
]
export interface InboxRequest {
id: string
gmailMessageId: string
threadId: string | null
fromAddress: string
fromName: string | null
subject: string | null
bodyText: string | null
receivedAt: string
status: RequestStatus
assignedContactId: string | null
draftReply: string | null
answeredAt: string | null
}
export interface RequestTriage {
status?: RequestStatus | null
assignedContactId?: string | null
}
/** Ergebnis von POST /api/requests/sync. */
export interface SyncResult {
imported: number
/** null = ok; "MailNotConfigured" | "MailAuthFailed" | … */
error: string | null
}
const BASE = '/api/requests'
export function listRequests(query: GridifyQuery): Promise<Paged<InboxRequest>> {
return api.get<Paged<InboxRequest>>(`${BASE}${toQueryString(query)}`)
}
export function getRequest(id: string): Promise<InboxRequest> {
return api.get<InboxRequest>(`${BASE}/${id}`)
}
export function triageRequest(id: string, triage: RequestTriage): Promise<void> {
return api.put<void>(`${BASE}/${id}`, triage)
}
export function syncRequests(): Promise<SyncResult> {
return api.post<SyncResult>(`${BASE}/sync`, {})
}
/** KI-Entwurf erzeugen (INBOX-2); 503 {code:'AiKeyMissing'} solange unkonfiguriert. */
export function draftReply(id: string): Promise<InboxRequest> {
return api.post<InboxRequest>(`${BASE}/${id}/draft`, {})
}
/** (Bearbeitete) Antwort senden (INBOX-3); setzt serverseitig Answered. */
export function sendReply(id: string, body: string): Promise<InboxRequest> {
return api.post<InboxRequest>(`${BASE}/${id}/send`, { body })
}
/**
* ProblemDetails-Titel aus einem ApiError lesen (Send-Endpunkt meldet
* MailNotConfigured/MailAuthFailed über `title`, nicht `code`).
*/
export function problemTitle(err: unknown): string | null {
if (err instanceof ApiError && err.body && typeof err.body === 'object' && 'title' in err.body) {
const title = (err.body as { title: unknown }).title
return typeof title === 'string' ? title : null
}
return null
}