Full codebase cleanup and optimization pass
- Fix all hardcoded light colors breaking dark mode (FileAttachment, TaskDetail, RequestDetail) - Parallelize sequential DB fetches in TaskDetail, CompanyDetail, MyProjects - Add error handling: NewRequest project/file upload, MyCompany update, CompanyDetail prices, AuthContext profile fetch - Fix currentUser.company_id → currentUser.company?.id in NewRequest - Remove stale company.email references from InvoiceDetail, ProjectDetail, TaskDetail - Clean up dead email field from Companies form reset Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -46,7 +46,7 @@ export default function FileAttachment({ files, onChange }) {
|
||||
<div style={{
|
||||
border: `2px dashed ${files.length > 0 ? 'var(--accent)' : 'var(--border)'}`,
|
||||
borderRadius: 8, padding: '18px 16px', textAlign: 'center',
|
||||
background: files.length > 0 ? '#fffbeb' : '#fafafa', transition: 'all 0.15s',
|
||||
background: 'var(--bg)', transition: 'all 0.15s',
|
||||
}}>
|
||||
<input type="file" multiple onChange={handleChange} style={{ display: 'none' }} id="req-file-upload" />
|
||||
<label htmlFor="req-file-upload" style={{ cursor: 'pointer' }}>
|
||||
@@ -67,7 +67,7 @@ export default function FileAttachment({ files, onChange }) {
|
||||
{files.map((file, i) => (
|
||||
<div key={i} style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '7px 12px', background: 'white', borderRadius: 8, border: '1px solid var(--border)',
|
||||
padding: '7px 12px', background: 'var(--card-bg)', borderRadius: 8, border: '1px solid var(--border)',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span>📄</span>
|
||||
|
||||
@@ -16,6 +16,7 @@ export function AuthProvider({ children }) {
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
const fetchAndCacheProfile = async (authUser) => {
|
||||
try {
|
||||
const { data } = await supabase
|
||||
.from('profiles')
|
||||
.select('*, company:companies(id, name, phone, address)')
|
||||
@@ -26,6 +27,9 @@ export function AuthProvider({ children }) {
|
||||
setCurrentUser(profile);
|
||||
localStorage.setItem(PROFILE_CACHE_KEY, JSON.stringify(profile));
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Profile fetch failed:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -27,12 +27,13 @@ export default function MyCompany() {
|
||||
const handleSave = async (e) => {
|
||||
e.preventDefault();
|
||||
setSaving(true);
|
||||
await supabase.from('companies').update({
|
||||
const { error } = await supabase.from('companies').update({
|
||||
name: form.name.trim(),
|
||||
phone: form.phone.trim(),
|
||||
address: form.address.trim(),
|
||||
}).eq('id', company.id);
|
||||
setSaving(false);
|
||||
if (error) { alert('Failed to save. Please try again.'); return; }
|
||||
setEditing(false);
|
||||
};
|
||||
|
||||
|
||||
@@ -125,15 +125,11 @@ export default function MyProjects() {
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const { data: p } = await supabase
|
||||
.from('projects').select('*').order('created_at', { ascending: false });
|
||||
const [{ data: p }, { data: t }] = await Promise.all([
|
||||
supabase.from('projects').select('*').order('created_at', { ascending: false }),
|
||||
supabase.from('tasks').select('*').order('submitted_at', { ascending: false }),
|
||||
]);
|
||||
setProjects(p || []);
|
||||
|
||||
if (!p || p.length === 0) { setLoading(false); return; }
|
||||
|
||||
const { data: t } = await supabase
|
||||
.from('tasks').select('*').in('project_id', p.map(pr => pr.id))
|
||||
.order('submitted_at', { ascending: false });
|
||||
setTasks(t || []);
|
||||
|
||||
if (t && t.length > 0) {
|
||||
|
||||
@@ -16,6 +16,7 @@ export default function NewRequest() {
|
||||
const [existingProjects, setExistingProjects] = useState([]);
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState(null);
|
||||
const [form, setForm] = useState({ project: preselectedProject, serviceType: '', title: '', deadline: '', description: '' });
|
||||
const [files, setFiles] = useState([]);
|
||||
const [customProjects, setCustomProjects] = useState([]);
|
||||
@@ -24,16 +25,16 @@ export default function NewRequest() {
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
if (!currentUser.company_id) return;
|
||||
if (!currentUser.company?.id) return;
|
||||
const { data: p } = await supabase
|
||||
.from('projects')
|
||||
.select('id, name')
|
||||
.eq('company_id', currentUser.company_id)
|
||||
.eq('company_id', currentUser.company.id)
|
||||
.order('created_at', { ascending: false });
|
||||
setExistingProjects((p || []).map(pr => ({ id: pr.id, name: pr.name })));
|
||||
}
|
||||
load();
|
||||
}, [currentUser.company_id]);
|
||||
}, [currentUser.company?.id]);
|
||||
|
||||
const allProjectNames = [
|
||||
...existingProjects.map(p => p.name),
|
||||
@@ -64,11 +65,12 @@ export default function NewRequest() {
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
if (!currentUser.company_id) {
|
||||
if (!currentUser.company?.id) {
|
||||
alert('Your account is not yet assigned to a company. Please contact support.');
|
||||
return;
|
||||
}
|
||||
setSaving(true);
|
||||
setError(null);
|
||||
|
||||
// Find existing project by name within this company, or create new one
|
||||
let projectId;
|
||||
@@ -76,12 +78,13 @@ export default function NewRequest() {
|
||||
if (existing) {
|
||||
projectId = existing.id;
|
||||
} else {
|
||||
const { data: newProject } = await supabase.from('projects').insert({
|
||||
company_id: currentUser.company_id,
|
||||
const { data: newProject, error: projectError } = await supabase.from('projects').insert({
|
||||
company_id: currentUser.company.id,
|
||||
name: form.project,
|
||||
status: 'active',
|
||||
}).select().single();
|
||||
projectId = newProject?.id;
|
||||
if (projectError || !newProject) { setError('Failed to create project.'); setSaving(false); return; }
|
||||
projectId = newProject.id;
|
||||
}
|
||||
|
||||
if (!projectId) { setSaving(false); return; }
|
||||
@@ -112,7 +115,12 @@ export default function NewRequest() {
|
||||
if (submission && files.length > 0) {
|
||||
for (const file of files) {
|
||||
const path = `${task.id}/${Date.now()}_${file.name}`;
|
||||
const { data: uploaded } = await supabase.storage.from('submissions').upload(path, file);
|
||||
const { data: uploaded, error: uploadError } = await supabase.storage.from('submissions').upload(path, file);
|
||||
if (uploadError) {
|
||||
setError(`Failed to upload "${file.name}": ${uploadError.message}`);
|
||||
setSaving(false);
|
||||
return;
|
||||
}
|
||||
if (uploaded) {
|
||||
await supabase.from('submission_files').insert({
|
||||
submission_id: submission.id,
|
||||
@@ -139,7 +147,7 @@ export default function NewRequest() {
|
||||
setSubmitted(true);
|
||||
};
|
||||
|
||||
if (!currentUser.company_id) {
|
||||
if (!currentUser.company?.id) {
|
||||
return (
|
||||
<Layout>
|
||||
<div style={{ maxWidth: 480, margin: '0 auto', textAlign: 'center', paddingTop: 48 }}>
|
||||
@@ -251,6 +259,12 @@ export default function NewRequest() {
|
||||
|
||||
<FileAttachment files={files} onChange={setFiles} />
|
||||
|
||||
{error && (
|
||||
<div className="notification notification-error" style={{ marginBottom: 16 }}>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="notification notification-info" style={{ marginBottom: 16 }}>
|
||||
Submitting as <strong>{currentUser?.name}</strong> · {currentUser?.company?.name}
|
||||
</div>
|
||||
|
||||
@@ -217,7 +217,7 @@ export default function RequestDetail() {
|
||||
</div>
|
||||
|
||||
{action === 'confirm-delete' && (
|
||||
<div className="card" style={{ background: '#fef2f2', borderColor: '#fecaca', marginBottom: 24 }}>
|
||||
<div className="card" style={{ background: 'var(--bg)', borderColor: 'var(--danger)', marginBottom: 24 }}>
|
||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>⚠ Delete this request?</div>
|
||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16 }}>
|
||||
This will permanently delete <strong>{titleWithVersion}</strong> and all its history. This cannot be undone.
|
||||
|
||||
@@ -44,7 +44,7 @@ export default function Companies() {
|
||||
setSaving(false);
|
||||
if (data) {
|
||||
setShowNew(false);
|
||||
setNewForm({ name: '', email: '', phone: '' });
|
||||
setNewForm({ name: '', phone: '', address: '' });
|
||||
navigate(`/companies/${data.id}`);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -25,27 +25,20 @@ export default function CompanyDetail() {
|
||||
}, [id]);
|
||||
|
||||
async function load() {
|
||||
const [{ data: co }, { data: p }, { data: pr }, { data: u }, { data: unassignedUsers }] = await Promise.all([
|
||||
const [{ data: co }, { data: p }, { data: pr }, { data: u }, { data: unassignedUsers }, { data: t }] = await Promise.all([
|
||||
supabase.from('companies').select('*').eq('id', id).single(),
|
||||
supabase.from('projects').select('*').eq('company_id', id).order('created_at', { ascending: false }),
|
||||
supabase.from('company_prices').select('*').eq('company_id', id),
|
||||
supabase.from('profiles').select('id, name, email, created_at').eq('company_id', id).eq('role', 'client'),
|
||||
supabase.from('profiles').select('id, name, email').eq('role', 'client').is('company_id', null),
|
||||
supabase.from('tasks').select('*, project:projects!inner(company_id)').eq('project.company_id', id),
|
||||
]);
|
||||
setCompany(co);
|
||||
const projectList = p || [];
|
||||
setProjects(projectList);
|
||||
setProjects(p || []);
|
||||
setPrices(pr || []);
|
||||
setUsers(u || []);
|
||||
setUnassigned(unassignedUsers || []);
|
||||
|
||||
if (projectList.length > 0) {
|
||||
const { data: t } = await supabase
|
||||
.from('tasks')
|
||||
.select('*')
|
||||
.in('project_id', projectList.map(pr => pr.id));
|
||||
setTasks(t || []);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
|
||||
@@ -86,11 +79,13 @@ export default function CompanyDetail() {
|
||||
const priceVal = getPrice(serviceType);
|
||||
const existing = prices.find(p => p.service_type === serviceType && p.id);
|
||||
if (existing) {
|
||||
await supabase.from('company_prices').update({ price: Number(priceVal) }).eq('id', existing.id);
|
||||
const { error: updateError } = await supabase.from('company_prices').update({ price: Number(priceVal) }).eq('id', existing.id);
|
||||
if (updateError) { setSavingPrice(null); alert('Failed to save price. Please try again.'); return; }
|
||||
} else {
|
||||
const { data } = await supabase.from('company_prices').insert({
|
||||
const { data, error: insertError } = await supabase.from('company_prices').insert({
|
||||
company_id: id, service_type: serviceType, price: Number(priceVal),
|
||||
}).select().single();
|
||||
if (insertError) { setSavingPrice(null); alert('Failed to save price. Please try again.'); return; }
|
||||
if (data) setPrices(prev => prev.map(p => p.service_type === serviceType ? data : p));
|
||||
}
|
||||
setSavingPrice(null);
|
||||
|
||||
@@ -89,7 +89,6 @@ export default function InvoiceDetail() {
|
||||
<div className="card">
|
||||
<div className="card-title">Bill To</div>
|
||||
<div style={{ fontSize: 15, fontWeight: 700 }}>{company?.name}</div>
|
||||
{company?.email && <div style={{ fontSize: 13, color: 'var(--text-muted)', marginTop: 2 }}>{company.email}</div>}
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Link to={`/companies/${company?.id}`} className="btn btn-outline btn-sm">View Company</Link>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,6 @@ export default function ProjectDetail() {
|
||||
<div className="card-title">Project Info</div>
|
||||
<div className="detail-grid" style={{ marginBottom: 0 }}>
|
||||
<div className="detail-item"><label>Company</label><p>{company?.name || '—'}</p></div>
|
||||
<div className="detail-item"><label>Contact</label><p>{company?.email || '—'}</p></div>
|
||||
<div className="detail-item"><label>Status</label><p><StatusBadge status={project.status} /></p></div>
|
||||
<div className="detail-item"><label>Started</label><p>{new Date(project.created_at).toLocaleDateString()}</p></div>
|
||||
</div>
|
||||
|
||||
@@ -37,18 +37,14 @@ export default function TaskDetail() {
|
||||
setTask(t);
|
||||
|
||||
const [{ data: p }, { data: subs }, { data: team }] = await Promise.all([
|
||||
supabase.from('projects').select('*').eq('id', t.project_id).single(),
|
||||
supabase.from('projects').select('*, company:companies(*)').eq('id', t.project_id).single(),
|
||||
supabase.from('submissions').select('*, delivery:deliveries(*, files:delivery_files(*))').eq('task_id', id).order('version_number'),
|
||||
supabase.from('profiles').select('*').eq('role', 'team'),
|
||||
]);
|
||||
setProject(p);
|
||||
setCompany(p?.company || null);
|
||||
setSubmissions(subs || []);
|
||||
setTeamMembers(team || []);
|
||||
|
||||
if (p) {
|
||||
const { data: co } = await supabase.from('companies').select('*').eq('id', p.company_id).single();
|
||||
setCompany(co);
|
||||
}
|
||||
setLoading(false);
|
||||
}
|
||||
load();
|
||||
@@ -187,7 +183,7 @@ export default function TaskDetail() {
|
||||
<div className="card" style={{ marginBottom: 24, border: '1px solid var(--accent)', borderRadius: 12 }}>
|
||||
<div className="card-title">Send to Client — {company?.name}</div>
|
||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16 }}>
|
||||
Upload the completed file and add an optional message. An email will be sent to <strong>{company?.email}</strong>.
|
||||
Upload the completed file and add an optional message for the client.
|
||||
</p>
|
||||
<form onSubmit={handleSendToClient}>
|
||||
<div className="form-group">
|
||||
@@ -200,7 +196,7 @@ export default function TaskDetail() {
|
||||
<div style={{
|
||||
border: `2px dashed ${sendForm.files.length > 0 ? 'var(--accent)' : 'var(--border)'}`,
|
||||
borderRadius: 8, padding: '20px 16px', textAlign: 'center',
|
||||
background: sendForm.files.length > 0 ? '#fffbeb' : '#fafafa',
|
||||
background: 'var(--bg)',
|
||||
}}>
|
||||
<input type="file" multiple onChange={handleFileChange} style={{ display: 'none' }} id="file-upload" />
|
||||
<label htmlFor="file-upload" style={{ cursor: 'pointer' }}>
|
||||
@@ -215,7 +211,7 @@ export default function TaskDetail() {
|
||||
{sendForm.files.length > 0 && (
|
||||
<div style={{ marginTop: 10, display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{sendForm.files.map((file, i) => (
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', background: 'white', borderRadius: 8, border: '1px solid var(--border)' }}>
|
||||
<div key={i} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '8px 12px', background: 'var(--card-bg)', borderRadius: 8, border: '1px solid var(--border)' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span>📄</span>
|
||||
<div>
|
||||
@@ -278,12 +274,12 @@ export default function TaskDetail() {
|
||||
<button className="btn btn-primary" onClick={handleResume} disabled={saving}>▶ Resume</button>
|
||||
)}
|
||||
{task.status === 'client_review' && (
|
||||
<div style={{ padding: '10px 14px', background: '#f5f3ff', borderRadius: 8, fontSize: 13, color: '#7c3aed', fontWeight: 500 }}>
|
||||
<div style={{ padding: '10px 14px', background: 'var(--bg)', borderRadius: 8, fontSize: 13, color: '#7c3aed', fontWeight: 500 }}>
|
||||
⏳ Awaiting client review — no action needed.
|
||||
</div>
|
||||
)}
|
||||
{task.status === 'client_approved' && (
|
||||
<div style={{ padding: '10px 14px', background: '#f0fdf4', borderRadius: 8, fontSize: 13, color: '#16a34a', fontWeight: 500 }}>
|
||||
<div style={{ padding: '10px 14px', background: 'var(--bg)', borderRadius: 8, fontSize: 13, color: '#16a34a', fontWeight: 500 }}>
|
||||
✓ Client approved this job.
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user