Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 81 additions & 111 deletions src/app/components/admin/maintenanceModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,18 +21,20 @@ export default function MaintenanceModal({
onClose,
isMaintainance,
timestamp,
type
type,
}: MaintenanceModalProps) {
const [isOpen, setIsOpen] = useState(false)
const [selectedCourt, setSelectedCourt] = useState<number>(1)
const [selectedSport, setSelectedSport] = useState<string>(sports[0])
const [selectedMaterials, setSelectedMaterials] = useState<string[]>([])

const [endHour, setEndHour] = useState<number>(
new Date(timestamp || 0).getHours() + 1
)
const [endMinute, setEndMinute] = useState<number>(
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', {
Expand All @@ -46,62 +48,12 @@ export default function MaintenanceModal({

const allowedSportsByCourt: Record<number, string[]> = 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']
}),
[]
Expand All @@ -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(() => {
Expand All @@ -136,7 +90,6 @@ export default function MaintenanceModal({
}, 100)
}

// Mapeamento de esporte para material
const sportMaterials: Record<string, string[]> = useMemo(
() => ({
Football: ['Bola de futebol'],
Expand All @@ -158,7 +111,6 @@ export default function MaintenanceModal({
[]
)

// Atualiza o material automaticamente ao selecionar o esporte
useEffect(() => {
if (validSelectedSport && sportMaterials[validSelectedSport]) {
setSelectedMaterials(sportMaterials[validSelectedSport])
Expand All @@ -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)
}
}
}

Expand All @@ -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' },
Expand All @@ -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 {
Expand All @@ -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<number>()
overlapping.forEach((b) => raw.add(b.court_number))
overlapping.forEach((b) => raw.add(Number(b.court_number)))
const toHide = new Set<number>(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)
Expand All @@ -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
Expand All @@ -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)) {
Expand All @@ -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 (
<div className={`fixed inset-0 z-20 flex items-center justify-center`}>
<div
Expand All @@ -307,15 +284,14 @@ export default function MaintenanceModal({
>
&times;
</button>

{/* Adicionado o atributo disabled nativo na tag button para travar cliques paralelos */}
<button
onClick={handleBook}
disabled={
createBookingMutation.isPending || deleteBookingMutation.isPending
}
disabled={isActionDisabled}
>
<div className="absolute bottom-4 right-4 flex w-40 items-center justify-center rounded-md bg-blue-primary p-2 text-xl text-white hover:cursor-pointer hover:text-gray-200 disabled:opacity-70">
{createBookingMutation.isPending ||
deleteBookingMutation.isPending ? (
<div className={`absolute bottom-4 right-4 flex w-40 items-center justify-center rounded-md bg-blue-primary p-2 text-xl text-white hover:cursor-pointer hover:text-gray-200 ${isActionDisabled ? 'opacity-50 cursor-not-allowed' : ''}`}>
{isActionDisabled ? (
<FiLoader className="h-6 w-6 animate-spin" />
) : isMaintainance && occupiedCourtsSet.has(selectedCourt) ? (
'Cancelar'
Expand All @@ -332,12 +308,10 @@ export default function MaintenanceModal({
</header>
<div className="flex h-[90%] items-start justify-start pt-8">
<div className="flex h-full w-1/3 flex-col items-center justify-start gap-4 px-4 py-2">
{/* data */}
<div className="flex h-12 w-full items-center justify-between gap-2 rounded-sm bg-yellow p-2">
<h1>Data:</h1>
<p>{date}</p>
</div>
{/* Local */}
<div className="flex w-full flex-col items-start justify-between gap-2 rounded-sm p-2">
<h1 className="text-xl font-bold">Local</h1>
{weekIsLoading ? (
Expand Down Expand Up @@ -367,7 +341,6 @@ export default function MaintenanceModal({
</select>
)}
</div>
{/* Modalidade */}
{!isMaintainance && (
<div className="flex w-full flex-col items-start justify-between gap-2 rounded-sm p-2">
<h1 className="text-xl font-bold">Modalidades</h1>
Expand All @@ -387,7 +360,6 @@ export default function MaintenanceModal({
</div>
<div className="flex w-2/3 flex-col items-start justify-start gap-4 py-2">
<div className="flex w-full flex-col items-center justify-start gap-4 px-4 py-2">
{/* horario */}
<h2 className="mb-4 w-full px-4 text-start text-2xl font-bold">
Horário
</h2>
Expand Down Expand Up @@ -431,11 +403,9 @@ export default function MaintenanceModal({
{endHour === 22 ? (
<option value={0}>00</option>
) : (
<>
<option value={0}>00</option>
<option value={30}>30</option>
</>
<option value={0}>00</option>
)}
{endHour !== 22 && <option value={30}>30</option>}
</select>
</div>
</div>
Expand All @@ -446,4 +416,4 @@ export default function MaintenanceModal({
</div>
</div>
)
}
}
4 changes: 2 additions & 2 deletions src/app/components/court-calendary.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@
fetchAccessToken()
localStorage.setItem('end_date', endOfTheWeek().toString())
localStorage.setItem('start_date', startOfTheWeek().toString())
}, [])

Check warning on line 58 in src/app/components/court-calendary.tsx

View workflow job for this annotation

GitHub Actions / Run ReactJS CI

React Hook useEffect has a missing dependency: 'fetchAccessToken'. Either include it or remove the dependency array

Check warning on line 58 in src/app/components/court-calendary.tsx

View workflow job for this annotation

GitHub Actions / Run ReactJS CI

React Hook useEffect has a missing dependency: 'fetchAccessToken'. Either include it or remove the dependency array

const { getBookingsOfTheWeek } = useBookingsQuery()

Expand Down Expand Up @@ -345,7 +345,7 @@
if (isField) {
return hour === 17 && minute === 0
} else {
return hour === 20 && minute === 1
return (hour >= 20 && minute === 1) || hour >= 21
}
}

Expand Down Expand Up @@ -478,7 +478,7 @@
))}
</div>
</div>
{[...Array(isField ? 18 : 25)].map((_, index) => {
{[...Array(isField ? 18 : 28)].map((_, index) => {
const hour = 8 + Math.floor((index + 1) / 2)
const minute = (index + 1) % 2
return (
Expand Down
Loading
Loading