diff --git a/apps/backend/src/app.ts b/apps/backend/src/app.ts index 662d52a..dc36f53 100644 --- a/apps/backend/src/app.ts +++ b/apps/backend/src/app.ts @@ -9,8 +9,8 @@ import { requestIdMiddleware } from './lib/http/request-id'; export function createApp() { const app = new Hono(); - app.use('*', requestIdMiddleware) - app.use('*', errorHandler) + app.use('*', requestIdMiddleware); + app.use('*', errorHandler); const allowedOrigins = (Bun.env.CORS_ORIGIN ?? 'http://localhost:5173,http://127.0.0.1:5173') .split(',') diff --git a/apps/backend/src/lib/errors.ts b/apps/backend/src/lib/errors.ts index 3555e82..90862dc 100644 --- a/apps/backend/src/lib/errors.ts +++ b/apps/backend/src/lib/errors.ts @@ -1,58 +1,58 @@ // lib/errors.ts export type ValidationIssue = { - path: string - message: string -} + path: string; + message: string; +}; export type AppError = | { - type: 'validation' - message: string - issues?: ValidationIssue[] + type: 'validation'; + message: string; + issues?: ValidationIssue[]; } | { - type: 'not_found' - message: string + type: 'not_found'; + message: string; } | { - type: 'conflict' - message: string + type: 'conflict'; + message: string; } | { - type: 'unauthorized' - message: string + type: 'unauthorized'; + message: string; } | { - type: 'forbidden' - message: string + type: 'forbidden'; + message: string; } | { - type: 'unexpected' - message: string - } + type: 'unexpected'; + message: string; + }; export const Errors = { validation(message: string, issues?: ValidationIssue[]): AppError { - return { type: 'validation', message, issues } + return { type: 'validation', message, issues }; }, notFound(message = 'Resource not found'): AppError { - return { type: 'not_found', message } + return { type: 'not_found', message }; }, conflict(message: string): AppError { - return { type: 'conflict', message } + return { type: 'conflict', message }; }, unauthorized(message = 'Unauthorized'): AppError { - return { type: 'unauthorized', message } + return { type: 'unauthorized', message }; }, forbidden(message = 'Forbidden'): AppError { - return { type: 'forbidden', message } + return { type: 'forbidden', message }; }, unexpected(message = 'Unexpected error'): AppError { - return { type: 'unexpected', message } - } -} \ No newline at end of file + return { type: 'unexpected', message }; + }, +}; diff --git a/apps/backend/src/lib/http/error-handler.ts b/apps/backend/src/lib/http/error-handler.ts index f1a833c..5b22c14 100644 --- a/apps/backend/src/lib/http/error-handler.ts +++ b/apps/backend/src/lib/http/error-handler.ts @@ -1,20 +1,20 @@ // http/error-handler.ts -import type { MiddlewareHandler } from 'hono' -import { unexpectedProblem } from './problem-builders' -import { logger } from '../logger' +import type { MiddlewareHandler } from 'hono'; +import { logger } from '../logger'; +import { unexpectedProblem } from './problem-builders'; export const errorHandler: MiddlewareHandler = async (c, next) => { try { - await next() + await next(); } catch (error) { - const requestId = c.get('requestId') as string | undefined - const instance = requestId ? `/requests/${requestId}` : undefined + const requestId = c.get('requestId') as string | undefined; + const instance = requestId ? `/requests/${requestId}` : undefined; logger.error({ requestId, - error - }) + error, + }); - return c.json(unexpectedProblem({ instance }), 500) + return c.json(unexpectedProblem({ instance }), 500); } -} \ No newline at end of file +}; diff --git a/apps/backend/src/lib/http/handle-result.ts b/apps/backend/src/lib/http/handle-result.ts index 3fda6cb..fb1f54f 100644 --- a/apps/backend/src/lib/http/handle-result.ts +++ b/apps/backend/src/lib/http/handle-result.ts @@ -1,7 +1,7 @@ -import type { Context } from 'hono' -import type { ContentfulStatusCode } from 'hono/utils/http-status' -import type { Result } from '@/lib/result' -import { mapAppErrorToProblem } from './problem-mapper' +import type { Result } from '@/lib/result'; +import type { Context } from 'hono'; +import type { ContentfulStatusCode } from 'hono/utils/http-status'; +import { mapAppErrorToProblem } from './problem-mapper'; export function handleResult( c: Context, @@ -9,12 +9,12 @@ export function handleResult( successStatus: ContentfulStatusCode = 200 ) { if (!result.ok) { - const requestId = c.get('requestId') as string | undefined - const instance = requestId ? `/requests/${requestId}` : undefined - const problem = mapAppErrorToProblem(result.error, instance) + const requestId = c.get('requestId') as string | undefined; + const instance = requestId ? `/requests/${requestId}` : undefined; + const problem = mapAppErrorToProblem(result.error, instance); - return c.json(problem, problem.status) + return c.json(problem, problem.status); } - return c.json(result.value, successStatus) -} \ No newline at end of file + return c.json(result.value, successStatus); +} diff --git a/apps/backend/src/lib/http/problem-builders.ts b/apps/backend/src/lib/http/problem-builders.ts index a159112..24b9a3a 100644 --- a/apps/backend/src/lib/http/problem-builders.ts +++ b/apps/backend/src/lib/http/problem-builders.ts @@ -1,10 +1,10 @@ // http/problem-builders.ts -import type { ProblemDetails } from './problem-details' +import type { ProblemDetails } from './problem-details'; export function validationProblem(params: { - detail?: string - instance?: string - errors?: Record + detail?: string; + instance?: string; + errors?: Record; }): ProblemDetails { return { type: 'https://api.myapp.dev/problems/validation', @@ -12,18 +12,18 @@ export function validationProblem(params: { status: 400, detail: params.detail ?? 'Invalid request data', instance: params.instance, - errors: params.errors - } + errors: params.errors, + }; } export function unexpectedProblem(params?: { - instance?: string + instance?: string; }): ProblemDetails { return { type: 'https://api.myapp.dev/problems/unexpected', title: 'Internal Server Error', status: 500, detail: 'Internal server error', - instance: params?.instance - } -} \ No newline at end of file + instance: params?.instance, + }; +} diff --git a/apps/backend/src/lib/http/problem-details.ts b/apps/backend/src/lib/http/problem-details.ts index 1631d7a..c530d36 100644 --- a/apps/backend/src/lib/http/problem-details.ts +++ b/apps/backend/src/lib/http/problem-details.ts @@ -1,10 +1,10 @@ -import type { ContentfulStatusCode } from 'hono/utils/http-status' +import type { ContentfulStatusCode } from 'hono/utils/http-status'; export type ProblemDetails = { - type: string - title: string - status: ContentfulStatusCode - detail?: string - instance?: string - errors?: Record -} \ No newline at end of file + type: string; + title: string; + status: ContentfulStatusCode; + detail?: string; + instance?: string; + errors?: Record; +}; diff --git a/apps/backend/src/lib/http/problem-mapper.ts b/apps/backend/src/lib/http/problem-mapper.ts index 386906a..b4be4a1 100644 --- a/apps/backend/src/lib/http/problem-mapper.ts +++ b/apps/backend/src/lib/http/problem-mapper.ts @@ -1,53 +1,50 @@ -import type { ContentfulStatusCode } from 'hono/utils/http-status' -import type { AppError } from '@/lib/errors' -import type { ProblemDetails } from './problem-details' +import type { AppError } from '@/lib/errors'; +import type { ContentfulStatusCode } from 'hono/utils/http-status'; +import type { ProblemDetails } from './problem-details'; function statusFromError(error: AppError): ContentfulStatusCode { switch (error.type) { case 'validation': - return 400 + return 400; case 'unauthorized': - return 401 + return 401; case 'forbidden': - return 403 + return 403; case 'not_found': - return 404 + return 404; case 'conflict': - return 409 + return 409; case 'unexpected': default: - return 500 + return 500; } } function titleFromStatus(status: ContentfulStatusCode): string { switch (status) { case 400: - return 'Bad Request' + return 'Bad Request'; case 401: - return 'Unauthorized' + return 'Unauthorized'; case 403: - return 'Forbidden' + return 'Forbidden'; case 404: - return 'Not Found' + return 'Not Found'; case 409: - return 'Conflict' + return 'Conflict'; default: - return 'Internal Server Error' + return 'Internal Server Error'; } } -export function mapAppErrorToProblem( - error: AppError, - instance?: string -): ProblemDetails { - const status = statusFromError(error) +export function mapAppErrorToProblem(error: AppError, instance?: string): ProblemDetails { + const status = statusFromError(error); return { type: `https://api.myapp.dev/problems/${error.type}`, title: titleFromStatus(status), status, detail: status === 500 ? 'Internal server error' : error.message, - instance - } -} \ No newline at end of file + instance, + }; +} diff --git a/apps/backend/src/lib/http/request-id.ts b/apps/backend/src/lib/http/request-id.ts index a7faf19..d0c6272 100644 --- a/apps/backend/src/lib/http/request-id.ts +++ b/apps/backend/src/lib/http/request-id.ts @@ -1,16 +1,16 @@ -import type { MiddlewareHandler } from 'hono' +import type { MiddlewareHandler } from 'hono'; type Env = { Variables: { - requestId: string - } -} + requestId: string; + }; +}; export const requestIdMiddleware: MiddlewareHandler = async (c, next) => { - const requestId = crypto.randomUUID() + const requestId = crypto.randomUUID(); - c.set('requestId', requestId) - c.header('X-Request-Id', requestId) + c.set('requestId', requestId); + c.header('X-Request-Id', requestId); - await next() -} \ No newline at end of file + await next(); +}; diff --git a/apps/backend/src/lib/http/validate.ts b/apps/backend/src/lib/http/validate.ts index 72fd9ab..772acf7 100644 --- a/apps/backend/src/lib/http/validate.ts +++ b/apps/backend/src/lib/http/validate.ts @@ -1,52 +1,48 @@ +import { zValidator } from '@hono/zod-validator'; +import type { ZodSchema } from 'zod'; +import { validationProblem } from './problem-builders'; +import { zodIssuesToRecord } from './zod-issues'; -import { zValidator } from '@hono/zod-validator' -import type { ZodSchema } from 'zod' -import { validationProblem } from './problem-builders' -import { zodIssuesToRecord } from './zod-issues' +type ValidationTarget = 'json' | 'query' | 'param' | 'header' | 'form'; -type ValidationTarget = 'json' | 'query' | 'param' | 'header' | 'form' - -function makeValidator( - target: ValidationTarget, - schema: TSchema -) { +function makeValidator(target: ValidationTarget, schema: TSchema) { return zValidator(target, schema, (result, c) => { if (result.success) { - return + return; } - const requestId = c.get('requestId') as string | undefined - const instance = requestId ? `/requests/${requestId}` : undefined + const requestId = c.get('requestId') as string | undefined; + const instance = requestId ? `/requests/${requestId}` : undefined; return c.json( validationProblem({ detail: `Invalid ${target} data`, instance, - errors: zodIssuesToRecord(result.error.issues) + errors: zodIssuesToRecord(result.error.issues), }), 400 - ) - }) + ); + }); } export const validate = { json(schema: TSchema) { - return makeValidator('json', schema) + return makeValidator('json', schema); }, query(schema: TSchema) { - return makeValidator('query', schema) + return makeValidator('query', schema); }, param(schema: TSchema) { - return makeValidator('param', schema) + return makeValidator('param', schema); }, header(schema: TSchema) { - return makeValidator('header', schema) + return makeValidator('header', schema); }, form(schema: TSchema) { - return makeValidator('form', schema) - } -} \ No newline at end of file + return makeValidator('form', schema); + }, +}; diff --git a/apps/backend/src/lib/http/zod-issues.ts b/apps/backend/src/lib/http/zod-issues.ts index eb77b49..1968cda 100644 --- a/apps/backend/src/lib/http/zod-issues.ts +++ b/apps/backend/src/lib/http/zod-issues.ts @@ -1,18 +1,16 @@ type IssueLike = { - path: PropertyKey[] - message: string -} + path: PropertyKey[]; + message: string; +}; -export function zodIssuesToRecord( - issues: IssueLike[] -): Record { - const out: Record = {} +export function zodIssuesToRecord(issues: IssueLike[]): Record { + const out: Record = {}; for (const issue of issues) { - const key = issue.path.length ? issue.path.join('.') : 'root' - out[key] ??= [] - out[key].push(issue.message) + const key = issue.path.length ? issue.path.join('.') : 'root'; + out[key] ??= []; + out[key].push(issue.message); } - return out -} \ No newline at end of file + return out; +} diff --git a/apps/backend/src/lib/result.ts b/apps/backend/src/lib/result.ts index 63143d9..5b2720d 100644 --- a/apps/backend/src/lib/result.ts +++ b/apps/backend/src/lib/result.ts @@ -1,14 +1,12 @@ // lib/result.ts import type { AppError } from '@/lib/errors'; -export type Result = - | { ok: true; value: T } - | { ok: false; error: E } +export type Result = { ok: true; value: T } | { ok: false; error: E }; export function ok(value: T): Result { - return { ok: true, value } + return { ok: true, value }; } export function err(error: E): Result { - return { ok: false, error } -} \ No newline at end of file + return { ok: false, error }; +} diff --git a/apps/backend/src/modules/admin-booking/services/admin-booking.service.ts b/apps/backend/src/modules/admin-booking/services/admin-booking.service.ts index 40c8337..2109c37 100644 --- a/apps/backend/src/modules/admin-booking/services/admin-booking.service.ts +++ b/apps/backend/src/modules/admin-booking/services/admin-booking.service.ts @@ -494,7 +494,7 @@ export async function updateAdminBookingStatus( ); } - if(input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') { + if (input.status === 'NOSHOW' && booking.status !== 'CONFIRMED') { throw new AdminBookingServiceError( 'Solo se pueden marcar como no show las reservas confirmadas.', 409 @@ -517,7 +517,7 @@ export async function updateAdminBookingStatus( }, }); - if(input.status === 'COMPLETED' || input.status ==='NOSHOW') { + if (input.status === 'COMPLETED' || input.status === 'NOSHOW') { await db.courtBooking.update({ where: { id: booking.id }, data: { @@ -528,8 +528,8 @@ export async function updateAdminBookingStatus( // if the booking is cancelled we delete it to free up the slot, but we keep a log of it with the cancelled status await db.courtBooking.delete({ where: { id: booking.id }, - }) + }); } - return mapBookingResponse({...booking, status: input.status }); + return mapBookingResponse({ ...booking, status: input.status }); } diff --git a/apps/backend/src/modules/sport/services/sport.service.ts b/apps/backend/src/modules/sport/services/sport.service.ts index 0f32c19..f56ae65 100644 --- a/apps/backend/src/modules/sport/services/sport.service.ts +++ b/apps/backend/src/modules/sport/services/sport.service.ts @@ -1,7 +1,7 @@ import { Prisma, Sport } from '@/generated/prisma/client'; import { Errors } from '@/lib/errors'; import { db } from '@/lib/prisma'; -import { err, ok, Result } from '@/lib/result'; +import { Result, err, ok } from '@/lib/result'; import { v7 as uuidv7 } from 'uuid'; export type CreateSportInput = { diff --git a/apps/backend/src/modules/sport/sport.routes.ts b/apps/backend/src/modules/sport/sport.routes.ts index e557a06..ba1c332 100644 --- a/apps/backend/src/modules/sport/sport.routes.ts +++ b/apps/backend/src/modules/sport/sport.routes.ts @@ -1,3 +1,4 @@ +import { validate } from '@/lib/http/validate'; import { requireAuth } from '@/middlewares/require-auth.middleware'; import { requireSuperAdmin } from '@/middlewares/require-super-admin.middleware'; import { createSportHandler } from '@/modules/sport/handlers/create-sport.handler'; @@ -8,7 +9,6 @@ import { zValidator } from '@hono/zod-validator'; import { createSportSchema, updateSportSchema } from '@repo/api-contract'; import { Hono } from 'hono'; import { z } from 'zod'; -import { validate } from '@/lib/http/validate' export const sportRoutes = new Hono(); const sportIdParamsSchema = z.object({ id: z.uuid() }); diff --git a/apps/backend/test/complex/invite-complex-user.handler.test.ts b/apps/backend/test/complex/invite-complex-user.handler.test.ts index a70c167..6277a66 100644 --- a/apps/backend/test/complex/invite-complex-user.handler.test.ts +++ b/apps/backend/test/complex/invite-complex-user.handler.test.ts @@ -1,11 +1,9 @@ import { beforeEach, expect, mock, test } from 'bun:test'; -import type { InviteComplexUserResponse } from '@repo/api-contract'; import { createInviteComplexUserHandler } from '@/modules/complex/handlers/invite-complex-user.handler'; +import type { InviteComplexUserResponse } from '@repo/api-contract'; -const inviteComplexUserMock = mock( - async () => undefined as unknown as InviteComplexUserResponse -); +const inviteComplexUserMock = mock(async () => undefined as unknown as InviteComplexUserResponse); class MockComplexMembersError extends Error { status: 400 | 403 | 404 | 409; diff --git a/apps/backend/test/complex/invite-complex-user.test.ts b/apps/backend/test/complex/invite-complex-user.test.ts index 5dfb832..10d59ec 100644 --- a/apps/backend/test/complex/invite-complex-user.test.ts +++ b/apps/backend/test/complex/invite-complex-user.test.ts @@ -1,10 +1,10 @@ import { beforeEach, expect, mock, test } from 'bun:test'; -import { prismaMock, sendMailMock, transactionMock } from '../support/prisma.mock'; import { ComplexMembersError, inviteComplexUser, } from '@/modules/complex/services/complex-members.service'; +import { prismaMock, sendMailMock, transactionMock } from '../support/prisma.mock'; beforeEach(() => { prismaMock._reset(); diff --git a/apps/backend/test/sport/create-sport.handler.test.ts b/apps/backend/test/sport/create-sport.handler.test.ts index 6ad0bb0..6b4a2b5 100644 --- a/apps/backend/test/sport/create-sport.handler.test.ts +++ b/apps/backend/test/sport/create-sport.handler.test.ts @@ -20,9 +20,7 @@ mock.module('@/modules/sport/services/sport.service', () => ({ createSport: createSportMock, })); -const { createSportHandler } = await import( - '@/modules/sport/handlers/create-sport.handler' -); +const { createSportHandler } = await import('@/modules/sport/handlers/create-sport.handler'); type HandlerContext = { get: (key: 'requestId') => string | undefined; diff --git a/apps/backend/test/support/prisma.mock.ts b/apps/backend/test/support/prisma.mock.ts index 27029e3..3f706a4 100644 --- a/apps/backend/test/support/prisma.mock.ts +++ b/apps/backend/test/support/prisma.mock.ts @@ -5,14 +5,16 @@ import type { PrismaClient } from '@/generated/prisma/client'; import type { PrismaClientMock } from 'bun-mock-prisma'; export const prismaMock = createPrismaMock() as PrismaClientMock; -export const sendMailMock = mock(async (_input: { - to: string; - subject: string; - html: string; - text: string; -}) => undefined); -export const transactionMock = mock(async (fn: (tx: PrismaClientMock) => Promise) => - fn(prismaMock) +export const sendMailMock = mock( + async (_input: { + to: string; + subject: string; + html: string; + text: string; + }) => undefined +); +export const transactionMock = mock( + async (fn: (tx: PrismaClientMock) => Promise) => fn(prismaMock) ); export const dbMock = { complexUser: prismaMock.complexUser, diff --git a/apps/frontend/src/features/booking/booking-provider.tsx b/apps/frontend/src/features/booking/booking-provider.tsx index 8781102..4b4121b 100644 --- a/apps/frontend/src/features/booking/booking-provider.tsx +++ b/apps/frontend/src/features/booking/booking-provider.tsx @@ -255,7 +255,10 @@ export function BookingProvider({ children, complex }: BookingProviderProps) { [bookingsQuery.data?.bookings, selectedDate] ); - const courts = useMemo(() => courtsQuery.data?.sort((a, b) => a.name.localeCompare(b.name)) ?? [], [courtsQuery.data]); + const courts = useMemo( + () => courtsQuery.data?.sort((a, b) => a.name.localeCompare(b.name)) ?? [], + [courtsQuery.data] + ); const filteredCourts = useMemo(() => { if (selectedSportId === 'all') return courts; @@ -293,7 +296,6 @@ export function BookingProvider({ children, complex }: BookingProviderProps) { }, []); const currentTime = useMemo(() => { - if (!isTodayIso(selectedDate)) return null; const nowMinutes = timeToMinutes(now); const start = timeToMinutes(visibleTimeRange.start); @@ -345,10 +347,12 @@ export function BookingProvider({ children, complex }: BookingProviderProps) { (segment?: BookingTimelineSegment) => { setSelectedSegment(segment ?? null); setBookingToolsOpen(true); - console.log(JSON.stringify(segment, null, 2)) - }, [selectedDate]); + console.log(JSON.stringify(segment, null, 2)); + }, + [selectedDate] + ); - const closeBookingTools = () => setBookingToolsOpen(false) + const closeBookingTools = () => setBookingToolsOpen(false); useEffect(() => { const handleOpenCreateBooking = () => { @@ -426,9 +430,9 @@ export function BookingProvider({ children, complex }: BookingProviderProps) { errorMessage: courtsQuery.isError || bookingsQuery.isError ? extractMessage( - courtsQuery.error ?? bookingsQuery.error, - 'No pudimos cargar el panel de reservas.' - ) + courtsQuery.error ?? bookingsQuery.error, + 'No pudimos cargar el panel de reservas.' + ) : null, isCreateBookingOpen, createBookingError: createBookingMutation.isError @@ -449,7 +453,7 @@ export function BookingProvider({ children, complex }: BookingProviderProps) { bookingToolsOpen, openBookingTools, closeBookingTools, - selectedSegment + selectedSegment, }), [ bookings, @@ -480,7 +484,7 @@ export function BookingProvider({ children, complex }: BookingProviderProps) { updateBookingStatus, viewMode, visibleTimeRange, - bookingToolsOpen + bookingToolsOpen, ] ); diff --git a/apps/frontend/src/features/booking/components/booking-timeline.tsx b/apps/frontend/src/features/booking/components/booking-timeline.tsx index 372fa94..f594e31 100644 --- a/apps/frontend/src/features/booking/components/booking-timeline.tsx +++ b/apps/frontend/src/features/booking/components/booking-timeline.tsx @@ -227,8 +227,7 @@ function BookingSlotBlock({ segment, schedule, rangeStart, totalMinutes }: Booki segment.status === 'maintenance' && 'border-maintenance/80 bg-maintenance/75 text-maintenance-foreground', segment.booking?.status === 'COMPLETED' && - 'border-reserved/70 bg-reserved/25 dark:text-reserved-foreground text-gray-600 line-through', - + 'border-reserved/70 bg-reserved/25 dark:text-reserved-foreground text-gray-600 line-through' )} style={{ left: `calc(${left}% + 4px)`, @@ -242,10 +241,10 @@ function BookingSlotBlock({ segment, schedule, rangeStart, totalMinutes }: Booki sportId: schedule.court.sportId, }); } else { - if(event.shiftKey && segment.booking?.status === 'CONFIRMED') { + if (event.shiftKey && segment.booking?.status === 'CONFIRMED') { updateBookingStatus(segment.booking.id, 'COMPLETED'); } else { - openBookingTools(segment); + openBookingTools(segment); } } }} @@ -269,7 +268,6 @@ function BookingSlotBlock({ segment, schedule, rangeStart, totalMinutes }: Booki {segment.booking?.customerName ?? 'Mantenimiento'} - )} diff --git a/apps/frontend/src/features/booking/components/booking-toolbar.tsx b/apps/frontend/src/features/booking/components/booking-toolbar.tsx index d63893c..8569554 100644 --- a/apps/frontend/src/features/booking/components/booking-toolbar.tsx +++ b/apps/frontend/src/features/booking/components/booking-toolbar.tsx @@ -12,7 +12,7 @@ import { ChevronLeft, ChevronRight, Clock, SlidersHorizontal } from 'lucide-reac import { useBooking } from '../booking-provider'; import type { BookingStatusFilter } from '../booking.types'; import { fromIsoDateLocal, toIsoDateLocal } from '../lib/booking-time'; - + const timeRangeOptions = [ { label: '08:00 - 22:00', start: '08:00', end: '22:00' }, { label: '06:00 - 00:00', start: '06:00', end: '23:59' }, diff --git a/apps/frontend/src/features/booking/components/booking-tools-dialog.tsx b/apps/frontend/src/features/booking/components/booking-tools-dialog.tsx index debaf9d..f396bca 100644 --- a/apps/frontend/src/features/booking/components/booking-tools-dialog.tsx +++ b/apps/frontend/src/features/booking/components/booking-tools-dialog.tsx @@ -1,233 +1,232 @@ -import { ResponsiveDialog, ResponsiveDialogClose, ResponsiveDialogContent, ResponsiveDialogDescription, ResponsiveDialogFooter, ResponsiveDialogHeader, ResponsiveDialogTitle } from "@/components/ui/responsive-dialog"; -import { useBooking } from "../booking-provider"; -import { Button } from "@/components/ui/button"; +import { Button } from '@/components/ui/button'; import { - CheckCircle2, - Clock, - MapPin, - MessageCircle, - Phone, - User, - XCircle, - AlertTriangle, - BadgeCheck, -} from "lucide-react"; -import { cn } from "@/lib/utils"; -import { Separator } from "@/components/ui/separator"; + ResponsiveDialog, + ResponsiveDialogClose, + ResponsiveDialogContent, + ResponsiveDialogDescription, + ResponsiveDialogFooter, + ResponsiveDialogHeader, + ResponsiveDialogTitle, +} from '@/components/ui/responsive-dialog'; +import { Separator } from '@/components/ui/separator'; +import { cn } from '@/lib/utils'; +import { + AlertTriangle, + BadgeCheck, + CheckCircle2, + Clock, + MapPin, + MessageCircle, + Phone, + User, + XCircle, +} from 'lucide-react'; +import { useBooking } from '../booking-provider'; function formatDate(date: string | undefined) { - if (!date) return ""; + if (!date) return ''; - const formatted = new Intl.DateTimeFormat("es-AR", { - weekday: "long", - day: "numeric", - month: "long", - }).format(new Date(`${date}T00:00:00`)); + const formatted = new Intl.DateTimeFormat('es-AR', { + weekday: 'long', + day: 'numeric', + month: 'long', + }).format(new Date(`${date}T00:00:00`)); - return formatted - .split(" ") - .map((word, index) => { - if (index === 0 || index === 3) { - return word.charAt(0).toUpperCase() + word.slice(1); - } - return word; - }) - .join(" "); + return formatted + .split(' ') + .map((word, index) => { + if (index === 0 || index === 3) { + return word.charAt(0).toUpperCase() + word.slice(1); + } + return word; + }) + .join(' '); } const statusConfig: Record = { - - CONFIRMED: { - label: "Reservada", - className: "bg-emerald-500/15 text-emerald-400 border-emerald-500/30", - }, - COMPLETED: { - label: "Completada", - className: "bg-sky-500/15 text-sky-400 border-sky-500/30", - }, - CANCELLED: { - label: "Cancelada", - className: "bg-red-500/15 text-red-400 border-red-500/30", - }, - NOSHOW: { - label: "No show", - className: "bg-amber-500/15 text-amber-400 border-amber-500/30", - }, - + CONFIRMED: { + label: 'Reservada', + className: 'bg-emerald-500/15 text-emerald-400 border-emerald-500/30', + }, + COMPLETED: { + label: 'Completada', + className: 'bg-sky-500/15 text-sky-400 border-sky-500/30', + }, + CANCELLED: { + label: 'Cancelada', + className: 'bg-red-500/15 text-red-400 border-red-500/30', + }, + NOSHOW: { + label: 'No show', + className: 'bg-amber-500/15 text-amber-400 border-amber-500/30', + }, }; export function BookingToolsDialog() { - const { - selectedSegment, - bookingToolsOpen, - closeBookingTools, - updateBookingStatus, - } = useBooking(); + const { selectedSegment, bookingToolsOpen, closeBookingTools, updateBookingStatus } = + useBooking(); + const status = statusConfig[selectedSegment?.booking?.status ?? 'CONFIRMED'] ?? { + label: selectedSegment?.booking?.status, + className: 'bg-slate-500/15 text-slate-300 border-slate-500/30', + }; - const status = statusConfig[selectedSegment?.booking?.status ?? "CONFIRMED"] ?? { - label: selectedSegment?.booking?.status, - className: "bg-slate-500/15 text-slate-300 border-slate-500/30", - }; - - const sendWhatsappReminder = () => { - const phone = selectedSegment?.booking?.customerPhone; - const message = ` + const sendWhatsappReminder = () => { + const phone = selectedSegment?.booking?.customerPhone; + const message = ` Hola ${selectedSegment?.booking?.customerName}. \nTe recordamos que tenés una reserva para el día *${formatDate(selectedSegment?.booking?.date)} de ${selectedSegment?.booking?.startTime} a ${selectedSegment?.booking?.endTime} en la cancha ${selectedSegment?.booking?.courtName} (${selectedSegment?.booking?.sport?.name})*. ¡Te esperamos! \nSi no vas a poder asistir, por favor avisanos para que otros clientes puedan reservar. Gracias.\n\n*Equipo de ${selectedSegment?.booking?.complexName}*`; - - const url = `https://wa.me/${phone}?text=${encodeURIComponent(message)}`; - window.open(url, '_blank'); - } - return ( - !open && closeBookingTools()} - > - event.preventDefault()} - > - - Detalles de la reserva - - Administra la reserva y su estado. - - + const url = `https://wa.me/${phone}?text=${encodeURIComponent(message)}`; + window.open(url, '_blank'); + }; -
-
- } - label="Cancha" - value={selectedSegment?.booking?.courtName ?? "-"} - helper={selectedSegment?.booking?.sport?.name} - withDivider - /> + return ( + !open && closeBookingTools()}> + event.preventDefault()} + > + + Detalles de la reserva + + Administra la reserva y su estado. + + - } - label="Estado actual" - value={status.label} - valueClassName="text-emerald-400 uppercase dark:text-emerald-500" - withDivider - /> +
+
+ } + label="Cancha" + value={selectedSegment?.booking?.courtName ?? '-'} + helper={selectedSegment?.booking?.sport?.name} + withDivider + /> - } - label="Cliente" - value={selectedSegment?.booking?.customerName ?? "-"} - /> + } + label="Estado actual" + value={status.label} + valueClassName="text-emerald-400 uppercase dark:text-emerald-500" + withDivider + /> - } - label="Fecha" - value={`${formatDate(selectedSegment?.booking?.date)}`} - withDivider - className="pt-10" - /> + } + label="Cliente" + value={selectedSegment?.booking?.customerName ?? '-'} + /> - } - label="Hora" - value={`${selectedSegment?.booking?.startTime} a ${selectedSegment?.booking?.endTime}`} - withDivider - className="pt-10" - /> + } + label="Fecha" + value={`${formatDate(selectedSegment?.booking?.date)}`} + withDivider + className="pt-10" + /> - } - label="Teléfono" - value={selectedSegment?.booking?.customerPhone ?? "-"} - className="pt-10" - /> + } + label="Hora" + value={`${selectedSegment?.booking?.startTime} a ${selectedSegment?.booking?.endTime}`} + withDivider + className="pt-10" + /> -
+ } + label="Teléfono" + value={selectedSegment?.booking?.customerPhone ?? '-'} + className="pt-10" + /> +
+
- + - +
+
+

Acciones disponibles

+

+ Elegí qué hacer según lo que ocurrió con el cliente. +

+
+
+ } + title="Completar reserva" + description="El cliente se presentó para usar la cancha." + className="border-emerald-500/40 bg-emerald-500/10 text-emerald-600 text-lg hover:bg-emerald-500/15 hover:text-esmerald-800" + onClick={() => { + updateBookingStatus(selectedSegment?.booking?.id!, 'COMPLETED'); + closeBookingTools(); + }} + /> -
-
-

Acciones disponibles

-

- Elegí qué hacer según lo que ocurrió con el cliente. -

-
-
- } - title="Completar reserva" - description="El cliente se presentó para usar la cancha." - className="border-emerald-500/40 bg-emerald-500/10 text-emerald-600 text-lg hover:bg-emerald-500/15 hover:text-esmerald-800" - onClick={() => updateBookingStatus(selectedSegment?.booking?.id!, 'COMPLETED')} - /> + } + title="Cancelar reserva" + description="El cliente avisó que no va a venir. La cancha vuelve a estar disponible." + className="border-red-500/40 bg-red-500/10 text-red-600 text-lg hover:bg-red-500/15 hover:text-red-800" + onClick={() => { + updateBookingStatus(selectedSegment?.booking?.id!, 'CANCELLED'); + closeBookingTools(); + }} + /> - } - title="Cancelar reserva" - description="El cliente avisó que no va a venir. La cancha vuelve a estar disponible." - className="border-red-500/40 bg-red-500/10 text-red-600 text-lg hover:bg-red-500/15 hover:text-red-800" - onClick={() => updateBookingStatus(selectedSegment?.booking?.id!, 'CANCELLED')} - /> + } + title="Enviar recordatorio" + description="Enviar un mensaje por WhatsApp al cliente." + className="border-sky-500/40 bg-sky-500/10 text-sky-600 text-lg hover:bg-sky-500/15 hover:text-sky-800" + onClick={sendWhatsappReminder} + /> - } - title="Enviar recordatorio" - description="Enviar un mensaje por WhatsApp al cliente." - className="border-sky-500/40 bg-sky-500/10 text-sky-600 text-lg hover:bg-sky-500/15 hover:text-sky-800" - onClick={sendWhatsappReminder} - /> + } + title="Marcar como No Show" + description="El cliente no vino y no canceló." + className="border-amber-500/40 bg-amber-500/10 text-amber-600 text-lg hover:bg-amber-500/15 hover:text-amber-800" + onClick={() => { + updateBookingStatus(selectedSegment?.booking?.id!, 'NOSHOW'); + closeBookingTools(); + }} + /> +
+
- } - title="Marcar como No Show" - description="El cliente no vino y no canceló." - className="border-amber-500/40 bg-amber-500/10 text-amber-600 text-lg hover:bg-amber-500/15 hover:text-amber-800" - onClick={() => updateBookingStatus(selectedSegment?.booking?.id!, 'NOSHOW')} - /> -
-
- - - - - - -
-
- ); + + + + + + + + ); } function DetailItem({ - icon, - label, - value, - helper, - valueClassName, - withDivider, - className, + icon, + label, + value, + helper, + valueClassName, + withDivider, + className, }: { - icon: React.ReactNode; - label: string; - value: string; - helper?: string; - valueClassName?: string; - withDivider?: boolean; - className?: string; + icon: React.ReactNode; + label: string; + value: string; + helper?: string; + valueClassName?: string; + withDivider?: boolean; + className?: string; }) { - - return ( + return ( +
+ {withDivider && (
- {withDivider && ( -
- )} + /> + )} -
- {icon} -
-
-

- {label} -

-

- {value} -

- {helper && ( -

- {helper} -

- )} -
-
- - ); +
+ {icon} +
+
+

{label}

+

+ {value} +

+ {helper &&

{helper}

} +
+
+ ); } function ActionButton({ - icon, - title, - description, - className, - onClick + icon, + title, + description, + className, + onClick, }: { - icon: React.ReactNode; - title: string; - description: string; - className: string; - onClick?: () => void; + icon: React.ReactNode; + title: string; + description: string; + className: string; + onClick?: () => void; }) { - return ( - - ); - -} \ No newline at end of file + return ( + + ); +}