Added payment discrepancy report

This commit is contained in:
Roba Boru
2026-07-20 15:50:03 +03:00
parent 5e9feab869
commit 1e7324d276
9 changed files with 892 additions and 124 deletions

View File

@@ -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>
);
}