Session 2026-05-30: tasks/projects unification, code consolidation, file renames
- Unified Tasks page: 3 role render blocks → 1, normalized row shape, single renderRow/sort/tabs - Fixed role scoping: external = all tasks in member projects, client = all tasks in company projects - Fixed client bug: was scoped by submitted_by, now company→project→tasks - Aligned dashboard client scope to match tasks page - Hot tasks dashboard: now filters to active statuses only (not completed/invoiced/paid) - Removed Projects.jsx (dead), fixed /project/ → /projects/ links - Renamed all page files to match folder path (Team*, External*, Client* prefixes) - Renamed: RequestsPage→Tasks, Settings→Profile, CompaniesPage→Companies, ProjectDetailPage→ProjectDetail - Dropped dead fetches from Tasks page (invoices, invoice_items, subcontractor_invoice_items) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,161 @@
|
||||
import { useState } from 'react';
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import Layout from '../../components/Layout';
|
||||
import RequestForm from '../../components/RequestForm';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { sendEmail } from '../../lib/email';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
import { createInitialSubmissionForRequest, createTaskForRequest, findOrCreateProject } from '../../lib/requestSubmission';
|
||||
import { uploadFilesToRequestInfo } from '../../lib/filebrowserFolders';
|
||||
import { logActivity } from '../../lib/activityLog';
|
||||
|
||||
export default function NewRequest() {
|
||||
const { currentUser } = useAuth();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
|
||||
const companyOptions = currentUser.companies?.length ? currentUser.companies : (currentUser.company ? [currentUser.company] : []);
|
||||
const initialCompanyId = companyOptions[0]?.id || '';
|
||||
|
||||
const [submitted, setSubmitted] = useState(false);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
const [requestKey, setRequestKey] = useState(() => crypto.randomUUID());
|
||||
const [formKey, setFormKey] = useState(0);
|
||||
const [lastServiceType, setLastServiceType] = useState('');
|
||||
const [lastProject, setLastProject] = useState('');
|
||||
|
||||
if (!companyOptions.length) {
|
||||
return (
|
||||
<Layout>
|
||||
<div style={{ maxWidth: 480, margin: '0 auto', textAlign: 'center', paddingTop: 48 }}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16 }}>⚠️</div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 400, marginBottom: 8 }}>Account Not Yet Active</h2>
|
||||
<p style={{ color: 'var(--text-secondary)', fontSize: 14 }}>
|
||||
Your account hasn't been linked to a company yet. Please contact the Fourge team to get set up.
|
||||
</p>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
if (submitted) {
|
||||
return (
|
||||
<Layout>
|
||||
<div style={{ maxWidth: 480, margin: '0 auto', textAlign: 'center', paddingTop: 48 }}>
|
||||
<div style={{ fontSize: 48, marginBottom: 16 }}>✅</div>
|
||||
<h2 style={{ fontSize: 20, fontWeight: 400, marginBottom: 8 }}>Request Submitted!</h2>
|
||||
<p style={{ color: 'var(--text-secondary)', marginBottom: 24, fontSize: 14 }}>
|
||||
Thanks, {currentUser?.name?.split(' ')[0]}! We've received your request for <span style={{ color: "var(--text-primary)" }}>{lastServiceType}</span>
|
||||
{lastProject && <> under <span style={{ color: "var(--text-primary)" }}>{lastProject}</span></>}.
|
||||
Our team will review it and update you shortly.
|
||||
</p>
|
||||
<div className="action-buttons" style={{ justifyContent: 'center' }}>
|
||||
<button className="btn btn-primary" onClick={() => navigate('/my-projects')}>View Projects</button>
|
||||
<button className="btn btn-outline" onClick={() => { setSubmitted(false); setRequestKey(crypto.randomUUID()); setFormKey(k => k + 1); }}>
|
||||
Submit Another
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
|
||||
const handleSubmit = async (formData, files, existingProjects) => {
|
||||
if (saving) return;
|
||||
setSaving(true);
|
||||
setError('');
|
||||
try {
|
||||
const selectedCompany = companyOptions.find(c => c.id === formData.companyId) || companyOptions[0];
|
||||
const resolvedProject = await findOrCreateProject(formData.companyId, formData.project.trim(), existingProjects);
|
||||
|
||||
const { task } = await createTaskForRequest({
|
||||
projectId: resolvedProject.id,
|
||||
title: formData.title.trim(),
|
||||
requestKey,
|
||||
});
|
||||
if (!task) { setSaving(false); return; }
|
||||
|
||||
logActivity({ actorId: currentUser.id, actorName: currentUser.name, action: 'request_submitted', taskId: task.id, taskTitle: formData.title.trim(), projectId: resolvedProject.id, projectName: resolvedProject.name }).catch(() => {});
|
||||
|
||||
const { submission } = await createInitialSubmissionForRequest({
|
||||
taskId: task.id,
|
||||
requestKey,
|
||||
isHot: formData.isHot,
|
||||
serviceType: formData.serviceType,
|
||||
deadline: formData.deadline,
|
||||
description: formData.description,
|
||||
submittedBy: currentUser.id,
|
||||
submittedByName: currentUser.name,
|
||||
});
|
||||
|
||||
if (submission && files.length > 0) {
|
||||
for (const file of files) {
|
||||
const path = `${task.id}/${Date.now()}_${file.name}`;
|
||||
const { data: uploaded, error: uploadError } = await supabase.storage.from('submissions').upload(path, file);
|
||||
if (uploadError) {
|
||||
await supabase.from('tasks').delete().eq('id', task.id);
|
||||
throw new Error(`Failed to upload "${file.name}": ${uploadError.message}`);
|
||||
}
|
||||
if (uploaded) {
|
||||
const { error: fileRecordError } = await supabase.from('submission_files').insert({
|
||||
submission_id: submission.id,
|
||||
name: file.name,
|
||||
storage_path: path,
|
||||
size: file.size,
|
||||
});
|
||||
if (fileRecordError) {
|
||||
await supabase.from('tasks').delete().eq('id', task.id);
|
||||
throw new Error(`Failed to save file record for "${file.name}": ${fileRecordError.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
uploadFilesToRequestInfo(files, selectedCompany?.name, resolvedProject.name, formData.title.trim()).catch(() => {});
|
||||
}
|
||||
|
||||
sendEmail('new_request', 'hello@fourgebranding.com', {
|
||||
clientName: currentUser.name,
|
||||
clientEmail: currentUser.email,
|
||||
company: selectedCompany?.name || '',
|
||||
serviceType: formData.serviceType,
|
||||
projectName: formData.project,
|
||||
deadline: formData.deadline,
|
||||
description: formData.description,
|
||||
taskId: task.id,
|
||||
}).catch(() => {});
|
||||
|
||||
setLastServiceType(formData.serviceType);
|
||||
setLastProject(formData.project);
|
||||
setSubmitted(true);
|
||||
} catch (err) {
|
||||
console.error('Request submission failed:', err);
|
||||
setError(err.message || 'Something went wrong. Please try again.');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<div className="page-title">New Request</div>
|
||||
<div className="page-subtitle">Tell us what you need and we'll get to work.</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card" style={{ maxWidth: 600 }}>
|
||||
<RequestForm
|
||||
key={formKey}
|
||||
companies={companyOptions}
|
||||
initialCompanyId={initialCompanyId}
|
||||
showRequester={false}
|
||||
onSubmit={handleSubmit}
|
||||
saving={saving}
|
||||
error={error}
|
||||
submitLabel="Submit Request"
|
||||
/>
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user