Add POI feature and imporve the overall backend implementation

This commit is contained in:
Urtzi Alfaro
2025-11-12 15:34:10 +01:00
parent e8096cd979
commit 5783c7ed05
173 changed files with 16862 additions and 9078 deletions

View File

@@ -19,17 +19,25 @@ import {
Euro,
ChevronDown,
ChevronUp,
X,
Package,
Building2,
Calendar,
Truck,
} from 'lucide-react';
import { ActionItem, ActionQueue } from '../../api/hooks/newDashboard';
import { useReasoningFormatter } from '../../hooks/useReasoningTranslation';
import { useTranslation } from 'react-i18next';
import { usePurchaseOrder } from '../../api/hooks/purchase-orders';
interface ActionQueueCardProps {
actionQueue: ActionQueue;
loading?: boolean;
onApprove?: (actionId: string) => void;
onReject?: (actionId: string, reason: string) => void;
onViewDetails?: (actionId: string) => void;
onModify?: (actionId: string) => void;
tenantId?: string;
}
const urgencyConfig = {
@@ -62,20 +70,34 @@ const urgencyConfig = {
function ActionItemCard({
action,
onApprove,
onReject,
onViewDetails,
onModify,
tenantId,
}: {
action: ActionItem;
onApprove?: (id: string) => void;
onReject?: (id: string, reason: string) => void;
onViewDetails?: (id: string) => void;
onModify?: (id: string) => void;
tenantId?: string;
}) {
const [expanded, setExpanded] = useState(false);
const [showDetails, setShowDetails] = useState(false);
const [showRejectModal, setShowRejectModal] = useState(false);
const [rejectionReason, setRejectionReason] = useState('');
const config = urgencyConfig[action.urgency as keyof typeof urgencyConfig] || urgencyConfig.normal;
const UrgencyIcon = config.icon;
const { formatPOAction } = useReasoningFormatter();
const { t } = useTranslation('reasoning');
// Fetch PO details if this is a PO action and details are expanded
const { data: poDetail } = usePurchaseOrder(
tenantId || '',
action.id,
{ enabled: !!tenantId && showDetails && action.type === 'po_approval' }
);
// Translate reasoning_data (or fallback to deprecated text fields)
// Memoize to prevent undefined values from being created on each render
const { reasoning, consequence, severity } = useMemo(() => {
@@ -166,6 +188,157 @@ function ActionItemCard({
</>
)}
{/* Inline PO Details (expandable) */}
{action.type === 'po_approval' && (
<>
<button
onClick={() => setShowDetails(!showDetails)}
className="flex items-center gap-2 text-sm font-medium transition-colors mb-3 w-full"
style={{ color: 'var(--color-info-700)' }}
>
{showDetails ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
<Package className="w-4 h-4" />
<span>View Order Details</span>
</button>
{showDetails && poDetail && (
<div
className="border rounded-md p-4 mb-3 space-y-3"
style={{
backgroundColor: 'var(--bg-primary)',
borderColor: 'var(--border-primary)',
}}
>
{/* Supplier Info */}
<div className="flex items-start gap-2">
<Building2 className="w-5 h-5 flex-shrink-0" style={{ color: 'var(--color-info-600)' }} />
<div className="flex-1">
<p className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
{poDetail.supplier?.name || 'Supplier'}
</p>
{poDetail.supplier?.contact_person && (
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
Contact: {poDetail.supplier.contact_person}
</p>
)}
{poDetail.supplier?.email && (
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{poDetail.supplier.email}
</p>
)}
</div>
</div>
{/* Delivery Date & Tracking */}
{poDetail.required_delivery_date && (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Calendar className="w-5 h-5" style={{ color: 'var(--color-warning-600)' }} />
<div className="flex-1">
<p className="text-xs font-medium" style={{ color: 'var(--text-secondary)' }}>
Required Delivery
</p>
<p className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
{new Date(poDetail.required_delivery_date).toLocaleDateString()}
</p>
</div>
</div>
{/* Estimated Delivery Date (shown after approval) */}
{poDetail.estimated_delivery_date && (
<div className="flex items-center gap-2 ml-7">
<Truck className="w-4 h-4" style={{ color: 'var(--color-info-600)' }} />
<div className="flex-1">
<p className="text-xs font-medium" style={{ color: 'var(--text-secondary)' }}>
Expected Arrival
</p>
<p className="text-sm font-semibold" style={{ color: 'var(--text-primary)' }}>
{new Date(poDetail.estimated_delivery_date).toLocaleDateString()}
</p>
</div>
{(() => {
const now = new Date();
const estimatedDate = new Date(poDetail.estimated_delivery_date);
const daysUntil = Math.ceil((estimatedDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24));
let statusColor = 'var(--color-success-600)';
let statusText = 'On Track';
if (daysUntil < 0) {
statusColor = 'var(--color-error-600)';
statusText = `${Math.abs(daysUntil)}d Overdue`;
} else if (daysUntil === 0) {
statusColor = 'var(--color-warning-600)';
statusText = 'Due Today';
} else if (daysUntil <= 2) {
statusColor = 'var(--color-warning-600)';
statusText = `${daysUntil}d Left`;
} else {
statusText = `${daysUntil}d Left`;
}
return (
<span
className="px-2 py-1 rounded text-xs font-semibold"
style={{
backgroundColor: statusColor.replace('600', '100'),
color: statusColor,
}}
>
{statusText}
</span>
);
})()}
</div>
)}
</div>
)}
{/* Line Items */}
{poDetail.items && poDetail.items.length > 0 && (
<div>
<p className="text-xs font-semibold mb-2" style={{ color: 'var(--text-secondary)' }}>
Order Items ({poDetail.items.length})
</p>
<div className="space-y-2 max-h-48 overflow-y-auto">
{poDetail.items.map((item, idx) => (
<div
key={idx}
className="flex justify-between items-start p-2 rounded"
style={{ backgroundColor: 'var(--bg-secondary)' }}
>
<div className="flex-1">
<p className="text-sm font-medium" style={{ color: 'var(--text-primary)' }}>
{item.product_name || item.product_code || 'Product'}
</p>
<p className="text-xs" style={{ color: 'var(--text-secondary)' }}>
{item.ordered_quantity} {item.unit_of_measure} × {parseFloat(item.unit_price).toFixed(2)}
</p>
</div>
<p className="text-sm font-bold" style={{ color: 'var(--text-primary)' }}>
{parseFloat(item.line_total).toFixed(2)}
</p>
</div>
))}
</div>
</div>
)}
{/* Total Amount */}
<div
className="border-t pt-2 flex justify-between items-center"
style={{ borderColor: 'var(--border-primary)' }}
>
<p className="text-sm font-bold" style={{ color: 'var(--text-primary)' }}>Total Amount</p>
<p className="text-lg font-bold" style={{ color: 'var(--color-info-700)' }}>
{parseFloat(poDetail.total_amount).toFixed(2)}
</p>
</div>
</div>
)}
</>
)}
{/* Time Estimate */}
<div className="flex items-center gap-2 text-xs mb-4" style={{ color: 'var(--text-tertiary)' }}>
<Clock className="w-4 h-4" />
@@ -174,6 +347,79 @@ function ActionItemCard({
</span>
</div>
{/* Rejection Modal */}
{showRejectModal && (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 z-50"
onClick={() => setShowRejectModal(false)}
>
<div
className="rounded-lg p-6 max-w-md w-full"
style={{ backgroundColor: 'var(--bg-primary)' }}
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-bold" style={{ color: 'var(--text-primary)' }}>
Reject Purchase Order
</h3>
<button
onClick={() => setShowRejectModal(false)}
className="p-1 rounded hover:bg-opacity-10 hover:bg-black"
>
<X className="w-5 h-5" style={{ color: 'var(--text-secondary)' }} />
</button>
</div>
<p className="text-sm mb-4" style={{ color: 'var(--text-secondary)' }}>
Please provide a reason for rejecting this purchase order:
</p>
<textarea
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
placeholder="Enter rejection reason..."
className="w-full p-3 border rounded-lg mb-4 min-h-24"
style={{
backgroundColor: 'var(--bg-secondary)',
borderColor: 'var(--border-primary)',
color: 'var(--text-primary)',
}}
/>
<div className="flex gap-3 justify-end">
<button
onClick={() => setShowRejectModal(false)}
className="px-4 py-2 rounded-lg font-semibold transition-colors"
style={{
backgroundColor: 'var(--bg-tertiary)',
color: 'var(--text-primary)',
}}
>
Cancel
</button>
<button
onClick={() => {
if (onReject && rejectionReason.trim()) {
onReject(action.id, rejectionReason);
setShowRejectModal(false);
setRejectionReason('');
}
}}
disabled={!rejectionReason.trim()}
className="px-4 py-2 rounded-lg font-semibold transition-colors"
style={{
backgroundColor: rejectionReason.trim() ? 'var(--color-error-600)' : 'var(--bg-quaternary)',
color: rejectionReason.trim() ? 'white' : 'var(--text-tertiary)',
cursor: rejectionReason.trim() ? 'pointer' : 'not-allowed',
}}
>
Reject Order
</button>
</div>
</div>
</div>
)}
{/* Action Buttons */}
<div className="flex flex-wrap gap-2">
{(action.actions || []).map((button, index) => {
@@ -210,6 +456,8 @@ function ActionItemCard({
const handleClick = () => {
if (button.action === 'approve' && onApprove) {
onApprove(action.id);
} else if (button.action === 'reject') {
setShowRejectModal(true);
} else if (button.action === 'view_details' && onViewDetails) {
onViewDetails(action.id);
} else if (button.action === 'modify' && onModify) {
@@ -232,6 +480,7 @@ function ActionItemCard({
style={currentStyle}
>
{button.action === 'approve' && <CheckCircle2 className="w-4 h-4" />}
{button.action === 'reject' && <X className="w-4 h-4" />}
{button.action === 'view_details' && <Eye className="w-4 h-4" />}
{button.action === 'modify' && <Edit className="w-4 h-4" />}
{button.label}
@@ -247,8 +496,10 @@ export function ActionQueueCard({
actionQueue,
loading,
onApprove,
onReject,
onViewDetails,
onModify,
tenantId,
}: ActionQueueCardProps) {
const [showAll, setShowAll] = useState(false);
const { t } = useTranslation('reasoning');
@@ -338,8 +589,10 @@ export function ActionQueueCard({
key={action.id}
action={action}
onApprove={onApprove}
onReject={onReject}
onViewDetails={onViewDetails}
onModify={onModify}
tenantId={tenantId}
/>
))}
</div>