Refactor: clients → companies schema v2
This commit is contained in:
Executable
+174
@@ -0,0 +1,174 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Layout from '../../components/Layout';
|
||||
import StatusBadge from '../../components/StatusBadge';
|
||||
import { supabase } from '../../lib/supabase';
|
||||
import { useAuth } from '../../context/AuthContext';
|
||||
|
||||
function CompanyGroup({ company, tasks, projects }) {
|
||||
const [open, setOpen] = useState(true);
|
||||
return (
|
||||
<div style={{ marginBottom: 10, border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden', background: 'var(--card-bg)' }}>
|
||||
<button
|
||||
onClick={() => setOpen(o => !o)}
|
||||
style={{
|
||||
width: '100%', display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'space-between', padding: '10px 14px',
|
||||
background: 'var(--card-bg-2)', border: 'none', cursor: 'pointer',
|
||||
borderBottom: open ? '1px solid var(--border)' : 'none',
|
||||
fontFamily: 'inherit',
|
||||
}}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text-primary)' }}>
|
||||
{company.name}
|
||||
</span>
|
||||
<span style={{ fontSize: 11, fontWeight: 600, padding: '1px 7px', borderRadius: 20, background: 'var(--accent)', color: '#1a1a1a' }}>
|
||||
{tasks.length}
|
||||
</span>
|
||||
</div>
|
||||
<span style={{ fontSize: 12, color: 'var(--text-muted)' }}>{open ? '▲' : '▼'}</span>
|
||||
</button>
|
||||
{open && (
|
||||
<div>
|
||||
{tasks.map(task => {
|
||||
const project = projects.find(p => p.id === task.project_id);
|
||||
return (
|
||||
<div key={task.id} style={{ padding: '10px 14px', borderBottom: '1px solid var(--border)', display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
<Link to={`/tasks/${task.id}`} className="table-link" style={{ fontSize: 13 }}>
|
||||
{task.title} <span style={{ fontWeight: 400, color: 'var(--text-muted)' }}>{'v' + String(task.current_version).padStart(2, '0')}</span>
|
||||
</Link>
|
||||
<StatusBadge status={task.status} />
|
||||
</div>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<span style={{ fontSize: 11, color: 'var(--text-secondary)' }}>{project?.name}</span>
|
||||
<span style={{ fontSize: 11, color: task.assigned_name ? 'var(--text-secondary)' : 'var(--text-muted)' }}>
|
||||
{task.assigned_name || 'Unassigned'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupedColumn({ tasks, companies, projects, emptyText }) {
|
||||
if (tasks.length === 0) return (
|
||||
<div style={{ padding: '24px 16px', textAlign: 'center', color: 'var(--text-muted)', fontSize: 13, background: 'var(--card-bg)', borderRadius: 8, border: '1px solid var(--border)' }}>
|
||||
{emptyText}
|
||||
</div>
|
||||
);
|
||||
|
||||
const groups = companies
|
||||
.map(company => {
|
||||
const companyProjectIds = projects.filter(p => p.company_id === company.id).map(p => p.id);
|
||||
const companyTasks = tasks.filter(t => companyProjectIds.includes(t.project_id));
|
||||
return { company, tasks: companyTasks };
|
||||
})
|
||||
.filter(g => g.tasks.length > 0);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{groups.map(({ company, tasks: groupTasks }) => (
|
||||
<CompanyGroup key={company.id} company={company} tasks={groupTasks} projects={projects} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Dashboard() {
|
||||
const { currentUser } = useAuth();
|
||||
const [tasks, setTasks] = useState([]);
|
||||
const [projects, setProjects] = useState([]);
|
||||
const [companies, setCompanies] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showCompleted, setShowCompleted] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
async function load() {
|
||||
const [{ data: t }, { data: p }, { data: co }] = await Promise.all([
|
||||
supabase.from('tasks').select('*').order('submitted_at', { ascending: false }),
|
||||
supabase.from('projects').select('*'),
|
||||
supabase.from('companies').select('*').order('name'),
|
||||
]);
|
||||
setTasks(t || []);
|
||||
setProjects(p || []);
|
||||
setCompanies(co || []);
|
||||
setLoading(false);
|
||||
}
|
||||
load();
|
||||
}, []);
|
||||
|
||||
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
||||
|
||||
const activeTasks = tasks.filter(t => ['in_progress', 'on_hold', 'client_review'].includes(t.status));
|
||||
const notStartedTasks = tasks.filter(t => t.status === 'not_started');
|
||||
const completedTasks = tasks.filter(t => t.status === 'client_approved');
|
||||
const activeProjects = projects.filter(p => p.status === 'active');
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div className="page-header">
|
||||
<div>
|
||||
<div className="page-title">Welcome back, {currentUser?.name?.split(' ')[0]}</div>
|
||||
<div className="page-subtitle">Here's what's happening across your projects.</div>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8 }}>
|
||||
<button className="btn btn-outline" onClick={() => setShowCompleted(s => !s)}>
|
||||
{showCompleted ? 'Hide Completed' : 'Show Completed'}
|
||||
</button>
|
||||
<Link to="/requests" className="btn btn-primary">View Requests</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stats-grid">
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">📁</div>
|
||||
<div className="stat-value">{activeProjects.length}</div>
|
||||
<div className="stat-label">Active Projects</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">⚡</div>
|
||||
<div className="stat-value">{activeTasks.length}</div>
|
||||
<div className="stat-label">Active Jobs</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">🔴</div>
|
||||
<div className="stat-value">{notStartedTasks.length}</div>
|
||||
<div className="stat-label">Not Started</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-icon">✅</div>
|
||||
<div className="stat-value">{completedTasks.length}</div>
|
||||
<div className="stat-label">Completed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style={{ display: 'grid', gridTemplateColumns: showCompleted ? '1fr 1fr 1fr' : '1fr 1fr', gap: 24 }}>
|
||||
<div>
|
||||
<div className="card-title">Not Started</div>
|
||||
<GroupedColumn tasks={notStartedTasks} companies={companies} projects={projects} emptyText="Nothing waiting to start" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="card-title">Active Jobs</div>
|
||||
<GroupedColumn tasks={activeTasks} companies={companies} projects={projects} emptyText="No active jobs" />
|
||||
</div>
|
||||
{showCompleted && (
|
||||
<div>
|
||||
<div className="card-title" style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||
Completed
|
||||
<button onClick={() => setShowCompleted(false)} style={{ fontSize: 11, color: 'var(--text-muted)', background: 'none', border: 'none', cursor: 'pointer' }}>
|
||||
Hide ✕
|
||||
</button>
|
||||
</div>
|
||||
<GroupedColumn tasks={completedTasks} companies={companies} projects={projects} emptyText="No completed jobs yet" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Layout>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user