292 lines
11 KiB
TypeScript
292 lines
11 KiB
TypeScript
/**
|
|
* ProcessDetailsCard Component
|
|
* Displays process-related details: IO Number, DMS Number, Claim Amount, and Budget Breakdowns
|
|
* Visibility controlled by user role
|
|
*/
|
|
|
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Activity, Receipt, DollarSign, Pen } from 'lucide-react';
|
|
import { format } from 'date-fns';
|
|
|
|
// Local minimal types to avoid external dependency issues
|
|
interface IODetails {
|
|
ioNumber?: string;
|
|
remarks?: string;
|
|
availableBalance?: number;
|
|
blockedAmount?: number;
|
|
remainingBalance?: number;
|
|
blockedByName?: string;
|
|
blockedAt?: string;
|
|
}
|
|
|
|
interface DMSDetails {
|
|
dmsNumber?: string;
|
|
remarks?: string;
|
|
createdByName?: string;
|
|
createdAt?: string;
|
|
}
|
|
|
|
interface ClaimAmountDetails {
|
|
amount: number;
|
|
lastUpdatedBy?: string;
|
|
lastUpdatedAt?: string;
|
|
}
|
|
|
|
interface CostBreakdownItem {
|
|
description: string;
|
|
amount: number;
|
|
}
|
|
|
|
interface RoleBasedVisibility {
|
|
showIODetails: boolean;
|
|
showDMSDetails: boolean;
|
|
showClaimAmount: boolean;
|
|
canEditClaimAmount: boolean;
|
|
}
|
|
|
|
interface ProcessDetailsCardProps {
|
|
ioDetails?: IODetails;
|
|
dmsDetails?: DMSDetails;
|
|
claimAmount?: ClaimAmountDetails;
|
|
estimatedBudgetBreakdown?: CostBreakdownItem[];
|
|
closedExpensesBreakdown?: CostBreakdownItem[];
|
|
visibility: RoleBasedVisibility;
|
|
onEditClaimAmount?: () => void;
|
|
className?: string;
|
|
}
|
|
|
|
export function ProcessDetailsCard({
|
|
ioDetails,
|
|
dmsDetails,
|
|
claimAmount,
|
|
estimatedBudgetBreakdown,
|
|
closedExpensesBreakdown,
|
|
visibility,
|
|
onEditClaimAmount,
|
|
className,
|
|
}: ProcessDetailsCardProps) {
|
|
const formatCurrency = (amount?: number | null) => {
|
|
if (amount === undefined || amount === null || Number.isNaN(amount)) {
|
|
return '₹0.00';
|
|
}
|
|
return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
|
};
|
|
|
|
const formatDate = (dateString?: string | null) => {
|
|
if (!dateString) return '';
|
|
try {
|
|
return format(new Date(dateString), 'MMM d, yyyy, h:mm a');
|
|
} catch {
|
|
return dateString || '';
|
|
}
|
|
};
|
|
|
|
const calculateTotal = (items?: CostBreakdownItem[]) => {
|
|
if (!items || items.length === 0) return 0;
|
|
return items.reduce((sum, item) => sum + (item.amount ?? 0), 0);
|
|
};
|
|
|
|
// Don't render if nothing to show
|
|
const hasContent =
|
|
(visibility.showIODetails && ioDetails) ||
|
|
(visibility.showDMSDetails && dmsDetails) ||
|
|
(visibility.showClaimAmount && claimAmount && claimAmount.amount !== undefined && claimAmount.amount !== null) ||
|
|
(estimatedBudgetBreakdown && estimatedBudgetBreakdown.length > 0) ||
|
|
(closedExpensesBreakdown && closedExpensesBreakdown.length > 0);
|
|
|
|
if (!hasContent) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<Card className={`bg-gradient-to-br from-blue-50 to-purple-50 border-2 border-blue-200 ${className}`}>
|
|
<CardHeader>
|
|
<CardTitle className="text-base flex items-center gap-2">
|
|
<Activity className="w-4 h-4 text-blue-600" />
|
|
Process Details
|
|
</CardTitle>
|
|
<CardDescription>Workflow reference numbers</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-3">
|
|
{/* IO Details - Only visible to internal RE users */}
|
|
{visibility.showIODetails && ioDetails && (
|
|
<div className="bg-white rounded-lg p-3 border border-blue-200">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Receipt className="w-4 h-4 text-blue-600" />
|
|
<Label className="text-xs font-semibold text-blue-900 uppercase tracking-wide">
|
|
IO Number
|
|
</Label>
|
|
</div>
|
|
<p className="font-bold text-gray-900 mb-2">{ioDetails.ioNumber}</p>
|
|
|
|
{ioDetails.remarks && (
|
|
<div className="pt-2 border-t border-blue-100">
|
|
<p className="text-xs text-gray-600 mb-1">Remark:</p>
|
|
<p className="text-xs text-gray-900">{ioDetails.remarks}</p>
|
|
</div>
|
|
)}
|
|
|
|
{/* Budget Details */}
|
|
{(ioDetails.availableBalance !== undefined || ioDetails.blockedAmount !== undefined) && (
|
|
<div className="pt-2 border-t border-blue-100 mt-2 space-y-1">
|
|
{ioDetails.availableBalance !== undefined && (
|
|
<div className="flex justify-between text-xs">
|
|
<span className="text-gray-600">Available Balance:</span>
|
|
<span className="font-medium text-gray-900">
|
|
{formatCurrency(ioDetails.availableBalance)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{ioDetails.blockedAmount !== undefined && (
|
|
<div className="flex justify-between text-xs">
|
|
<span className="text-gray-600">Blocked Amount:</span>
|
|
<span className="font-medium text-blue-700">
|
|
{formatCurrency(ioDetails.blockedAmount)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
{ioDetails.remainingBalance !== undefined && (
|
|
<div className="flex justify-between text-xs">
|
|
<span className="text-gray-600">Remaining Balance:</span>
|
|
<span className="font-medium text-green-700">
|
|
{formatCurrency(ioDetails.remainingBalance)}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="pt-2 border-t border-blue-100 mt-2">
|
|
<p className="text-xs text-gray-500">By {ioDetails.blockedByName}</p>
|
|
<p className="text-xs text-gray-500">{formatDate(ioDetails.blockedAt)}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* DMS Details */}
|
|
{visibility.showDMSDetails && dmsDetails && (
|
|
<div className="bg-white rounded-lg p-3 border border-purple-200">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Activity className="w-4 h-4 text-purple-600" />
|
|
<Label className="text-xs font-semibold text-purple-900 uppercase tracking-wide">
|
|
DMS Number
|
|
</Label>
|
|
</div>
|
|
<p className="font-bold text-gray-900 mb-2">{dmsDetails.dmsNumber}</p>
|
|
|
|
{dmsDetails.remarks && (
|
|
<div className="pt-2 border-t border-purple-100">
|
|
<p className="text-xs text-gray-600 mb-1">Remarks:</p>
|
|
<p className="text-xs text-gray-900">{dmsDetails.remarks}</p>
|
|
</div>
|
|
)}
|
|
|
|
<div className="pt-2 border-t border-purple-100 mt-2">
|
|
<p className="text-xs text-gray-500">By {dmsDetails.createdByName}</p>
|
|
<p className="text-xs text-gray-500">{formatDate(dmsDetails.createdAt)}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Claim Amount */}
|
|
{visibility.showClaimAmount && claimAmount && (
|
|
<div className="bg-white rounded-lg p-3 border border-green-200">
|
|
<div className="flex items-center justify-between gap-2 mb-2">
|
|
<div className="flex items-center gap-2">
|
|
<DollarSign className="w-4 h-4 text-green-600" />
|
|
<Label className="text-xs font-semibold text-green-900 uppercase tracking-wide">
|
|
Claim Amount
|
|
</Label>
|
|
</div>
|
|
{visibility.canEditClaimAmount && onEditClaimAmount && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={onEditClaimAmount}
|
|
className="h-7 px-2 text-xs border-green-300 hover:bg-green-50"
|
|
>
|
|
<Pen className="w-3 h-3 mr-1 text-green-700" />
|
|
Edit
|
|
</Button>
|
|
)}
|
|
</div>
|
|
<p className="text-2xl font-bold text-green-700">
|
|
{formatCurrency(claimAmount.amount)}
|
|
</p>
|
|
{claimAmount.lastUpdatedBy && (
|
|
<div className="mt-2 pt-2 border-t border-green-100">
|
|
<p className="text-xs text-gray-500">
|
|
Last updated by {claimAmount.lastUpdatedBy}
|
|
</p>
|
|
{claimAmount.lastUpdatedAt && (
|
|
<p className="text-xs text-gray-500">
|
|
{formatDate(claimAmount.lastUpdatedAt)}
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Estimated Budget Breakdown */}
|
|
{estimatedBudgetBreakdown && estimatedBudgetBreakdown.length > 0 && (
|
|
<div className="bg-white rounded-lg p-3 border border-amber-200">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Receipt className="w-4 h-4 text-amber-600" />
|
|
<Label className="text-xs font-semibold text-amber-900 uppercase tracking-wide">
|
|
Estimated Budget Breakdown
|
|
</Label>
|
|
</div>
|
|
<div className="space-y-1.5 pt-1">
|
|
{estimatedBudgetBreakdown.map((item, index) => (
|
|
<div key={index} className="flex justify-between items-center text-xs">
|
|
<span className="text-gray-700">{item.description}</span>
|
|
<span className="font-medium text-gray-900">
|
|
{formatCurrency(item.amount)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
<div className="pt-2 border-t border-amber-200 flex justify-between items-center">
|
|
<span className="font-semibold text-gray-900 text-xs">Total</span>
|
|
<span className="font-bold text-amber-700">
|
|
{formatCurrency(calculateTotal(estimatedBudgetBreakdown))}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Closed Expenses Breakdown */}
|
|
{closedExpensesBreakdown && closedExpensesBreakdown.length > 0 && (
|
|
<div className="bg-white rounded-lg p-3 border border-indigo-200">
|
|
<div className="flex items-center gap-2 mb-2">
|
|
<Receipt className="w-4 h-4 text-indigo-600" />
|
|
<Label className="text-xs font-semibold text-indigo-900 uppercase tracking-wide">
|
|
Closed Expenses Breakdown
|
|
</Label>
|
|
</div>
|
|
<div className="space-y-1.5 pt-1">
|
|
{closedExpensesBreakdown.map((item, index) => (
|
|
<div key={index} className="flex justify-between items-center text-xs">
|
|
<span className="text-gray-700">{item.description}</span>
|
|
<span className="font-medium text-gray-900">
|
|
{formatCurrency(item.amount)}
|
|
</span>
|
|
</div>
|
|
))}
|
|
<div className="pt-2 border-t border-indigo-200 flex justify-between items-center">
|
|
<span className="font-semibold text-gray-900 text-xs">Total</span>
|
|
<span className="font-bold text-indigo-700">
|
|
{formatCurrency(calculateTotal(closedExpensesBreakdown))}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
);
|
|
}
|