mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 13:28:11 +00:00
Added payment discrepancy report
This commit is contained in:
@@ -121,9 +121,10 @@ const navigationSections: { title: string; items: NavItem[] }[] = [
|
||||
{
|
||||
title: 'Analytics & Reports',
|
||||
items: [
|
||||
{ name: 'Overall', href: '/reports', icon: BarChart3, permission: PERMS.reports.view },
|
||||
{ name: 'Seats', href: '/reports/seats', icon: Armchair, permission: PERMS.reports.view },
|
||||
{ name: 'Passengers', href: '/reports/passengers', icon: Users, permission: PERMS.reports.view },
|
||||
{ name: 'Overall', href: '/reports', icon: BarChart3, permission: PERMS.reports.view },
|
||||
{ name: 'Seats', href: '/reports/seats', icon: Armchair, permission: PERMS.reports.view },
|
||||
{ name: 'Passengers', href: '/reports/passengers', icon: Users, permission: PERMS.reports.view },
|
||||
{ name: 'Payment Discrepancy', href: '/reports/payment-discrepancy', icon: AlertTriangle, permission: PERMS.reports.view },
|
||||
// { name: 'Operational Reports', href: '/operational-reports', icon: FileText, permission: PERMS.reports.view },
|
||||
]
|
||||
},
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { DayPicker } from 'react-day-picker';
|
||||
import { ChevronLeft, ChevronRight, Calendar, X } from 'lucide-react';
|
||||
import { format, parse, isValid } from 'date-fns';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DatePickerProps {
|
||||
value: string; // YYYY-MM-DD
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
disabled?: boolean;
|
||||
minDate?: Date;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function DatePicker({
|
||||
value,
|
||||
onChange,
|
||||
placeholder = 'Pick a date',
|
||||
disabled = false,
|
||||
minDate,
|
||||
className,
|
||||
}: DatePickerProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
||||
const [popoverPos, setPopoverPos] = useState({ top: 0, left: 0 });
|
||||
|
||||
useEffect(() => { setMounted(true); }, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return;
|
||||
const handler = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false); };
|
||||
document.addEventListener('keydown', handler);
|
||||
return () => document.removeEventListener('keydown', handler);
|
||||
}, [open]);
|
||||
|
||||
const selected = value
|
||||
? parse(value, 'yyyy-MM-dd', new Date())
|
||||
: undefined;
|
||||
const validSelected = selected && isValid(selected) ? selected : undefined;
|
||||
|
||||
// Clear the value if it's now before the minDate
|
||||
useEffect(() => {
|
||||
if (minDate && validSelected && validSelected < minDate) {
|
||||
onChange('');
|
||||
}
|
||||
}, [minDate, validSelected, onChange]);
|
||||
|
||||
function handleOpen() {
|
||||
if (disabled) return;
|
||||
if (triggerRef.current) {
|
||||
const rect = triggerRef.current.getBoundingClientRect();
|
||||
setPopoverPos({
|
||||
top: rect.bottom + window.scrollY + 6,
|
||||
left: rect.left + window.scrollX,
|
||||
});
|
||||
}
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
function handleSelect(date: Date | undefined) {
|
||||
if (date && isValid(date)) {
|
||||
onChange(format(date, 'yyyy-MM-dd'));
|
||||
}
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
function handleClear(e: React.MouseEvent) {
|
||||
e.stopPropagation();
|
||||
onChange('');
|
||||
}
|
||||
|
||||
const displayText = validSelected
|
||||
? format(validSelected, 'MMM d, yyyy')
|
||||
: null;
|
||||
|
||||
const popover = open && mounted ? createPortal(
|
||||
<>
|
||||
<div
|
||||
className="fixed inset-0"
|
||||
style={{ zIndex: 9999 }}
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<div
|
||||
className="absolute bg-white dark:bg-gray-900 border border-gray-200 dark:border-gray-700 rounded-xl shadow-xl p-3"
|
||||
style={{ zIndex: 10000, top: popoverPos.top, left: popoverPos.left }}
|
||||
onClick={e => e.stopPropagation()}
|
||||
>
|
||||
<DayPicker
|
||||
mode="single"
|
||||
selected={validSelected}
|
||||
onSelect={handleSelect}
|
||||
disabled={minDate ? { before: minDate } : undefined}
|
||||
showOutsideDays
|
||||
classNames={{
|
||||
root: 'w-full',
|
||||
months: 'w-full',
|
||||
month: 'w-full',
|
||||
month_caption: 'flex items-center justify-between mb-3',
|
||||
caption_label: 'text-sm font-semibold text-gray-900 dark:text-white',
|
||||
nav: 'flex items-center gap-1',
|
||||
button_previous: 'h-7 w-7 rounded-lg flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors',
|
||||
button_next: 'h-7 w-7 rounded-lg flex items-center justify-center text-gray-500 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors',
|
||||
month_grid: 'w-full border-collapse',
|
||||
weekdays: 'flex w-full mb-1',
|
||||
weekday: 'flex-1 text-center text-xs font-medium text-gray-400 dark:text-gray-500 py-1',
|
||||
weeks: '',
|
||||
week: 'flex w-full mt-0.5',
|
||||
day: 'flex-1 flex items-center justify-center p-0',
|
||||
day_button: 'h-8 w-8 text-xs rounded-lg flex items-center justify-center transition-colors hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer text-gray-700 dark:text-gray-300',
|
||||
selected: '',
|
||||
today: '',
|
||||
outside: 'opacity-30',
|
||||
disabled: 'opacity-20 cursor-not-allowed',
|
||||
hidden: 'invisible',
|
||||
range_start: '',
|
||||
range_end: '',
|
||||
range_middle: '',
|
||||
focused: 'ring-1 ring-blue-500/50',
|
||||
chevron: '',
|
||||
dropdowns: '',
|
||||
dropdown: '',
|
||||
dropdown_root: '',
|
||||
footer: '',
|
||||
months_dropdown: '',
|
||||
week_number: '',
|
||||
week_number_header: '',
|
||||
years_dropdown: '',
|
||||
weeks_after_enter: '',
|
||||
weeks_after_exit: '',
|
||||
weeks_before_enter: '',
|
||||
weeks_before_exit: '',
|
||||
}}
|
||||
components={{
|
||||
Chevron: ({ orientation }) =>
|
||||
orientation === 'left'
|
||||
? <ChevronLeft className="h-4 w-4" />
|
||||
: <ChevronRight className="h-4 w-4" />,
|
||||
DayButton: ({ day, modifiers, className: cls, ...props }) => (
|
||||
<button
|
||||
{...props}
|
||||
className={cn(
|
||||
cls,
|
||||
modifiers.selected && 'bg-blue-600 text-white font-semibold hover:bg-blue-700',
|
||||
modifiers.today && !modifiers.selected && 'text-blue-600 dark:text-blue-400 font-bold',
|
||||
)}
|
||||
/>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</>,
|
||||
document.body,
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className={cn('relative', className)}>
|
||||
<button
|
||||
ref={triggerRef}
|
||||
type="button"
|
||||
onClick={handleOpen}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3 py-2 rounded-lg border text-sm transition-colors',
|
||||
'border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-800',
|
||||
'text-gray-900 dark:text-white',
|
||||
'focus:outline-none focus:ring-2 focus:ring-blue-500',
|
||||
'disabled:opacity-40 disabled:cursor-not-allowed',
|
||||
!displayText && 'text-gray-400 dark:text-gray-500',
|
||||
)}
|
||||
>
|
||||
<Calendar className="w-4 h-4 shrink-0 text-gray-400 dark:text-gray-500" />
|
||||
<span className="min-w-[90px] text-left">{displayText ?? placeholder}</span>
|
||||
{displayText && !disabled && (
|
||||
<X
|
||||
className="w-3.5 h-3.5 shrink-0 text-gray-400 hover:text-gray-600 dark:hover:text-gray-200 ml-1"
|
||||
onClick={handleClear}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
{popover}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user