Files
fourge-portal/src/pages/external/MyPurchaseOrders.jsx
T
Krao Hasanee eee0885811 Fix file sharing load speed and move error; misc updates
- 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>
2026-05-13 14:20:38 -04:00

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>
);
}