266 lines
9.9 KiB
React
Executable File
266 lines
9.9 KiB
React
Executable File
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>
|
||
);
|
||
}
|