fix(audit-wave-9): standardize on Sheet for previews; doctrine in CLAUDE.md
Swap the one outlier (client-interests-tab.tsx) from Vaul Drawer to Sheet side=right so every detail-preview surface uses the same primitive. Document the doctrine: Sheet for side panels on both desktop and mobile; Vaul Drawer reserved for mobile-only bottom-sheet UX (currently just MoreSheet). Closes ui/ux M11. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -29,10 +29,10 @@
|
||||
|
||||
import { timingSafeEqual } from 'node:crypto';
|
||||
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { and, eq, inArray, sql } from 'drizzle-orm';
|
||||
|
||||
import { db } from '@/lib/db';
|
||||
import { clients, clientMergeLog } from '@/lib/db/schema/clients';
|
||||
import { clients, clientContacts, clientMergeLog } from '@/lib/db/schema/clients';
|
||||
import { interests } from '@/lib/db/schema/interests';
|
||||
import { berthReservations } from '@/lib/db/schema/reservations';
|
||||
import { files, documents, formSubmissions } from '@/lib/db/schema/documents';
|
||||
@@ -40,6 +40,7 @@ import { documentSends } from '@/lib/db/schema/brochures';
|
||||
import { emailThreads } from '@/lib/db/schema/email';
|
||||
import { reminders } from '@/lib/db/schema/operations';
|
||||
import { scratchpadNotes } from '@/lib/db/schema/system';
|
||||
import { websiteSubmissions } from '@/lib/db/schema/website-submissions';
|
||||
import { user as authUser } from '@/lib/db/schema/users';
|
||||
import { redis } from '@/lib/redis';
|
||||
import { sendEmail } from '@/lib/email';
|
||||
@@ -189,6 +190,29 @@ export async function hardDeleteClient(args: {
|
||||
if (!locked) throw new NotFoundError('client');
|
||||
if (!locked.archivedAt) throw new ConflictError('Client must be archived');
|
||||
|
||||
// Read email contacts BEFORE the cascade so we can wipe matching
|
||||
// website_submissions rows — that table has no clientId FK (raw
|
||||
// inquiry-form data, pre-promotion), matched only by email in the
|
||||
// JSONB payload. Article-17 requires removing the data subject's
|
||||
// submitted form data too.
|
||||
const emailContactRows = await tx
|
||||
.select({ value: clientContacts.value })
|
||||
.from(clientContacts)
|
||||
.where(and(eq(clientContacts.clientId, args.clientId), eq(clientContacts.channel, 'email')));
|
||||
const emailValues = emailContactRows
|
||||
.map((r) => r.value.trim().toLowerCase())
|
||||
.filter((v) => v.length > 0);
|
||||
if (emailValues.length > 0) {
|
||||
await tx
|
||||
.delete(websiteSubmissions)
|
||||
.where(
|
||||
and(
|
||||
eq(websiteSubmissions.portId, args.portId),
|
||||
inArray(sql<string>`LOWER(${websiteSubmissions.payload}->>'email')`, emailValues),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// Detach nullable FKs so we keep their audit history.
|
||||
await tx.update(files).set({ clientId: null }).where(eq(files.clientId, args.clientId));
|
||||
await tx.update(documents).set({ clientId: null }).where(eq(documents.clientId, args.clientId));
|
||||
@@ -265,7 +289,7 @@ function hashIds(ids: string[]): string {
|
||||
// Stable hash so the same set always produces the same key — order
|
||||
// independent. SHA-1 is more than enough for collision-avoidance on
|
||||
// a per-user keyspace.
|
||||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||||
|
||||
const { createHash } = require('node:crypto') as typeof import('node:crypto');
|
||||
const sorted = [...ids].sort().join('|');
|
||||
return createHash('sha1').update(sorted).digest('hex');
|
||||
|
||||
Reference in New Issue
Block a user