{t('quotes.field.acceptedAt', 'Accepted at')}
{fmtDateTime(q.acceptedAt)}
}
{q.declinedAt && {t('quotes.field.declinedAt', 'Declined at')}
{fmtDateTime(q.declinedAt)}
}
+ {q.declineReason && {t('quotes.field.responseWindow', 'Response window')}
{t('quotes.responseWindowOpen', 'Open until {{at}}', { at: q.responseLockedAt ? fmtDateTime(q.responseLockedAt) : '' })}
diff --git a/frontend/src/services/quotes.service.ts b/frontend/src/services/quotes.service.ts
index 8feb9923..ffc00aec 100644
--- a/frontend/src/services/quotes.service.ts
+++ b/frontend/src/services/quotes.service.ts
@@ -98,6 +98,10 @@ export interface QuoteDetail extends QuoteSummary {
ccPdfEmail: string | null;
respondedAt: string | null;
responseLockedAt: string | null;
+ /** Free-text reason captured when an admin declines on the customer's
+ * behalf (migration 115). Null for customer-side declines + non-declined
+ * quotes. */
+ declineReason: string | null;
pdfPath: string | null;
businessBankAccountId: number | null;
}
@@ -252,6 +256,14 @@ export const quotesService = {
return data.data || data;
},
+ /** Admin decline-on-behalf — flips the quote to `declined` without the
+ * customer's public response page. Optional free-text reason. Used
+ * when the customer says no by phone/email. */
+ async declineOnBehalf(id: number, reason?: string): Promise<{ status: string; declinedAt: string }> {
+ const { data } = await api.post(`/admin/quotes/${id}/decline`, reason ? { reason } : {});
+ return data.data || data;
+ },
+
async convert(id: number): Promise<{ eventId: number; alreadyConverted: boolean }> {
const { data } = await api.post(`/admin/quotes/${id}/convert`);
return data.data || data;