diff --git a/src/app/components/admin/maintenanceModal.tsx b/src/app/components/admin/maintenanceModal.tsx index f753f38..a7ae79c 100644 --- a/src/app/components/admin/maintenanceModal.tsx +++ b/src/app/components/admin/maintenanceModal.tsx @@ -21,18 +21,20 @@ export default function MaintenanceModal({ onClose, isMaintainance, timestamp, - type + type, }: MaintenanceModalProps) { const [isOpen, setIsOpen] = useState(false) const [selectedCourt, setSelectedCourt] = useState(1) const [selectedSport, setSelectedSport] = useState(sports[0]) const [selectedMaterials, setSelectedMaterials] = useState([]) + const [endHour, setEndHour] = useState( new Date(timestamp || 0).getHours() + 1 ) const [endMinute, setEndMinute] = useState( new Date(timestamp || 0).getMinutes() ) + const hour = new Date(timestamp || 0).getHours() const minute = new Date(timestamp || 0).getMinutes() const date = new Date(timestamp || 0).toLocaleDateString('pt-BR', { @@ -46,62 +48,12 @@ export default function MaintenanceModal({ const allowedSportsByCourt: Record = useMemo( () => ({ - 0: [ - 'Football', - 'Rugby', - SportName.NA, - 'Atividades Academicas ou Eventos', - 'Funcional' - ], + 0: ['Football', 'Rugby', SportName.NA, 'Atividades Academicas ou Eventos', 'Funcional'], 6: ['Beach Tennis', SportName.NA], - 1: [ - 'Volleyball', - 'Basketball', - 'Futsal', - 'Handball', - 'Tennis', - SportName.NA, - 'Tenis de Mesa', - 'Atividades Academicas ou Eventos', - 'Funcional', - 'Judo' - ], - 2: [ - 'Volleyball', - 'Basketball', - 'Futsal', - 'Handball', - 'Tennis', - SportName.NA, - 'Tenis de Mesa', - 'Atividades Academicas ou Eventos', - 'Funcional', - 'Judo' - ], - 3: [ - 'Volleyball', - 'Basketball', - 'Futsal', - 'Handball', - 'Tennis', - SportName.NA, - 'Tenis de Mesa', - 'Atividades Academicas ou Eventos', - 'Funcional', - 'Judo' - ], - 4: [ - 'Volleyball', - 'Basketball', - 'Futsal', - 'Handball', - 'Tennis', - SportName.NA, - 'Tenis de Mesa', - 'Atividades Academicas ou Eventos', - 'Funcional', - 'Judo' - ], + 1: ['Volleyball', 'Basketball', 'Futsal', 'Handball', 'Tennis', SportName.NA, 'Tenis de Mesa', 'Atividades Academicas ou Eventos', 'Funcional', 'Judo'], + 2: ['Volleyball', 'Basketball', 'Futsal', 'Handball', 'Tennis', SportName.NA, 'Tenis de Mesa', 'Atividades Academicas ou Eventos', 'Funcional', 'Judo'], + 3: ['Volleyball', 'Basketball', 'Futsal', 'Handball', 'Tennis', SportName.NA, 'Tenis de Mesa', 'Atividades Academicas ou Eventos', 'Funcional', 'Judo'], + 4: ['Volleyball', 'Basketball', 'Futsal', 'Handball', 'Tennis', SportName.NA, 'Tenis de Mesa', 'Atividades Academicas ou Eventos', 'Funcional', 'Judo'], 5: ['Natação'] }), [] @@ -117,9 +69,11 @@ export default function MaintenanceModal({ ? selectedSport : currentAllowedSports[0] }, [selectedSport, currentAllowedSports]) + const weekData = getBookingsOfTheWeek.data const weekIsLoading = getBookingsOfTheWeek.isLoading const refetchWeek = getBookingsOfTheWeek.refetch + useEffect(() => { if (isVisible) { setTimeout(() => { @@ -136,7 +90,6 @@ export default function MaintenanceModal({ }, 100) } - // Mapeamento de esporte para material const sportMaterials: Record = useMemo( () => ({ Football: ['Bola de futebol'], @@ -158,7 +111,6 @@ export default function MaintenanceModal({ [] ) - // Atualiza o material automaticamente ao selecionar o esporte useEffect(() => { if (validSelectedSport && sportMaterials[validSelectedSport]) { setSelectedMaterials(sportMaterials[validSelectedSport]) @@ -168,50 +120,69 @@ export default function MaintenanceModal({ }, [validSelectedSport, sportMaterials]) const handleBook = async () => { - // Calcula end_date baseado nos selects - const start = Number(timestamp) - const startDateObj = new Date(start) + // Normalização estrita para evitar quebras por milissegundos residuais + const startObj = new Date(Number(timestamp)) + startObj.setSeconds(0, 0) + const start = startObj.getTime() + const computedEnd = new Date(start) computedEnd.setHours(endHour) computedEnd.setMinutes(endMinute) - computedEnd.setSeconds(0) - computedEnd.setMilliseconds(0) + computedEnd.setSeconds(0, 0) const endDateMs = - computedEnd.getTime() <= startDateObj.getTime() - ? start + 3600000 // fallback mínimo 1h se usuário escolher algo inválido + computedEnd.getTime() <= start + ? start + 3600000 : computedEnd.getTime() - //Se for manutenção e a quadra já está ocupada no horário selecionado, cancelar a reserva existente - if (!isMaintainance && selectedCourt === 4 && weekData?.bookings) { - const courtsToCancel = [1, 2, 3] + if (weekData?.bookings) { + // 1. Caso selecione a Quadra 4 (Completa) -> Cancela as sub-quadras 1, 2 e 3 + if (selectedCourt === 4) { + const courtsToCancel = [1, 2, 3] + + const overlappingSubCourts = weekData.bookings.filter( + (b) => + courtsToCancel.includes(Number(b.court_number)) && + Number(b.start_date) < endDateMs && + Number(b.end_date) > start + ) + + if (overlappingSubCourts.length > 0) { + const deletePromises = overlappingSubCourts.map((booking) => { + if (booking.booking_id) { + return deleteBookingMutation.mutateAsync(booking.booking_id) + } + return Promise.resolve() + }) + await Promise.all(deletePromises) + } + } - for (const courtNumber of courtsToCancel) { - const overlappingBooking = weekData.bookings.find( + // 2. Caso selecione sub-quadra (1, 2 ou 3) -> Cancela a Quadra 4 se houver sobreposição + if ([1, 2, 3].includes(selectedCourt)) { + const overlappingCourt4 = weekData.bookings.find( (b) => - b.court_number === courtNumber && - b.start_date < endDateMs && - b.end_date > start + Number(b.court_number) === 4 && + Number(b.start_date) < endDateMs && + Number(b.end_date) > start ) - if (overlappingBooking?.booking_id) { - await deleteBookingMutation.mutateAsync(overlappingBooking.booking_id) + if (overlappingCourt4?.booking_id) { + await deleteBookingMutation.mutateAsync(overlappingCourt4.booking_id) } } - } - if (isMaintainance && weekData?.bookings) { - const overlapping = weekData.bookings.find( - (b) => - b.court_number === selectedCourt && - b.start_date < endDateMs && - b.end_date > start - ) - if (overlapping?.booking_id) { - await deleteBookingMutation.mutateAsync(overlapping.booking_id) - getBookingsOfTheWeek.refetch() - handleClose() - return + // 3. Manutenção limpa reservas anteriores do mesmo local + if (isMaintainance) { + const overlappingSameCourt = weekData.bookings.find( + (b) => + Number(b.court_number) === selectedCourt && + Number(b.start_date) < endDateMs && + Number(b.end_date) > start + ) + if (overlappingSameCourt?.booking_id) { + await deleteBookingMutation.mutateAsync(overlappingSameCourt.booking_id) + } } } @@ -223,13 +194,14 @@ export default function MaintenanceModal({ materials: selectedMaterials, type: type } + await createBookingMutation.mutateAsync(bookingdata) + // Força a atualização do cache global antes de fechar a janela + await getBookingsOfTheWeek.refetch() handleClose() - getBookingsOfTheWeek.refetch() } - // Courts list const courts = useMemo( () => [ { value: 6, label: 'Beach Tenis' }, @@ -243,7 +215,6 @@ export default function MaintenanceModal({ [] ) - // Courts ocupadas no horário selecionado (janela de 1h a partir do start) const { occupiedCourtsSet, occupiedCourtsToHide } = useMemo(() => { if (!weekData?.bookings || !timestamp) return { @@ -253,12 +224,12 @@ export default function MaintenanceModal({ const start = Number(timestamp) const end = start + 3600000 const overlapping = weekData.bookings.filter( - (b) => b.start_date < end && b.end_date > start + (b) => Number(b.start_date) < end && Number(b.end_date) > start ) const raw = new Set() - overlapping.forEach((b) => raw.add(b.court_number)) + overlapping.forEach((b) => raw.add(Number(b.court_number))) const toHide = new Set(raw) - // Regra de esconder 1,2,3 quando 4 ocupada só para não manutenção + if (toHide.has(4)) { toHide.add(1) toHide.add(2) @@ -267,7 +238,6 @@ export default function MaintenanceModal({ return { occupiedCourtsSet: raw, occupiedCourtsToHide: toHide } }, [weekData, timestamp]) - // Lista de quadras disponíveis (ou todas se manutenção) const availableCourts = useMemo( () => isMaintainance @@ -276,7 +246,6 @@ export default function MaintenanceModal({ [courts, occupiedCourtsToHide, isMaintainance] ) - // Ensure selected court remains valid useEffect(() => { if (availableCourts.length > 0) { if (!availableCourts.some((c) => c.value === selectedCourt)) { @@ -286,7 +255,15 @@ export default function MaintenanceModal({ setSelectedCourt(-1) } }, [availableCourts, selectedCourt]) + if (!isVisible) return null + + // Bloqueia cliques se a API estiver carregando os dados da semana atualizados + const isActionDisabled = + createBookingMutation.isPending || + deleteBookingMutation.isPending || + weekIsLoading + return (
× + + {/* Adicionado o atributo disabled nativo na tag button para travar cliques paralelos */}
- {[...Array(28)].map((_, index) => { + {[...Array(29)].map((_, index) => { const hour = 8 + Math.floor(index / 2) const minute = index % 2 const isHourSeparator = minute === 0 diff --git a/src/app/pages/rules-warnings.tsx b/src/app/pages/rules-warnings.tsx index 9afcf15..85aac9b 100644 --- a/src/app/pages/rules-warnings.tsx +++ b/src/app/pages/rules-warnings.tsx @@ -91,7 +91,6 @@ export default function RulesWarnings() {

Regras

-