From e8caafa7a19bd5457ec82119dc77d1be19ff112f Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Fri, 9 Jan 2026 18:55:40 +0530 Subject: [PATCH] uniform format ate picker added and documents preview for activity completio documents added onDMS push step --- src/components/modals/NewRequestModal.tsx | 2 +- src/components/ui/date-picker.tsx | 186 +++++++ .../components/UserAllRequestsFilters.tsx | 31 +- .../DealerUserAllRequestsFilters.tsx | 29 +- .../ClaimManagementWizard.tsx | 8 +- .../components/request-detail/WorkflowTab.tsx | 76 +++ .../request-detail/modals/DMSPushModal.tsx | 498 +++++++++++++++++- .../modals/DealerCompletionDocumentsModal.tsx | 21 +- .../modals/DealerProposalSubmissionModal.tsx | 19 +- .../components/DashboardFiltersBar.tsx | 113 +++- .../components/sections/TATBreachReport.tsx | 5 + .../components/DateRangeFilter.tsx | 36 +- src/pages/OpenRequests/OpenRequests.tsx | 38 +- src/pages/Requests/Requests.tsx | 90 ++-- 14 files changed, 1020 insertions(+), 132 deletions(-) create mode 100644 src/components/ui/date-picker.tsx diff --git a/src/components/modals/NewRequestModal.tsx b/src/components/modals/NewRequestModal.tsx index a3f3dff..6d323b1 100644 --- a/src/components/modals/NewRequestModal.tsx +++ b/src/components/modals/NewRequestModal.tsx @@ -183,7 +183,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp diff --git a/src/components/ui/date-picker.tsx b/src/components/ui/date-picker.tsx new file mode 100644 index 0000000..069278a --- /dev/null +++ b/src/components/ui/date-picker.tsx @@ -0,0 +1,186 @@ +"use client"; + +import * as React from "react"; +import { format, parse, isValid } from "date-fns"; +import { Calendar as CalendarIcon } from "lucide-react"; +import { cn } from "./utils"; +import { Button } from "./button"; +import { Calendar } from "./calendar"; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "./popover"; + +export interface CustomDatePickerProps { + /** + * Selected date value as string in YYYY-MM-DD format (for form compatibility) + * or Date object + */ + value?: string | Date | null; + + /** + * Callback when date changes. Returns date string in YYYY-MM-DD format + */ + onChange?: (date: string | null) => void; + + /** + * Minimum selectable date as string (YYYY-MM-DD) or Date object + */ + minDate?: string | Date | null; + + /** + * Maximum selectable date as string (YYYY-MM-DD) or Date object + */ + maxDate?: string | Date | null; + + /** + * Placeholder text + */ + placeholderText?: string; + + /** + * Whether the date picker is disabled + */ + disabled?: boolean; + + /** + * Additional CSS classes + */ + className?: string; + + /** + * CSS classes for the wrapper div + */ + wrapperClassName?: string; + + /** + * Error state - shows red border + */ + error?: boolean; + + /** + * Display format (default: "dd/MM/yyyy") + */ + displayFormat?: string; + + /** + * ID for accessibility + */ + id?: string; +} + +/** + * Reusable DatePicker component with consistent dd/MM/yyyy format and button trigger. + * Uses native Calendar component wrapped in a Popover. + */ +export function CustomDatePicker({ + value, + onChange, + minDate, + maxDate, + placeholderText = "dd/mm/yyyy", + disabled = false, + className, + wrapperClassName, + error = false, + displayFormat = "dd/MM/yyyy", + id, +}: CustomDatePickerProps) { + const [isPopoverOpen, setIsPopoverOpen] = React.useState(false); + + // Convert input value to Date object for Calendar + const selectedDate = React.useMemo(() => { + if (!value) return undefined; + if (value instanceof Date) { + return isValid(value) ? value : undefined; + } + if (typeof value === "string") { + try { + const parsed = parse(value, "yyyy-MM-dd", new Date()); + return isValid(parsed) ? parsed : undefined; + } catch (e) { + return undefined; + } + } + return undefined; + }, [value]); + + // Convert minDate + const minDateObj = React.useMemo(() => { + if (!minDate) return undefined; + if (minDate instanceof Date) return isValid(minDate) ? minDate : undefined; + if (typeof minDate === "string") { + const parsed = parse(minDate, "yyyy-MM-dd", new Date()); + return isValid(parsed) ? parsed : undefined; + } + return undefined; + }, [minDate]); + + // Convert maxDate + const maxDateObj = React.useMemo(() => { + if (!maxDate) return undefined; + if (maxDate instanceof Date) return isValid(maxDate) ? maxDate : undefined; + if (typeof maxDate === "string") { + const parsed = parse(maxDate, "yyyy-MM-dd", new Date()); + return isValid(parsed) ? parsed : undefined; + } + return undefined; + }, [maxDate]); + + const handleSelect = (date: Date | undefined) => { + setIsPopoverOpen(false); + if (!onChange) return; + + if (!date) { + onChange(null); + return; + } + + // Return YYYY-MM-DD string + onChange(format(date, "yyyy-MM-dd")); + }; + + return ( +
+ + + + + + { + if (minDateObj && date < minDateObj) return true; + if (maxDateObj && date > maxDateObj) return true; + return false; + }} + initialFocus + /> + + +
+ ); +} + +export default CustomDatePicker; + diff --git a/src/custom/components/UserAllRequestsFilters.tsx b/src/custom/components/UserAllRequestsFilters.tsx index 478d335..2e432ab 100644 --- a/src/custom/components/UserAllRequestsFilters.tsx +++ b/src/custom/components/UserAllRequestsFilters.tsx @@ -17,6 +17,7 @@ import { Separator } from '@/components/ui/separator'; import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react'; import { format } from 'date-fns'; import type { DateRange } from '@/services/dashboard.service'; +import { CustomDatePicker } from '@/components/ui/date-picker'; interface StandardUserAllRequestsFiltersProps { // Filters @@ -381,12 +382,10 @@ export function StandardUserAllRequestsFilters({
- { - const date = e.target.value ? new Date(e.target.value) : undefined; + { + const date = dateStr ? new Date(dateStr) : undefined; if (date) { onCustomStartDateChange?.(date); if (customEndDate && date > customEndDate) { @@ -396,18 +395,17 @@ export function StandardUserAllRequestsFilters({ onCustomStartDateChange?.(undefined); } }} - max={format(new Date(), 'yyyy-MM-dd')} + maxDate={new Date()} + placeholderText="dd/mm/yyyy" className="w-full" />
- { - const date = e.target.value ? new Date(e.target.value) : undefined; + { + const date = dateStr ? new Date(dateStr) : undefined; if (date) { onCustomEndDateChange?.(date); if (customStartDate && date < customStartDate) { @@ -417,8 +415,9 @@ export function StandardUserAllRequestsFilters({ onCustomEndDateChange?.(undefined); } }} - min={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : undefined} - max={format(new Date(), 'yyyy-MM-dd')} + minDate={customStartDate || undefined} + maxDate={new Date()} + placeholderText="dd/mm/yyyy" className="w-full" />
@@ -430,7 +429,7 @@ export function StandardUserAllRequestsFilters({ disabled={!customStartDate || !customEndDate} className="flex-1 bg-re-green hover:bg-re-green/90" > - Apply + Apply Range @@ -681,7 +681,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar className="w-full justify-start text-left mt-2 h-12 pl-3" > - {formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Start date'} + {formData.periodStartDate ? format(formData.periodStartDate, 'd MMM yyyy') : 'Start date'} @@ -707,7 +707,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar disabled={!formData.periodStartDate} > - {formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'End date'} + {formData.periodEndDate ? format(formData.periodEndDate, 'd MMM yyyy') : 'End date'} @@ -730,7 +730,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
{formData.periodStartDate && formData.periodEndDate ? (

- Period: {format(formData.periodStartDate, 'MMM dd, yyyy')} - {format(formData.periodEndDate, 'MMM dd, yyyy')} + Period: {format(formData.periodStartDate, 'd MMM yyyy')} - {format(formData.periodEndDate, 'd MMM yyyy')}

) : (

diff --git a/src/dealer-claim/components/request-detail/WorkflowTab.tsx b/src/dealer-claim/components/request-detail/WorkflowTab.tsx index 39cf419..49bc4ff 100644 --- a/src/dealer-claim/components/request-detail/WorkflowTab.tsx +++ b/src/dealer-claim/components/request-detail/WorkflowTab.tsx @@ -1073,6 +1073,81 @@ export function DealerClaimWorkflowTab({ loadProposalData(); }, [request]); + // Extract completion documents data from request/documents + const [completionDocumentsData, setCompletionDocumentsData] = useState(null); + + useEffect(() => { + if (!request) { + setCompletionDocumentsData(null); + return; + } + + const loadCompletionDocuments = async () => { + try { + const requestId = request.id || request.requestId; + if (!requestId) { + setCompletionDocumentsData(null); + return; + } + + // Get workflow details which includes all documents + const details = await getWorkflowDetails(requestId); + const documents = details?.documents || []; + + // Filter and categorize documents + const completionDocs: any[] = []; + const activityPhotos: any[] = []; + const invoicesReceipts: any[] = []; + let attendanceSheet: any = null; + + documents.forEach((doc: any) => { + const category = (doc.category || doc.documentCategory || doc.type || '').toUpperCase(); + const name = (doc.fileName || doc.file_name || doc.name || '').toLowerCase(); + + const docObj = { + name: doc.fileName || doc.file_name || doc.name, + id: doc.documentId || doc.document_id || doc.id, + url: doc.url || doc.storageUrl || doc.storage_url, + }; + + if (category === 'COMPLETION' || category === 'COMPLETION_DOCUMENT') { + completionDocs.push(docObj); + } else if (category === 'ACTIVITY_PHOTO' || category === 'PHOTO' || category === 'IMAGE') { + activityPhotos.push(docObj); + } else if (category === 'ATTENDANCE' || category === 'ATTENDANCE_SHEET') { + attendanceSheet = docObj; + } else if (category === 'SUPPORTING' || category === 'INVOICE' || category === 'RECEIPT') { + // Check if it's likely an attendance sheet based on name if not already found + if (!attendanceSheet && (name.includes('attendance') || name.includes('participant'))) { + attendanceSheet = docObj; + } else { + invoicesReceipts.push(docObj); + } + } + }); + + // If documents came from the completionDetails directly (some backends might structure it this way) + if (completionDocs.length === 0 && activityPhotos.length === 0 && request.completionDetails) { + // Try to extract from request.completionDetails if available/applicable + // This is a fallback in case documents aren't flattened in the main documents list yet + } + + setCompletionDocumentsData({ + completionDocuments: completionDocs, + activityPhotos: activityPhotos, + invoicesReceipts: invoicesReceipts, + attendanceSheet: attendanceSheet, + }); + + } catch (error) { + console.warn('Failed to load completion documents:', error); + setCompletionDocumentsData(null); + } + }; + + loadCompletionDocuments(); + }, [request]); + // Get dealer and activity info const dealerName = request?.claimDetails?.dealerName || request?.dealerInfo?.name || @@ -1749,6 +1824,7 @@ export function DealerClaimWorkflowTab({ availableBalance: request?.internalOrder?.ioAvailableBalance || request?.internalOrder?.io_available_balance || request?.internal_order?.ioAvailableBalance || request?.internal_order?.io_available_balance, remainingBalance: request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance, }} + completionDocuments={completionDocumentsData} requestTitle={request?.title} requestNumber={request?.requestNumber || request?.request_number || request?.id} /> diff --git a/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx b/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx index 37b66d9..1d44313 100644 --- a/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx +++ b/src/dealer-claim/components/request-detail/modals/DMSPushModal.tsx @@ -4,7 +4,7 @@ * Allows user to verify completion details and expenses before pushing to DMS */ -import { useState, useMemo } from 'react'; +import { useState, useMemo, useEffect } from 'react'; import { Dialog, DialogContent, @@ -18,14 +18,19 @@ 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, +import { + Receipt, + DollarSign, TriangleAlert, Activity, CheckCircle2, + Download, + Eye, + Loader2, } from 'lucide-react'; import { toast } from 'sonner'; +import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi'; +import '@/components/common/FilePreview/FilePreview.css'; import './DMSPushModal.css'; interface ExpenseItem { @@ -48,12 +53,36 @@ interface IODetails { remainingBalance?: number; } +interface CompletionDocuments { + completionDocuments?: Array<{ + name: string; + url?: string; + id?: string; + }>; + activityPhotos?: Array<{ + name: string; + url?: string; + id?: string; + }>; + invoicesReceipts?: Array<{ + name: string; + url?: string; + id?: string; + }>; + attendanceSheet?: { + name: string; + url?: string; + id?: string; + }; +} + interface DMSPushModalProps { isOpen: boolean; onClose: () => void; onPush: (comments: string) => Promise; completionDetails?: CompletionDetails | null; ioDetails?: IODetails | null; + completionDocuments?: CompletionDocuments | null; requestTitle?: string; requestNumber?: string; } @@ -64,11 +93,19 @@ export function DMSPushModal({ onPush, completionDetails, ioDetails, + completionDocuments, requestTitle, requestNumber, }: DMSPushModalProps) { const [comments, setComments] = useState(''); const [submitting, setSubmitting] = useState(false); + const [previewDocument, setPreviewDocument] = useState<{ + name: string; + url: string; + type?: string; + size?: number; + } | null>(null); + const [previewLoading, setPreviewLoading] = useState(false); const commentsChars = comments.length; const maxCommentsChars = 500; @@ -107,6 +144,88 @@ export function DMSPushModal({ return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`; }; + // Check if document can be previewed + const canPreviewDocument = (doc: { name: string; url?: string; id?: string }): boolean => { + if (!doc.name) return false; + const name = doc.name.toLowerCase(); + return name.endsWith('.pdf') || + name.endsWith('.jpg') || + name.endsWith('.jpeg') || + name.endsWith('.png') || + name.endsWith('.gif') || + name.endsWith('.webp'); + }; + + // Handle document preview - fetch as blob to avoid CSP issues + const handlePreviewDocument = async (doc: { name: string; url?: string; id?: string }) => { + if (!doc.id) { + toast.error('Document preview not available - document ID missing'); + return; + } + + setPreviewLoading(true); + try { + const previewUrl = getDocumentPreviewUrl(doc.id); + + // Determine file type from name + const fileName = doc.name.toLowerCase(); + const isPDF = fileName.endsWith('.pdf'); + const isImage = fileName.match(/\.(jpg|jpeg|png|gif|webp)$/i); + + // Fetch document as a blob to create a blob URL (CSP compliant) + const isProduction = import.meta.env.PROD || import.meta.env.MODE === 'production'; + const token = isProduction ? null : localStorage.getItem('accessToken'); + + const headers: HeadersInit = { + 'Accept': isPDF ? 'application/pdf' : '*/*' + }; + + if (!isProduction && token) { + headers['Authorization'] = `Bearer ${token}`; + } + + const response = await fetch(previewUrl, { + headers, + credentials: 'include', + mode: 'cors' + }); + + if (!response.ok) { + throw new Error(`Failed to load file: ${response.status} ${response.statusText}`); + } + + const blob = await response.blob(); + + if (blob.size === 0) { + throw new Error('File is empty or could not be loaded'); + } + + // Create blob URL (CSP compliant - uses 'blob:' protocol) + const blobUrl = window.URL.createObjectURL(blob); + + setPreviewDocument({ + name: doc.name, + url: blobUrl, + type: blob.type || (isPDF ? 'application/pdf' : isImage ? 'image' : undefined), + size: blob.size, + }); + } catch (error) { + console.error('Failed to load document preview:', error); + toast.error('Failed to load document preview'); + } finally { + setPreviewLoading(false); + } + }; + + // Cleanup blob URLs on unmount + useEffect(() => { + return () => { + if (previewDocument?.url && previewDocument.url.startsWith('blob:')) { + window.URL.revokeObjectURL(previewDocument.url); + } + }; + }, [previewDocument]); + const handleSubmit = async () => { if (!comments.trim()) { toast.error('Please provide comments before pushing to DMS'); @@ -308,6 +427,273 @@ export function DMSPushModal({ )}

+ {/* Completion Documents Section */} + {completionDocuments && ( +
+ {/* Completion Documents */} + {completionDocuments.completionDocuments && completionDocuments.completionDocuments.length > 0 && ( +
+
+

+ + Completion Documents +

+ + {completionDocuments.completionDocuments.length} file(s) + +
+
+ {completionDocuments.completionDocuments.map((doc, index) => ( +
+
+ +
+

+ {doc.name} +

+
+
+ {doc.id && ( +
+ {canPreviewDocument(doc) && ( + + )} + +
+ )} +
+ ))} +
+
+ )} + + {/* Activity Photos */} + {completionDocuments.activityPhotos && completionDocuments.activityPhotos.length > 0 && ( +
+
+

+ + Activity Photos +

+ + {completionDocuments.activityPhotos.length} file(s) + +
+
+ {completionDocuments.activityPhotos.map((doc, index) => ( +
+
+ +
+

+ {doc.name} +

+
+
+ {doc.id && ( +
+ {canPreviewDocument(doc) && ( + + )} + +
+ )} +
+ ))} +
+
+ )} + + {/* Invoices / Receipts */} + {completionDocuments.invoicesReceipts && completionDocuments.invoicesReceipts.length > 0 && ( +
+
+

+ + Invoices / Receipts +

+ + {completionDocuments.invoicesReceipts.length} file(s) + +
+
+ {completionDocuments.invoicesReceipts.map((doc, index) => ( +
+
+ +
+

+ {doc.name} +

+
+
+ {doc.id && ( +
+ {canPreviewDocument(doc) && ( + + )} + +
+ )} +
+ ))} +
+
+ )} + + {/* Attendance Sheet */} + {completionDocuments.attendanceSheet && ( +
+
+

+ + Attendance Sheet +

+
+
+
+ +
+

+ {completionDocuments.attendanceSheet.name} +

+
+
+ {completionDocuments.attendanceSheet.id && ( +
+ {canPreviewDocument(completionDocuments.attendanceSheet) && ( + + )} + +
+ )} +
+
+ )} +
+ )} + {/* Verification Warning */}
@@ -376,6 +762,110 @@ export function DMSPushModal({ + + {/* File Preview Modal - Matching DocumentsTab style */} + {previewDocument && ( + setPreviewDocument(null)} + > + +
+ +
+
+ +
+ + {previewDocument.name} + + {previewDocument.type && ( +

+ {previewDocument.type} {previewDocument.size && `• ${(previewDocument.size / 1024).toFixed(1)} KB`} +

+ )} +
+
+
+ +
+
+
+ +
+ {previewLoading ? ( +
+
+ +

Loading preview...

+
+
+ ) : previewDocument.name.toLowerCase().endsWith('.pdf') || previewDocument.type?.includes('pdf') ? ( +
+