uniform format ate picker added and documents preview for activity completio documents added onDMS push step
This commit is contained in:
parent
4c3d7fd28b
commit
e8caafa7a1
@ -183,7 +183,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
|
|||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="outline" className="w-full justify-start text-left">
|
<Button variant="outline" className="w-full justify-start text-left">
|
||||||
<CalendarIcon className="mr-2 h-4 w-4" />
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
{formData.slaEndDate ? format(formData.slaEndDate, 'PPP') : 'Pick a date'}
|
{formData.slaEndDate ? format(formData.slaEndDate, 'd MMM yyyy') : 'Pick a date'}
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0">
|
<PopoverContent className="w-auto p-0">
|
||||||
|
|||||||
186
src/components/ui/date-picker.tsx
Normal file
186
src/components/ui/date-picker.tsx
Normal file
@ -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 (
|
||||||
|
<div className={cn("relative", wrapperClassName)}>
|
||||||
|
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button
|
||||||
|
id={id}
|
||||||
|
disabled={disabled}
|
||||||
|
variant="outline"
|
||||||
|
className={cn(
|
||||||
|
"w-full justify-start text-left font-normal",
|
||||||
|
!selectedDate && "text-muted-foreground",
|
||||||
|
error && "border-destructive ring-destructive/20",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<CalendarIcon className="mr-2 h-4 w-4" />
|
||||||
|
{selectedDate ? (
|
||||||
|
format(selectedDate, displayFormat)
|
||||||
|
) : (
|
||||||
|
<span>{placeholderText}</span>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
|
<Calendar
|
||||||
|
mode="single"
|
||||||
|
selected={selectedDate}
|
||||||
|
onSelect={handleSelect}
|
||||||
|
disabled={(date) => {
|
||||||
|
if (minDateObj && date < minDateObj) return true;
|
||||||
|
if (maxDateObj && date > maxDateObj) return true;
|
||||||
|
return false;
|
||||||
|
}}
|
||||||
|
initialFocus
|
||||||
|
/>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CustomDatePicker;
|
||||||
|
|
||||||
@ -17,6 +17,7 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react';
|
import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import type { DateRange } from '@/services/dashboard.service';
|
import type { DateRange } from '@/services/dashboard.service';
|
||||||
|
import { CustomDatePicker } from '@/components/ui/date-picker';
|
||||||
|
|
||||||
interface StandardUserAllRequestsFiltersProps {
|
interface StandardUserAllRequestsFiltersProps {
|
||||||
// Filters
|
// Filters
|
||||||
@ -381,12 +382,10 @@ export function StandardUserAllRequestsFilters({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="start-date">Start Date</Label>
|
<Label htmlFor="start-date">Start Date</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id="start-date"
|
value={customStartDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
if (date) {
|
if (date) {
|
||||||
onCustomStartDateChange?.(date);
|
onCustomStartDateChange?.(date);
|
||||||
if (customEndDate && date > customEndDate) {
|
if (customEndDate && date > customEndDate) {
|
||||||
@ -396,18 +395,17 @@ export function StandardUserAllRequestsFilters({
|
|||||||
onCustomStartDateChange?.(undefined);
|
onCustomStartDateChange?.(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="end-date">End Date</Label>
|
<Label htmlFor="end-date">End Date</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id="end-date"
|
value={customEndDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={customEndDate ? format(customEndDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
if (date) {
|
if (date) {
|
||||||
onCustomEndDateChange?.(date);
|
onCustomEndDateChange?.(date);
|
||||||
if (customStartDate && date < customStartDate) {
|
if (customStartDate && date < customStartDate) {
|
||||||
@ -417,8 +415,9 @@ export function StandardUserAllRequestsFilters({
|
|||||||
onCustomEndDateChange?.(undefined);
|
onCustomEndDateChange?.(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
min={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : undefined}
|
minDate={customStartDate || undefined}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -430,7 +429,7 @@ export function StandardUserAllRequestsFilters({
|
|||||||
disabled={!customStartDate || !customEndDate}
|
disabled={!customStartDate || !customEndDate}
|
||||||
className="flex-1 bg-re-green hover:bg-re-green/90"
|
className="flex-1 bg-re-green hover:bg-re-green/90"
|
||||||
>
|
>
|
||||||
Apply
|
Apply Range
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import { Separator } from '@/components/ui/separator';
|
|||||||
import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react';
|
import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import type { DateRange } from '@/services/dashboard.service';
|
import type { DateRange } from '@/services/dashboard.service';
|
||||||
|
import { CustomDatePicker } from '@/components/ui/date-picker';
|
||||||
|
|
||||||
interface DealerUserAllRequestsFiltersProps {
|
interface DealerUserAllRequestsFiltersProps {
|
||||||
// Filters
|
// Filters
|
||||||
@ -313,12 +314,10 @@ export function DealerUserAllRequestsFilters({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="start-date">Start Date</Label>
|
<Label htmlFor="start-date">Start Date</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id="start-date"
|
value={customStartDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
if (date) {
|
if (date) {
|
||||||
onCustomStartDateChange?.(date);
|
onCustomStartDateChange?.(date);
|
||||||
if (customEndDate && date > customEndDate) {
|
if (customEndDate && date > customEndDate) {
|
||||||
@ -328,18 +327,17 @@ export function DealerUserAllRequestsFilters({
|
|||||||
onCustomStartDateChange?.(undefined);
|
onCustomStartDateChange?.(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="end-date">End Date</Label>
|
<Label htmlFor="end-date">End Date</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id="end-date"
|
value={customEndDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={customEndDate ? format(customEndDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
if (date) {
|
if (date) {
|
||||||
onCustomEndDateChange?.(date);
|
onCustomEndDateChange?.(date);
|
||||||
if (customStartDate && date < customStartDate) {
|
if (customStartDate && date < customStartDate) {
|
||||||
@ -349,8 +347,9 @@ export function DealerUserAllRequestsFilters({
|
|||||||
onCustomEndDateChange?.(undefined);
|
onCustomEndDateChange?.(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
min={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : undefined}
|
minDate={customStartDate || undefined}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -621,7 +621,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
className="w-full justify-start text-left mt-2 h-12 pl-3"
|
className="w-full justify-start text-left mt-2 h-12 pl-3"
|
||||||
>
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||||
<span className="flex-1 text-left">{formData.activityDate ? format(formData.activityDate, 'PPP') : 'Select date'}</span>
|
<span className="flex-1 text-left">{formData.activityDate ? format(formData.activityDate, 'd MMM yyyy') : 'Select date'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
@ -681,7 +681,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
className="w-full justify-start text-left mt-2 h-12 pl-3"
|
className="w-full justify-start text-left mt-2 h-12 pl-3"
|
||||||
>
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||||
<span className="flex-1 text-left">{formData.periodStartDate ? format(formData.periodStartDate, 'PPP') : 'Start date'}</span>
|
<span className="flex-1 text-left">{formData.periodStartDate ? format(formData.periodStartDate, 'd MMM yyyy') : 'Start date'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
@ -707,7 +707,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
disabled={!formData.periodStartDate}
|
disabled={!formData.periodStartDate}
|
||||||
>
|
>
|
||||||
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
<CalendarIcon className="mr-2 h-4 w-4 flex-shrink-0" />
|
||||||
<span className="flex-1 text-left">{formData.periodEndDate ? format(formData.periodEndDate, 'PPP') : 'End date'}</span>
|
<span className="flex-1 text-left">{formData.periodEndDate ? format(formData.periodEndDate, 'd MMM yyyy') : 'End date'}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto p-0" align="start">
|
<PopoverContent className="w-auto p-0" align="start">
|
||||||
@ -730,7 +730,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
|
|||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
{formData.periodStartDate && formData.periodEndDate ? (
|
{formData.periodStartDate && formData.periodEndDate ? (
|
||||||
<p className="text-xs text-gray-600">
|
<p className="text-xs text-gray-600">
|
||||||
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')}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<p className="text-xs text-gray-500">
|
<p className="text-xs text-gray-500">
|
||||||
|
|||||||
@ -1073,6 +1073,81 @@ export function DealerClaimWorkflowTab({
|
|||||||
loadProposalData();
|
loadProposalData();
|
||||||
}, [request]);
|
}, [request]);
|
||||||
|
|
||||||
|
// Extract completion documents data from request/documents
|
||||||
|
const [completionDocumentsData, setCompletionDocumentsData] = useState<any | null>(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
|
// Get dealer and activity info
|
||||||
const dealerName = request?.claimDetails?.dealerName ||
|
const dealerName = request?.claimDetails?.dealerName ||
|
||||||
request?.dealerInfo?.name ||
|
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,
|
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,
|
remainingBalance: request?.internalOrder?.ioRemainingBalance || request?.internalOrder?.io_remaining_balance || request?.internal_order?.ioRemainingBalance || request?.internal_order?.io_remaining_balance,
|
||||||
}}
|
}}
|
||||||
|
completionDocuments={completionDocumentsData}
|
||||||
requestTitle={request?.title}
|
requestTitle={request?.title}
|
||||||
requestNumber={request?.requestNumber || request?.request_number || request?.id}
|
requestNumber={request?.requestNumber || request?.request_number || request?.id}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -4,7 +4,7 @@
|
|||||||
* Allows user to verify completion details and expenses before pushing to DMS
|
* Allows user to verify completion details and expenses before pushing to DMS
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@ -24,8 +24,13 @@ import {
|
|||||||
TriangleAlert,
|
TriangleAlert,
|
||||||
Activity,
|
Activity,
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
|
Download,
|
||||||
|
Eye,
|
||||||
|
Loader2,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
import { getDocumentPreviewUrl, downloadDocument } from '@/services/workflowApi';
|
||||||
|
import '@/components/common/FilePreview/FilePreview.css';
|
||||||
import './DMSPushModal.css';
|
import './DMSPushModal.css';
|
||||||
|
|
||||||
interface ExpenseItem {
|
interface ExpenseItem {
|
||||||
@ -48,12 +53,36 @@ interface IODetails {
|
|||||||
remainingBalance?: number;
|
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 {
|
interface DMSPushModalProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
onPush: (comments: string) => Promise<void>;
|
onPush: (comments: string) => Promise<void>;
|
||||||
completionDetails?: CompletionDetails | null;
|
completionDetails?: CompletionDetails | null;
|
||||||
ioDetails?: IODetails | null;
|
ioDetails?: IODetails | null;
|
||||||
|
completionDocuments?: CompletionDocuments | null;
|
||||||
requestTitle?: string;
|
requestTitle?: string;
|
||||||
requestNumber?: string;
|
requestNumber?: string;
|
||||||
}
|
}
|
||||||
@ -64,11 +93,19 @@ export function DMSPushModal({
|
|||||||
onPush,
|
onPush,
|
||||||
completionDetails,
|
completionDetails,
|
||||||
ioDetails,
|
ioDetails,
|
||||||
|
completionDocuments,
|
||||||
requestTitle,
|
requestTitle,
|
||||||
requestNumber,
|
requestNumber,
|
||||||
}: DMSPushModalProps) {
|
}: DMSPushModalProps) {
|
||||||
const [comments, setComments] = useState('');
|
const [comments, setComments] = useState('');
|
||||||
const [submitting, setSubmitting] = useState(false);
|
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 commentsChars = comments.length;
|
||||||
const maxCommentsChars = 500;
|
const maxCommentsChars = 500;
|
||||||
@ -107,6 +144,88 @@ export function DMSPushModal({
|
|||||||
return `₹${amount.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
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 () => {
|
const handleSubmit = async () => {
|
||||||
if (!comments.trim()) {
|
if (!comments.trim()) {
|
||||||
toast.error('Please provide comments before pushing to DMS');
|
toast.error('Please provide comments before pushing to DMS');
|
||||||
@ -308,6 +427,273 @@ export function DMSPushModal({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Completion Documents Section */}
|
||||||
|
{completionDocuments && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Completion Documents */}
|
||||||
|
{completionDocuments.completionDocuments && completionDocuments.completionDocuments.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
|
||||||
|
<CheckCircle2 className="w-4 h-4 sm:w-5 sm:h-5 text-green-600" />
|
||||||
|
Completion Documents
|
||||||
|
</h3>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{completionDocuments.completionDocuments.length} file(s)
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 max-h-[200px] overflow-y-auto">
|
||||||
|
{completionDocuments.completionDocuments.map((doc, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
||||||
|
<CheckCircle2 className="w-4 h-4 lg:w-5 lg:h-5 text-green-600 flex-shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
|
||||||
|
{doc.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{doc.id && (
|
||||||
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
{canPreviewDocument(doc) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePreviewDocument(doc)}
|
||||||
|
disabled={previewLoading}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Preview document"
|
||||||
|
>
|
||||||
|
{previewLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (doc.id) {
|
||||||
|
await downloadDocument(doc.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download document:', error);
|
||||||
|
toast.error('Failed to download document');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
title="Download document"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Activity Photos */}
|
||||||
|
{completionDocuments.activityPhotos && completionDocuments.activityPhotos.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
|
||||||
|
<Activity className="w-4 h-4 sm:w-5 sm:h-5 text-blue-600" />
|
||||||
|
Activity Photos
|
||||||
|
</h3>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{completionDocuments.activityPhotos.length} file(s)
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 max-h-[200px] overflow-y-auto">
|
||||||
|
{completionDocuments.activityPhotos.map((doc, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
||||||
|
<Activity className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
|
||||||
|
{doc.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{doc.id && (
|
||||||
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
{canPreviewDocument(doc) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePreviewDocument(doc)}
|
||||||
|
disabled={previewLoading}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Preview photo"
|
||||||
|
>
|
||||||
|
{previewLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (doc.id) {
|
||||||
|
await downloadDocument(doc.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download document:', error);
|
||||||
|
toast.error('Failed to download document');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
title="Download photo"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Invoices / Receipts */}
|
||||||
|
{completionDocuments.invoicesReceipts && completionDocuments.invoicesReceipts.length > 0 && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
|
||||||
|
<Receipt className="w-4 h-4 sm:w-5 sm:h-5 text-purple-600" />
|
||||||
|
Invoices / Receipts
|
||||||
|
</h3>
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{completionDocuments.invoicesReceipts.length} file(s)
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 max-h-[200px] overflow-y-auto">
|
||||||
|
{completionDocuments.invoicesReceipts.map((doc, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
||||||
|
<Receipt className="w-4 h-4 lg:w-5 lg:h-5 text-purple-600 flex-shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={doc.name}>
|
||||||
|
{doc.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{doc.id && (
|
||||||
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
{canPreviewDocument(doc) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePreviewDocument(doc)}
|
||||||
|
disabled={previewLoading}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Preview document"
|
||||||
|
>
|
||||||
|
{previewLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (doc.id) {
|
||||||
|
await downloadDocument(doc.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download document:', error);
|
||||||
|
toast.error('Failed to download document');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
title="Download document"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Attendance Sheet */}
|
||||||
|
{completionDocuments.attendanceSheet && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<h3 className="font-semibold text-sm sm:text-base flex items-center gap-2">
|
||||||
|
<Activity className="w-4 h-4 sm:w-5 sm:h-5 text-indigo-600" />
|
||||||
|
Attendance Sheet
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div className="border rounded-lg p-2.5 lg:p-3 bg-gray-50 flex items-center justify-between gap-2">
|
||||||
|
<div className="flex items-center gap-2 lg:gap-3 min-w-0 flex-1">
|
||||||
|
<Activity className="w-4 h-4 lg:w-5 lg:h-5 text-indigo-600 flex-shrink-0" />
|
||||||
|
<div className="min-w-0 flex-1">
|
||||||
|
<p className="font-medium text-xs lg:text-sm text-gray-900 truncate" title={completionDocuments.attendanceSheet.name}>
|
||||||
|
{completionDocuments.attendanceSheet.name}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{completionDocuments.attendanceSheet.id && (
|
||||||
|
<div className="flex items-center gap-1 flex-shrink-0">
|
||||||
|
{canPreviewDocument(completionDocuments.attendanceSheet) && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => handlePreviewDocument(completionDocuments.attendanceSheet!)}
|
||||||
|
disabled={previewLoading}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
title="Preview document"
|
||||||
|
>
|
||||||
|
{previewLoading ? (
|
||||||
|
<Loader2 className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Eye className="w-4 h-4 lg:w-5 lg:h-5 text-blue-600" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
if (completionDocuments.attendanceSheet?.id) {
|
||||||
|
await downloadDocument(completionDocuments.attendanceSheet.id);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to download document:', error);
|
||||||
|
toast.error('Failed to download document');
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="p-2 hover:bg-gray-200 rounded-lg transition-colors"
|
||||||
|
title="Download document"
|
||||||
|
>
|
||||||
|
<Download className="w-4 h-4 lg:w-5 lg:h-5 text-gray-600" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Verification Warning */}
|
{/* Verification Warning */}
|
||||||
<div className="p-2.5 sm:p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
<div className="p-2.5 sm:p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||||
<div className="flex items-start gap-2">
|
<div className="flex items-start gap-2">
|
||||||
@ -376,6 +762,110 @@ export function DMSPushModal({
|
|||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|
||||||
|
{/* File Preview Modal - Matching DocumentsTab style */}
|
||||||
|
{previewDocument && (
|
||||||
|
<Dialog
|
||||||
|
open={!!previewDocument}
|
||||||
|
onOpenChange={() => setPreviewDocument(null)}
|
||||||
|
>
|
||||||
|
<DialogContent className="file-preview-dialog p-3 sm:p-6">
|
||||||
|
<div className="file-preview-content">
|
||||||
|
<DialogHeader className="pb-4 flex-shrink-0 pr-8">
|
||||||
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||||
|
<Eye className="w-5 h-5 text-blue-600 flex-shrink-0" />
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<DialogTitle className="text-base sm:text-lg font-bold text-gray-900 truncate pr-2">
|
||||||
|
{previewDocument.name}
|
||||||
|
</DialogTitle>
|
||||||
|
{previewDocument.type && (
|
||||||
|
<p className="text-xs sm:text-sm text-gray-500">
|
||||||
|
{previewDocument.type} {previewDocument.size && `• ${(previewDocument.size / 1024).toFixed(1)} KB`}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-2 flex-wrap mr-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = previewDocument.url;
|
||||||
|
link.download = previewDocument.name;
|
||||||
|
link.click();
|
||||||
|
}}
|
||||||
|
className="gap-2 h-9"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
<span className="hidden sm:inline">Download</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="file-preview-body bg-gray-100 rounded-lg p-2 sm:p-4">
|
||||||
|
{previewLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-full min-h-[70vh]">
|
||||||
|
<div className="text-center">
|
||||||
|
<Loader2 className="w-8 h-8 animate-spin text-blue-600 mx-auto mb-2" />
|
||||||
|
<p className="text-sm text-gray-600">Loading preview...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : previewDocument.name.toLowerCase().endsWith('.pdf') || previewDocument.type?.includes('pdf') ? (
|
||||||
|
<div className="w-full h-full flex items-center justify-center">
|
||||||
|
<iframe
|
||||||
|
src={previewDocument.url}
|
||||||
|
className="w-full h-full rounded-lg border-0"
|
||||||
|
title={previewDocument.name}
|
||||||
|
style={{
|
||||||
|
minHeight: '70vh',
|
||||||
|
height: '100%'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : previewDocument.name.match(/\.(jpg|jpeg|png|gif|webp)$/i) || previewDocument.type?.includes('image') ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<img
|
||||||
|
src={previewDocument.url}
|
||||||
|
alt={previewDocument.name}
|
||||||
|
style={{
|
||||||
|
maxWidth: '100%',
|
||||||
|
maxHeight: '100%',
|
||||||
|
objectFit: 'contain'
|
||||||
|
}}
|
||||||
|
className="rounded-lg shadow-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-col items-center justify-center h-full text-center">
|
||||||
|
<div className="w-20 h-20 bg-gray-200 rounded-full flex items-center justify-center mb-4">
|
||||||
|
<Eye className="w-10 h-10 text-gray-400" />
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold text-gray-900 mb-2">Preview Not Available</h3>
|
||||||
|
<p className="text-sm text-gray-600 mb-6">
|
||||||
|
This file type cannot be previewed. Please download to view.
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = previewDocument.url;
|
||||||
|
link.download = previewDocument.name;
|
||||||
|
link.click();
|
||||||
|
}}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4" />
|
||||||
|
Download {previewDocument.name}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { CustomDatePicker } from '@/components/ui/date-picker';
|
||||||
import { Upload, Plus, X, Calendar, FileText, Image, Receipt, CircleAlert, CheckCircle2, Eye, Download } from 'lucide-react';
|
import { Upload, Plus, X, Calendar, FileText, Image, Receipt, CircleAlert, CheckCircle2, Eye, Download } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import '@/components/common/FilePreview/FilePreview.css';
|
import '@/components/common/FilePreview/FilePreview.css';
|
||||||
@ -326,19 +327,13 @@ export function DealerCompletionDocumentsModal({
|
|||||||
<Calendar className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
<Calendar className="w-3.5 h-3.5 sm:w-4 sm:h-4" />
|
||||||
Activity Completion Date *
|
Activity Completion Date *
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
type="date"
|
value={activityCompletionDate || null}
|
||||||
id="completionDate"
|
onChange={(date) => setActivityCompletionDate(date || '')}
|
||||||
max={maxDate}
|
maxDate={maxDate}
|
||||||
value={activityCompletionDate}
|
placeholderText="dd/mm/yyyy"
|
||||||
onChange={(e) => setActivityCompletionDate(e.target.value)}
|
className="w-full max-w-[280px]"
|
||||||
onClick={(e) => {
|
wrapperClassName="max-w-[280px]"
|
||||||
// Open calendar picker when clicking anywhere on the input
|
|
||||||
if (e.currentTarget.showPicker) {
|
|
||||||
e.currentTarget.showPicker();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="w-full max-w-[280px] text-left pr-10 cursor-pointer"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import { Input } from '@/components/ui/input';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Textarea } from '@/components/ui/textarea';
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { CustomDatePicker } from '@/components/ui/date-picker';
|
||||||
import { Upload, Plus, X, Calendar, IndianRupee, CircleAlert, CheckCircle2, FileText, Eye, Download } from 'lucide-react';
|
import { Upload, Plus, X, Calendar, IndianRupee, CircleAlert, CheckCircle2, FileText, Eye, Download } from 'lucide-react';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import '@/components/common/FilePreview/FilePreview.css';
|
import '@/components/common/FilePreview/FilePreview.css';
|
||||||
@ -579,18 +580,12 @@ export function DealerProposalSubmissionModal({
|
|||||||
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
|
<Label className="text-xs lg:text-sm font-medium mb-1.5 lg:mb-2 block">
|
||||||
Expected Completion Date
|
Expected Completion Date
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
type="date"
|
value={expectedCompletionDate || null}
|
||||||
min={minDate}
|
onChange={(date) => setExpectedCompletionDate(date || '')}
|
||||||
value={expectedCompletionDate}
|
minDate={minDate}
|
||||||
onChange={(e) => setExpectedCompletionDate(e.target.value)}
|
placeholderText="dd/mm/yyyy"
|
||||||
onClick={(e) => {
|
className="w-full"
|
||||||
// Open calendar picker when clicking anywhere on the input
|
|
||||||
if (e.currentTarget.showPicker) {
|
|
||||||
e.currentTarget.showPicker();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
className="h-9 lg:h-10 w-full text-left pr-10 cursor-pointer"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@ -9,11 +9,11 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Filter, Calendar as CalendarIcon, RefreshCw } from 'lucide-react';
|
import { Filter, Calendar as CalendarIcon, RefreshCw } from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { DateRange } from '@/services/dashboard.service';
|
import { DateRange } from '@/services/dashboard.service';
|
||||||
|
import { CustomDatePicker } from '@/components/ui/date-picker';
|
||||||
|
|
||||||
interface DashboardFiltersBarProps {
|
interface DashboardFiltersBarProps {
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
@ -96,12 +96,10 @@ export function DashboardFiltersBar({
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="start-date" className="text-sm font-medium">Start Date</Label>
|
<Label htmlFor="start-date" className="text-sm font-medium">Start Date</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id="start-date"
|
value={customStartDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
if (date) {
|
if (date) {
|
||||||
onCustomStartDateChange(date);
|
onCustomStartDateChange(date);
|
||||||
if (customEndDate && date > customEndDate) {
|
if (customEndDate && date > customEndDate) {
|
||||||
@ -111,19 +109,18 @@ export function DashboardFiltersBar({
|
|||||||
onCustomStartDateChange(undefined);
|
onCustomStartDateChange(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
data-testid="start-date-input"
|
data-testid="start-date-input"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="end-date" className="text-sm font-medium">End Date</Label>
|
<Label htmlFor="end-date" className="text-sm font-medium">End Date</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id="end-date"
|
value={customEndDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={customEndDate ? format(customEndDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
if (date) {
|
if (date) {
|
||||||
onCustomEndDateChange(date);
|
onCustomEndDateChange(date);
|
||||||
if (customStartDate && date < customStartDate) {
|
if (customStartDate && date < customStartDate) {
|
||||||
@ -133,8 +130,9 @@ export function DashboardFiltersBar({
|
|||||||
onCustomEndDateChange(undefined);
|
onCustomEndDateChange(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
min={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : undefined}
|
minDate={customStartDate || undefined}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
data-testid="end-date-input"
|
data-testid="end-date-input"
|
||||||
/>
|
/>
|
||||||
@ -181,6 +179,88 @@ export function DashboardFiltersBar({
|
|||||||
<SelectItem value="custom">Custom Range</SelectItem>
|
<SelectItem value="custom">Custom Range</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
|
||||||
|
{/* Custom Date Range Picker for Normal Users */}
|
||||||
|
{dateRange === 'custom' && (
|
||||||
|
<Popover open={showCustomDatePicker} onOpenChange={onShowCustomDatePickerChange}>
|
||||||
|
<PopoverTrigger asChild>
|
||||||
|
<Button variant="outline" size="sm" className="gap-2" data-testid="custom-date-trigger">
|
||||||
|
<CalendarIcon className="w-4 h-4" />
|
||||||
|
{customStartDate && customEndDate
|
||||||
|
? `${format(customStartDate, 'MMM d, yyyy')} - ${format(customEndDate, 'MMM d, yyyy')}`
|
||||||
|
: 'Select dates'}
|
||||||
|
</Button>
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent className="w-auto p-4" align="start" sideOffset={8} data-testid="custom-date-picker">
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="start-date-user" className="text-sm font-medium">Start Date</Label>
|
||||||
|
<CustomDatePicker
|
||||||
|
value={customStartDate || null}
|
||||||
|
onChange={(dateStr: string | null) => {
|
||||||
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
|
if (date) {
|
||||||
|
onCustomStartDateChange(date);
|
||||||
|
if (customEndDate && date > customEndDate) {
|
||||||
|
onCustomEndDateChange(date);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onCustomStartDateChange(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
|
className="w-full"
|
||||||
|
data-testid="start-date-input-user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="end-date-user" className="text-sm font-medium">End Date</Label>
|
||||||
|
<CustomDatePicker
|
||||||
|
value={customEndDate || null}
|
||||||
|
onChange={(dateStr: string | null) => {
|
||||||
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
|
if (date) {
|
||||||
|
onCustomEndDateChange(date);
|
||||||
|
if (customStartDate && date < customStartDate) {
|
||||||
|
onCustomStartDateChange(date);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onCustomEndDateChange(undefined);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
minDate={customStartDate || undefined}
|
||||||
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
|
className="w-full"
|
||||||
|
data-testid="end-date-input-user"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2 border-t">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={onApplyCustomDate}
|
||||||
|
disabled={!customStartDate || !customEndDate}
|
||||||
|
className="flex-1 bg-re-green hover:bg-re-green/90"
|
||||||
|
data-testid="apply-custom-date-user"
|
||||||
|
>
|
||||||
|
Apply
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
onClick={onResetCustomDates}
|
||||||
|
data-testid="cancel-custom-date-user"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PopoverContent>
|
||||||
|
</Popover>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -206,4 +286,3 @@ export function DashboardFiltersBar({
|
|||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -122,6 +122,11 @@ export function TATBreachReport({
|
|||||||
params.set('approver', req.approverId!);
|
params.set('approver', req.approverId!);
|
||||||
params.set('approverType', 'current');
|
params.set('approverType', 'current');
|
||||||
params.set('slaCompliance', 'breached');
|
params.set('slaCompliance', 'breached');
|
||||||
|
|
||||||
|
if (dateRange) params.set('dateRange', dateRange);
|
||||||
|
if (customStartDate) params.set('startDate', customStartDate.toISOString());
|
||||||
|
if (customEndDate) params.set('endDate', customEndDate.toISOString());
|
||||||
|
|
||||||
onNavigate(`requests?${params.toString()}`);
|
onNavigate(`requests?${params.toString()}`);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -3,13 +3,13 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Calendar as CalendarIcon } from 'lucide-react';
|
import { Calendar as CalendarIcon } from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
import { DateRange } from '@/services/dashboard.service';
|
import { DateRange } from '@/services/dashboard.service';
|
||||||
|
import { CustomDatePicker } from '@/components/ui/date-picker';
|
||||||
|
|
||||||
interface DateRangeFilterProps {
|
interface DateRangeFilterProps {
|
||||||
dateRange: DateRange;
|
dateRange: DateRange;
|
||||||
@ -44,7 +44,7 @@ export function DateRangeFilter({
|
|||||||
}: DateRangeFilterProps) {
|
}: DateRangeFilterProps) {
|
||||||
const displayDateRange =
|
const displayDateRange =
|
||||||
customStartDate && customEndDate
|
customStartDate && customEndDate
|
||||||
? `${format(customStartDate, 'MMM d, yyyy')} - ${format(customEndDate, 'MMM d, yyyy')}`
|
? `${format(customStartDate, 'd MMM yyyy')} - ${format(customEndDate, 'd MMM yyyy')}`
|
||||||
: 'Select dates';
|
: 'Select dates';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -78,35 +78,33 @@ export function DateRangeFilter({
|
|||||||
<Label htmlFor={`${testIdPrefix}-start-date`} className="text-sm font-medium">
|
<Label htmlFor={`${testIdPrefix}-start-date`} className="text-sm font-medium">
|
||||||
Start Date
|
Start Date
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id={`${testIdPrefix}-start-date`}
|
value={tempCustomStartDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={tempCustomStartDate ? format(tempCustomStartDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
onStartDateChange(date);
|
onStartDateChange(date);
|
||||||
}}
|
}}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
data-testid={`${testIdPrefix}-start-input`}
|
id={`${testIdPrefix}-start-date`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor={`${testIdPrefix}-end-date`} className="text-sm font-medium">
|
<Label htmlFor={`${testIdPrefix}-end-date`} className="text-sm font-medium">
|
||||||
End Date
|
End Date
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id={`${testIdPrefix}-end-date`}
|
value={tempCustomEndDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={tempCustomEndDate ? format(tempCustomEndDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
onEndDateChange(date);
|
onEndDateChange(date);
|
||||||
}}
|
}}
|
||||||
min={tempCustomStartDate ? format(tempCustomStartDate, 'yyyy-MM-dd') : undefined}
|
minDate={tempCustomStartDate || undefined}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
data-testid={`${testIdPrefix}-end-input`}
|
id={`${testIdPrefix}-end-date`}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -269,15 +269,51 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []); // Only on mount
|
}, []); // Only on mount
|
||||||
|
|
||||||
|
// Track previous filter values to detect changes
|
||||||
|
const prevFiltersRef = useRef({
|
||||||
|
searchTerm: filters.searchTerm,
|
||||||
|
statusFilter: filters.statusFilter,
|
||||||
|
priorityFilter: filters.priorityFilter,
|
||||||
|
templateTypeFilter: filters.templateTypeFilter,
|
||||||
|
sortBy: filters.sortBy,
|
||||||
|
sortOrder: filters.sortOrder,
|
||||||
|
isDealer
|
||||||
|
});
|
||||||
|
|
||||||
// Track filter changes and refetch
|
// Track filter changes and refetch
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Skip until initial fetch has completed
|
// Skip until initial fetch has completed
|
||||||
if (!hasInitialFetchRun.current) return;
|
if (!hasInitialFetchRun.current) return;
|
||||||
|
|
||||||
|
const prev = prevFiltersRef.current;
|
||||||
|
|
||||||
|
// Check if any filter actually changed
|
||||||
|
const hasChanged =
|
||||||
|
prev.searchTerm !== filters.searchTerm ||
|
||||||
|
prev.statusFilter !== filters.statusFilter ||
|
||||||
|
prev.priorityFilter !== filters.priorityFilter ||
|
||||||
|
prev.templateTypeFilter !== filters.templateTypeFilter ||
|
||||||
|
prev.sortBy !== filters.sortBy ||
|
||||||
|
prev.sortOrder !== filters.sortOrder ||
|
||||||
|
prev.isDealer !== isDealer;
|
||||||
|
|
||||||
|
if (!hasChanged) return;
|
||||||
|
|
||||||
// Debounce search
|
// Debounce search
|
||||||
const timeoutId = setTimeout(() => {
|
const timeoutId = setTimeout(() => {
|
||||||
filters.setCurrentPage(1); // Reset to page 1 when filters change
|
filters.setCurrentPage(1); // Reset to page 1 when filters change (but not on initial mount or back navigation)
|
||||||
fetchRequests(1, getFilterParams(true));
|
fetchRequests(1, getFilterParams(true));
|
||||||
|
|
||||||
|
// Update previous filters ref
|
||||||
|
prevFiltersRef.current = {
|
||||||
|
searchTerm: filters.searchTerm,
|
||||||
|
statusFilter: filters.statusFilter,
|
||||||
|
priorityFilter: filters.priorityFilter,
|
||||||
|
templateTypeFilter: filters.templateTypeFilter,
|
||||||
|
sortBy: filters.sortBy,
|
||||||
|
sortOrder: filters.sortOrder,
|
||||||
|
isDealer
|
||||||
|
};
|
||||||
}, filters.searchTerm ? 500 : 0);
|
}, filters.searchTerm ? 500 : 0);
|
||||||
|
|
||||||
return () => clearTimeout(timeoutId);
|
return () => clearTimeout(timeoutId);
|
||||||
|
|||||||
@ -45,6 +45,7 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react';
|
import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
import { CustomDatePicker } from '@/components/ui/date-picker';
|
||||||
|
|
||||||
export function Requests({ onViewRequest }: RequestsProps) {
|
export function Requests({ onViewRequest }: RequestsProps) {
|
||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
@ -250,6 +251,38 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
|||||||
fetchBackendStatsRef.current = fetchBackendStats;
|
fetchBackendStatsRef.current = fetchBackendStats;
|
||||||
}, [filters, fetchBackendStats]);
|
}, [filters, fetchBackendStats]);
|
||||||
|
|
||||||
|
// Parse URL search params
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
// Approver Filter (from Dashboard TAT Breach Report link)
|
||||||
|
const approver = params.get('approver');
|
||||||
|
const approverType = params.get('approverType');
|
||||||
|
const slaCompliance = params.get('slaCompliance');
|
||||||
|
const dateRange = params.get('dateRange');
|
||||||
|
const startDate = params.get('startDate');
|
||||||
|
const endDate = params.get('endDate');
|
||||||
|
|
||||||
|
if (approver) {
|
||||||
|
filters.setApproverFilter(approver);
|
||||||
|
}
|
||||||
|
if (approverType === 'current' || approverType === 'any') {
|
||||||
|
filters.setApproverFilterType(approverType);
|
||||||
|
}
|
||||||
|
if (slaCompliance) {
|
||||||
|
filters.setSlaComplianceFilter(slaCompliance);
|
||||||
|
}
|
||||||
|
if (dateRange) {
|
||||||
|
filters.setDateRange(dateRange as any);
|
||||||
|
}
|
||||||
|
if (startDate) {
|
||||||
|
filters.setCustomStartDate(new Date(startDate));
|
||||||
|
}
|
||||||
|
if (endDate) {
|
||||||
|
filters.setCustomEndDate(new Date(endDate));
|
||||||
|
}
|
||||||
|
}, []); // Run only once on mount
|
||||||
|
|
||||||
// Fetch requests
|
// Fetch requests
|
||||||
const fetchRequests = useCallback(async (page: number = 1) => {
|
const fetchRequests = useCallback(async (page: number = 1) => {
|
||||||
try {
|
try {
|
||||||
@ -759,33 +792,30 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
|||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="start-date">Start Date</Label>
|
<Label htmlFor="start-date">Start Date</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id="start-date"
|
value={filters.customStartDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={filters.customStartDate ? format(filters.customStartDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
if (date) {
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
filters.setCustomStartDate(date);
|
||||||
if (date) {
|
if (filters.customEndDate && date > filters.customEndDate) {
|
||||||
filters.setCustomStartDate(date);
|
filters.setCustomEndDate(date);
|
||||||
if (filters.customEndDate && date > filters.customEndDate) {
|
}
|
||||||
filters.setCustomEndDate(date);
|
} else {
|
||||||
|
filters.setCustomStartDate(undefined);
|
||||||
}
|
}
|
||||||
} else {
|
}}
|
||||||
filters.setCustomStartDate(undefined);
|
maxDate={new Date()}
|
||||||
}
|
placeholderText="dd/mm/yyyy"
|
||||||
}}
|
className="w-full"
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
/>
|
||||||
className="w-full"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="end-date">End Date</Label>
|
<Label htmlFor="end-date">End Date</Label>
|
||||||
<Input
|
<CustomDatePicker
|
||||||
id="end-date"
|
value={filters.customEndDate || null}
|
||||||
type="date"
|
onChange={(dateStr: string | null) => {
|
||||||
value={filters.customEndDate ? format(filters.customEndDate, 'yyyy-MM-dd') : ''}
|
const date = dateStr ? new Date(dateStr) : undefined;
|
||||||
onChange={(e) => {
|
|
||||||
const date = e.target.value ? new Date(e.target.value) : undefined;
|
|
||||||
if (date) {
|
if (date) {
|
||||||
filters.setCustomEndDate(date);
|
filters.setCustomEndDate(date);
|
||||||
if (filters.customStartDate && date < filters.customStartDate) {
|
if (filters.customStartDate && date < filters.customStartDate) {
|
||||||
@ -795,21 +825,21 @@ export function Requests({ onViewRequest }: RequestsProps) {
|
|||||||
filters.setCustomEndDate(undefined);
|
filters.setCustomEndDate(undefined);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
min={filters.customStartDate ? format(filters.customStartDate, 'yyyy-MM-dd') : undefined}
|
minDate={filters.customStartDate || undefined}
|
||||||
max={format(new Date(), 'yyyy-MM-dd')}
|
maxDate={new Date()}
|
||||||
|
placeholderText="dd/mm/yyyy"
|
||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
<div className="flex gap-2 pt-2 border-t">
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
|
||||||
onClick={filters.handleApplyCustomDate}
|
onClick={filters.handleApplyCustomDate}
|
||||||
disabled={!filters.customStartDate || !filters.customEndDate}
|
disabled={!filters.customStartDate || !filters.customEndDate}
|
||||||
className="flex-1 bg-re-green hover:bg-re-green/90"
|
|
||||||
>
|
>
|
||||||
Apply
|
Apply Range
|
||||||
</Button>
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2 pt-2 border-t">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user