fix: crash bugs in TeamInvoices + faster load

Bug fixes:
- TeamInvoices: add useAuth import/currentUser (invoice-create crash)
- TeamInvoices: setChartYear -> setExportYear (year-dropdown crash)
- TeamDashboard: drop redundant setState-in-effect (cascading renders)
- Companies: delete dead _UnusedClientCompanies (illegal hook calls)
- annotate intentional empty PDF-fallback catches

Load speed:
- preconnect/dns-prefetch to Supabase origin
- lazy-load heic-to in Converters: page chunk 2737KB -> 9KB
- split recharts into its own 'charts' vendor chunk

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Krao Hasanee
2026-06-08 12:59:11 -04:00
parent 85625f4d95
commit 04e0911e9f
101 changed files with 11786 additions and 7445 deletions
+276 -35
View File
@@ -4,6 +4,7 @@ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
const RESEND_API_KEY = Deno.env.get('RESEND_API_KEY');
const SUPABASE_URL = Deno.env.get('SUPABASE_URL');
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY');
const SUPABASE_WEBHOOK_SECRET = Deno.env.get('SUPABASE_WEBHOOK_SECRET');
const FROM = 'Fourge Branding <hello@fourgebranding.com>';
const TEAM_EMAILS = [
'krao@fourgebranding.com',
@@ -12,7 +13,7 @@ const TEAM_EMAILS = [
'twebb@fourgebranding.com',
];
const ALLOWED_TYPES = ['new_request', 'sent_to_client', 'revision_submitted', 'client_approved', 'invoice_sent', 'receipt_sent', 'subcontractor_po_sent', 'subcontractor_invoice_submitted'] as const;
const ALLOWED_TYPES = ['new_request', 'sent_to_client', 'revision_submitted', 'client_approved', 'invoice_sent', 'receipt_sent', 'subcontractor_po_sent', 'subcontractor_invoice_submitted', 'task_status_update'] as const;
type EmailType = typeof ALLOWED_TYPES[number];
// Types that only team members may trigger
@@ -53,6 +54,8 @@ const optStr = (val: unknown, max = 500): string => {
return val.trim();
};
const optBool = (val: unknown): boolean => val === true;
/** Basic UUID format check. */
const isUuid = (v: unknown): v is string =>
typeof v === 'string' && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(v);
@@ -102,26 +105,32 @@ serve(async (req) => {
// ── 1. Authentication ────────────────────────────────────────────────────
const authHeader = req.headers.get('Authorization') ?? '';
const accessToken = authHeader.startsWith('Bearer ') ? authHeader.slice(7) : '';
if (!accessToken) {
const webhookSecret = req.headers.get('x-webhook-secret') ?? '';
const isInternalWebhook = !!SUPABASE_WEBHOOK_SECRET && webhookSecret === SUPABASE_WEBHOOK_SECRET;
if (!accessToken && !isInternalWebhook) {
return respond({ error: 'Unauthorized' }, 401);
}
// Use service role client to validate the token — works with ES256 JWTs
const adminClient = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY);
const { data: userData, error: authError } = await adminClient.auth.getUser(accessToken);
if (authError || !userData?.user) {
return respond({ error: `Auth failed: ${authError?.message ?? 'no user'}` }, 401);
let userData: { user?: { id: string } } | null = null;
if (!isInternalWebhook) {
const authResult = await adminClient.auth.getUser(accessToken);
userData = authResult.data;
if (authResult.error || !userData?.user) {
return respond({ error: `Auth failed: ${authResult.error?.message ?? 'no user'}` }, 401);
}
}
// ── 2. Role lookup ───────────────────────────────────────────────────────
const { data: profile } = await adminClient
.from('profiles')
.select('role')
.eq('id', userData.user.id)
.single();
const role: string = profile?.role ?? '';
const role: string = isInternalWebhook
? 'team'
: ((await adminClient
.from('profiles')
.select('role')
.eq('id', userData?.user?.id)
.single()).data?.role ?? '');
if (!['team', 'client', 'external'].includes(role)) {
return respond({ error: 'Forbidden' }, 403);
}
@@ -163,17 +172,55 @@ serve(async (req) => {
subject = `New Request: ${esc(serviceType)}${esc(clientName)}`;
html = `
<h2>New Request Received</h2>
<p><strong>From:</strong> ${esc(clientName)} (${esc(clientEmail)})</p>
<p><strong>Company:</strong> ${esc(company) || '—'}</p>
<p><strong>Service:</strong> ${esc(serviceType)}</p>
<p><strong>Project:</strong> ${esc(projectName)}</p>
${deadline ? `<p><strong>Deadline:</strong> ${esc(deadline)}</p>` : ''}
<hr />
<p><strong>Description:</strong></p>
<p>${escMultiline(description)}</p>
<br />
<a href="https://portal.fourgebranding.com/tasks/${esc(taskId)}" style="background:#F5A523;color:#1a1a1a;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">View Job</a>
<div style="font-family:sans-serif;max-width:560px;margin:0 auto;color:#1a1a1a;">
<div style="background:#141414;padding:20px 28px;border-radius:8px 8px 0 0;">
<img src="https://portal.fourgebranding.com/fourge-logo.png" alt="Fourge Branding" style="height:28px;" />
</div>
<div style="background:#fff;padding:28px;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px;">
<h2 style="margin:0 0 8px;font-size:20px;">New Request Received</h2>
<p style="color:#555;margin:0 0 24px;">A new request has been created in the portal.</p>
<table style="width:100%;border-collapse:collapse;margin-bottom:24px;">
<tr style="background:#f9f9f9;">
<td style="padding:10px 14px;font-size:13px;color:#666;">From</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(clientName)}</td>
</tr>
<tr>
<td style="padding:10px 14px;font-size:13px;color:#666;">Email</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(clientEmail)}</td>
</tr>
${company ? `
<tr style="background:#f9f9f9;">
<td style="padding:10px 14px;font-size:13px;color:#666;">Company</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(company)}</td>
</tr>` : ''}
<tr>
<td style="padding:10px 14px;font-size:13px;color:#666;">Service</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(serviceType)}</td>
</tr>
<tr style="background:#f9f9f9;">
<td style="padding:10px 14px;font-size:13px;color:#666;">Project</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(projectName)}</td>
</tr>
${deadline ? `
<tr>
<td style="padding:10px 14px;font-size:13px;color:#666;">Deadline</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(deadline)}</td>
</tr>` : ''}
</table>
<div style="background:#f9f9f9;padding:12px 14px;border-radius:6px;font-size:13px;color:#555;margin-bottom:24px;">
<strong>Description</strong><br />
${escMultiline(description)}
</div>
<a href="https://portal.fourgebranding.com/tasks/${esc(taskId)}" style="display:block;background:#141414;color:#fff;text-align:center;padding:14px;border-radius:8px;text-decoration:none;font-weight:700;font-size:16px;margin-bottom:20px;">View Task</a>
<p style="font-size:12px;color:#999;text-align:center;margin:0;">
Questions? <a href="mailto:hello@fourgebranding.com" style="color:#555;">hello@fourgebranding.com</a>
</p>
</div>
</div>
`;
}
@@ -210,16 +257,50 @@ serve(async (req) => {
subject = `Revision Request: ${esc(serviceType)}${esc(clientName)}`;
html = `
<h2>Revision Requested</h2>
<p><strong>From:</strong> ${esc(clientName)}</p>
<p><strong>Job:</strong> ${esc(serviceType)}${esc(projectName)}</p>
<p><strong>New Version:</strong> ${esc(version)}</p>
${deadline ? `<p><strong>New Deadline:</strong> ${esc(deadline)}</p>` : ''}
<hr />
<p><strong>Requested changes:</strong></p>
<p>${escMultiline(description)}</p>
<br />
<a href="https://portal.fourgebranding.com/tasks/${esc(taskId)}" style="background:#F5A523;color:#1a1a1a;padding:10px 20px;border-radius:6px;text-decoration:none;font-weight:600;">View Job</a>
<div style="font-family:sans-serif;max-width:560px;margin:0 auto;color:#1a1a1a;">
<div style="background:#141414;padding:20px 28px;border-radius:8px 8px 0 0;">
<img src="https://portal.fourgebranding.com/fourge-logo.png" alt="Fourge Branding" style="height:28px;" />
</div>
<div style="background:#fff;padding:28px;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px;">
<h2 style="margin:0 0 8px;font-size:20px;">Revision Requested</h2>
<p style="color:#555;margin:0 0 24px;">A revision was requested in the portal.</p>
<table style="width:100%;border-collapse:collapse;margin-bottom:24px;">
<tr style="background:#f9f9f9;">
<td style="padding:10px 14px;font-size:13px;color:#666;">From</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(clientName)}</td>
</tr>
<tr>
<td style="padding:10px 14px;font-size:13px;color:#666;">Task</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(serviceType)}</td>
</tr>
<tr style="background:#f9f9f9;">
<td style="padding:10px 14px;font-size:13px;color:#666;">Project</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(projectName)}</td>
</tr>
<tr>
<td style="padding:10px 14px;font-size:13px;color:#666;">Version</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(version)}</td>
</tr>
${deadline ? `
<tr style="background:#f9f9f9;">
<td style="padding:10px 14px;font-size:13px;color:#666;">Deadline</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(deadline)}</td>
</tr>` : ''}
</table>
<div style="background:#f9f9f9;padding:12px 14px;border-radius:6px;font-size:13px;color:#555;margin-bottom:24px;">
<strong>Requested changes</strong><br />
${escMultiline(description)}
</div>
<a href="https://portal.fourgebranding.com/tasks/${esc(taskId)}" style="display:block;background:#141414;color:#fff;text-align:center;padding:14px;border-radius:8px;text-decoration:none;font-weight:700;font-size:16px;margin-bottom:20px;">View Task</a>
<p style="font-size:12px;color:#999;text-align:center;margin:0;">
Questions? <a href="mailto:hello@fourgebranding.com" style="color:#555;">hello@fourgebranding.com</a>
</p>
</div>
</div>
`;
}
@@ -428,12 +509,168 @@ serve(async (req) => {
`;
}
else if (type === 'task_status_update') {
const taskTitle = requireStr(data?.taskTitle);
const statusLabel = requireStr(data?.statusLabel);
const message = requireStr(data?.message, 10000);
const taskId = data?.taskId;
if (!isUuid(taskId)) throw new Error('Invalid taskId');
const subjectLine = optStr(data?.subject, 200) || `${statusLabel}: ${taskTitle}`;
const headline = optStr(data?.headline, 200) || statusLabel;
const projectName = optStr(data?.projectName, 200);
const companyName = optStr(data?.companyName, 200);
const buttonLabel = optStr(data?.buttonLabel, 80) || 'View Task';
subject = subjectLine;
html = `
<div style="font-family:sans-serif;max-width:560px;margin:0 auto;color:#1a1a1a;">
<div style="background:#141414;padding:20px 28px;border-radius:8px 8px 0 0;">
<img src="https://portal.fourgebranding.com/fourge-logo.png" alt="Fourge Branding" style="height:28px;" />
</div>
<div style="background:#fff;padding:28px;border:1px solid #e5e7eb;border-top:none;border-radius:0 0 8px 8px;">
<h2 style="margin:0 0 8px;font-size:20px;">${esc(headline)}</h2>
<p style="color:#555;margin:0 0 24px;">${escMultiline(message)}</p>
<table style="width:100%;border-collapse:collapse;margin-bottom:24px;">
<tr style="background:#f9f9f9;">
<td style="padding:10px 14px;font-size:13px;color:#666;">Task</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(taskTitle)}</td>
</tr>
${projectName ? `
<tr>
<td style="padding:10px 14px;font-size:13px;color:#666;">Project</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(projectName)}</td>
</tr>` : ''}
${companyName ? `
<tr style="background:#f9f9f9;">
<td style="padding:10px 14px;font-size:13px;color:#666;">Company</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(companyName)}</td>
</tr>` : ''}
<tr>
<td style="padding:10px 14px;font-size:13px;color:#666;">Status</td>
<td style="padding:10px 14px;font-size:13px;font-weight:700;text-align:right;">${esc(statusLabel)}</td>
</tr>
</table>
<a href="https://portal.fourgebranding.com/tasks/${esc(taskId)}" style="display:block;background:#141414;color:#fff;text-align:center;padding:14px;border-radius:8px;text-decoration:none;font-weight:700;font-size:16px;margin-bottom:20px;">${esc(buttonLabel)}</a>
<p style="font-size:12px;color:#999;text-align:center;margin:0;">
Questions? <a href="mailto:hello@fourgebranding.com" style="color:#555;">hello@fourgebranding.com</a>
</p>
</div>
</div>
`;
}
// ── 5. Resolve recipients ────────────────────────────────────────────────
const teamTypes = ['new_request', 'revision_submitted', 'client_approved', 'subcontractor_invoice_submitted'];
const teamTypes = ['revision_submitted', 'client_approved'];
let recipients: string[];
let cc: string[] | undefined;
if (teamTypes.includes(type)) {
if (type === 'task_status_update') {
const includeTeam = optBool(data?.includeTeam);
const includeAssigned = optBool(data?.includeAssigned);
const includeClient = optBool(data?.includeClient);
const includeProjectMembers = optBool(data?.includeProjectMembers);
const assignedProfileId = data?.assignedProfileId;
const companyId = data?.companyId;
const projectId = data?.projectId;
const recipientStrategy = optStr(data?.recipientStrategy, 80);
const recipientSet = new Set<string>();
if (recipientStrategy === 'revision_request') {
if (isUuid(assignedProfileId)) {
const { data: assignedProfile } = await adminClient
.from('profiles')
.select('email, role')
.eq('id', assignedProfileId)
.single();
if (assignedProfile?.role === 'external') {
if (isEmail(assignedProfile?.email)) recipientSet.add(assignedProfile.email);
} else if (assignedProfile?.role === 'team') {
TEAM_EMAILS.forEach(email => recipientSet.add(email));
} else {
TEAM_EMAILS.forEach(email => recipientSet.add(email));
if (isUuid(projectId)) {
const { data: memberships } = await adminClient
.from('project_members')
.select('profile:profiles!inner(email, role)')
.eq('project_id', projectId);
for (const membership of (memberships || [])) {
const profile = Array.isArray(membership.profile) ? membership.profile[0] : membership.profile;
if (profile?.role === 'external' && isEmail(profile?.email)) recipientSet.add(profile.email);
}
}
}
} else {
TEAM_EMAILS.forEach(email => recipientSet.add(email));
if (isUuid(projectId)) {
const { data: memberships } = await adminClient
.from('project_members')
.select('profile:profiles!inner(email, role)')
.eq('project_id', projectId);
for (const membership of (memberships || [])) {
const profile = Array.isArray(membership.profile) ? membership.profile[0] : membership.profile;
if (profile?.role === 'external' && isEmail(profile?.email)) recipientSet.add(profile.email);
}
}
}
} else {
if (includeTeam) TEAM_EMAILS.forEach(email => recipientSet.add(email));
if (includeAssigned && isUuid(assignedProfileId)) {
const { data: assignedProfile } = await adminClient
.from('profiles')
.select('email')
.eq('id', assignedProfileId)
.single();
if (isEmail(assignedProfile?.email)) recipientSet.add(assignedProfile.email);
}
if (includeClient && isUuid(companyId)) {
const [{ data: company }, { data: clientProfiles }] = await Promise.all([
adminClient.from('companies').select('contact_email').eq('id', companyId).single(),
adminClient.from('profiles').select('email').eq('company_id', companyId).eq('role', 'client'),
]);
if (isEmail(company?.contact_email)) recipientSet.add(company.contact_email);
for (const profile of (clientProfiles || [])) {
if (isEmail(profile?.email)) recipientSet.add(profile.email);
}
}
if (includeProjectMembers && isUuid(projectId)) {
const { data: memberships } = await adminClient
.from('project_members')
.select('profile:profiles!inner(email)')
.eq('project_id', projectId);
for (const membership of (memberships || [])) {
const profile = Array.isArray(membership.profile) ? membership.profile[0] : membership.profile;
if (isEmail(profile?.email)) recipientSet.add(profile.email);
}
}
}
recipients = [...recipientSet].filter(isEmail);
if (recipients.length === 0) throw new Error('No valid recipient emails');
}
else if (type === 'new_request') {
const recipientSet = new Set<string>();
TEAM_EMAILS.forEach(email => recipientSet.add(email));
recipients = [...recipientSet].filter(isEmail);
if (recipients.length === 0) throw new Error('No valid recipient emails');
}
else if (type === 'subcontractor_invoice_submitted') {
const recipientSet = new Set<string>();
TEAM_EMAILS.forEach(email => recipientSet.add(email));
const subEmail = optStr(data?.subEmail);
if (isEmail(subEmail)) recipientSet.add(subEmail);
recipients = [...recipientSet].filter(isEmail);
if (recipients.length === 0) throw new Error('No valid recipient emails');
}
else if (teamTypes.includes(type)) {
recipients = TEAM_EMAILS;
} else {
// Validate caller-supplied recipient list
@@ -444,6 +681,10 @@ serve(async (req) => {
const senderEmail = optStr(data?.senderEmail);
if (senderEmail && isEmail(senderEmail)) cc = [senderEmail];
if (type === 'invoice_sent' || type === 'receipt_sent') {
TEAM_EMAILS.forEach(email => recipients.push(email));
recipients = [...new Set(recipients.filter(isEmail))];
}
if (type === 'invoice_sent') {
cc = [...new Set([...(cc || []), 'hello@fourgebranding.com'])];
}