uniform format ate picker added and documents preview for activity completio documents added onDMS push step

This commit is contained in:
laxmanhalaki 2026-01-09 18:55:40 +05:30
parent 4c3d7fd28b
commit e8caafa7a1
14 changed files with 1020 additions and 132 deletions

View File

@ -183,7 +183,7 @@ export function NewRequestModal({ open, onClose, onSubmit }: NewRequestModalProp
<PopoverTrigger asChild>
<Button variant="outline" className="w-full justify-start text-left">
<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>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">

View 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;

View File

@ -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({
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="start-date">Start Date</Label>
<Input
id="start-date"
type="date"
value={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={customStartDate || null}
onChange={(dateStr: string | null) => {
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"
/>
</div>
<div className="space-y-2">
<Label htmlFor="end-date">End Date</Label>
<Input
id="end-date"
type="date"
value={customEndDate ? format(customEndDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={customEndDate || null}
onChange={(dateStr: string | null) => {
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"
/>
</div>
@ -430,7 +429,7 @@ export function StandardUserAllRequestsFilters({
disabled={!customStartDate || !customEndDate}
className="flex-1 bg-re-green hover:bg-re-green/90"
>
Apply
Apply Range
</Button>
<Button
size="sm"

View File

@ -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 DealerUserAllRequestsFiltersProps {
// Filters
@ -313,12 +314,10 @@ export function DealerUserAllRequestsFilters({
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="start-date">Start Date</Label>
<Input
id="start-date"
type="date"
value={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={customStartDate || null}
onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined;
if (date) {
onCustomStartDateChange?.(date);
if (customEndDate && date > customEndDate) {
@ -328,18 +327,17 @@ export function DealerUserAllRequestsFilters({
onCustomStartDateChange?.(undefined);
}
}}
max={format(new Date(), 'yyyy-MM-dd')}
maxDate={new Date()}
placeholderText="dd/mm/yyyy"
className="w-full"
/>
</div>
<div className="space-y-2">
<Label htmlFor="end-date">End Date</Label>
<Input
id="end-date"
type="date"
value={customEndDate ? format(customEndDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={customEndDate || null}
onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined;
if (date) {
onCustomEndDateChange?.(date);
if (customStartDate && date < customStartDate) {
@ -349,8 +347,9 @@ export function DealerUserAllRequestsFilters({
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"
/>
</div>

View File

@ -621,7 +621,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
className="w-full justify-start text-left mt-2 h-12 pl-3"
>
<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>
</PopoverTrigger>
<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"
>
<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>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
@ -707,7 +707,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
disabled={!formData.periodStartDate}
>
<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>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
@ -730,7 +730,7 @@ export function ClaimManagementWizard({ onBack, onSubmit }: ClaimManagementWizar
<div className="mt-2">
{formData.periodStartDate && formData.periodEndDate ? (
<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 className="text-xs text-gray-500">

View File

@ -1073,6 +1073,81 @@ export function DealerClaimWorkflowTab({
loadProposalData();
}, [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
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}
/>

View File

@ -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<void>;
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({
)}
</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 */}
<div className="p-2.5 sm:p-3 bg-yellow-50 border border-yellow-200 rounded-lg">
<div className="flex items-start gap-2">
@ -376,6 +762,110 @@ export function DMSPushModal({
</Button>
</DialogFooter>
</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>
);
}

View File

@ -18,6 +18,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
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 { toast } from 'sonner';
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" />
Activity Completion Date *
</Label>
<Input
type="date"
id="completionDate"
max={maxDate}
value={activityCompletionDate}
onChange={(e) => setActivityCompletionDate(e.target.value)}
onClick={(e) => {
// 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"
<CustomDatePicker
value={activityCompletionDate || null}
onChange={(date) => setActivityCompletionDate(date || '')}
maxDate={maxDate}
placeholderText="dd/mm/yyyy"
className="w-full max-w-[280px]"
wrapperClassName="max-w-[280px]"
/>
</div>

View File

@ -18,6 +18,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
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 { toast } from 'sonner';
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">
Expected Completion Date
</Label>
<Input
type="date"
min={minDate}
value={expectedCompletionDate}
onChange={(e) => setExpectedCompletionDate(e.target.value)}
onClick={(e) => {
// 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"
<CustomDatePicker
value={expectedCompletionDate || null}
onChange={(date) => setExpectedCompletionDate(date || '')}
minDate={minDate}
placeholderText="dd/mm/yyyy"
className="w-full"
/>
</div>
) : (

View File

@ -9,11 +9,11 @@ import { Button } from '@/components/ui/button';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Separator } from '@/components/ui/separator';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Filter, Calendar as CalendarIcon, RefreshCw } from 'lucide-react';
import { format } from 'date-fns';
import { DateRange } from '@/services/dashboard.service';
import { CustomDatePicker } from '@/components/ui/date-picker';
interface DashboardFiltersBarProps {
isAdmin: boolean;
@ -96,12 +96,10 @@ export function DashboardFiltersBar({
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="start-date" className="text-sm font-medium">Start Date</Label>
<Input
id="start-date"
type="date"
value={customStartDate ? format(customStartDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={customStartDate || null}
onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined;
if (date) {
onCustomStartDateChange(date);
if (customEndDate && date > customEndDate) {
@ -111,19 +109,18 @@ export function DashboardFiltersBar({
onCustomStartDateChange(undefined);
}
}}
max={format(new Date(), 'yyyy-MM-dd')}
maxDate={new Date()}
placeholderText="dd/mm/yyyy"
className="w-full"
data-testid="start-date-input"
/>
</div>
<div className="space-y-2">
<Label htmlFor="end-date" className="text-sm font-medium">End Date</Label>
<Input
id="end-date"
type="date"
value={customEndDate ? format(customEndDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={customEndDate || null}
onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined;
if (date) {
onCustomEndDateChange(date);
if (customStartDate && date < customStartDate) {
@ -133,8 +130,9 @@ export function DashboardFiltersBar({
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"
data-testid="end-date-input"
/>
@ -181,6 +179,88 @@ export function DashboardFiltersBar({
<SelectItem value="custom">Custom Range</SelectItem>
</SelectContent>
</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>
)}
@ -206,4 +286,3 @@ export function DashboardFiltersBar({
</Card>
);
}

View File

@ -122,6 +122,11 @@ export function TATBreachReport({
params.set('approver', req.approverId!);
params.set('approverType', 'current');
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()}`);
}
}}

View File

@ -3,13 +3,13 @@
*/
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Calendar as CalendarIcon } from 'lucide-react';
import { format } from 'date-fns';
import { DateRange } from '@/services/dashboard.service';
import { CustomDatePicker } from '@/components/ui/date-picker';
interface DateRangeFilterProps {
dateRange: DateRange;
@ -44,7 +44,7 @@ export function DateRangeFilter({
}: DateRangeFilterProps) {
const displayDateRange =
customStartDate && customEndDate
? `${format(customStartDate, 'MMM d, yyyy')} - ${format(customEndDate, 'MMM d, yyyy')}`
? `${format(customStartDate, 'd MMM yyyy')} - ${format(customEndDate, 'd MMM yyyy')}`
: 'Select dates';
return (
@ -78,35 +78,33 @@ export function DateRangeFilter({
<Label htmlFor={`${testIdPrefix}-start-date`} className="text-sm font-medium">
Start Date
</Label>
<Input
id={`${testIdPrefix}-start-date`}
type="date"
value={tempCustomStartDate ? format(tempCustomStartDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={tempCustomStartDate || null}
onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined;
onStartDateChange(date);
}}
max={format(new Date(), 'yyyy-MM-dd')}
maxDate={new Date()}
placeholderText="dd/mm/yyyy"
className="w-full"
data-testid={`${testIdPrefix}-start-input`}
id={`${testIdPrefix}-start-date`}
/>
</div>
<div className="space-y-2">
<Label htmlFor={`${testIdPrefix}-end-date`} className="text-sm font-medium">
End Date
</Label>
<Input
id={`${testIdPrefix}-end-date`}
type="date"
value={tempCustomEndDate ? format(tempCustomEndDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={tempCustomEndDate || null}
onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined;
onEndDateChange(date);
}}
min={tempCustomStartDate ? format(tempCustomStartDate, 'yyyy-MM-dd') : undefined}
max={format(new Date(), 'yyyy-MM-dd')}
minDate={tempCustomStartDate || undefined}
maxDate={new Date()}
placeholderText="dd/mm/yyyy"
className="w-full"
data-testid={`${testIdPrefix}-end-input`}
id={`${testIdPrefix}-end-date`}
/>
</div>
</div>

View File

@ -269,15 +269,51 @@ export function OpenRequests({ onViewRequest }: OpenRequestsProps) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // 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
useEffect(() => {
// Skip until initial fetch has completed
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
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));
// 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);
return () => clearTimeout(timeoutId);

View File

@ -45,6 +45,7 @@ import { Label } from '@/components/ui/label';
import { Separator } from '@/components/ui/separator';
import { X, Search, Filter, RefreshCw, Calendar as CalendarIcon } from 'lucide-react';
import { format } from 'date-fns';
import { CustomDatePicker } from '@/components/ui/date-picker';
export function Requests({ onViewRequest }: RequestsProps) {
const { user } = useAuth();
@ -250,6 +251,38 @@ export function Requests({ onViewRequest }: RequestsProps) {
fetchBackendStatsRef.current = 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
const fetchRequests = useCallback(async (page: number = 1) => {
try {
@ -759,33 +792,30 @@ export function Requests({ onViewRequest }: RequestsProps) {
<div className="space-y-3">
<div className="space-y-2">
<Label htmlFor="start-date">Start Date</Label>
<Input
id="start-date"
type="date"
value={filters.customStartDate ? format(filters.customStartDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
if (date) {
filters.setCustomStartDate(date);
if (filters.customEndDate && date > filters.customEndDate) {
filters.setCustomEndDate(date);
<CustomDatePicker
value={filters.customStartDate || null}
onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined;
if (date) {
filters.setCustomStartDate(date);
if (filters.customEndDate && date > filters.customEndDate) {
filters.setCustomEndDate(date);
}
} else {
filters.setCustomStartDate(undefined);
}
} else {
filters.setCustomStartDate(undefined);
}
}}
max={format(new Date(), 'yyyy-MM-dd')}
className="w-full"
/>
}}
maxDate={new Date()}
placeholderText="dd/mm/yyyy"
className="w-full"
/>
</div>
<div className="space-y-2">
<Label htmlFor="end-date">End Date</Label>
<Input
id="end-date"
type="date"
value={filters.customEndDate ? format(filters.customEndDate, 'yyyy-MM-dd') : ''}
onChange={(e) => {
const date = e.target.value ? new Date(e.target.value) : undefined;
<CustomDatePicker
value={filters.customEndDate || null}
onChange={(dateStr: string | null) => {
const date = dateStr ? new Date(dateStr) : undefined;
if (date) {
filters.setCustomEndDate(date);
if (filters.customStartDate && date < filters.customStartDate) {
@ -795,21 +825,21 @@ export function Requests({ onViewRequest }: RequestsProps) {
filters.setCustomEndDate(undefined);
}
}}
min={filters.customStartDate ? format(filters.customStartDate, 'yyyy-MM-dd') : undefined}
max={format(new Date(), 'yyyy-MM-dd')}
minDate={filters.customStartDate || undefined}
maxDate={new Date()}
placeholderText="dd/mm/yyyy"
className="w-full"
/>
</div>
</div>
<div className="flex gap-2 pt-2 border-t">
<Button
size="sm"
className="w-full bg-[#2d4a3e] hover:bg-[#1f3329] text-white"
onClick={filters.handleApplyCustomDate}
disabled={!filters.customStartDate || !filters.customEndDate}
className="flex-1 bg-re-green hover:bg-re-green/90"
>
Apply
Apply Range
</Button>
</div>
<div className="flex gap-2 pt-2 border-t">
<Button
size="sm"
variant="outline"