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
+6 -7
View File
@@ -1,4 +1,9 @@
import jsPDF from 'jspdf';
import { formatLongDate } from './dates';
function formatDate(dateStr) {
return formatLongDate(dateStr ? `${dateStr}T12:00:00` : '', '-');
}
// Letter landscape: 792 x 612 pt
const W = 792;
@@ -8,12 +13,6 @@ const ACCENT = [245, 165, 35];
const DARK = [18, 18, 18];
const HEADER_H = 64;
function formatDate(dateStr) {
if (!dateStr) return '-';
const d = new Date(dateStr + 'T00:00:00');
return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
}
function formatCoverAddress(address) {
const parts = String(address || '')
.split(',')
@@ -384,7 +383,7 @@ export async function generateBrandBookEditorPDF(data) {
let pageNum = 0;
const rev = String(revision || '01').padStart(2, '0');
const displayDate = bookDate
? new Date(bookDate + 'T12:00:00').toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })
? formatLongDate(`${bookDate}T12:00:00`, '-')
: '';
// ─── COVER PAGE ──────────────────────────────────────────────────────────────
+40
View File
@@ -8,6 +8,46 @@ export function formatDateEST(value) {
});
}
export function formatShortDate(value, fallback = '—') {
if (!value) return fallback;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return fallback;
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
});
}
export function formatLongDate(value, fallback = '—') {
if (!value) return fallback;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return fallback;
return date.toLocaleDateString('en-US', {
month: 'long',
day: 'numeric',
year: 'numeric',
});
}
export function formatShortDateTime(value, fallback = '—') {
if (!value) return fallback;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return fallback;
return `${formatShortDate(date, fallback)} ${date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true,
})}`;
}
export function formatDateAtNoon(value, fallback = '—') {
if (!value) return fallback;
const date = new Date(`${value}T12:00:00`);
if (Number.isNaN(date.getTime())) return fallback;
return formatLongDate(date, fallback);
}
export function parseDateOnly(value) {
if (!value) return null;
const [year, month, day] = String(value).split('-').map(Number);
-114
View File
@@ -1,114 +0,0 @@
import { supabase } from './supabase';
const FBQ_FN = `${import.meta.env.VITE_SUPABASE_URL}/functions/v1/fbq-proxy`;
async function fbq(op, path, extra = {}) {
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) return;
const headers = {
Authorization: `Bearer ${session.access_token}`,
'X-Operation': op,
'X-Path': path,
...extra.headers,
};
await fetch(FBQ_FN, { method: 'POST', headers, body: extra.body ?? undefined }).catch(() => {});
}
export function safeName(v) {
return String(v || '').trim()
.replace(/[\\/:*?"<>|#%{}^~[\]`]+/g, '-')
.replace(/\s+/g, ' ')
.replace(/^-+|-+$/g, '');
}
export async function createProjectFolder(companyName, projectName) {
if (!companyName || !projectName) return;
const co = safeName(companyName);
const proj = safeName(projectName);
await fbq('mkdir', `/Clients/${co}/`);
await fbq('mkdir', `/Clients/${co}/Projects/`);
await fbq('mkdir', `/Clients/${co}/Projects/${proj}/`);
await fbq('mkdir', `/Clients/${co}/Projects/${proj}/00 Project Files/`);
}
export async function createTeamMemberFolder(name) {
if (!name) return;
await fbq('mkdir', '/Team/');
await fbq('mkdir', `/Team/${safeName(name)}/`);
}
export async function createTaskFolder(companyName, projectName, taskTitle) {
if (!companyName || !projectName || !taskTitle) return;
const co = safeName(companyName);
const proj = safeName(projectName);
const task = safeName(taskTitle);
const base = `/Clients/${co}/Projects/${proj}/${task}`;
const mkdirs = [
`/Clients/${co}/`,
`/Clients/${co}/Projects/`,
`/Clients/${co}/Projects/${proj}/`,
`/Clients/${co}/Projects/${proj}/00 Project Files/`,
`${base}/`,
`${base}/Request Info/`,
`${base}/Old Books/`,
];
for (const dir of mkdirs) {
await fbq('mkdir', dir);
}
}
export async function createClientFolder(companyName) {
if (!companyName) return;
await fbq('mkdir', '/Clients/');
await fbq('mkdir', `/Clients/${companyName}/`);
}
export async function backfillClientFolders() {
const { data } = await supabase.from('companies').select('name');
if (!data?.length) return;
await fbq('mkdir', '/Clients/');
for (const company of data) {
if (company.name) await fbq('mkdir', `/Clients/${company.name}/`);
}
}
export async function uploadFilesToRequestInfo(files, companyName, projectName, taskTitle, versionNumber = 0) {
if (!files?.length || !companyName || !projectName || !taskTitle) return;
const co = safeName(companyName);
const proj = safeName(projectName);
const task = safeName(taskTitle);
const rev = `R${String(versionNumber).padStart(2, '0')}`;
const base = `/Clients/${co}/Projects/${proj}/${task}/Request Info/${rev}`;
const mkdirs = [
`/Clients/${co}/`,
`/Clients/${co}/Projects/`,
`/Clients/${co}/Projects/${proj}/`,
`/Clients/${co}/Projects/${proj}/${task}/`,
`/Clients/${co}/Projects/${proj}/${task}/Request Info/`,
`/Clients/${co}/Projects/${proj}/${task}/Working Files/`,
`/Clients/${co}/Projects/${proj}/${task}/Old Books/`,
`${base}/`,
];
for (const dir of mkdirs) {
await fbq('mkdir', dir);
}
const { data: { session } } = await supabase.auth.getSession();
if (!session?.access_token) return;
for (const file of files) {
const filePath = `${base}/${file.name}`;
const fd = new FormData(); fd.append('file', file);
await fetch(FBQ_FN, {
method: 'POST',
headers: {
Authorization: `Bearer ${session.access_token}`,
'X-Operation': 'upload',
'X-Path': filePath,
},
body: fd,
}).catch(() => {});
}
}
+62
View File
@@ -0,0 +1,62 @@
import { supabase } from './supabase';
async function callFolderSync(action, body) {
const { data: { session } } = await supabase.auth.getSession();
const accessToken = session?.access_token;
if (!accessToken) return { ok: false, skipped: true, warning: 'No active session.' };
const response = await fetch(`/api/filebrowser?action=${encodeURIComponent(action)}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload?.error || 'Folder sync failed.');
}
return payload;
}
export async function ensureCompanyFolder({ companyId }) {
if (!companyId) return { ok: false, skipped: true };
return callFolderSync('ensure-company-folder', { companyId });
}
export async function renameCompanyFolder({ companyId, oldName, newName }) {
if (!companyId || !oldName || !newName) return { ok: false, skipped: true };
return callFolderSync('rename-company-folder', { companyId, oldName, newName });
}
export async function ensureProjectFolder({ projectId }) {
if (!projectId) return { ok: false, skipped: true };
return callFolderSync('ensure-project-folder', { projectId });
}
export async function renameProjectFolder({ projectId, oldName, newName }) {
if (!projectId || !oldName || !newName) return { ok: false, skipped: true };
return callFolderSync('rename-project-folder', { projectId, oldName, newName });
}
export async function ensureSubcontractorFolder({ profileId }) {
if (!profileId) return { ok: false, skipped: true };
return callFolderSync('ensure-profile-folder', { profileId });
}
export async function renameSubcontractorFolder({ profileId, oldName, newName }) {
if (!profileId || !oldName || !newName) return { ok: false, skipped: true };
return callFolderSync('rename-profile-folder', { profileId, oldName, newName });
}
export async function ensureRequestFolder({ projectId, taskTitle }) {
if (!projectId || !taskTitle) return { ok: false, skipped: true };
return callFolderSync('ensure-request-folder', { projectId, taskTitle });
}
export async function uploadRequestFileToSurvey({ projectId, taskTitle, storagePath, fileName, bucket = 'submissions' }) {
if (!projectId || !taskTitle || !storagePath || !fileName) return { ok: false, skipped: true };
return callFolderSync('upload-request-file', { projectId, taskTitle, storagePath, fileName, bucket });
}
+18
View File
@@ -0,0 +1,18 @@
export const CLIENT_APPROVED_OR_BETTER = new Set(['client_approved', 'invoiced', 'paid']);
export function isCompletedVersionEligible(task, versionNumber) {
const taskVersion = Number(task?.current_version || 0);
const version = Number(versionNumber || 0);
if (version < taskVersion) return true;
return version === taskVersion && CLIENT_APPROVED_OR_BETTER.has(task?.status);
}
export function isInitialVersionEligible(task) {
if (!task || task.invoiced) return false;
return isCompletedVersionEligible(task, 0);
}
export function getRevisionChargeQuantity(versionNumber) {
const version = Number(versionNumber || 0);
return version >= 2 ? 1 : 0;
}
+45 -16
View File
@@ -1,9 +1,18 @@
import { supabase } from './supabase';
import { ensureProjectFolder } from './folderSync';
function normalizeProjectName(name = '') {
return String(name).trim().toLowerCase();
}
function serializeSubmissionSigns(normalizedSigns = []) {
return normalizedSigns.map((s, i) => ({
sign_number: i + 1,
sign_name: s.signName,
sign_family: s.signFamily || null,
}));
}
export async function findOrCreateProject(companyId, projectName, knownProjects = []) {
const normalized = normalizeProjectName(projectName);
if (!companyId || !normalized) throw new Error('Project company and name are required.');
@@ -31,7 +40,14 @@ export async function findOrCreateProject(companyId, projectName, knownProjects
.select('id, name, company_id, status')
.single();
if (!insertError && newProject) return newProject;
if (!insertError && newProject) {
try {
await ensureProjectFolder({ projectId: newProject.id });
} catch (folderError) {
console.error('Project folder sync failed:', folderError);
}
return newProject;
}
if (insertError?.code === '23505') {
const { data: retryProjects, error: retryError } = await supabase
@@ -88,6 +104,20 @@ export async function createInitialSubmissionForRequest({
submittedBy,
submittedByName,
}) {
const normalizedSignCount = Number.isFinite(Number(signCount)) ? Number(signCount) : null;
const normalizedSigns = Array.isArray(signs)
? signs
.map((sign = {}) => ({
signName: String(sign.signName || '').trim(),
signFamily: String(sign.signFamily || '').trim(),
}))
.filter(sign => sign.signName || sign.signFamily)
: [];
if ((normalizedSignCount || 0) > 0 && normalizedSigns.length !== normalizedSignCount) {
throw new Error('Please fill in the sign information for each sign before submitting.');
}
const { data: submission, error } = await supabase
.from('submissions')
.insert({
@@ -98,7 +128,8 @@ export async function createInitialSubmissionForRequest({
is_hot: isHot,
service_type: serviceType,
sign_family: signFamily || null,
sign_count: signCount || null,
sign_count: normalizedSignCount || null,
signs: serializeSubmissionSigns(normalizedSigns),
deadline: deadline || null,
description,
submitted_by: submittedBy,
@@ -107,19 +138,7 @@ export async function createInitialSubmissionForRequest({
.select()
.single();
if (!error && submission) {
if (signs?.length > 0) {
await supabase.from('submission_signs').insert(
signs.map((s, i) => ({
submission_id: submission.id,
sign_number: i + 1,
sign_name: s.signName,
sign_family: s.signFamily || null,
}))
);
}
return { submission, duplicate: false };
}
if (!error && submission) return { submission, duplicate: false };
if (error?.code === '23505' && requestKey) {
const { data: existingSubmission, error: existingError } = await supabase
@@ -128,7 +147,17 @@ export async function createInitialSubmissionForRequest({
.eq('request_key', requestKey)
.single();
if (existingError) throw existingError;
if (existingSubmission) return { submission: existingSubmission, duplicate: true };
if (existingSubmission) {
const existingSigns = Array.isArray(existingSubmission.signs) ? existingSubmission.signs : [];
if (normalizedSigns.length > 0 && existingSigns.length === 0) {
const { error: repairError } = await supabase
.from('submissions')
.update({ signs: serializeSubmissionSigns(normalizedSigns) })
.eq('id', existingSubmission.id);
if (repairError) throw repairError;
}
return { submission: existingSubmission, duplicate: true };
}
}
throw error || new Error('Failed to create submission.');
+16
View File
@@ -0,0 +1,16 @@
export function mergeSubmissionDisplayNames(submissions = [], profiles = []) {
const nameById = new Map(
(profiles || [])
.filter((profile) => profile?.id)
.map((profile) => [profile.id, profile.name || ''])
);
return (submissions || []).map((submission) => ({
...submission,
display_submitted_by_name: nameById.get(submission?.submitted_by) || submission?.submitted_by_name || '',
}));
}
export function getSubmissionDisplayName(submission) {
return submission?.display_submitted_by_name || submission?.submitted_by_name || '';
}
+2 -3
View File
@@ -1,4 +1,5 @@
import jsPDF from 'jspdf';
import { formatDateAtNoon } from './dates';
const W = 792;
const H = 612;
@@ -8,9 +9,7 @@ const DARK = [18, 18, 18];
const HEADER_H = 64;
function formatDate(dateStr) {
if (!dateStr) return '-';
const d = new Date(`${dateStr}T12:00:00`);
return d.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
return formatDateAtNoon(dateStr, '-');
}
async function blobToDataUrl(blob) {
+42
View File
@@ -0,0 +1,42 @@
import { sendEmail } from './email';
export async function sendTaskStatusUpdate({
taskId,
taskTitle,
projectId,
projectName = '',
companyId,
companyName = '',
assignedProfileId,
subject,
headline,
statusLabel,
message,
buttonLabel = 'View Task',
includeTeam = false,
includeAssigned = false,
includeClient = false,
includeProjectMembers = false,
recipientStrategy = '',
}) {
if (!taskId || !taskTitle || !statusLabel || !message) return;
return sendEmail('task_status_update', [], {
taskId,
taskTitle,
projectId,
projectName,
companyId,
companyName,
assignedProfileId,
subject,
headline,
statusLabel,
message,
buttonLabel,
includeTeam,
includeAssigned,
includeClient,
includeProjectMembers,
recipientStrategy,
});
}
+70
View File
@@ -0,0 +1,70 @@
import { popupSurfaceStyle } from './popupStyles';
export const TASK_TABLE_TH_STYLE = {
fontSize: 10,
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: 0.6,
color: 'var(--text-muted)',
textAlign: 'left',
padding: '0 0 12px 5px',
border: 'none',
background: 'transparent',
verticalAlign: 'top',
};
export const TASK_TABLE_TD_BASE = {
padding: '5px',
border: 'none',
background: 'transparent',
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
};
export const TASK_MODAL_TITLE_STYLE = {
fontSize: 11,
fontWeight: 500,
textTransform: 'uppercase',
letterSpacing: 0.8,
color: 'var(--text-secondary)',
};
export const TASK_MODAL_LABEL_STYLE = {
fontSize: 11,
fontWeight: 500,
color: 'var(--text-secondary)',
textTransform: 'uppercase',
letterSpacing: 0.8,
marginBottom: 6,
};
export const TASK_MODAL_INPUT_STYLE = {
fontSize: 13,
padding: '0 5px',
textAlign: 'left',
lineHeight: 1,
};
export const TASK_MODAL_BUTTON_STYLE = {
lineHeight: 1,
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
};
export const TASK_MODAL_SHELL_STYLE = {
...popupSurfaceStyle,
background: 'var(--card-bg)',
width: 'min(720px, 96vw)',
maxHeight: 'calc(100vh - 48px)',
overflowY: 'auto',
};
export const PROJECT_MODAL_SHELL_STYLE = {
...popupSurfaceStyle,
background: 'var(--card-bg)',
width: 'min(434px, 100%)',
maxHeight: 'calc(100vh - 48px)',
overflowY: 'auto',
};
+90
View File
@@ -0,0 +1,90 @@
import { getCurrentVersionForTask, getDeadlineSourceSubmission } from './taskDeadlines';
import { serviceTypes } from '../data/mockData';
export const REJECT_NOTE_PREFIX = '[rejected-note]';
export const REVIEW_SHADOW_PREFIX = '[review-shadow]';
const SERVICE_TYPE_SET = new Set(serviceTypes);
function isRecognizedServiceType(value) {
const normalized = String(value || '').trim();
return normalized ? SERVICE_TYPE_SET.has(normalized) : false;
}
export function parseRejectedNote(body = '') {
if (!String(body).startsWith(REJECT_NOTE_PREFIX)) return null;
const rest = String(body).slice(REJECT_NOTE_PREFIX.length);
const [versionRaw, ...noteParts] = rest.split('::');
const versionNumber = Number(versionRaw);
if (!Number.isFinite(versionNumber)) return null;
return { versionNumber, body: noteParts.join('::').trim() };
}
export function encodeRejectedNote(versionNumber, body) {
return `${REJECT_NOTE_PREFIX}${versionNumber}::${body}`;
}
export function isReviewShadowSubmission(submission) {
return String(submission?.description || '').startsWith(REVIEW_SHADOW_PREFIX);
}
export function isReviewShadowDescription(description = '') {
return String(description).startsWith(REVIEW_SHADOW_PREFIX);
}
export function getVisibleTaskSubmissions(submissions = []) {
return (submissions || []).filter(submission => !isReviewShadowSubmission(submission));
}
export function getLatestVisibleSubmissionForVersion(taskId, submissions = [], versionNumber = 0) {
return getVisibleTaskSubmissions(submissions)
.filter(submission => submission?.task_id === taskId && (submission?.version_number || 0) === versionNumber)
.sort((a, b) => new Date(b.submitted_at || 0).getTime() - new Date(a.submitted_at || 0).getTime())[0] || null;
}
export function getLatestDeliveryForVersion(taskId, submissions = [], deliveries = [], versionNumber = 0) {
const versionSubmissionIds = new Set(
(submissions || [])
.filter(submission => submission?.task_id === taskId && (submission?.version_number || 0) === versionNumber)
.map(submission => submission.id)
);
if (versionSubmissionIds.size === 0) return null;
return (deliveries || [])
.filter(delivery => versionSubmissionIds.has(delivery?.submission_id))
.sort((a, b) => new Date(b.sent_at || 0).getTime() - new Date(a.sent_at || 0).getTime())[0] || null;
}
export function getTaskDerivedState(task, submissions = [], deliveries = []) {
const taskSubs = (submissions || []).filter(submission => submission?.task_id === task?.id);
const visibleTaskSubs = getVisibleTaskSubmissions(taskSubs);
const currentVersion = getCurrentVersionForTask(task, taskSubs);
const deadlineSource = getDeadlineSourceSubmission(task, taskSubs);
const latestVisibleSubmission = getLatestVisibleSubmissionForVersion(task?.id, taskSubs, currentVersion);
const latestDelivery = getLatestDeliveryForVersion(task?.id, taskSubs, deliveries, currentVersion);
const initialSubmission = visibleTaskSubs.find(submission => submission?.type === 'initial') || null;
const canonicalServiceType =
(isRecognizedServiceType(initialSubmission?.service_type) && initialSubmission?.service_type) ||
visibleTaskSubs.find(submission => isRecognizedServiceType(submission?.service_type))?.service_type ||
(isRecognizedServiceType(deadlineSource?.service_type) && deadlineSource?.service_type) ||
'—';
const deadline = deadlineSource?.deadline || latestVisibleSubmission?.deadline || null;
const isHot = Boolean(deadlineSource?.is_hot || latestVisibleSubmission?.is_hot);
const latestActivityAt = Math.max(
latestVisibleSubmission?.submitted_at ? new Date(latestVisibleSubmission.submitted_at).getTime() : 0,
latestDelivery?.sent_at ? new Date(latestDelivery.sent_at).getTime() : 0,
task?.submitted_at ? new Date(task.submitted_at).getTime() : 0
);
return {
taskSubs,
visibleTaskSubs,
currentVersion,
deadlineSource,
latestVisibleSubmission,
latestDelivery,
initialSubmission,
serviceType: canonicalServiceType,
deadline,
isHot,
latestActivityAt,
};
}
+51
View File
@@ -0,0 +1,51 @@
import { supabase } from './supabase';
export function getCurrentUserCompanyIds(currentUser) {
return [
...(currentUser?.companies?.map((company) => company?.company?.id || company?.id) || []),
...(currentUser?.company_id ? [currentUser.company_id] : []),
...(currentUser?.company?.id ? [currentUser.company.id] : []),
].filter(Boolean).filter((value, index, list) => list.indexOf(value) === index);
}
export async function resolveScopedWorkIds(currentUser, { isClient = false, isExternal = false } = {}) {
let scopedCompanyIds = null;
let scopedProjectIds = null;
let scopedTaskIds = null;
if (isClient) {
scopedCompanyIds = getCurrentUserCompanyIds(currentUser);
if (scopedCompanyIds.length > 0) {
const { data: projectRows } = await supabase
.from('projects')
.select('id, tasks(id)')
.in('company_id', scopedCompanyIds);
scopedProjectIds = (projectRows || []).map((project) => project.id).filter(Boolean);
scopedTaskIds = (projectRows || [])
.flatMap((project) => (project.tasks || []).map((task) => task.id))
.filter(Boolean);
} else {
scopedProjectIds = [];
scopedTaskIds = [];
}
}
if (isExternal) {
const { data: memberRows } = await supabase
.from('project_members')
.select('project_id')
.eq('profile_id', currentUser?.id);
scopedProjectIds = (memberRows || []).map((row) => row.project_id).filter(Boolean);
if (scopedProjectIds.length > 0) {
const { data: taskRows } = await supabase
.from('tasks')
.select('id')
.in('project_id', scopedProjectIds);
scopedTaskIds = (taskRows || []).map((row) => row.id).filter(Boolean);
} else {
scopedTaskIds = [];
}
}
return { scopedCompanyIds, scopedProjectIds, scopedTaskIds };
}