ADD new frontend
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
// ================================================================
|
||||
// frontend/src/components/procurement/ProcurementPlanCard.tsx
|
||||
// ================================================================
|
||||
/**
|
||||
* Procurement Plan Card Component
|
||||
* Displays a procurement plan with key information and actions
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import type { ProcurementPlan } from '@/api/types/procurement';
|
||||
import { PlanStatus, Priority } from '@/api/types/procurement';
|
||||
|
||||
export interface ProcurementPlanCardProps {
|
||||
plan: ProcurementPlan;
|
||||
onViewDetails?: (planId: string) => void;
|
||||
onUpdateStatus?: (planId: string, status: string) => void;
|
||||
showActions?: boolean;
|
||||
}
|
||||
|
||||
export const ProcurementPlanCard: React.FC<ProcurementPlanCardProps> = ({
|
||||
plan,
|
||||
onViewDetails,
|
||||
onUpdateStatus,
|
||||
showActions = false,
|
||||
}) => {
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
[PlanStatus.DRAFT]: 'bg-gray-100 text-gray-800',
|
||||
[PlanStatus.PENDING_APPROVAL]: 'bg-yellow-100 text-yellow-800',
|
||||
[PlanStatus.APPROVED]: 'bg-blue-100 text-blue-800',
|
||||
[PlanStatus.IN_EXECUTION]: 'bg-green-100 text-green-800',
|
||||
[PlanStatus.COMPLETED]: 'bg-green-100 text-green-800',
|
||||
[PlanStatus.CANCELLED]: 'bg-red-100 text-red-800',
|
||||
};
|
||||
return colors[status as keyof typeof colors] || 'bg-gray-100 text-gray-800';
|
||||
};
|
||||
|
||||
const getPriorityColor = (priority: string) => {
|
||||
const colors = {
|
||||
[Priority.CRITICAL]: 'text-red-600',
|
||||
[Priority.HIGH]: 'text-orange-600',
|
||||
[Priority.NORMAL]: 'text-blue-600',
|
||||
[Priority.LOW]: 'text-gray-600',
|
||||
};
|
||||
return colors[priority as keyof typeof colors] || 'text-gray-600';
|
||||
};
|
||||
|
||||
const getRiskColor = (risk: string) => {
|
||||
const colors = {
|
||||
'critical': 'text-red-600',
|
||||
'high': 'text-orange-600',
|
||||
'medium': 'text-yellow-600',
|
||||
'low': 'text-green-600',
|
||||
};
|
||||
return colors[risk as keyof typeof colors] || 'text-gray-600';
|
||||
};
|
||||
|
||||
const formatCurrency = (amount: number) => {
|
||||
return new Intl.NumberFormat('es-ES', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('es-ES', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
year: 'numeric'
|
||||
});
|
||||
};
|
||||
|
||||
const nextStatusOptions = () => {
|
||||
const options = {
|
||||
[PlanStatus.DRAFT]: [PlanStatus.PENDING_APPROVAL, PlanStatus.CANCELLED],
|
||||
[PlanStatus.PENDING_APPROVAL]: [PlanStatus.APPROVED, PlanStatus.CANCELLED],
|
||||
[PlanStatus.APPROVED]: [PlanStatus.IN_EXECUTION, PlanStatus.CANCELLED],
|
||||
[PlanStatus.IN_EXECUTION]: [PlanStatus.COMPLETED, PlanStatus.CANCELLED],
|
||||
[PlanStatus.COMPLETED]: [],
|
||||
[PlanStatus.CANCELLED]: [],
|
||||
};
|
||||
return options[plan.status as keyof typeof options] || [];
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border border-gray-200">
|
||||
<div className="p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center space-x-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{plan.plan_number}
|
||||
</h3>
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${getStatusColor(plan.status)}`}>
|
||||
{plan.status.replace('_', ' ').toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm text-gray-600 mt-1">
|
||||
Plan Date: {formatDate(plan.plan_date)} |
|
||||
Period: {formatDate(plan.plan_period_start)} - {formatDate(plan.plan_period_end)}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="text-right">
|
||||
<div className={`text-sm font-medium ${getPriorityColor(plan.priority)}`}>
|
||||
{plan.priority.toUpperCase()} Priority
|
||||
</div>
|
||||
<div className={`text-xs ${getRiskColor(plan.supply_risk_level)}`}>
|
||||
{plan.supply_risk_level.toUpperCase()} Risk
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Metrics */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{plan.total_requirements}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Requirements</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{formatCurrency(plan.total_estimated_cost)}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Est. Cost</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{plan.primary_suppliers_count}
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Suppliers</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{plan.safety_stock_buffer}%
|
||||
</div>
|
||||
<div className="text-xs text-gray-500">Safety Buffer</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Requirements Summary */}
|
||||
{plan.requirements && plan.requirements.length > 0 && (
|
||||
<div className="mb-4">
|
||||
<h4 className="text-sm font-medium text-gray-700 mb-2">
|
||||
Top Requirements ({plan.requirements.length} total)
|
||||
</h4>
|
||||
<div className="space-y-1">
|
||||
{plan.requirements.slice(0, 3).map((req) => (
|
||||
<div key={req.id} className="flex justify-between items-center text-sm">
|
||||
<span className="truncate">{req.product_name}</span>
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="text-gray-500">
|
||||
{req.net_requirement} {req.unit_of_measure}
|
||||
</span>
|
||||
<span className={`px-1 py-0.5 rounded text-xs ${
|
||||
req.priority === Priority.CRITICAL ? 'bg-red-100 text-red-700' :
|
||||
req.priority === Priority.HIGH ? 'bg-orange-100 text-orange-700' :
|
||||
'bg-gray-100 text-gray-700'
|
||||
}`}>
|
||||
{req.priority}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{plan.requirements.length > 3 && (
|
||||
<div className="text-xs text-gray-500 text-center">
|
||||
+{plan.requirements.length - 3} more requirements
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Performance Metrics */}
|
||||
{(plan.fulfillment_rate || plan.on_time_delivery_rate) && (
|
||||
<div className="flex items-center space-x-4 text-sm text-gray-600 mb-4">
|
||||
{plan.fulfillment_rate && (
|
||||
<span>Fulfillment: {plan.fulfillment_rate}%</span>
|
||||
)}
|
||||
{plan.on_time_delivery_rate && (
|
||||
<span>On-time: {plan.on_time_delivery_rate}%</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
{showActions && (
|
||||
<div className="flex items-center justify-between pt-4 border-t border-gray-100">
|
||||
<div className="flex space-x-2">
|
||||
{nextStatusOptions().map((status) => (
|
||||
<Button
|
||||
key={status}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => onUpdateStatus?.(plan.id, status)}
|
||||
>
|
||||
{status === PlanStatus.PENDING_APPROVAL && 'Submit for Approval'}
|
||||
{status === PlanStatus.APPROVED && 'Approve'}
|
||||
{status === PlanStatus.IN_EXECUTION && 'Start Execution'}
|
||||
{status === PlanStatus.COMPLETED && 'Mark Complete'}
|
||||
{status === PlanStatus.CANCELLED && 'Cancel'}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => onViewDetails?.(plan.id)}
|
||||
>
|
||||
View Details →
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Special Requirements */}
|
||||
{plan.special_requirements && (
|
||||
<div className="mt-4 p-3 bg-blue-50 rounded-lg">
|
||||
<h5 className="text-sm font-medium text-blue-800 mb-1">
|
||||
Special Requirements
|
||||
</h5>
|
||||
<p className="text-sm text-blue-700">{plan.special_requirements}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user