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:
@@ -8,6 +8,7 @@ const defaultDeadline = () => addDaysToDateOnly(getTodayDateOnlyEST(), 3);
|
||||
const modalLabelStyle = { fontSize: 11, fontWeight: 500, color: 'var(--text-secondary)', textTransform: 'uppercase', letterSpacing: 0.8, marginBottom: 6 };
|
||||
const modalInputStyle = { fontSize: 13, padding: '0 5px', textAlign: 'left', lineHeight: 1 };
|
||||
const modalTextAreaStyle = { ...modalInputStyle, minHeight: 100, padding: '8px 5px', lineHeight: 1.4 };
|
||||
const modalActionButtonStyle = { width: 132, justifyContent: 'center', flex: '0 0 132px' };
|
||||
const emptyForm = (companyId = '') => ({
|
||||
companyId,
|
||||
project: '',
|
||||
@@ -43,7 +44,11 @@ export default function RequestForm({
|
||||
initialValues = null,
|
||||
lockedFields = [],
|
||||
}) {
|
||||
const [form, setForm] = useState(() => ({ ...emptyForm(initialCompanyId), ...(initialValues || {}) }));
|
||||
const [form, setForm] = useState(() => ({
|
||||
...emptyForm(initialCompanyId),
|
||||
requestedBy: showRequester ? (currentUser?.id || '') : '',
|
||||
...(initialValues || {}),
|
||||
}));
|
||||
const prevCompanyIdRef = useRef(initialValues?.companyId || initialCompanyId || null);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [existingProjects, setExistingProjects] = useState([]);
|
||||
@@ -54,16 +59,17 @@ export default function RequestForm({
|
||||
const [signFamilies, setSignFamilies] = useState([]);
|
||||
const [signCount, setSignCount] = useState('');
|
||||
const [signs, setSigns] = useState([]);
|
||||
const [localError, setLocalError] = useState('');
|
||||
|
||||
const isBrandBook = form.serviceType === 'Brand Book';
|
||||
const usesSignFields = form.serviceType === 'Brand Book';
|
||||
|
||||
useEffect(() => {
|
||||
supabase.from('sign_families').select('name').order('sort_order').then(({ data }) => setSignFamilies((data || []).map(r => r.name)));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrandBook) { setSignCount(''); setSigns([]); }
|
||||
}, [isBrandBook]);
|
||||
if (!usesSignFields) { setSignCount(''); setSigns([]); }
|
||||
}, [usesSignFields]);
|
||||
|
||||
useEffect(() => {
|
||||
const n = Math.max(0, parseInt(signCount) || 0);
|
||||
@@ -113,9 +119,15 @@ export default function RequestForm({
|
||||
prevCompanyIdRef.current = companyId;
|
||||
}, [companyId]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const set = (field) => (e) => setForm(f => ({ ...f, [field]: e.target.value }));
|
||||
const set = (field) => (e) => {
|
||||
if (localError) setLocalError('');
|
||||
setForm(f => ({ ...f, [field]: e.target.value }));
|
||||
};
|
||||
|
||||
const setSign = (id, field, value) => setSigns(prev => prev.map(s => s.id === id ? { ...s, [field]: value } : s));
|
||||
const setSign = (id, field, value) => {
|
||||
if (localError) setLocalError('');
|
||||
setSigns(prev => prev.map(s => s.id === id ? { ...s, [field]: value } : s));
|
||||
};
|
||||
|
||||
const allProjectNames = [
|
||||
...existingProjects.map(p => p.name),
|
||||
@@ -143,18 +155,56 @@ export default function RequestForm({
|
||||
};
|
||||
|
||||
const showCompanySelect = companies.length > 1 || showRequester;
|
||||
const requesterOptions = [
|
||||
...(currentUser ? [{ id: currentUser.id, name: `${currentUser.name} (You)` }] : []),
|
||||
...companyUsers.filter(u => u.id !== currentUser?.id),
|
||||
];
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
const requestedByName = currentUser?.name || '';
|
||||
setLocalError('');
|
||||
const selectedRequester = showRequester
|
||||
? requesterOptions.find(option => option.id === form.requestedBy) || null
|
||||
: null;
|
||||
const requestedBy = showRequester
|
||||
? (selectedRequester?.id || '')
|
||||
: (currentUser?.id || '');
|
||||
const requestedByName = showRequester
|
||||
? ((selectedRequester?.name || '').replace(/\s+\(You\)$/, '') || '')
|
||||
: (currentUser?.name || '');
|
||||
let normalizedSignCount = null;
|
||||
let normalizedSigns = [];
|
||||
|
||||
if (usesSignFields) {
|
||||
normalizedSignCount = parseInt(signCount, 10) || null;
|
||||
const formData = new FormData(e.currentTarget);
|
||||
normalizedSigns = Array.from({ length: normalizedSignCount || 0 }, (_, index) => {
|
||||
const stateSign = signs[index] || {};
|
||||
const domValue = formData.get(`sign_info_${index + 1}`);
|
||||
const signName = typeof domValue === 'string'
|
||||
? domValue.trim()
|
||||
: String(stateSign.signName || '').trim();
|
||||
return {
|
||||
id: stateSign.id || `sign-${index + 1}`,
|
||||
signName,
|
||||
signFamily: stateSign.signFamily || '',
|
||||
};
|
||||
}).filter(sign => sign.signName || sign.signFamily);
|
||||
|
||||
if ((normalizedSignCount || 0) > 0 && normalizedSigns.length !== normalizedSignCount) {
|
||||
setLocalError('Please fill in the sign information for each sign before submitting.');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit(
|
||||
{
|
||||
...form,
|
||||
companyId,
|
||||
requestedBy: currentUser?.id || '',
|
||||
requestedBy,
|
||||
requestedByName,
|
||||
signCount: isBrandBook ? (parseInt(signCount) || null) : null,
|
||||
signs: isBrandBook ? signs : [],
|
||||
signCount: usesSignFields ? normalizedSignCount : null,
|
||||
signs: usesSignFields ? normalizedSigns : [],
|
||||
},
|
||||
files,
|
||||
existingProjects
|
||||
@@ -214,6 +264,24 @@ export default function RequestForm({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{showRequester && (
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Requested By *</label>
|
||||
<select
|
||||
value={form.requestedBy}
|
||||
onChange={set('requestedBy')}
|
||||
required
|
||||
disabled={!companyId}
|
||||
style={modalInputStyle}
|
||||
>
|
||||
<option value="">{companyId ? 'Select requester...' : 'Select company first'}</option>
|
||||
{requesterOptions.map(user => (
|
||||
<option key={user.id} value={user.id}>{user.name}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 3: Title/Location + Mark as Hot inline */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12, alignItems: 'end', marginBottom: 16 }}>
|
||||
<div className="form-group" style={{ marginBottom: 0 }}>
|
||||
@@ -226,7 +294,7 @@ export default function RequestForm({
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{isBrandBook && (
|
||||
{usesSignFields && (
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>Sign Count *</label>
|
||||
<input
|
||||
@@ -242,7 +310,7 @@ export default function RequestForm({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isBrandBook && signs.length > 0 && (
|
||||
{usesSignFields && signs.length > 0 && (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 8, marginBottom: 16 }}>
|
||||
{signs.map((sign, i) => (
|
||||
<div key={sign.id} style={{ border: '1px solid var(--border)', borderRadius: 6, padding: '10px 12px' }}>
|
||||
@@ -251,6 +319,7 @@ export default function RequestForm({
|
||||
<label style={modalLabelStyle}>Sign Information *</label>
|
||||
<input
|
||||
type="text"
|
||||
name={`sign_info_${i + 1}`}
|
||||
placeholder="e.g. Main Entry, Drive Thru"
|
||||
value={sign.signName}
|
||||
onChange={e => setSign(sign.id, 'signName', e.target.value)}
|
||||
@@ -264,19 +333,19 @@ export default function RequestForm({
|
||||
)}
|
||||
|
||||
<div className="form-group">
|
||||
<label style={modalLabelStyle}>{isBrandBook ? 'Notes' : 'Description'} *</label>
|
||||
<label style={modalLabelStyle}>{usesSignFields ? 'Notes' : 'Description'} *</label>
|
||||
<textarea placeholder="Notes on the request..." value={form.description} onChange={set('description')} style={modalTextAreaStyle} required />
|
||||
</div>
|
||||
|
||||
<FileAttachment files={files} onChange={setFiles} />
|
||||
|
||||
{error && <div style={{ fontSize: 13, color: 'var(--danger)', marginBottom: 12 }}>⚠ {error}</div>}
|
||||
{(localError || error) && <div style={{ fontSize: 13, color: 'var(--danger)', marginBottom: 12 }}>⚠ {localError || error}</div>}
|
||||
|
||||
<div className="action-buttons" style={{ justifyContent: 'flex-end' }}>
|
||||
<button type="submit" className="btn btn-outline" disabled={saving}>
|
||||
<div className="action-buttons modal-action-row" style={{ justifyContent: 'flex-end' }}>
|
||||
<button type="submit" className="btn btn-outline" disabled={saving} style={modalActionButtonStyle}>
|
||||
{saving ? 'Submitting...' : submitLabel}
|
||||
</button>
|
||||
{onCancel && <button type="button" className="btn btn-outline" onClick={onCancel}>Cancel</button>}
|
||||
{onCancel && <button type="button" className="btn btn-outline" onClick={onCancel} style={modalActionButtonStyle}>Cancel</button>}
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user