Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
import { useState, useCallback } from 'react';
|
|
|
|
|
import { Loader2, Mail, Copy, Check } from 'lucide-react';
|
|
|
|
|
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import {
|
|
|
|
|
Sheet,
|
|
|
|
|
SheetContent,
|
|
|
|
|
SheetHeader,
|
|
|
|
|
SheetTitle,
|
|
|
|
|
SheetDescription,
|
|
|
|
|
} from '@/components/ui/sheet';
|
|
|
|
|
import { useFeatureFlag } from '@/hooks/use-feature-flag';
|
|
|
|
|
import { apiFetch } from '@/lib/api/client';
|
|
|
|
|
|
|
|
|
|
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
interface EmailDraftButtonProps {
|
|
|
|
|
interestId: string;
|
|
|
|
|
clientId: string;
|
|
|
|
|
context?: 'follow_up' | 'introduction' | 'stage_update' | 'general';
|
|
|
|
|
additionalInstructions?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface DraftResult {
|
|
|
|
|
subject: string;
|
|
|
|
|
body: string;
|
|
|
|
|
generatedAt: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Polling helper ───────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
const POLL_INTERVAL_MS = 1500;
|
|
|
|
|
const POLL_MAX_ATTEMPTS = 20; // 30 s total
|
|
|
|
|
|
|
|
|
|
async function pollDraftResult(jobId: string): Promise<DraftResult> {
|
|
|
|
|
for (let attempt = 0; attempt < POLL_MAX_ATTEMPTS; attempt++) {
|
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
|
|
|
|
|
|
|
|
const res = await apiFetch<{ status: string; data?: DraftResult }>(
|
|
|
|
|
`/api/v1/ai/email-draft/${jobId}`,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (res.status === 'complete' && res.data) {
|
|
|
|
|
return res.data;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
throw new Error('Email draft generation timed out. Please try again.');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ─── Component ────────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export function EmailDraftButton({
|
|
|
|
|
interestId,
|
|
|
|
|
clientId,
|
|
|
|
|
context = 'follow_up',
|
|
|
|
|
additionalInstructions,
|
|
|
|
|
}: EmailDraftButtonProps) {
|
|
|
|
|
const featureEnabled = useFeatureFlag('ai_email_drafts');
|
|
|
|
|
|
|
|
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
|
const [sheetOpen, setSheetOpen] = useState(false);
|
|
|
|
|
const [draft, setDraft] = useState<DraftResult | null>(null);
|
|
|
|
|
const [error, setError] = useState<string | null>(null);
|
|
|
|
|
const [copied, setCopied] = useState(false);
|
|
|
|
|
|
|
|
|
|
const handleGenerateDraft = useCallback(async () => {
|
|
|
|
|
setIsLoading(true);
|
|
|
|
|
setError(null);
|
|
|
|
|
setDraft(null);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const { jobId } = await apiFetch<{ jobId: string }>('/api/v1/ai/email-draft', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
body: { interestId, clientId, context, additionalInstructions },
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const result = await pollDraftResult(jobId);
|
|
|
|
|
setDraft(result);
|
|
|
|
|
setSheetOpen(true);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setError(err instanceof Error ? err.message : 'Failed to generate draft');
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(false);
|
|
|
|
|
}
|
|
|
|
|
}, [interestId, clientId, context, additionalInstructions]);
|
|
|
|
|
|
|
|
|
|
const handleCopy = useCallback(async () => {
|
|
|
|
|
if (!draft) return;
|
|
|
|
|
const text = `Subject: ${draft.subject}\n\n${draft.body}`;
|
|
|
|
|
await navigator.clipboard.writeText(text);
|
|
|
|
|
setCopied(true);
|
|
|
|
|
setTimeout(() => setCopied(false), 2000);
|
|
|
|
|
}, [draft]);
|
|
|
|
|
|
|
|
|
|
if (!featureEnabled) return null;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<>
|
2026-05-11 13:01:47 +02:00
|
|
|
<Button variant="outline" size="sm" onClick={handleGenerateDraft} disabled={isLoading}>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
{isLoading ? (
|
|
|
|
|
<>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
Generating...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Mail className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
Draft Email
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
|
2026-05-11 13:01:47 +02:00
|
|
|
{error && <p className="text-xs text-destructive mt-1">{error}</p>}
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
|
|
|
|
|
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
|
|
|
|
<SheetContent side="right" className="w-full max-w-lg flex flex-col">
|
|
|
|
|
<SheetHeader>
|
|
|
|
|
<SheetTitle>Generated Email Draft</SheetTitle>
|
2026-05-11 13:01:47 +02:00
|
|
|
<SheetDescription>Review and edit the draft before sending.</SheetDescription>
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
</SheetHeader>
|
|
|
|
|
|
|
|
|
|
{draft && (
|
|
|
|
|
<div className="flex-1 overflow-y-auto mt-4 space-y-4">
|
|
|
|
|
{/* Subject */}
|
|
|
|
|
<div>
|
|
|
|
|
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Subject
|
|
|
|
|
</label>
|
|
|
|
|
<p
|
|
|
|
|
contentEditable
|
|
|
|
|
suppressContentEditableWarning
|
2026-05-12 22:14:38 +02:00
|
|
|
className="mt-1 text-sm font-medium border rounded-md px-3 py-2 focus:outline-hidden focus:ring-2 focus:ring-ring"
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
>
|
|
|
|
|
{draft.subject}
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Body */}
|
|
|
|
|
<div className="flex-1">
|
|
|
|
|
<label className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
|
|
|
|
|
Body
|
|
|
|
|
</label>
|
|
|
|
|
<pre
|
|
|
|
|
contentEditable
|
|
|
|
|
suppressContentEditableWarning
|
2026-05-12 22:14:38 +02:00
|
|
|
className="mt-1 text-sm whitespace-pre-wrap font-sans border rounded-md px-3 py-2 min-h-[300px] focus:outline-hidden focus:ring-2 focus:ring-ring"
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
>
|
|
|
|
|
{draft.body}
|
|
|
|
|
</pre>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Actions */}
|
|
|
|
|
<div className="flex gap-2 pt-2 border-t">
|
|
|
|
|
<Button variant="outline" size="sm" onClick={handleCopy}>
|
|
|
|
|
{copied ? (
|
|
|
|
|
<>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Check className="mr-1.5 h-3.5 w-3.5 text-green-600" aria-hidden />
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
Copied
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Copy className="mr-1.5 h-3.5 w-3.5" aria-hidden />
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
Copy to clipboard
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
variant="outline"
|
|
|
|
|
size="sm"
|
|
|
|
|
onClick={handleGenerateDraft}
|
|
|
|
|
disabled={isLoading}
|
|
|
|
|
>
|
|
|
|
|
{isLoading ? (
|
|
|
|
|
<>
|
fix(audit-wave-10): aria-hidden sweep on decorative Lucide icons (#69)
Mechanical codemod added \`aria-hidden\` to 444 self-closing single-line
Lucide icon JSX elements across 267 .tsx files in:
- shared/, layout/, dashboard/
- admin/ (all sections)
- clients/, berths/, yachts/, companies/, interests/, documents/
- reminders/, reservations/, residential/, expenses/, email/
The regex targeted only the safe pattern \`<IconName className="..." />\`
(no other props, self-closing, capitalized component name). Every match
inspected is a decorative companion to visible text or sits inside a
button whose accessible name comes from \`aria-label\` / sr-only text
— the icon itself should not be announced.
Screen readers no longer double-read the icon + the adjacent label
text (e.g. "Pencil Pencil Edit" → just "Edit"). The existing
@axe-core/playwright smoke test (\`20-accessibility.spec.ts\`) continues
to pass.
Test suite stays at 1315/1315 vitest. typescript clean.
Closes task #69 (aria-hidden sweep) from the AUDIT-2026-05-12 follow-ups
backlog.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:37:22 +02:00
|
|
|
<Loader2 className="mr-1.5 h-3.5 w-3.5 animate-spin" aria-hidden />
|
Initial commit: Port Nimara CRM (Layers 0-4)
Full CRM rebuild with Next.js 15, TypeScript, Tailwind, Drizzle ORM,
PostgreSQL, Redis, BullMQ, MinIO, and Socket.io. Includes 461 source
files covering clients, berths, interests/pipeline, documents/EOI,
expenses/invoices, email, notifications, dashboard, admin, and
client portal. CI/CD via Gitea Actions with Docker builds.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 11:52:51 +01:00
|
|
|
Regenerating...
|
|
|
|
|
</>
|
|
|
|
|
) : (
|
|
|
|
|
'Regenerate'
|
|
|
|
|
)}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</SheetContent>
|
|
|
|
|
</Sheet>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|