e7174d392c
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
207 lines
8.9 KiB
React
207 lines
8.9 KiB
React
import { useState, useEffect } from 'react';
|
|
import { Link, useNavigate } from 'react-router-dom';
|
|
import Layout from '../../components/Layout';
|
|
import { supabase } from '../../lib/supabase';
|
|
|
|
export default function Companies() {
|
|
const navigate = useNavigate();
|
|
const [companies, setCompanies] = useState([]);
|
|
const [profiles, setProfiles] = useState([]);
|
|
const [projects, setProjects] = useState([]);
|
|
const [tasks, setTasks] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [showNew, setShowNew] = useState(false);
|
|
const [newForm, setNewForm] = useState({ name: '', phone: '', address: '' });
|
|
const [saving, setSaving] = useState(false);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, []);
|
|
|
|
async function load() {
|
|
const [{ data: co }, { data: prof }, { data: p }, { data: t }] = await Promise.all([
|
|
supabase.from('companies').select('*').order('name'),
|
|
supabase.from('profiles').select('id, name, email, company_id').eq('role', 'client'),
|
|
supabase.from('projects').select('id, company_id, status'),
|
|
supabase.from('tasks').select('id, project_id, status'),
|
|
]);
|
|
setCompanies(co || []);
|
|
setProfiles(prof || []);
|
|
setProjects(p || []);
|
|
setTasks(t || []);
|
|
setLoading(false);
|
|
}
|
|
|
|
const handleCreate = async (e) => {
|
|
e.preventDefault();
|
|
if (!newForm.name.trim()) return;
|
|
setSaving(true);
|
|
const { data } = await supabase.from('companies').insert({
|
|
name: newForm.name.trim(),
|
|
phone: newForm.phone.trim(),
|
|
address: newForm.address.trim(),
|
|
}).select().single();
|
|
setSaving(false);
|
|
if (data) {
|
|
setShowNew(false);
|
|
setNewForm({ name: '', email: '', phone: '' });
|
|
navigate(`/companies/${data.id}`);
|
|
}
|
|
};
|
|
|
|
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
|
|
|
const unassigned = profiles.filter(p => !p.company_id);
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="page-header">
|
|
<div>
|
|
<div className="page-title">Companies</div>
|
|
<div className="page-subtitle">
|
|
{companies.length} company{companies.length !== 1 ? 'ies' : ''}
|
|
{unassigned.length > 0 && (
|
|
<span style={{ marginLeft: 10, color: 'var(--danger)', fontWeight: 600 }}>
|
|
· {unassigned.length} unassigned user{unassigned.length !== 1 ? 's' : ''}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
<button className="btn btn-primary" onClick={() => setShowNew(s => !s)}>
|
|
{showNew ? 'Cancel' : '+ New Company'}
|
|
</button>
|
|
</div>
|
|
|
|
{showNew && (
|
|
<div className="card" style={{ marginBottom: 24, maxWidth: 480 }}>
|
|
<div className="card-title">New Company</div>
|
|
<form onSubmit={handleCreate}>
|
|
<div className="form-group">
|
|
<label>Company Name *</label>
|
|
<input
|
|
type="text"
|
|
placeholder="Acme Corp"
|
|
value={newForm.name}
|
|
onChange={e => setNewForm(f => ({ ...f, name: e.target.value }))}
|
|
required
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
<div className="grid-2">
|
|
<div className="form-group">
|
|
<label>Phone</label>
|
|
<input
|
|
type="text"
|
|
placeholder="+1 (555) 000-0000"
|
|
value={newForm.phone}
|
|
onChange={e => setNewForm(f => ({ ...f, phone: e.target.value }))}
|
|
/>
|
|
</div>
|
|
<div className="form-group">
|
|
<label>Address</label>
|
|
<input
|
|
type="text"
|
|
placeholder="123 Main St, City, State"
|
|
value={newForm.address}
|
|
onChange={e => setNewForm(f => ({ ...f, address: e.target.value }))}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="action-buttons">
|
|
<button type="submit" className="btn btn-primary" disabled={saving || !newForm.name.trim()}>
|
|
{saving ? 'Creating...' : 'Create Company'}
|
|
</button>
|
|
<button type="button" className="btn btn-outline" onClick={() => setShowNew(false)}>Cancel</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
)}
|
|
|
|
{unassigned.length > 0 && (
|
|
<div className="card" style={{ marginBottom: 24, borderColor: 'var(--danger)' }}>
|
|
<div className="card-title" style={{ color: 'var(--danger)' }}>Unassigned Users</div>
|
|
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 12 }}>
|
|
These users have signed up but haven't been assigned to a company yet.
|
|
</p>
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{unassigned.map(user => (
|
|
<div key={user.id} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '10px 14px', background: 'var(--bg)', borderRadius: 8, border: '1px solid var(--border)' }}>
|
|
<div>
|
|
<div style={{ fontWeight: 600, fontSize: 13 }}>{user.name}</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)' }}>{user.email}</div>
|
|
</div>
|
|
<span style={{ fontSize: 12, color: 'var(--danger)', fontWeight: 500 }}>No company</span>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<p style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 10 }}>
|
|
Open a company and assign them from the Users tab.
|
|
</p>
|
|
</div>
|
|
)}
|
|
|
|
{companies.length === 0 ? (
|
|
<div className="empty-state">
|
|
<h3>No companies yet</h3>
|
|
<p>Create a company to get started.</p>
|
|
<button className="btn btn-primary" style={{ marginTop: 16 }} onClick={() => setShowNew(true)}>+ New Company</button>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
|
{companies.map(company => {
|
|
const companyProfiles = profiles.filter(p => p.company_id === company.id);
|
|
const companyProjects = projects.filter(p => p.company_id === company.id);
|
|
const projectIds = companyProjects.map(p => p.id);
|
|
const activeTasks = tasks.filter(t => projectIds.includes(t.project_id) && t.status !== 'client_approved');
|
|
|
|
return (
|
|
<div key={company.id} style={{ border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden', background: 'var(--card-bg)' }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '14px 18px', background: 'var(--card-bg-2)', borderBottom: '1px solid var(--border)' }}>
|
|
<div>
|
|
<div style={{ fontWeight: 700, fontSize: 15, color: 'var(--text-primary)' }}>{company.name}</div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 3 }}>
|
|
{companyProfiles.length} user{companyProfiles.length !== 1 ? 's' : ''}
|
|
{' · '}
|
|
{companyProjects.length} project{companyProjects.length !== 1 ? 's' : ''}
|
|
{activeTasks.length > 0 && <> · <span style={{ color: 'var(--accent)', fontWeight: 600 }}>{activeTasks.length} active</span></>}
|
|
{company.phone && <> · {company.phone}</>}
|
|
</div>
|
|
</div>
|
|
<Link to={`/companies/${company.id}`} className="btn btn-outline btn-sm">View</Link>
|
|
</div>
|
|
|
|
{companyProfiles.length > 0 && (
|
|
<div>
|
|
{companyProfiles.map((profile, i) => (
|
|
<div
|
|
key={profile.id}
|
|
style={{
|
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
padding: '10px 18px',
|
|
borderBottom: i < companyProfiles.length - 1 ? '1px solid var(--border)' : 'none',
|
|
}}
|
|
>
|
|
<div style={{
|
|
width: 28, height: 28, borderRadius: 4, background: 'var(--accent)',
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
fontSize: 11, fontWeight: 700, color: '#111', flexShrink: 0,
|
|
}}>
|
|
{profile.name?.split(' ').map(n => n[0]).join('').toUpperCase().slice(0, 2)}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 13, fontWeight: 600, color: 'var(--text-primary)' }}>{profile.name}</div>
|
|
<div style={{ fontSize: 11, color: 'var(--text-muted)' }}>{profile.email || '—'}</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</Layout>
|
|
);
|
|
}
|