feat(client-archive): bulk-archive wizard with per-high-stakes confirmation
Replaces the single window.confirm() with a 3-stage wizard: - preflight: counts auto/needs-reason/blocked (POST /bulk-archive-preflight) - reasons: carousel through high-stakes clients capturing per-client reason (≥5 chars) — bulk endpoint accepts reasonsByClientId map - confirm: shows the final archivable count and submits Low-stakes still auto-archives with safe defaults; blocked clients are skipped with a per-row reason in the preflight summary. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -20,6 +20,10 @@ const bulkSchema = z.discriminatedUnion('action', [
|
||||
z.object({
|
||||
action: z.literal('archive'),
|
||||
ids: z.array(z.string().min(1)).min(1).max(100),
|
||||
/** When provided, lifts the high-stakes block on listed clients
|
||||
* individually. The bulk-archive wizard collects these from the
|
||||
* operator one client at a time. Reasons must be ≥5 characters. */
|
||||
reasonsByClientId: z.record(z.string(), z.string().min(5).max(2000)).optional(),
|
||||
}),
|
||||
z.object({
|
||||
action: z.literal('add_tag'),
|
||||
@@ -59,18 +63,21 @@ export const POST = withAuth(async (req, ctx) => {
|
||||
userAgent: ctx.userAgent,
|
||||
};
|
||||
|
||||
const reasonsByClientId = body.action === 'archive' ? (body.reasonsByClientId ?? {}) : {};
|
||||
|
||||
const { results, summary } = await runBulk(body.ids, async (id) => {
|
||||
if (body.action === 'archive') {
|
||||
// Bulk archive uses the smart-archive backend with sensible
|
||||
// low-stakes defaults: release available/under-offer berths,
|
||||
// retain sold ones, cancel active reservations, leave invoices,
|
||||
// leave Documenso envelopes pending. High-stakes clients are
|
||||
// refused — the operator must use the single-client smart dialog
|
||||
// for those (which captures the per-client reason + decisions).
|
||||
// leave Documenso envelopes pending. High-stakes clients require
|
||||
// a per-client reason supplied via reasonsByClientId; the bulk-
|
||||
// archive wizard captures these one at a time before submitting.
|
||||
const dossier = await getClientArchiveDossier(id, ctx.portId);
|
||||
if (dossier.stakeLevel === 'high') {
|
||||
const perClientReason = reasonsByClientId[id];
|
||||
if (dossier.stakeLevel === 'high' && !perClientReason) {
|
||||
throw new Error(
|
||||
`Client at ${dossier.highStakesStage} requires individual archive (open the client to confirm + supply a reason).`,
|
||||
`Client at ${dossier.highStakesStage} requires a per-client reason; supply one in reasonsByClientId.`,
|
||||
);
|
||||
}
|
||||
if (dossier.blockers.length > 0) {
|
||||
@@ -79,10 +86,11 @@ export const POST = withAuth(async (req, ctx) => {
|
||||
const hasSignedDocs = dossier.documents.some(
|
||||
(d) => d.status === 'completed' || d.status === 'signed',
|
||||
);
|
||||
const reason = perClientReason ?? 'Bulk archive (low-stakes auto-mode)';
|
||||
await archiveClientWithDecisions({
|
||||
dossier,
|
||||
decisions: {
|
||||
reason: 'Bulk archive (low-stakes auto-mode)',
|
||||
reason,
|
||||
acknowledgedSignedDocuments: hasSignedDocs,
|
||||
berthDecisions: dossier.berths.map((b) => ({
|
||||
berthId: b.berthId,
|
||||
|
||||
Reference in New Issue
Block a user