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
+206
View File
@@ -0,0 +1,206 @@
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: '', email: '', phone: '' });
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(),
email: newForm.email.trim(),
phone: newForm.phone.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>Email</label>
<input
type="email"
placeholder="contact@acme.com"
value={newForm.email}
onChange={e => setNewForm(f => ({ ...f, email: e.target.value }))}
/>
</div>
<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>
<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.email && <> · {company.email}</>}
</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>
);
}