Fix dark mode light boxes, add client dashboard stats, reorder nav
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -25,9 +25,9 @@ function ClientNav({ onNav }) {
|
|||||||
<div className="sidebar-section">
|
<div className="sidebar-section">
|
||||||
<div className="sidebar-section-label">My Work</div>
|
<div className="sidebar-section-label">My Work</div>
|
||||||
{[
|
{[
|
||||||
{ to: '/my-company', label: 'Company' },
|
|
||||||
{ to: '/my-projects', label: 'Projects' },
|
{ to: '/my-projects', label: 'Projects' },
|
||||||
{ to: '/my-invoices', label: 'Invoices' },
|
{ to: '/my-invoices', label: 'Invoices' },
|
||||||
|
{ to: '/my-company', label: 'Company' },
|
||||||
].map(({ to, label }) => (
|
].map(({ to, label }) => (
|
||||||
<NavLink key={to} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
<NavLink key={to} to={to} onClick={onNav} className={({ isActive }) => `sidebar-link${isActive ? ' active' : ''}`}>
|
||||||
{label}
|
{label}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import Layout from '../../components/Layout';
|
import Layout from '../../components/Layout';
|
||||||
|
import StatusBadge from '../../components/StatusBadge';
|
||||||
import { supabase } from '../../lib/supabase';
|
import { supabase } from '../../lib/supabase';
|
||||||
import { useAuth } from '../../context/AuthContext';
|
import { useAuth } from '../../context/AuthContext';
|
||||||
|
|
||||||
@@ -8,21 +9,27 @@ export default function MyCompany() {
|
|||||||
const { currentUser } = useAuth();
|
const { currentUser } = useAuth();
|
||||||
const company = currentUser?.company;
|
const company = currentUser?.company;
|
||||||
const [members, setMembers] = useState([]);
|
const [members, setMembers] = useState([]);
|
||||||
const [stats, setStats] = useState({ projects: 0, active: 0 });
|
const [projects, setProjects] = useState([]);
|
||||||
|
const [tasks, setTasks] = useState([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!company?.id) { setLoading(false); return; }
|
if (!company?.id) { setLoading(false); return; }
|
||||||
async function load() {
|
async function load() {
|
||||||
const [{ data: m }, { data: p }, { data: t }] = await Promise.all([
|
const [{ data: m }, { data: p }] = await Promise.all([
|
||||||
supabase.from('profiles').select('id, name, email').eq('company_id', company.id).eq('role', 'client'),
|
supabase.from('profiles').select('id, name, email').eq('company_id', company.id).eq('role', 'client'),
|
||||||
supabase.from('projects').select('id').eq('company_id', company.id),
|
supabase.from('projects').select('*').eq('company_id', company.id).order('created_at', { ascending: false }),
|
||||||
supabase.from('tasks').select('id, status, project_id'),
|
|
||||||
]);
|
]);
|
||||||
setMembers(m || []);
|
setMembers(m || []);
|
||||||
const projectIds = (p || []).map(pr => pr.id);
|
const projectList = p || [];
|
||||||
const activeTasks = (t || []).filter(task => projectIds.includes(task.project_id) && task.status !== 'client_approved');
|
setProjects(projectList);
|
||||||
setStats({ projects: (p || []).length, active: activeTasks.length });
|
|
||||||
|
if (projectList.length > 0) {
|
||||||
|
const { data: t } = await supabase
|
||||||
|
.from('tasks').select('*')
|
||||||
|
.in('project_id', projectList.map(pr => pr.id));
|
||||||
|
setTasks(t || []);
|
||||||
|
}
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
load();
|
load();
|
||||||
@@ -30,6 +37,11 @@ export default function MyCompany() {
|
|||||||
|
|
||||||
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
if (loading) return <Layout><p style={{ padding: 24, color: 'var(--text-muted)' }}>Loading...</p></Layout>;
|
||||||
|
|
||||||
|
const notStarted = tasks.filter(t => t.status === 'not_started').length;
|
||||||
|
const inProgress = tasks.filter(t => ['in_progress', 'on_hold'].includes(t.status)).length;
|
||||||
|
const awaitingReview = tasks.filter(t => t.status === 'client_review').length;
|
||||||
|
const completed = tasks.filter(t => t.status === 'client_approved').length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<div className="page-header">
|
<div className="page-header">
|
||||||
@@ -39,25 +51,61 @@ export default function MyCompany() {
|
|||||||
{[company?.phone, company?.address].filter(Boolean).join(' · ') || 'No contact info on file'}
|
{[company?.phone, company?.address].filter(Boolean).join(' · ') || 'No contact info on file'}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Link to="/new-request" className="btn btn-primary">+ New Request</Link>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="stats-grid" style={{ marginBottom: 28 }}>
|
<div className="stats-grid" style={{ marginBottom: 28 }}>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-value">{stats.projects}</div>
|
<div className="stat-value">{notStarted}</div>
|
||||||
<div className="stat-label">Projects</div>
|
<div className="stat-label">Not Started</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-value">{stats.active}</div>
|
<div className="stat-value">{inProgress}</div>
|
||||||
<div className="stat-label">Active Jobs</div>
|
<div className="stat-label">In Progress</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat-card" style={{ position: 'relative' }}>
|
||||||
|
<div className="stat-value" style={{ color: awaitingReview > 0 ? 'var(--accent)' : undefined }}>{awaitingReview}</div>
|
||||||
|
<div className="stat-label">Awaiting Review</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stat-card">
|
<div className="stat-card">
|
||||||
<div className="stat-value">{members.length}</div>
|
<div className="stat-value">{completed}</div>
|
||||||
<div className="stat-label">Team Members</div>
|
<div className="stat-label">Completed</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Projects */}
|
||||||
|
{projects.length > 0 && (
|
||||||
|
<div style={{ marginBottom: 28 }}>
|
||||||
|
<div className="card-title" style={{ marginBottom: 12 }}>Projects</div>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
|
||||||
|
{projects.map(project => {
|
||||||
|
const projectTasks = tasks.filter(t => t.project_id === project.id);
|
||||||
|
const pendingReview = projectTasks.filter(t => t.status === 'client_review').length;
|
||||||
|
const active = projectTasks.filter(t => ['in_progress', 'on_hold', 'not_started'].includes(t.status)).length;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
key={project.id}
|
||||||
|
to={`/my-projects`}
|
||||||
|
style={{ textDecoration: 'none' }}
|
||||||
|
>
|
||||||
|
<div style={{ padding: '12px 16px', border: `1px solid ${pendingReview > 0 ? 'var(--accent)' : 'var(--border)'}`, borderRadius: 8, background: 'var(--card-bg)', display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
|
||||||
|
<div>
|
||||||
|
<div style={{ fontWeight: 600, fontSize: 14, color: 'var(--text-primary)' }}>{project.name}</div>
|
||||||
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', marginTop: 3 }}>
|
||||||
|
{projectTasks.length} job{projectTasks.length !== 1 ? 's' : ''}
|
||||||
|
{active > 0 && <> · {active} active</>}
|
||||||
|
{pendingReview > 0 && <span style={{ color: 'var(--accent)', fontWeight: 600 }}> · {pendingReview} awaiting review</span>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<StatusBadge status={project.status} />
|
||||||
|
</div>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Team members */}
|
{/* Team members */}
|
||||||
<div className="card">
|
<div className="card">
|
||||||
<div className="card-title">People</div>
|
<div className="card-title">People</div>
|
||||||
|
|||||||
@@ -244,7 +244,7 @@ export default function RequestDetail() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{canReview && !submitted && action !== 'confirm-delete' && action !== 'revision' && (
|
{canReview && !submitted && action !== 'confirm-delete' && action !== 'revision' && (
|
||||||
<div className="card" style={{ background: '#fffbeb', borderColor: '#fde68a', marginBottom: 24 }}>
|
<div className="card" style={{ borderColor: 'var(--accent)', marginBottom: 24 }}>
|
||||||
<div style={{ fontWeight: 600, marginBottom: 8 }}>🎨 Your work is ready for review!</div>
|
<div style={{ fontWeight: 600, marginBottom: 8 }}>🎨 Your work is ready for review!</div>
|
||||||
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16 }}>
|
<p style={{ fontSize: 13, color: 'var(--text-secondary)', marginBottom: 16 }}>
|
||||||
Please review the delivered work for <strong>{titleWithVersion}</strong> and let us know if you're happy or need changes.
|
Please review the delivered work for <strong>{titleWithVersion}</strong> and let us know if you're happy or need changes.
|
||||||
@@ -350,12 +350,12 @@ export default function RequestDetail() {
|
|||||||
))}
|
))}
|
||||||
|
|
||||||
{delivery && delivery.files && delivery.files.length > 0 && (
|
{delivery && delivery.files && delivery.files.length > 0 && (
|
||||||
<div style={{ marginTop: 12, padding: '10px 14px', background: '#f0fdf4', borderRadius: 8, border: '1px solid #bbf7d0' }}>
|
<div style={{ marginTop: 12, padding: '10px 14px', background: 'var(--bg)', borderRadius: 8, border: '1px solid var(--border)' }}>
|
||||||
<div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', color: '#16a34a', marginBottom: 8 }}>
|
<div style={{ fontSize: 11, fontWeight: 600, textTransform: 'uppercase', color: '#16a34a', marginBottom: 8 }}>
|
||||||
✓ Delivered {new Date(delivery.sent_at).toLocaleDateString()}
|
✓ Delivered {new Date(delivery.sent_at).toLocaleDateString()}
|
||||||
</div>
|
</div>
|
||||||
{delivery.files.map((file, fi) => (
|
{delivery.files.map((file, fi) => (
|
||||||
<div key={fi} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 10px', background: 'white', borderRadius: 6, border: '1px solid #bbf7d0', marginBottom: 4 }}>
|
<div key={fi} style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', padding: '6px 10px', background: 'var(--card-bg)', borderRadius: 6, border: '1px solid var(--border)', marginBottom: 4 }}>
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
<div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||||
<span>📄</span>
|
<span>📄</span>
|
||||||
<span style={{ fontSize: 13, fontWeight: 500 }}>{file.name}</span>
|
<span style={{ fontSize: 13, fontWeight: 500 }}>{file.name}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user