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>
This commit is contained in:
+183
@@ -0,0 +1,183 @@
|
||||
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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user