diff --git a/src/components/PayoutRescheduleModal.tsx b/src/components/PayoutRescheduleModal.tsx index eb6ceac..9fc09d2 100644 --- a/src/components/PayoutRescheduleModal.tsx +++ b/src/components/PayoutRescheduleModal.tsx @@ -1,11 +1,14 @@ -import React, { useState } from 'react'; -import { AlertTriangle, Clock, Calendar as CalendarIcon, CheckCircle } from 'lucide-react'; +import React, { useEffect, useId, useRef, useState } from 'react'; +import { AlertTriangle, Clock } from 'lucide-react'; +import { analyzeConflicts } from './PayoutTimeline/conflictAnalysis'; +import type { PayoutEvent } from './PayoutTimeline/PayoutTimeline'; export type ConflictType = 'hard' | 'soft'; export interface Conflict { id: string; - type: ConflictType; + type?: ConflictType; + severity?: ConflictType; message: string; } @@ -14,7 +17,11 @@ export interface PayoutRescheduleModalProps { onClose: () => void; onConfirm: (newDate: string, note: string) => void; initialDate: string; - conflicts: Conflict[]; + currentDate?: string; + conflicts?: Conflict[]; + allPayouts?: PayoutEvent[]; + payoutId?: string; + getConflicts?: (date: string) => Conflict[]; } export const PayoutRescheduleModal: React.FC = ({ @@ -22,28 +29,131 @@ export const PayoutRescheduleModal: React.FC = ({ onClose, onConfirm, initialDate, - conflicts, + currentDate, + conflicts: conflictsProp, + allPayouts, + payoutId, + getConflicts, }) => { const [newDate, setNewDate] = useState(initialDate); const [note, setNote] = useState(''); + const dateId = useId(); + const noteId = useId(); + const modalRef = useRef(null); + const confirmRef = useRef(null); + const cancelRef = useRef(null); + + useEffect(() => { + if (isOpen) { + setNewDate(initialDate); + setNote(''); + } + }, [isOpen, initialDate]); + + useEffect(() => { + if (!isOpen) return; + + const focusable = modalRef.current?.querySelectorAll( + 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])', + ); + const first = focusable?.[0]; + const last = focusable?.[focusable.length - 1]; + + const handleKeyDown = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + e.preventDefault(); + onClose(); + return; + } + if (e.key !== 'Tab' || !focusable || focusable.length === 0) return; + if (e.shiftKey) { + if (document.activeElement === first) { + e.preventDefault(); + last?.focus(); + } + } else { + if (document.activeElement === last) { + e.preventDefault(); + first?.focus(); + } + } + }; + + document.addEventListener('keydown', handleKeyDown); + setTimeout(() => { + confirmRef.current?.focus(); + }, 0); + + return () => { + document.removeEventListener('keydown', handleKeyDown); + }; + }, [isOpen, onClose]); if (!isOpen) return null; - const hasHardConflict = conflicts.some((c) => c.type === 'hard'); + let computedConflicts: Conflict[] = []; + if (getConflicts) { + computedConflicts = getConflicts(newDate); + } else if (allPayouts && payoutId) { + computedConflicts = analyzeConflicts(payoutId, newDate, allPayouts).map((c) => ({ + id: c.id, + severity: c.severity, + type: c.severity, + message: c.message, + })); + } else if (conflictsProp) { + computedConflicts = conflictsProp; + } + + const conflicts = computedConflicts; + const hasHardConflict = conflicts.some((c) => c.type === 'hard' || c.severity === 'hard'); + + const today = new Date(); + const todayStr = today.toISOString().split('T')[0]; + const isEmpty = !newDate; + const isPast = newDate ? newDate < todayStr : false; + const isUnchanged = currentDate ? newDate === currentDate : initialDate ? newDate === initialDate && currentDate === undefined ? false : currentDate ? newDate === currentDate : false : false; + // If currentDate not provided, treat as unchanged only if equals initial and we want to be strict? Issue says "invalid dates" - empty/past/unchanged + const unchanged = currentDate !== undefined ? newDate === currentDate : newDate === initialDate && note === ''; + const invalid = isEmpty || isPast || newDate === (currentDate ?? initialDate); + const canConfirm = !hasHardConflict && !invalid; return ( -
-
-

Reschedule Payout

+
{ + if (e.target === e.currentTarget) { + onClose(); + } + }} + > +
+

Reschedule Payout

- - setNewDate(e.target.value)} - className="w-full p-2 bg-slate-800 border border-slate-700 rounded-lg" - /> +
+ + setNewDate(e.target.value)} + className="w-full p-2 bg-slate-800 border border-slate-700 rounded-lg" + aria-invalid={invalid} + /> + {(isEmpty || isPast || (currentDate !== undefined ? newDate === currentDate : newDate === initialDate)) && ( +

+ {isEmpty ? 'Date is required' : isPast ? 'Date cannot be in the past' : 'Date must be different from the current date'} +

+ )} +
{conflicts.length > 0 && (
@@ -62,22 +172,26 @@ export const PayoutRescheduleModal: React.FC = ({
)} - -