Refactor: clients → companies schema v2

This commit is contained in:
Krao Hasanee
2026-03-26 23:42:06 -04:00
commit 719209fa25
61 changed files with 8192 additions and 0 deletions
+265
View File
@@ -0,0 +1,265 @@
import { useState, useEffect } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import Layout from '../../components/Layout';
import FileAttachment from '../../components/FileAttachment';
import { serviceTypes } from '../../data/mockData';
import { supabase } from '../../lib/supabase';
import { sendEmail } from '../../lib/email';
import { useAuth } from '../../context/AuthContext';
export default function NewRequest() {
const { currentUser } = useAuth();
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const preselectedProject = searchParams.get('project') || '';
const [existingProjects, setExistingProjects] = useState([]);
const [submitted, setSubmitted] = useState(false);
const [saving, setSaving] = useState(false);
const [form, setForm] = useState({ project: preselectedProject, serviceType: '', title: '', deadline: '', description: '' });
const [files, setFiles] = useState([]);
const [customProjects, setCustomProjects] = useState([]);
const [isTypingProject, setIsTypingProject] = useState(false);
const [newProjectName, setNewProjectName] = useState('');
useEffect(() => {
async function load() {
if (!currentUser.company_id) return;
const { data: p } = await supabase
.from('projects')
.select('id, name')
.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]);
const allProjectNames = [
...existingProjects.map(p => p.name),
...customProjects.filter(name => !existingProjects.some(p => p.name === name)),
];
const set = (field) => (e) => setForm(f => ({ ...f, [field]: e.target.value }));
const handleProjectSelect = (e) => {
if (e.target.value === '__new__') {
setIsTypingProject(true);
setForm(f => ({ ...f, project: '' }));
} else {
setForm(f => ({ ...f, project: e.target.value }));
}
};
const handleAddProject = () => {
const name = newProjectName.trim();
if (!name) return;
if (!customProjects.includes(name) && !existingProjects.some(p => p.name === name)) {
setCustomProjects(prev => [...prev, name]);
}
setForm(f => ({ ...f, project: name }));
setIsTypingProject(false);
setNewProjectName('');
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!currentUser.company_id) {
alert('Your account is not yet assigned to a company. Please contact support.');
return;
}
setSaving(true);
// Find existing project by name within this company, or create new one
let projectId;
const existing = existingProjects.find(p => p.name === form.project);
if (existing) {
projectId = existing.id;
} else {
const { data: newProject } = await supabase.from('projects').insert({
company_id: currentUser.company_id,
name: form.project,
status: 'active',
}).select().single();
projectId = newProject?.id;
}
if (!projectId) { setSaving(false); return; }
// Create task
const { data: task } = await supabase.from('tasks').insert({
project_id: projectId,
title: form.title.trim() || form.serviceType,
status: 'not_started',
current_version: 0,
}).select().single();
if (!task) { setSaving(false); return; }
// Create submission
const { data: submission } = await supabase.from('submissions').insert({
task_id: task.id,
version_number: 1,
type: 'initial',
service_type: form.serviceType,
deadline: form.deadline || null,
description: form.description,
submitted_by: currentUser.id,
submitted_by_name: currentUser.name,
}).select().single();
// Upload files
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);
if (uploaded) {
await supabase.from('submission_files').insert({
submission_id: submission.id,
name: file.name,
storage_path: path,
size: file.size,
});
}
}
}
sendEmail('new_request', 'hello@fourgebranding.com', {
clientName: currentUser.name,
clientEmail: currentUser.email,
company: currentUser.company?.name || '',
serviceType: form.serviceType,
projectName: form.project,
deadline: form.deadline,
description: form.description,
taskId: task.id,
});
setSaving(false);
setSubmitted(true);
};
if (!currentUser.company_id) {
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: 700, 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: 700, 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 <strong>{form.serviceType}</strong>
{form.project && <> under <strong>{form.project}</strong></>}.
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); setForm({ project: '', serviceType: '', title: '', deadline: '', description: '' }); setFiles([]); }}>
Submit Another
</button>
</div>
</div>
</Layout>
);
}
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 }}>
<form onSubmit={handleSubmit}>
<div className="form-group">
<label>Project *</label>
{isTypingProject ? (
<div style={{ display: 'flex', gap: 8 }}>
<input
type="text"
placeholder="Enter project name..."
value={newProjectName}
onChange={e => setNewProjectName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') { e.preventDefault(); handleAddProject(); } }}
autoFocus
style={{ flex: 1 }}
/>
<button type="button" className="btn btn-primary" onClick={handleAddProject} disabled={!newProjectName.trim()}>Add</button>
<button type="button" className="btn btn-outline" onClick={() => { setIsTypingProject(false); setNewProjectName(''); }}>Cancel</button>
</div>
) : (
<select value={form.project} onChange={handleProjectSelect} required>
<option value="">Select a project...</option>
{allProjectNames.map(name => <option key={name} value={name}>{name}</option>)}
<option value="__new__"> Create new project...</option>
</select>
)}
</div>
<div className="grid-2">
<div className="form-group">
<label>Service Type *</label>
<select value={form.serviceType} onChange={set('serviceType')} required>
<option value="">Select a service...</option>
{serviceTypes.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
<div className="form-group">
<label>Desired Deadline</label>
<input type="date" value={form.deadline} onChange={set('deadline')} />
</div>
</div>
<div className="form-group">
<label>
Request Title
<span style={{ fontWeight: 400, color: 'var(--text-muted)', marginLeft: 6 }}>optional — defaults to service type if left blank</span>
</label>
<input
type="text"
placeholder={form.serviceType ? `e.g. ${form.serviceType} — 2026` : 'e.g. Brand Book 2026'}
value={form.title}
onChange={set('title')}
/>
</div>
<div className="form-group">
<label>Project Description *</label>
<textarea
placeholder="Tell us about your project — what you need, your brand, style preferences, any references..."
value={form.description}
onChange={set('description')}
style={{ minHeight: 140 }}
required
/>
</div>
<FileAttachment files={files} onChange={setFiles} />
<div className="notification notification-info" style={{ marginBottom: 16 }}>
Submitting as <strong>{currentUser?.name}</strong> · {currentUser?.company?.name}
</div>
<button type="submit" className="btn btn-primary btn-lg" disabled={saving}>
{saving ? 'Submitting...' : 'Submit Request'}
</button>
</form>
</div>
</Layout>
);
}