Re_Figma_Code/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx

377 lines
13 KiB
TypeScript

/**
* DMSPushModal Component
* Modal for Step 6: Push to DMS Verification
* Allows user to verify completion details and expenses before pushing to DMS
*/
import { useState, useMemo } from 'react';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import {
Receipt,
DollarSign,
TriangleAlert,
Activity,
CheckCircle2,
} from 'lucide-react';
import { toast } from 'sonner';
interface ExpenseItem {
description: string;
amount: number;
}
interface CompletionDetails {
activityCompletionDate?: string;
numberOfParticipants?: number;
closedExpenses?: ExpenseItem[];
totalClosedExpenses?: number;
completionDescription?: string;
}
interface IODetails {
ioNumber?: string;
blockedAmount?: number;
availableBalance?: number;
remainingBalance?: number;
}
interface DMSPushModalProps {
isOpen: boolean;
onClose: () => void;
onPush: (comments: string) => Promise<void>;
completionDetails?: CompletionDetails | null;
ioDetails?: IODetails | null;
requestTitle?: string;
requestNumber?: string;
}
export function DMSPushModal({
isOpen,
onClose,
onPush,
completionDetails,
ioDetails,
requestTitle,
requestNumber,
}: DMSPushModalProps) {
const [comments, setComments] = useState('');
const [submitting, setSubmitting] = useState(false);
const commentsChars = comments.length;
const maxCommentsChars = 500;
// Calculate total closed expenses
const totalClosedExpenses = useMemo(() => {
if (completionDetails?.totalClosedExpenses) {
return completionDetails.totalClosedExpenses;
}
if (completionDetails?.closedExpenses && Array.isArray(completionDetails.closedExpenses)) {
return completionDetails.closedExpenses.reduce((sum, item) => {
const amount = typeof item === 'object' ? (item.amount || 0) : 0;
return sum + (Number(amount) || 0);
}, 0);
}
return 0;
}, [completionDetails]);
// Format date
const formatDate = (dateString?: string) => {
if (!dateString) return '—';
try {
const date = new Date(dateString);
return date.toLocaleDateString('en-IN', {
year: 'numeric',
month: 'long',
day: 'numeric',
});
} catch {
return dateString;
}
};
// Format currency
const formatCurrency = (amount: number) => {
return `${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
};
const handleSubmit = async () => {
if (!comments.trim()) {
toast.error('Please provide comments before pushing to DMS');
return;
}
try {
setSubmitting(true);
await onPush(comments.trim());
handleReset();
onClose();
} catch (error) {
console.error('Failed to push to DMS:', error);
toast.error('Failed to push to DMS. Please try again.');
} finally {
setSubmitting(false);
}
};
const handleReset = () => {
setComments('');
};
const handleClose = () => {
if (!submitting) {
handleReset();
onClose();
}
};
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-3xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<div className="flex items-center gap-3 mb-2">
<div className="p-2 rounded-lg bg-indigo-100">
<Activity className="w-6 h-6 text-indigo-600" />
</div>
<div className="flex-1">
<DialogTitle className="font-semibold text-xl">
Push to DMS - Verification
</DialogTitle>
<DialogDescription className="text-sm mt-1">
Review completion details and expenses before pushing to DMS for e-invoice generation
</DialogDescription>
</div>
</div>
{/* Request Info Card */}
<div className="space-y-3 p-4 bg-gray-50 rounded-lg border">
<div className="flex items-center justify-between">
<span className="font-medium text-gray-900">Workflow Step:</span>
<Badge variant="outline" className="font-mono">Step 6</Badge>
</div>
{requestNumber && (
<div>
<span className="font-medium text-gray-900">Request Number:</span>
<p className="text-gray-700 mt-1 font-mono">{requestNumber}</p>
</div>
)}
<div>
<span className="font-medium text-gray-900">Title:</span>
<p className="text-gray-700 mt-1">{requestTitle || '—'}</p>
</div>
<div className="flex items-center gap-2">
<span className="font-medium text-gray-900">Action:</span>
<Badge className="bg-indigo-100 text-indigo-800 border-indigo-200">
<Activity className="w-3 h-3 mr-1" />
PUSH TO DMS
</Badge>
</div>
</div>
</DialogHeader>
<div className="space-y-4">
{/* Completion Details Card */}
{completionDetails && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<CheckCircle2 className="w-5 h-5 text-green-600" />
Completion Details
</CardTitle>
<CardDescription>
Review activity completion information
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
{completionDetails.activityCompletionDate && (
<div className="flex items-center justify-between py-2 border-b">
<span className="text-sm text-gray-600">Activity Completion Date:</span>
<span className="text-sm font-semibold text-gray-900">
{formatDate(completionDetails.activityCompletionDate)}
</span>
</div>
)}
{completionDetails.numberOfParticipants !== undefined && (
<div className="flex items-center justify-between py-2 border-b">
<span className="text-sm text-gray-600">Number of Participants:</span>
<span className="text-sm font-semibold text-gray-900">
{completionDetails.numberOfParticipants}
</span>
</div>
)}
{completionDetails.completionDescription && (
<div className="pt-2">
<p className="text-xs text-gray-600 mb-1">Completion Description:</p>
<p className="text-sm text-gray-900">
{completionDetails.completionDescription}
</p>
</div>
)}
</CardContent>
</Card>
)}
{/* Expense Breakdown Card */}
{completionDetails?.closedExpenses && completionDetails.closedExpenses.length > 0 && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<DollarSign className="w-5 h-5 text-blue-600" />
Expense Breakdown
</CardTitle>
<CardDescription>
Review closed expenses before pushing to DMS
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-2">
{completionDetails.closedExpenses.map((expense, index) => (
<div
key={index}
className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded border"
>
<div className="flex-1">
<p className="text-sm font-medium text-gray-900">
{expense.description || `Expense ${index + 1}`}
</p>
</div>
<div className="ml-4">
<p className="text-sm font-semibold text-gray-900">
{formatCurrency(typeof expense === 'object' ? (expense.amount || 0) : 0)}
</p>
</div>
</div>
))}
<div className="flex items-center justify-between py-3 px-3 bg-blue-50 rounded border-2 border-blue-200 mt-3">
<span className="text-sm font-semibold text-gray-900">Total Closed Expenses:</span>
<span className="text-lg font-bold text-blue-700">
{formatCurrency(totalClosedExpenses)}
</span>
</div>
</div>
</CardContent>
</Card>
)}
{/* IO Details Card */}
{ioDetails && ioDetails.ioNumber && (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2 text-lg">
<Receipt className="w-5 h-5 text-purple-600" />
IO Details
</CardTitle>
<CardDescription>
Internal Order information for budget reference
</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<div className="flex items-center justify-between py-2 border-b">
<span className="text-sm text-gray-600">IO Number:</span>
<span className="text-sm font-semibold text-gray-900 font-mono">
{ioDetails.ioNumber}
</span>
</div>
{ioDetails.blockedAmount !== undefined && ioDetails.blockedAmount > 0 && (
<div className="flex items-center justify-between py-2 border-b">
<span className="text-sm text-gray-600">Blocked Amount:</span>
<span className="text-sm font-bold text-green-700">
{formatCurrency(ioDetails.blockedAmount)}
</span>
</div>
)}
{ioDetails.remainingBalance !== undefined && ioDetails.remainingBalance !== null && (
<div className="flex items-center justify-between py-2">
<span className="text-sm text-gray-600">Remaining Balance:</span>
<span className="text-sm font-semibold text-gray-900">
{formatCurrency(ioDetails.remainingBalance)}
</span>
</div>
)}
</CardContent>
</Card>
)}
{/* Verification Warning */}
<div className="p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-start gap-2">
<TriangleAlert className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" />
<div>
<p className="text-sm font-semibold text-yellow-900">
Please verify all details before pushing to DMS
</p>
<p className="text-xs text-yellow-700 mt-1">
Once pushed, the system will automatically generate an e-invoice and the workflow will proceed to Step 7.
</p>
</div>
</div>
</div>
{/* Comments & Remarks */}
<div className="space-y-1.5">
<Label htmlFor="comment" className="text-sm font-semibold text-gray-900 flex items-center gap-2">
Comments & Remarks <span className="text-red-500">*</span>
</Label>
<Textarea
id="comment"
placeholder="Enter your comments about pushing to DMS (e.g., verified expenses, ready for invoice generation)..."
value={comments}
onChange={(e) => {
const value = e.target.value;
if (value.length <= maxCommentsChars) {
setComments(value);
}
}}
rows={4}
className="text-sm min-h-[80px] resize-none"
/>
<div className="flex items-center justify-between text-xs text-gray-500">
<div className="flex items-center gap-1">
<TriangleAlert className="w-3 h-3" />
Required and visible to all
</div>
<span>{commentsChars}/{maxCommentsChars}</span>
</div>
</div>
</div>
<DialogFooter className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
<Button
variant="outline"
onClick={handleClose}
disabled={submitting}
>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={!comments.trim() || submitting}
className="bg-indigo-600 hover:bg-indigo-700 text-white"
>
{submitting ? (
'Pushing to DMS...'
) : (
<>
<Activity className="w-4 h-4 mr-2" />
Push to DMS
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}