eee0885811
- Remove recursive directory size calculations (single Seafile API call per list) - Remove 'Used in this location' usage display - Fix move using v2 per-type endpoints instead of broken batch endpoint - Send entry type from frontend for correct move routing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
184 lines
7.6 KiB
React
184 lines
7.6 KiB
React
import { useEffect, useState } from 'react';
|
|
import Layout from '../../components/Layout';
|
|
import { supabase } from '../../lib/supabase';
|
|
import { useAuth } from '../../context/AuthContext';
|
|
import { withTimeout } from '../../lib/withTimeout';
|
|
|
|
const poStatusColor = {
|
|
draft: 'not_started',
|
|
sent: 'in_progress',
|
|
approved: 'client_approved',
|
|
ready_to_pay: 'in_progress',
|
|
paid: 'client_approved',
|
|
cancelled: 'needs_revision',
|
|
};
|
|
|
|
const poStatusLabel = {
|
|
draft: 'Draft',
|
|
sent: 'Sent',
|
|
approved: 'Approved',
|
|
ready_to_pay: 'Ready to Pay',
|
|
paid: 'Paid',
|
|
cancelled: 'Cancelled',
|
|
};
|
|
|
|
export default function MyPurchaseOrders() {
|
|
const { currentUser } = useAuth();
|
|
const [purchaseOrders, setPurchaseOrders] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [error, setError] = useState('');
|
|
const [savingId, setSavingId] = useState('');
|
|
|
|
useEffect(() => {
|
|
async function load() {
|
|
if (!currentUser?.id) {
|
|
setLoading(false);
|
|
return;
|
|
}
|
|
try {
|
|
const { data, error: loadError } = await withTimeout(
|
|
supabase
|
|
.from('subcontractor_payments')
|
|
.select('*, project:projects(id, name, company:companies(name)), items:subcontractor_po_items(*, task:tasks(id, title))')
|
|
.eq('profile_id', currentUser.id)
|
|
.order('date', { ascending: false }),
|
|
12000,
|
|
'Purchase orders load'
|
|
);
|
|
|
|
if (loadError) {
|
|
console.error('Failed to load purchase orders:', loadError);
|
|
setError(loadError.message || 'Failed to load purchase orders.');
|
|
setPurchaseOrders([]);
|
|
} else {
|
|
setPurchaseOrders(data || []);
|
|
setError('');
|
|
}
|
|
} catch (error) {
|
|
console.error('Purchase orders load failed:', error);
|
|
setError(error.message || 'Failed to load purchase orders.');
|
|
setPurchaseOrders([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}
|
|
|
|
load();
|
|
}, [currentUser?.id]);
|
|
|
|
const handleApprove = async (po) => {
|
|
setSavingId(po.id);
|
|
const { data, error: approveError } = await supabase
|
|
.from('subcontractor_payments')
|
|
.update({ status: 'approved', approved_at: new Date().toISOString() })
|
|
.eq('id', po.id)
|
|
.eq('profile_id', currentUser.id)
|
|
.select('*, project:projects(id, name, company:companies(name)), items:subcontractor_po_items(*, task:tasks(id, title))')
|
|
.single();
|
|
|
|
if (approveError) {
|
|
alert(`Failed to approve PO: ${approveError.message}`);
|
|
} else if (data) {
|
|
setPurchaseOrders(prev => prev.map(row => row.id === po.id ? data : row));
|
|
}
|
|
setSavingId('');
|
|
};
|
|
|
|
return (
|
|
<Layout>
|
|
<div className="page-header">
|
|
<div>
|
|
<div className="page-title">Purchase Orders</div>
|
|
<div className="page-subtitle">Review and approve subcontractor work orders assigned to you.</div>
|
|
</div>
|
|
</div>
|
|
|
|
{loading ? (
|
|
<p style={{ color: 'var(--text-muted)' }}>Loading...</p>
|
|
) : error ? (
|
|
<div className="card" style={{ color: 'var(--danger)' }}>{error}</div>
|
|
) : purchaseOrders.length === 0 ? (
|
|
<div className="empty-state">
|
|
<h3>No purchase orders</h3>
|
|
<p>New POs will appear here when the Fourge team sends them.</p>
|
|
</div>
|
|
) : (
|
|
<div style={{ display: 'grid', gap: 12 }}>
|
|
{purchaseOrders.map(po => (
|
|
<div key={po.id} className="card">
|
|
<div style={{ display: 'flex', alignItems: 'flex-start', justifyContent: 'space-between', gap: 16, marginBottom: 12 }}>
|
|
<div>
|
|
<div style={{ fontSize: 12, color: 'var(--text-muted)', fontWeight: 700, textTransform: 'uppercase' }}>
|
|
{po.po_number || 'Purchase Order'}
|
|
</div>
|
|
<div className="card-title" style={{ marginBottom: 4 }}>{po.project?.name || 'Subcontractor Work'}</div>
|
|
<div style={{ color: 'var(--text-muted)', fontSize: 13 }}>
|
|
{po.project?.company?.name || 'Fourge Branding'} · {new Date(po.date).toLocaleDateString()}
|
|
{po.due_date ? ` · Due ${new Date(po.due_date).toLocaleDateString()}` : ''}
|
|
</div>
|
|
</div>
|
|
<span className={`badge badge-${poStatusColor[po.status] || 'not_started'}`}>
|
|
{poStatusLabel[po.status] || po.status}
|
|
</span>
|
|
</div>
|
|
|
|
<div style={{ display: 'grid', gap: 10 }}>
|
|
<div>
|
|
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', marginBottom: 4 }}>Scope</div>
|
|
<div style={{ whiteSpace: 'pre-wrap' }}>{po.description}</div>
|
|
</div>
|
|
{po.items?.length > 0 && (
|
|
<div>
|
|
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', marginBottom: 4 }}>Line Items</div>
|
|
<div style={{ border: '1px solid var(--border)', borderRadius: 8, overflow: 'hidden' }}>
|
|
{po.items
|
|
.slice()
|
|
.sort((a, b) => Number(a.sort_order || 0) - Number(b.sort_order || 0))
|
|
.map(item => (
|
|
<div key={item.id} style={{ display: 'grid', gridTemplateColumns: '1fr auto', gap: 12, padding: '10px 12px', borderBottom: '1px solid var(--border)' }}>
|
|
<div>
|
|
<div style={{ fontWeight: 700 }}>{item.description || item.task?.title}</div>
|
|
{item.task?.title && item.description !== item.task.title && (
|
|
<div style={{ color: 'var(--text-muted)', fontSize: 12 }}>{item.task.title}</div>
|
|
)}
|
|
</div>
|
|
<div style={{ fontWeight: 800 }}>${Number(item.amount).toFixed(2)}</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, minmax(0, 1fr))', gap: 10 }}>
|
|
<div>
|
|
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase' }}>Amount</div>
|
|
<div style={{ fontWeight: 800 }}>${Number(po.amount).toFixed(2)}</div>
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase' }}>Terms</div>
|
|
<div>{po.terms || 'Net 15'}</div>
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 11, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase' }}>Paid</div>
|
|
<div>{po.paid_at ? new Date(po.paid_at).toLocaleDateString() : 'Not paid'}</div>
|
|
</div>
|
|
</div>
|
|
{po.notes && (
|
|
<div style={{ color: 'var(--text-muted)', fontSize: 13 }}>{po.notes}</div>
|
|
)}
|
|
</div>
|
|
|
|
{po.status === 'sent' && (
|
|
<div style={{ marginTop: 14, display: 'flex', justifyContent: 'flex-end' }}>
|
|
<button className="btn btn-primary btn-sm" onClick={() => handleApprove(po)} disabled={savingId === po.id}>
|
|
{savingId === po.id ? 'Approving...' : 'Approve PO'}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</Layout>
|
|
);
|
|
}
|