Better court configuration
This commit is contained in:
@@ -9,6 +9,14 @@ import {
|
||||
} from '@repo/api-contract'
|
||||
import { Controller, useFieldArray, useForm } from 'react-hook-form'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Field, FieldError, FieldLabel } from '@/components/ui/field'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import {
|
||||
@@ -134,16 +142,22 @@ function createDefaultValues(): CreateCourtInput {
|
||||
type CourtFormSectionProps = {
|
||||
complexId: string | null
|
||||
editingCourt: Court | null
|
||||
initialDraft: CreateCourtInput | null
|
||||
onCancelEdit: () => void
|
||||
}
|
||||
|
||||
function CourtFormSection({
|
||||
complexId,
|
||||
editingCourt,
|
||||
initialDraft,
|
||||
onCancelEdit,
|
||||
}: CourtFormSectionProps) {
|
||||
const queryClient = useQueryClient()
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const [isWeekModalOpen, setIsWeekModalOpen] = useState(false)
|
||||
const [weekDefaultStartTime, setWeekDefaultStartTime] = useState('08:00')
|
||||
const [weekDefaultEndTime, setWeekDefaultEndTime] = useState('22:00')
|
||||
const [weekModalError, setWeekModalError] = useState<string | null>(null)
|
||||
|
||||
const sportsQuery = useQuery({
|
||||
queryKey: ['sports'],
|
||||
@@ -201,23 +215,28 @@ function CourtFormSection({
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!editingCourt) {
|
||||
form.reset(createDefaultValues())
|
||||
if (editingCourt) {
|
||||
form.reset({
|
||||
name: editingCourt.name,
|
||||
sportId: editingCourt.sportId,
|
||||
slotDurationMinutes: editingCourt.slotDurationMinutes,
|
||||
basePrice: editingCourt.basePrice,
|
||||
availability: editingCourt.availability.map((slot) => ({
|
||||
dayOfWeek: slot.dayOfWeek,
|
||||
startTime: slot.startTime,
|
||||
endTime: slot.endTime,
|
||||
})),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
form.reset({
|
||||
name: editingCourt.name,
|
||||
sportId: editingCourt.sportId,
|
||||
slotDurationMinutes: editingCourt.slotDurationMinutes,
|
||||
basePrice: editingCourt.basePrice,
|
||||
availability: editingCourt.availability.map((slot) => ({
|
||||
dayOfWeek: slot.dayOfWeek,
|
||||
startTime: slot.startTime,
|
||||
endTime: slot.endTime,
|
||||
})),
|
||||
})
|
||||
}, [editingCourt, form])
|
||||
if (initialDraft) {
|
||||
form.reset(initialDraft)
|
||||
return
|
||||
}
|
||||
|
||||
form.reset(createDefaultValues())
|
||||
}, [editingCourt, form, initialDraft])
|
||||
|
||||
const title = editingCourt ? 'Editar cancha' : 'Nueva cancha'
|
||||
const buttonText = editingCourt ? 'Guardar cambios' : 'Agregar cancha'
|
||||
@@ -317,23 +336,10 @@ function CourtFormSection({
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => {
|
||||
const availability = form.getValues('availability')
|
||||
const baseStartTime = availability[0]?.startTime ?? '08:00'
|
||||
const baseEndTime = availability[0]?.endTime ?? '22:00'
|
||||
const existingDays = new Set(
|
||||
availability.map((item) => item.dayOfWeek),
|
||||
)
|
||||
|
||||
const missingDays = DAY_OPTIONS.filter(
|
||||
(option) => !existingDays.has(option.value),
|
||||
).map((option) => ({
|
||||
dayOfWeek: option.value,
|
||||
startTime: baseStartTime,
|
||||
endTime: baseEndTime,
|
||||
}))
|
||||
|
||||
if (missingDays.length > 0) {
|
||||
availabilityFieldArray.append(missingDays)
|
||||
}
|
||||
setWeekDefaultStartTime(availability[0]?.startTime ?? '08:00')
|
||||
setWeekDefaultEndTime(availability[0]?.endTime ?? '22:00')
|
||||
setWeekModalError(null)
|
||||
setIsWeekModalOpen(true)
|
||||
}}
|
||||
>
|
||||
Agregar semana completa
|
||||
@@ -442,6 +448,101 @@ function CourtFormSection({
|
||||
)}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<Dialog
|
||||
open={isWeekModalOpen}
|
||||
onOpenChange={(nextOpen) => {
|
||||
setIsWeekModalOpen(nextOpen)
|
||||
if (!nextOpen) {
|
||||
setWeekModalError(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Horario por defecto</DialogTitle>
|
||||
<DialogDescription>
|
||||
Ingresa el horario para completar los días faltantes de la semana.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="grid gap-3 sm:grid-cols-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="week-start-time">Desde</FieldLabel>
|
||||
<Input
|
||||
id="week-start-time"
|
||||
type="time"
|
||||
value={weekDefaultStartTime}
|
||||
onChange={(event) => {
|
||||
setWeekDefaultStartTime(event.target.value)
|
||||
setWeekModalError(null)
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
|
||||
<Field>
|
||||
<FieldLabel htmlFor="week-end-time">Hasta</FieldLabel>
|
||||
<Input
|
||||
id="week-end-time"
|
||||
type="time"
|
||||
value={weekDefaultEndTime}
|
||||
onChange={(event) => {
|
||||
setWeekDefaultEndTime(event.target.value)
|
||||
setWeekModalError(null)
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
</div>
|
||||
|
||||
{weekModalError && <p className="text-sm text-destructive">{weekModalError}</p>}
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setIsWeekModalOpen(false)
|
||||
setWeekModalError(null)
|
||||
}}
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (weekDefaultStartTime >= weekDefaultEndTime) {
|
||||
setWeekModalError(
|
||||
'El horario de inicio debe ser menor al horario de fin.',
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
const availability = form.getValues('availability')
|
||||
const existingDays = new Set(
|
||||
availability.map((item) => item.dayOfWeek),
|
||||
)
|
||||
|
||||
const missingDays = DAY_OPTIONS.filter(
|
||||
(option) => !existingDays.has(option.value),
|
||||
).map((option) => ({
|
||||
dayOfWeek: option.value,
|
||||
startTime: weekDefaultStartTime,
|
||||
endTime: weekDefaultEndTime,
|
||||
}))
|
||||
|
||||
if (missingDays.length > 0) {
|
||||
availabilityFieldArray.append(missingDays)
|
||||
}
|
||||
|
||||
setIsWeekModalOpen(false)
|
||||
setWeekModalError(null)
|
||||
}}
|
||||
>
|
||||
Agregar días faltantes
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -452,6 +553,7 @@ type ComplexCourtsPageProps = {
|
||||
|
||||
export function ComplexCourtsPage({ complexSlug }: ComplexCourtsPageProps) {
|
||||
const [editingCourt, setEditingCourt] = useState<Court | null>(null)
|
||||
const [initialDraft, setInitialDraft] = useState<CreateCourtInput | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentComplexSlug(complexSlug)
|
||||
@@ -484,6 +586,7 @@ export function ComplexCourtsPage({ complexSlug }: ComplexCourtsPageProps) {
|
||||
<CourtFormSection
|
||||
complexId={complexId}
|
||||
editingCourt={editingCourt}
|
||||
initialDraft={initialDraft}
|
||||
onCancelEdit={() => {
|
||||
setEditingCourt(null)
|
||||
}}
|
||||
@@ -519,16 +622,42 @@ export function ComplexCourtsPage({ complexSlug }: ComplexCourtsPageProps) {
|
||||
{formatCurrency(court.basePrice)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => {
|
||||
setEditingCourt(court)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
<div className="flex w-full flex-col gap-2 sm:w-auto sm:flex-row">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => {
|
||||
setEditingCourt(null)
|
||||
setInitialDraft({
|
||||
name: `${court.name} - Copy`,
|
||||
sportId: court.sportId,
|
||||
slotDurationMinutes: court.slotDurationMinutes,
|
||||
basePrice: court.basePrice,
|
||||
availability: court.availability.map((slot) => ({
|
||||
dayOfWeek: slot.dayOfWeek,
|
||||
startTime: slot.startTime,
|
||||
endTime: slot.endTime,
|
||||
})),
|
||||
})
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}}
|
||||
>
|
||||
Duplicar cancha
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="w-full sm:w-auto"
|
||||
onClick={() => {
|
||||
setInitialDraft(null)
|
||||
setEditingCourt(court)
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' })
|
||||
}}
|
||||
>
|
||||
Editar
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-2 text-xs text-muted-foreground">
|
||||
|
||||
Reference in New Issue
Block a user