From e2aa08df3b4734a9b9f894e03a0525f612b48cb1 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 01:59:47 +0100 Subject: [PATCH 01/26] feat: Add towns seed for local development --- supabase/seed.sql | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 supabase/seed.sql diff --git a/supabase/seed.sql b/supabase/seed.sql new file mode 100644 index 00000000..9f1ace76 --- /dev/null +++ b/supabase/seed.sql @@ -0,0 +1,33 @@ +INSERT INTO "public"."towns" ("id", "created_at", "name", "people_helping", "help_needed") + VALUES ('1', '2024-11-02 17:52:17.645308+00', 'Aldaia', '0', '0'), + ('2', '2024-11-02 17:52:17.645308+00', 'Alfafar', '0', '0'), + ('3', '2024-11-02 17:52:17.645308+00', 'Albal', '0', '0'), + ('4', '2024-11-02 17:52:17.645308+00', 'Alcudia', '0', '0'), + ('5', '2024-11-02 17:52:17.645308+00', 'Algemesí', '0', '0'), + ('6', '2024-11-02 17:52:17.645308+00', 'Bugarra', '0', '0'), + ('7', '2024-11-02 17:52:17.645308+00', 'Catarroja', '0', '0'), + ('8', '2024-11-02 17:52:17.645308+00', 'Castelló', '0', '0'), + ('9', '2024-11-02 17:52:17.645308+00', 'Cheste', '0', '0'), + ('10', '2024-11-02 17:52:17.645308+00', 'Chiva', '0', '0'), + ('11', '2024-11-02 17:52:17.645308+00', 'Gestalgar', '0', '0'), + ('12', '2024-11-02 17:52:17.645308+00', 'Guadassuar', '0', '0'), + ('14', '2024-11-02 17:52:17.645308+00', 'Manuel', '0', '0'), + ('15', '2024-11-02 17:52:17.645308+00', 'Massanassa', '0', '0'), + ('16', '2024-11-02 17:52:17.645308+00', 'Montserrat', '0', '0'), + ('17', '2024-11-02 17:52:17.645308+00', 'Paiporta', '0', '0'), + ('18', '2024-11-02 17:52:17.645308+00', 'Pedralba', '0', '0'), + ('19', '2024-11-02 17:52:17.645308+00', 'Riba-roja de Túria', '0', '0'), + ('20', '2024-11-02 17:52:17.645308+00', 'Sedaví', '0', '0'), + ('21', '2024-11-02 17:52:17.645308+00', 'Sot de Chera', '0', '0'), + ('22', '2024-11-02 17:52:17.645308+00', 'Torrent', '0', '0'), + ('23', '2024-11-02 17:52:17.645308+00', 'Utiel', '0', '0'), + ('24', '2024-11-02 17:52:17.645308+00', 'Villar del Arzobispo', '0', '0'), + ('25', '2024-11-03 09:34:00.825499+00', 'Alzira', '0', '0'), + ('26', '2024-11-03 08:44:02.724841+00', 'Benetusser', '0', '0'), + ('27', '2024-11-03 08:44:34.023343+00', 'Turís', '0', '0'), + ('28', '2024-11-03 08:47:46.2913+00', 'Picanya', '0', '0'), + ('29', '2024-11-03 08:57:38.119705+00', 'La Torre, Valencia', '0', '0'), + ('30', '2024-11-03 09:05:24.008619+00', 'Benimaclet', '0', '0'), + ('31', '2024-11-03 09:21:27.852067+00', 'Godelleta', '0', '0'), + ('32', '2024-11-05 10:38:38.669406+00', 'Llocnou de la Corona', '0', '0'), + ('33', '2024-11-05 10:38:52.085488+00', 'Beniparrell', '0', '0'); \ No newline at end of file From 567d0b069e5cf46824c3b5dfcc3383d218434e96 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:11:14 +0100 Subject: [PATCH 02/26] refactor(session-provider): add types --- src/context/SessionProvider.js | 10 ---------- src/context/SessionProvider.tsx | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) delete mode 100644 src/context/SessionProvider.js create mode 100644 src/context/SessionProvider.tsx diff --git a/src/context/SessionProvider.js b/src/context/SessionProvider.js deleted file mode 100644 index 561a1f24..00000000 --- a/src/context/SessionProvider.js +++ /dev/null @@ -1,10 +0,0 @@ -'use client'; -import { createContext, useContext } from 'react'; - -const SessionContext = createContext(); - -export const SessionProvider = ({ children, session }) => { - return {children}; -}; - -export const useSession = () => useContext(SessionContext); diff --git a/src/context/SessionProvider.tsx b/src/context/SessionProvider.tsx new file mode 100644 index 00000000..b45f9582 --- /dev/null +++ b/src/context/SessionProvider.tsx @@ -0,0 +1,16 @@ +'use client'; +import React, { createContext, ReactNode, useContext } from 'react'; +import { Session } from '@supabase/supabase-js'; + +const SessionContext = createContext(null); + +type SessionProviderProps = { + children: ReactNode; + session: Session | null; +} + +export const SessionProvider: React.FC = ({ children, session }) => { + return {children}; +}; + +export const useSession = () => useContext(SessionContext); From c841a18674c79e2e3d082066bdf047c7e934fe54 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:29:44 +0100 Subject: [PATCH 03/26] refactor(server-client): add types --- src/lib/supabase/server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index a6d8816c..61c61f8d 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -1,10 +1,11 @@ import { createServerClient, type CookieOptions } from '@supabase/ssr' import { cookies } from 'next/headers' +import { Database } from '@/types/database'; export async function createClient() { const cookieStore = await cookies() - return createServerClient( + return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!, { From 8861cb8cb55d23c535cdde2ac510ed013f105b93 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:30:29 +0100 Subject: [PATCH 04/26] refactor(get-towns): extract logic to service --- src/app/layout.js | 12 ++++-------- src/lib/service.ts | 19 +++++++++++++++++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/app/layout.js b/src/app/layout.js index ff84eee2..662fa535 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -2,19 +2,15 @@ import './globals.css'; import 'leaflet/dist/leaflet.css'; import EmergencyLayout from '@/components/layout/EmergencyLayout'; import { EmergencyProvider } from '@/context/EmergencyProvider'; -import { TownsProvider } from '../context/TownProvider'; +import { TownsProvider } from '@/context/TownProvider'; import { createClient } from '@/lib/supabase/server'; -import { SessionProvider } from '../context/SessionProvider'; +import { SessionProvider } from '@/context/SessionProvider'; +import { townsService } from '@/lib/service'; export const metadata = { title: 'Ajuda Dana - Sistema de Coordinación', description: 'Sistema de coordinación para emergencias en la Comunidad Valenciana', }; -const getTowns = async () => { - const supabase = await createClient(); - const { data, error } = await supabase.from('towns').select('id, name'); - return data; -}; const getSession = async () => { const supabase = await createClient(); @@ -24,7 +20,7 @@ const getSession = async () => { export default async function RootLayout({ children }) { const session = await getSession(); - const towns = await getTowns(); + const towns = await townsService.getTowns(); return ( diff --git a/src/lib/service.ts b/src/lib/service.ts index e715aecc..351adede 100644 --- a/src/lib/service.ts +++ b/src/lib/service.ts @@ -1,5 +1,6 @@ import { supabase } from './supabase/client'; import { HelpRequestAssignmentData, HelpRequestData } from '@/types/Requests'; +import { createClient } from '@/lib/supabase/server'; export const helpRequestService = { async createRequest(requestData:HelpRequestData) { @@ -155,6 +156,14 @@ export const mapService = { }, }; +export const townsService = { + async getTowns() { + const supabase = await getSupabaseClient(); + const { data, error } = await supabase.from('towns').select('id, name'); + return data; + } +} + // Add this function to test the connection export const testSupabaseConnection = async () => { try { @@ -199,3 +208,13 @@ export const authService = { return supabase.auth.updateUser({ ...metadata }); }, }; + +const getSupabaseClient = async () => { + if (typeof window === 'undefined') { + // Si estamos en el servidor, usa el cliente del servidor + return await createClient(); + } else { + // Si estamos en el cliente, usa el cliente del navegador + return supabase; + } +}; \ No newline at end of file From 6eb0263712145a0418ce172c8c1eb7bef06058f5 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:30:45 +0100 Subject: [PATCH 05/26] refactor: add town type --- src/types/Town.ts | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/types/Town.ts diff --git a/src/types/Town.ts b/src/types/Town.ts new file mode 100644 index 00000000..f1d882d3 --- /dev/null +++ b/src/types/Town.ts @@ -0,0 +1,3 @@ +import { Database } from '@/types/database'; + +export type Town = Database['public']['Tables']['towns']['Row']; \ No newline at end of file From 195085f6f409a8a72e0cdee0f0d58adb2bf05b3c Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:33:28 +0100 Subject: [PATCH 06/26] refactor: add town type --- src/context/TownProvider.js | 10 ---------- src/context/TownProvider.tsx | 16 ++++++++++++++++ 2 files changed, 16 insertions(+), 10 deletions(-) delete mode 100644 src/context/TownProvider.js create mode 100644 src/context/TownProvider.tsx diff --git a/src/context/TownProvider.js b/src/context/TownProvider.js deleted file mode 100644 index f0b6e6cb..00000000 --- a/src/context/TownProvider.js +++ /dev/null @@ -1,10 +0,0 @@ -'use client'; -import { createContext, useContext } from 'react'; - -const TownsContext = createContext(); - -export const TownsProvider = ({ children, towns }) => { - return {children}; -}; - -export const useTowns = () => useContext(TownsContext); diff --git a/src/context/TownProvider.tsx b/src/context/TownProvider.tsx new file mode 100644 index 00000000..535114a3 --- /dev/null +++ b/src/context/TownProvider.tsx @@ -0,0 +1,16 @@ +'use client'; +import React, { createContext, ReactNode, useContext } from 'react'; +import { Town } from '@/types/Town'; + +const TownsContext = createContext([]); + +type TownsProviderProps = { + children: ReactNode; + towns: Town[]; +} + +export const TownsProvider: React.FC = ({ children, towns }) => { + return {children}; +}; + +export const useTowns = () => useContext(TownsContext); From a4b52f729110ce0cda8fa60f9856781dde5c32ab Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:42:22 +0100 Subject: [PATCH 07/26] refactor: switch to ts --- src/helpers/{constants.js => constants.ts} | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) rename src/helpers/{constants.js => constants.ts} (88%) diff --git a/src/helpers/constants.js b/src/helpers/constants.ts similarity index 88% rename from src/helpers/constants.js rename to src/helpers/constants.ts index 798c6ba5..fde19fd9 100644 --- a/src/helpers/constants.js +++ b/src/helpers/constants.ts @@ -7,6 +7,7 @@ export const tiposAyudaOptions = { medica: 'Asistencia médica', psicologico: 'Apoyo psicológico', logistico: 'Apoyo logístico', + otros: 'Ayuda general' }; export const tiposAyudaAcepta = [ @@ -18,7 +19,7 @@ export const tiposAyudaAcepta = [ "Medicamentos" ] -export const mapToIdAndLabel = (data) => { +export const mapToIdAndLabel = (data:any) => { return Object.keys(data).map((key) => ({ id: key, label: data[key], From d7fff378823c9673e77133b9989a214c53609e95 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:42:52 +0100 Subject: [PATCH 08/26] refactor: add additional info type --- src/types/Requests.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/types/Requests.ts b/src/types/Requests.ts index 8c52aaff..593659b8 100644 --- a/src/types/Requests.ts +++ b/src/types/Requests.ts @@ -6,6 +6,11 @@ export type HelpRequestStatusType = 'pending' | 'in_progress' | 'active'; export type HelpRequestData = Database['public']['Tables']['help_requests']['Row']; export type HelpRequestAssignmentData = Database['public']['Tables']['help_request_assignments']['Row']; +export type HelpRequestAdditionalInfo = { + special_situations?: string; + email?: string; +} + export type CollectionPointType = 'permanente' | 'temporal'; export type CollectionPointStatus = 'active' | 'inactive'; export type CollectionPointData = Database['public']['Tables']['collection_points']['Row']; From cb4fa99c2b52d74fd672c13717e48ac9ece43c0d Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:43:05 +0100 Subject: [PATCH 09/26] refactor: add types --- .../{SolicitudCard.js => SolicitudCard.tsx} | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) rename src/components/{SolicitudCard.js => SolicitudCard.tsx} (87%) diff --git a/src/components/SolicitudCard.js b/src/components/SolicitudCard.tsx similarity index 87% rename from src/components/SolicitudCard.js rename to src/components/SolicitudCard.tsx index 099ecbe6..fa013e22 100644 --- a/src/components/SolicitudCard.js +++ b/src/components/SolicitudCard.tsx @@ -1,7 +1,22 @@ import { AlertTriangle, Calendar, MapPin, MapPinned, Megaphone, Phone, Users } from 'lucide-react'; import { tiposAyudaOptions } from '@/helpers/constants'; import Link from 'next/link'; -import { useSession } from '../context/SessionProvider'; +import { useSession } from '@/context/SessionProvider'; +import { HelpRequestAdditionalInfo, HelpRequestData } from '@/types/Requests'; +import { Town } from '@/types/Town'; + +type SolicitudCardProps = { + caso: HelpRequestData, + towns: Town[], + isHref: boolean, + button:SolicitudCardButton, + isEdit: boolean, +}; + +type SolicitudCardButton = { + text: string; + link: string; +} export default function SolicitudCard({ caso, @@ -9,8 +24,13 @@ export default function SolicitudCard({ isHref, button = { text: 'Ver solicitud', link: '/solicitud/' }, isEdit = false, -}) { +}:SolicitudCardProps) { const session = useSession(); + + const additionalInfo = caso.additional_info as HelpRequestAdditionalInfo; + + const special_situations = 'special_situations' in additionalInfo ? additionalInfo.special_situations : undefined; + const email = 'email' in additionalInfo ? additionalInfo.email : undefined; return ( <>
Fecha:{' '} - {new Date(caso.created_at).toLocaleDateString() + + {new Date(caso.created_at!).toLocaleDateString() + ' ' + - new Date(caso.created_at).toLocaleTimeString([], { + new Date(caso.created_at!).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', })} @@ -127,10 +147,10 @@ export default function SolicitudCard({
)} - {caso.additional_info?.special_situations && ( + {special_situations && (
Situaciones especiales: -

{caso.additional_info.special_situations}

+

{special_situations}

)} {caso.number_of_people && ( @@ -146,7 +166,7 @@ export default function SolicitudCard({ {session && session.user && session.user.email && - session.user.email === caso.additional_info.email && + session.user.email === email && !isEdit && ( Date: Wed, 6 Nov 2024 10:55:23 +0100 Subject: [PATCH 10/26] fix: fix compile error for supabase server client --- src/lib/supabase/server.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/lib/supabase/server.ts b/src/lib/supabase/server.ts index 61c61f8d..5f4cdd4f 100644 --- a/src/lib/supabase/server.ts +++ b/src/lib/supabase/server.ts @@ -1,4 +1,6 @@ -import { createServerClient, type CookieOptions } from '@supabase/ssr' +"use server" + +import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' import { Database } from '@/types/database'; From a1212dbd08d87cd3708f7d18f60b10cd4ca1fd9c Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 10:58:37 +0100 Subject: [PATCH 11/26] refactor: make params optional --- src/components/SolicitudCard.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/SolicitudCard.tsx b/src/components/SolicitudCard.tsx index fa013e22..b8033f1a 100644 --- a/src/components/SolicitudCard.tsx +++ b/src/components/SolicitudCard.tsx @@ -8,9 +8,9 @@ import { Town } from '@/types/Town'; type SolicitudCardProps = { caso: HelpRequestData, towns: Town[], - isHref: boolean, - button:SolicitudCardButton, - isEdit: boolean, + isHref?: boolean, + button?:SolicitudCardButton, + isEdit?: boolean, }; type SolicitudCardButton = { @@ -21,7 +21,7 @@ type SolicitudCardButton = { export default function SolicitudCard({ caso, towns, - isHref, + isHref = true, button = { text: 'Ver solicitud', link: '/solicitud/' }, isEdit = false, }:SolicitudCardProps) { From c9912715a8acfd31350a7d1b7fa47f615281e7e4 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 12:56:18 +0100 Subject: [PATCH 12/26] feat: add policies for inserting and selecting help_request_assignments --- ...10421_add-missing-policy-help-request-assignments.sql | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 supabase/migrations/20241106110421_add-missing-policy-help-request-assignments.sql diff --git a/supabase/migrations/20241106110421_add-missing-policy-help-request-assignments.sql b/supabase/migrations/20241106110421_add-missing-policy-help-request-assignments.sql new file mode 100644 index 00000000..532177af --- /dev/null +++ b/supabase/migrations/20241106110421_add-missing-policy-help-request-assignments.sql @@ -0,0 +1,9 @@ +CREATE POLICY "authenticated_can_insert_own_records" +ON public.help_request_assignments +FOR INSERT +WITH CHECK ((select auth.uid()) = user_id); + +CREATE POLICY "Enable read access for all users" +ON public.help_request_assignments +FOR SELECT +USING (true); \ No newline at end of file From b2b4bbf9f9784b6cf4674c5826e8ee3cf855c463 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:10:50 +0100 Subject: [PATCH 13/26] feat: add assign button --- src/components/AsignarSolicitudButton.tsx | 107 ++++++++++++++++++++++ src/components/SolicitudCard.tsx | 40 ++++---- src/components/Spinner.tsx | 8 ++ src/lib/service.ts | 38 +++++--- src/types/Requests.ts | 3 +- 5 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 src/components/AsignarSolicitudButton.tsx create mode 100644 src/components/Spinner.tsx diff --git a/src/components/AsignarSolicitudButton.tsx b/src/components/AsignarSolicitudButton.tsx new file mode 100644 index 00000000..370f57fd --- /dev/null +++ b/src/components/AsignarSolicitudButton.tsx @@ -0,0 +1,107 @@ +'use client'; + +import { useSession } from '@/context/SessionProvider'; +import { HelpRequestAssignmentData, HelpRequestData } from '@/types/Requests'; +import { helpRequestService } from '@/lib/service'; +import { MouseEvent, useCallback, useEffect, useState } from 'react'; +import { Spinner } from '@/components/Spinner'; +import Link from 'next/link'; + +type AsignarSolicitudButtonProps = { + helpRequest: HelpRequestData; +}; + +export default function AsignarSolicitudButton({ helpRequest }: AsignarSolicitudButtonProps) { + const session = useSession(); + + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const [assignments, setAssignments] = useState([]); + + const userAssignment = assignments.find((x) => x.user_id === session?.user.id); + const userIsAssigned = !!userAssignment; + + const fetchAssignments = useCallback(async () => { + setLoading(true); + try { + const data = await helpRequestService.getAssignments(helpRequest.id); + setAssignments(data); + } finally { + setLoading(false); + } + }, [helpRequest.id, setAssignments]); + + useEffect(() => { + fetchAssignments(); + }, [fetchAssignments]); + + async function handleSubmit(e: MouseEvent) { + e.preventDefault(); + setLoading(true); + setError(null); + + if (!session) return; + + try { + await helpRequestService.assign({ + help_request_id: helpRequest.id, + user_id: session.user.id, + phone_number: session.user.user_metadata.telefono!, + }); + } catch (error: any) { + console.error('Error al asignarte', error); + setError(error.message || 'Error al asignarte a esta solicitud de ayuda'); + } finally { + setLoading(false); + fetchAssignments(); + } + } + async function handleCancel(e: MouseEvent) { + e.preventDefault(); + setLoading(true); + setError(null); + + if (!session) return; + if (!userAssignment) return; + + try { + await helpRequestService.unassign(userAssignment.id); + } catch (error: any) { + console.error('Error al asignarte', error); + setError(error.message || 'Error al asignarte a esta solicitud de ayuda'); + } finally { + setLoading(false); + fetchAssignments(); + } + } + + if (loading) return ; + + if (!session || !session.user) + return ( + + Quiero ayudar + + ); + + return ( + <> + {userIsAssigned ? ( + + ) : ( + + )} + + ); +} diff --git a/src/components/SolicitudCard.tsx b/src/components/SolicitudCard.tsx index b8033f1a..38c8df0f 100644 --- a/src/components/SolicitudCard.tsx +++ b/src/components/SolicitudCard.tsx @@ -4,19 +4,20 @@ import Link from 'next/link'; import { useSession } from '@/context/SessionProvider'; import { HelpRequestAdditionalInfo, HelpRequestData } from '@/types/Requests'; import { Town } from '@/types/Town'; +import AsignarSolicitudButton from '@/components/AsignarSolicitudButton'; type SolicitudCardProps = { - caso: HelpRequestData, - towns: Town[], - isHref?: boolean, - button?:SolicitudCardButton, - isEdit?: boolean, + caso: HelpRequestData; + towns: Town[]; + isHref?: boolean; + button?: SolicitudCardButton; + isEdit?: boolean; }; type SolicitudCardButton = { text: string; link: string; -} +}; export default function SolicitudCard({ caso, @@ -24,7 +25,7 @@ export default function SolicitudCard({ isHref = true, button = { text: 'Ver solicitud', link: '/solicitud/' }, isEdit = false, -}:SolicitudCardProps) { +}: SolicitudCardProps) { const session = useSession(); const additionalInfo = caso.additional_info as HelpRequestAdditionalInfo; @@ -163,20 +164,16 @@ export default function SolicitudCard({ )}
- {session && - session.user && - session.user.email && - session.user.email === email && - !isEdit && ( - - Editar - - )} + {session && session.user && session.user.email && session.user.email === email && !isEdit && ( + + Editar + + )} {isHref && ( )} +
diff --git a/src/components/Spinner.tsx b/src/components/Spinner.tsx new file mode 100644 index 00000000..db435f18 --- /dev/null +++ b/src/components/Spinner.tsx @@ -0,0 +1,8 @@ +export function Spinner() { + return ( +
+ ); +} diff --git a/src/lib/service.ts b/src/lib/service.ts index 351adede..1c09c3ed 100644 --- a/src/lib/service.ts +++ b/src/lib/service.ts @@ -1,9 +1,9 @@ import { supabase } from './supabase/client'; -import { HelpRequestAssignmentData, HelpRequestData } from '@/types/Requests'; +import { HelpRequestAssignmentInsert, HelpRequestData } from '@/types/Requests'; import { createClient } from '@/lib/supabase/server'; export const helpRequestService = { - async createRequest(requestData:HelpRequestData) { + async createRequest(requestData: HelpRequestData) { const { data, error } = await supabase.from('help_requests').insert([requestData]).select(); if (error) throw error; @@ -21,14 +21,26 @@ export const helpRequestService = { return data; }, - async assign(requestData:HelpRequestAssignmentData) { + async getAssignments(id: number) { + const { data, error } = await supabase.from('help_request_assignments').select('*').eq('help_request_id', id); + + if (error) throw error; + return data; + }, + + async assign(requestData: HelpRequestAssignmentInsert) { const { data, error } = await supabase.from('help_request_assignments').insert([requestData]).select(); if (error) throw error; return data[0]; }, + async unassign(id: number) { + const { error } = await supabase.from('help_request_assignments').delete().eq('id', id); + + if (error) throw error; + }, - async getByType(type:any) { + async getByType(type: any) { const { data, error } = await supabase .from('help_requests') .select('*') @@ -41,7 +53,7 @@ export const helpRequestService = { }; export const missingPersonService = { - async create(data:any) { + async create(data: any) { const { data: result, error } = await supabase.from('missing_persons').insert([data]).select(); if (error) throw error; @@ -61,7 +73,7 @@ export const missingPersonService = { }; export const collectionPointService = { - create: async (data:any) => { + create: async (data: any) => { try { // Validate required fields if (!data.name) throw new Error('El nombre del centro es requerido'); @@ -146,7 +158,7 @@ export const mapService = { missingPersons: missingPersonsResponse.data || [], collectionPoints: collectionPointsResponse.data || [], }; - } catch (error:any) { + } catch (error: any) { console.error('MapService Error Details:', { message: error.message, error: error, @@ -161,8 +173,8 @@ export const townsService = { const supabase = await getSupabaseClient(); const { data, error } = await supabase.from('towns').select('id, name'); return data; - } -} + }, +}; // Add this function to test the connection export const testSupabaseConnection = async () => { @@ -186,7 +198,7 @@ export const authService = { async getSessionUser() { return supabase.auth.getUser(); }, - async signUp(email:any, password:any, nombre:any, telefono:any) { + async signUp(email: any, password: any, nombre: any, telefono: any) { return supabase.auth.signUp({ email, password, @@ -201,10 +213,10 @@ export const authService = { async signOut() { return supabase.auth.signOut(); }, - async signIn(email:any, password:any) { + async signIn(email: any, password: any) { return supabase.auth.signInWithPassword({ email, password }); }, - async updateUser(metadata:any) { + async updateUser(metadata: any) { return supabase.auth.updateUser({ ...metadata }); }, }; @@ -217,4 +229,4 @@ const getSupabaseClient = async () => { // Si estamos en el cliente, usa el cliente del navegador return supabase; } -}; \ No newline at end of file +}; diff --git a/src/types/Requests.ts b/src/types/Requests.ts index 593659b8..be7daf9e 100644 --- a/src/types/Requests.ts +++ b/src/types/Requests.ts @@ -5,11 +5,12 @@ export type HelpRequestUrgencyType = 'alta' | 'media' | 'baja'; export type HelpRequestStatusType = 'pending' | 'in_progress' | 'active'; export type HelpRequestData = Database['public']['Tables']['help_requests']['Row']; export type HelpRequestAssignmentData = Database['public']['Tables']['help_request_assignments']['Row']; +export type HelpRequestAssignmentInsert = Database['public']['Tables']['help_request_assignments']['Insert']; export type HelpRequestAdditionalInfo = { special_situations?: string; email?: string; -} +}; export type CollectionPointType = 'permanente' | 'temporal'; export type CollectionPointStatus = 'active' | 'inactive'; From 5511f48176d9623c6701c4867780f4a293704e46 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:19:13 +0100 Subject: [PATCH 14/26] feat: add TanStack Query --- package.json | 5 +++-- src/app/layout.js | 15 ++++++++++----- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 219112dc..ebeec8af 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "@react-google-maps/api": "^2.20.3", "@supabase/ssr": "^0.5.1", "@supabase/supabase-js": "^2.46.1", + "@tanstack/react-query": "^5.59.19", "deck.gl": "^9.0.34", "leaflet": "^1.9.4", "lucide-react": "^0.454.0", @@ -32,9 +33,9 @@ "eslint-config-prettier": "^9.1.0", "postcss": "^8", "prettier": "3.3.3", + "supabase": "^1.215.0", "tailwindcss": "^3.4.1", - "typescript": "^5.6.3", - "supabase": "^1.215.0" + "typescript": "^5.6.3" }, "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee" } diff --git a/src/app/layout.js b/src/app/layout.js index 662fa535..ee12742e 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -6,12 +6,15 @@ import { TownsProvider } from '@/context/TownProvider'; import { createClient } from '@/lib/supabase/server'; import { SessionProvider } from '@/context/SessionProvider'; import { townsService } from '@/lib/service'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; export const metadata = { title: 'Ajuda Dana - Sistema de Coordinación', description: 'Sistema de coordinación para emergencias en la Comunidad Valenciana', }; +const queryClient = new QueryClient(); + const getSession = async () => { const supabase = await createClient(); const { data, error } = await supabase.auth.getUser(); @@ -25,11 +28,13 @@ export default async function RootLayout({ children }) { - - - {children} - - + + + + {children} + + + From 11ccb0dc3296cc30ad2ffc4e4fbdff703413edc9 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:20:28 +0100 Subject: [PATCH 15/26] feat: add Sonner for toasts --- package.json | 3 ++- src/app/layout.js | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index ebeec8af..51a26afc 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "maplibre-gl": "^4.7.1", "next": "15.0.2", "react": "^18.3.1", - "react-dom": "^18.3.1" + "react-dom": "^18.3.1", + "sonner": "^1.7.0" }, "devDependencies": { "@types/node": "^22.8.7", diff --git a/src/app/layout.js b/src/app/layout.js index ee12742e..abaa2d7a 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -7,6 +7,7 @@ import { createClient } from '@/lib/supabase/server'; import { SessionProvider } from '@/context/SessionProvider'; import { townsService } from '@/lib/service'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { Toaster } from 'sonner'; export const metadata = { title: 'Ajuda Dana - Sistema de Coordinación', @@ -27,6 +28,7 @@ export default async function RootLayout({ children }) { return ( + From 716e9a8d81f34bdaf4bf98b2305bd71285980785 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:21:05 +0100 Subject: [PATCH 16/26] feat: configure Sonner --- src/app/layout.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/layout.js b/src/app/layout.js index abaa2d7a..8cb9c7cb 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -28,7 +28,7 @@ export default async function RootLayout({ children }) { return ( - + From 383931d59eb0673fa9ee9bee098287f3558d1bd3 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:45:48 +0100 Subject: [PATCH 17/26] fix: Correct types --- src/context/SessionProvider.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/context/SessionProvider.tsx b/src/context/SessionProvider.tsx index b45f9582..c94f7344 100644 --- a/src/context/SessionProvider.tsx +++ b/src/context/SessionProvider.tsx @@ -1,13 +1,15 @@ 'use client'; import React, { createContext, ReactNode, useContext } from 'react'; -import { Session } from '@supabase/supabase-js'; +import { User } from '@supabase/auth-js'; -const SessionContext = createContext(null); +const SessionContext = createContext({ user: null }); + +type UserSession = { user: User } | { user: null }; type SessionProviderProps = { children: ReactNode; - session: Session | null; -} + session: UserSession; +}; export const SessionProvider: React.FC = ({ children, session }) => { return {children}; From d7f7cd28aac72716ff5b95d90d74f29f6529c060 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:46:04 +0100 Subject: [PATCH 18/26] feat: add types to emergency provider --- src/context/EmergencyProvider.js | 21 --------------------- src/context/EmergencyProvider.tsx | 30 ++++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 21 deletions(-) delete mode 100644 src/context/EmergencyProvider.js create mode 100644 src/context/EmergencyProvider.tsx diff --git a/src/context/EmergencyProvider.js b/src/context/EmergencyProvider.js deleted file mode 100644 index d1e9226e..00000000 --- a/src/context/EmergencyProvider.js +++ /dev/null @@ -1,21 +0,0 @@ -'use client'; - -import React, { createContext, useContext, useState } from 'react'; - -const EmergencyContext = createContext(undefined); - -export const EmergencyProvider = ({ children }) => { - const [showModal, setShowModal] = useState(false); - - const toggleModal = (force) => setShowModal((prev) => (force !== undefined ? force : !prev)); - - return {children}; -}; - -export const useModal = () => { - const context = useContext(EmergencyContext); - if (!context) { - throw new Error('useModal must be used within an EmergencyProvider'); - } - return context; -}; diff --git a/src/context/EmergencyProvider.tsx b/src/context/EmergencyProvider.tsx new file mode 100644 index 00000000..fcd02baa --- /dev/null +++ b/src/context/EmergencyProvider.tsx @@ -0,0 +1,30 @@ +'use client'; + +import React, { createContext, ReactNode, useContext, useState } from 'react'; + +const EmergencyContext = createContext({ showModal: false, toggleModal: () => {} }); + +type EmergencyCtx = { + showModal: boolean; + toggleModal: (force: boolean) => void; +}; + +type SessionProviderProps = { + children: ReactNode; +}; + +export const EmergencyProvider: React.FC = ({ children }) => { + const [showModal, setShowModal] = useState(false); + + const toggleModal = (force: boolean) => setShowModal((prev) => (force !== undefined ? force : !prev)); + + return {children}; +}; + +export const useModal = () => { + const context = useContext(EmergencyContext); + if (!context) { + throw new Error('useModal must be used within an EmergencyProvider'); + } + return context; +}; From bf66bf06f7e940b97aac8a27e0e78548693ca37e Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:48:15 +0100 Subject: [PATCH 19/26] feat: add types to layouts --- src/app/{layout.js => layout.tsx} | 3 ++- .../layout/{EmergencyLayout.js => EmergencyLayout.tsx} | 7 +++---- src/components/layout/{Footer.js => Footer.tsx} | 0 3 files changed, 5 insertions(+), 5 deletions(-) rename src/app/{layout.js => layout.tsx} (92%) rename src/components/layout/{EmergencyLayout.js => EmergencyLayout.tsx} (87%) rename src/components/layout/{Footer.js => Footer.tsx} (100%) diff --git a/src/app/layout.js b/src/app/layout.tsx similarity index 92% rename from src/app/layout.js rename to src/app/layout.tsx index 8cb9c7cb..a8a80bac 100644 --- a/src/app/layout.js +++ b/src/app/layout.tsx @@ -8,6 +8,7 @@ import { SessionProvider } from '@/context/SessionProvider'; import { townsService } from '@/lib/service'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'sonner'; +import { PropsWithChildren } from 'react'; export const metadata = { title: 'Ajuda Dana - Sistema de Coordinación', @@ -22,7 +23,7 @@ const getSession = async () => { return data; }; -export default async function RootLayout({ children }) { +export default async function RootLayout({ children }: PropsWithChildren<{}>) { const session = await getSession(); const towns = await townsService.getTowns(); return ( diff --git a/src/components/layout/EmergencyLayout.js b/src/components/layout/EmergencyLayout.tsx similarity index 87% rename from src/components/layout/EmergencyLayout.js rename to src/components/layout/EmergencyLayout.tsx index 7962a634..44ff7265 100644 --- a/src/components/layout/EmergencyLayout.js +++ b/src/components/layout/EmergencyLayout.tsx @@ -1,12 +1,11 @@ -// components/layout/EmergencyLayout.js - 'use client'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, PropsWithChildren } from 'react'; +// @ts-ignore import Sidebar from './Sidebar'; import Footer from './Footer'; -export default function EmergencyLayout({ children }) { +export default function EmergencyLayout({ children }: PropsWithChildren<{}>) { const [isSidebarOpen, setIsSidebarOpen] = useState(true); // Por defecto abierto useEffect(() => { diff --git a/src/components/layout/Footer.js b/src/components/layout/Footer.tsx similarity index 100% rename from src/components/layout/Footer.js rename to src/components/layout/Footer.tsx From 99fc044b659990798c658ac911172c097632dbdd Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:57:12 +0100 Subject: [PATCH 20/26] refactor: remove unnecessary generic type --- src/components/layout/EmergencyLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/layout/EmergencyLayout.tsx b/src/components/layout/EmergencyLayout.tsx index 44ff7265..e7f3e4e1 100644 --- a/src/components/layout/EmergencyLayout.tsx +++ b/src/components/layout/EmergencyLayout.tsx @@ -5,7 +5,7 @@ import { useState, useEffect, PropsWithChildren } from 'react'; import Sidebar from './Sidebar'; import Footer from './Footer'; -export default function EmergencyLayout({ children }: PropsWithChildren<{}>) { +export default function EmergencyLayout({ children }: PropsWithChildren) { const [isSidebarOpen, setIsSidebarOpen] = useState(true); // Por defecto abierto useEffect(() => { From db47958ee937f7e58053a7d712219771354579b7 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:57:26 +0100 Subject: [PATCH 21/26] fix: throw on error --- src/lib/service.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/service.ts b/src/lib/service.ts index 1c09c3ed..69876452 100644 --- a/src/lib/service.ts +++ b/src/lib/service.ts @@ -171,7 +171,8 @@ export const mapService = { export const townsService = { async getTowns() { const supabase = await getSupabaseClient(); - const { data, error } = await supabase.from('towns').select('id, name'); + const { data, error } = await supabase.from('towns').select(); + if (error) throw error; return data; }, }; From 80f4abf8e323da996aef77dc114b5f255090a5b0 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 13:57:47 +0100 Subject: [PATCH 22/26] refactor: move query client provider to a client component --- src/app/layout.tsx | 8 +++----- src/context/QueryClientProvider.tsx | 10 ++++++++++ 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 src/context/QueryClientProvider.tsx diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a8a80bac..3513c0bf 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,24 +6,22 @@ import { TownsProvider } from '@/context/TownProvider'; import { createClient } from '@/lib/supabase/server'; import { SessionProvider } from '@/context/SessionProvider'; import { townsService } from '@/lib/service'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Toaster } from 'sonner'; import { PropsWithChildren } from 'react'; +import { QueryClientProvider } from '@/context/QueryClientProvider'; export const metadata = { title: 'Ajuda Dana - Sistema de Coordinación', description: 'Sistema de coordinación para emergencias en la Comunidad Valenciana', }; -const queryClient = new QueryClient(); - const getSession = async () => { const supabase = await createClient(); const { data, error } = await supabase.auth.getUser(); return data; }; -export default async function RootLayout({ children }: PropsWithChildren<{}>) { +export default async function RootLayout({ children }: PropsWithChildren) { const session = await getSession(); const towns = await townsService.getTowns(); return ( @@ -31,7 +29,7 @@ export default async function RootLayout({ children }: PropsWithChildren<{}>) { - + {children} diff --git a/src/context/QueryClientProvider.tsx b/src/context/QueryClientProvider.tsx new file mode 100644 index 00000000..d52a1c08 --- /dev/null +++ b/src/context/QueryClientProvider.tsx @@ -0,0 +1,10 @@ +'use client'; + +import { PropsWithChildren, useState } from 'react'; +import { QueryClient, QueryClientProvider as TanstackQueryClientProvider } from '@tanstack/react-query'; + +export const QueryClientProvider = ({ children }: PropsWithChildren) => { + const [client] = useState(new QueryClient()); + + return {children}; +}; From da3d93de8ee56adeae1e61f2c54d0d99d8b89b61 Mon Sep 17 00:00:00 2001 From: Pinx0 Date: Wed, 6 Nov 2024 14:00:11 +0100 Subject: [PATCH 23/26] feat: add button to assign to a help request and show a badge with the count --- src/components/AsignarSolicitudButton.tsx | 103 +++++++++++----------- src/components/SolicitudCard.tsx | 2 + src/components/SolicitudHelpCount.tsx | 33 +++++++ 3 files changed, 85 insertions(+), 53 deletions(-) create mode 100644 src/components/SolicitudHelpCount.tsx diff --git a/src/components/AsignarSolicitudButton.tsx b/src/components/AsignarSolicitudButton.tsx index 370f57fd..bdda3d70 100644 --- a/src/components/AsignarSolicitudButton.tsx +++ b/src/components/AsignarSolicitudButton.tsx @@ -3,9 +3,11 @@ import { useSession } from '@/context/SessionProvider'; import { HelpRequestAssignmentData, HelpRequestData } from '@/types/Requests'; import { helpRequestService } from '@/lib/service'; -import { MouseEvent, useCallback, useEffect, useState } from 'react'; +import { MouseEvent } from 'react'; import { Spinner } from '@/components/Spinner'; import Link from 'next/link'; +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; +import { toast } from 'sonner'; type AsignarSolicitudButtonProps = { helpRequest: HelpRequestData; @@ -14,69 +16,64 @@ type AsignarSolicitudButtonProps = { export default function AsignarSolicitudButton({ helpRequest }: AsignarSolicitudButtonProps) { const session = useSession(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); + const { + data: assignments, + isLoading, + error, + } = useQuery({ + queryKey: ['help_request_assignments', { id: helpRequest.id }], + queryFn: () => helpRequestService.getAssignments(helpRequest.id), + }); - const [assignments, setAssignments] = useState([]); + const queryClient = useQueryClient(); - const userAssignment = assignments.find((x) => x.user_id === session?.user.id); - const userIsAssigned = !!userAssignment; - - const fetchAssignments = useCallback(async () => { - setLoading(true); - try { - const data = await helpRequestService.getAssignments(helpRequest.id); - setAssignments(data); - } finally { - setLoading(false); - } - }, [helpRequest.id, setAssignments]); - - useEffect(() => { - fetchAssignments(); - }, [fetchAssignments]); - - async function handleSubmit(e: MouseEvent) { - e.preventDefault(); - setLoading(true); - setError(null); - - if (!session) return; - - try { + const assignMutation = useMutation({ + mutationFn: async () => { + if (!session.user) return; await helpRequestService.assign({ help_request_id: helpRequest.id, user_id: session.user.id, phone_number: session.user.user_metadata.telefono!, }); - } catch (error: any) { - console.error('Error al asignarte', error); - setError(error.message || 'Error al asignarte a esta solicitud de ayuda'); - } finally { - setLoading(false); - fetchAssignments(); - } + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['help_request_assignments'] }); + }, + onError: (e) => { + console.error('Error al asignarte a la petición de ayuda', e); + toast.error('Error al asignarte :('); + }, + }); + + const unassignMutation = useMutation({ + mutationFn: async () => { + if (!session.user) return; + if (!userAssignment) return; + await helpRequestService.unassign(userAssignment.id); + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['help_request_assignments'] }); + }, + onError: (e) => { + console.error('Error al asignarte a la petición de ayuda', e); + toast.error('Error al asignarte :('); + }, + }); + + async function handleSubmit(e: MouseEvent) { + e.preventDefault(); + assignMutation.mutate(); } async function handleCancel(e: MouseEvent) { e.preventDefault(); - setLoading(true); - setError(null); - - if (!session) return; - if (!userAssignment) return; - - try { - await helpRequestService.unassign(userAssignment.id); - } catch (error: any) { - console.error('Error al asignarte', error); - setError(error.message || 'Error al asignarte a esta solicitud de ayuda'); - } finally { - setLoading(false); - fetchAssignments(); - } + unassignMutation.mutate(); } - if (loading) return ; + if (isLoading) return ; + if (error || assignments === undefined) return <>; + + const userAssignment = assignments.find((x) => x.user_id === session.user?.id); + const userIsAssigned = !!userAssignment; if (!session || !session.user) return ( @@ -92,7 +89,7 @@ export default function AsignarSolicitudButton({ helpRequest }: AsignarSolicitud onClick={handleCancel} className={`rounded-lg text-white py-2 px-4 w-full sm:w-auto text-center bg-red-500`} > - Cancelar ayuda :( + Cancelar mi ayuda ) : (