feat: implement complex selection flow

This commit is contained in:
Jose Selesan
2026-04-20 15:47:47 -03:00
parent a5e39c94c5
commit 28fd51f926
17 changed files with 453 additions and 38 deletions

View File

@@ -20,13 +20,10 @@ import {
SelectValue,
} from '@/components/ui/select';
import { ApiClientError, apiClient } from '@/lib/api-client';
import {
getCurrentComplexSlug,
setCurrentComplexSlug as persistCurrentComplexSlug,
} from '@/lib/current-complex';
import { zodResolver } from '@hookform/resolvers/zod';
import type { AdminBooking } from '@repo/api-contract';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { Check, Copy } from 'lucide-react';
import { useEffect, useMemo, useState } from 'react';
import { useForm } from 'react-hook-form';
@@ -123,9 +120,9 @@ function effectiveStatusClassName(status: 'CONFIRMED' | 'CANCELLED' | 'COMPLETED
export function HomePage() {
const queryClient = useQueryClient();
const navigate = useNavigate();
const todayIso = useMemo(() => toIsoDateLocal(new Date()), []);
const [currentComplexSlug, setCurrentComplexSlug] = useState<string | null>(null);
const [fromDate, setFromDate] = useState(todayIso);
const [manualDate, setManualDate] = useState(todayIso);
const [selectedSportId, setSelectedSportId] = useState<string | undefined>();
@@ -134,35 +131,32 @@ export function HomePage() {
const [isManualBookingOpen, setIsManualBookingOpen] = useState(false);
const [urlCopied, setUrlCopied] = useState(false);
useEffect(() => {
setCurrentComplexSlug(getCurrentComplexSlug());
}, []);
const currentComplexQuery = useQuery({
queryKey: ['current-complex'],
queryFn: () => apiClient.complexes.getCurrent(),
});
const myComplexesQuery = useQuery({
queryKey: ['my-complexes'],
queryFn: () => apiClient.complexes.listMine(),
});
const selectedComplex = useMemo(() => {
const complexes = myComplexesQuery.data ?? [];
if (complexes.length === 0) return null;
if (currentComplexSlug) {
const match = complexes.find((complex) => complex.complexSlug === currentComplexSlug);
if (match) return match;
useEffect(() => {
async function autoSelect() {
if (myComplexesQuery.data && myComplexesQuery.data.length === 1) {
await apiClient.complexes.select({ complexId: myComplexesQuery.data[0].id });
queryClient.invalidateQueries({ queryKey: ['current-complex'] });
} else if (myComplexesQuery.data && myComplexesQuery.data.length > 1 && !currentComplexQuery.data) {
navigate({ to: '/select-complex' });
}
}
return complexes[0];
}, [currentComplexSlug, myComplexesQuery.data]);
if (!currentComplexQuery.isLoading && !currentComplexQuery.data && myComplexesQuery.data) {
autoSelect();
}
}, [currentComplexQuery.isLoading, currentComplexQuery.data, myComplexesQuery.data, navigate, queryClient]);
useEffect(() => {
if (!selectedComplex) return;
setCurrentComplexSlug(selectedComplex.complexSlug);
persistCurrentComplexSlug(selectedComplex.complexSlug);
}, [selectedComplex]);
const selectedComplex = currentComplexQuery.data ?? null;
useEffect(() => {
if (!selectedComplex?.id) return;

View File

@@ -1,7 +1,7 @@
import { Button } from '@/components/ui/button';
import { Field, FieldError, FieldLabel } from '@/components/ui/field';
import { Input } from '@/components/ui/input';
import { authClient } from '@/lib/api-client';
import { apiClient, authClient } from '@/lib/api-client';
import { useAuth } from '@/lib/auth';
import { zodResolver } from '@hookform/resolvers/zod';
import { Link, useNavigate } from '@tanstack/react-router';
@@ -38,12 +38,30 @@ export function LoginPage({ redirectTo = '/' }: LoginPageProps) {
},
});
async function handlePostLogin() {
const complexes = await apiClient.complexes.listMine();
if (complexes.length === 1) {
await apiClient.complexes.select({ complexId: complexes[0].id });
navigate({ to: redirectTo });
} else if (complexes.length > 1) {
const current = await apiClient.complexes.getCurrent();
if (current) {
navigate({ to: redirectTo });
} else {
navigate({ to: '/select-complex' });
}
} else {
await navigate({ to: redirectTo });
}
}
const onSubmit = async (values: LoginForm) => {
setSubmitError(null);
try {
await signInWithPassword(values);
await navigate({ to: redirectTo });
await handlePostLogin();
} catch {
setSubmitError('No pudimos iniciar sesión. Verificá tus credenciales.');
}
@@ -55,9 +73,7 @@ export function LoginPage({ redirectTo = '/' }: LoginPageProps) {
try {
await authClient.signIn.social({
provider: 'google',
callbackURL: redirectTo.startsWith('http')
? redirectTo
: `${window.location.origin}${redirectTo}`,
callbackURL: `${window.location.origin}/auth-callback`,
});
} catch {
setSubmitError('No pudimos iniciar sesión con Google.');

View File

@@ -0,0 +1,98 @@
import { Button } from '@/components/ui/button';
import { apiClient } from '@/lib/api-client';
import type { ComplexWithRole } from '@repo/api-contract';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useNavigate } from '@tanstack/react-router';
import { Building2, Loader2 } from 'lucide-react';
export function SelectComplexPage() {
const navigate = useNavigate();
const complexesQuery = useQuery({
queryKey: ['my-complexes'],
queryFn: () => apiClient.complexes.listMine(),
});
const selectMutation = useMutation({
mutationFn: async (complexId: string) => {
const response = await apiClient.complexes.select({ complexId });
return response;
},
onSuccess: () => {
navigate({ to: '/' });
},
});
const handleSelect = (complex: ComplexWithRole) => {
selectMutation.mutate(complex.id);
};
if (complexesQuery.isLoading) {
return (
<main className="flex min-h-screen w-full items-center justify-center px-6">
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</main>
);
}
if (complexesQuery.isError) {
return (
<main className="flex min-h-screen w-full items-center justify-center px-6">
<div className="text-center">
<p className="text-destructive">Error al cargar tus complejos.</p>
<Button variant="link" className="mt-2" onClick={() => complexesQuery.refetch()}>
Reintentar
</Button>
</div>
</main>
);
}
const complexes = complexesQuery.data ?? [];
if (complexes.length === 0) {
return (
<main className="flex min-h-screen w-full items-center justify-center px-6">
<div className="text-center">
<p className="text-muted-foreground">No tenés complejos asociados.</p>
</div>
</main>
);
}
return (
<main className="flex min-h-screen w-full items-center justify-center px-6">
<section className="w-full max-w-md space-y-6">
<div className="space-y-2">
<h1 className="text-2xl font-semibold">Seleccioná un complejo</h1>
<p className="text-sm text-muted-foreground">
Elegí el complejo con el que vas a trabajar.
</p>
</div>
<div className="space-y-2">
{complexes.map((complex) => (
<button
key={complex.id}
type="button"
className="flex w-full items-center gap-3 rounded-lg border p-4 text-left transition-colors hover:bg-accent"
onClick={() => handleSelect(complex)}
disabled={selectMutation.isPending}
>
<Building2 className="h-5 w-5 text-muted-foreground" />
<div className="flex-1">
<p className="font-medium">{complex.complexName}</p>
<p className="text-sm text-muted-foreground">
{complex.city ?? 'Sin ciudad'} · {complex.role}
</p>
</div>
{selectMutation.isPending && selectMutation.variables === complex.id && (
<Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
)}
</button>
))}
</div>
</section>
</main>
);
}