From 2372b73b011dbce910aa78e25e6a7e37ea2d5587 Mon Sep 17 00:00:00 2001 From: joanmon Date: Thu, 14 Nov 2024 18:19:26 +0100 Subject: [PATCH 1/5] feat(crm): Add CRM log notes (#214) --- src/components/CRMLog.tsx | 115 ++++++++++++++++++ src/components/CRMNotes.tsx | 14 ++- src/components/ChangeCRMStatus.tsx | 40 +++--- src/components/SolicitudCard.tsx | 2 + src/lib/actions.ts | 21 ++++ src/types/Requests.ts | 1 + src/types/database.ts | 27 ++++ .../migrations/20241114165749_crm_log.sql | 107 ++++++++++++++++ 8 files changed, 311 insertions(+), 16 deletions(-) create mode 100644 src/components/CRMLog.tsx create mode 100644 supabase/migrations/20241114165749_crm_log.sql diff --git a/src/components/CRMLog.tsx b/src/components/CRMLog.tsx new file mode 100644 index 0000000..85240bd --- /dev/null +++ b/src/components/CRMLog.tsx @@ -0,0 +1,115 @@ +'use client'; + +import { MouseEvent, useCallback, useEffect, useState } from 'react'; +import Modal from '@/components/Modal'; +import { useModal } from '@/context/ModalProvider'; +import { ScrollText } from 'lucide-react'; +import { getCRMLogEntries } from '@/lib/actions'; + +type CRMLogButtonProps = { + helpRequestId: number; +}; + +type LogEntry = { + date: Date | null; + email: string; + diff: string | null; +}; + +export default function CRMLog({ helpRequestId }: CRMLogButtonProps) { + const { toggleModal } = useModal(); + const [opened, setOpened] = useState(false); + const [logEntries, setLogEntries] = useState([]); + useEffect(() => { + if (opened) { + const fetchLogEntries = async () => { + const data = await getCRMLogEntries(helpRequestId); + setLogEntries( + data?.map((entry) => ({ + date: entry.created_at ? new Date(entry.created_at) : null, + email: entry.email, + diff: entry.diff, + })), + ); + }; + fetchLogEntries(); + } + }, [opened]); + const MODAL_NAME = `Ver-LogBook-${helpRequestId}`; + + const handleCloseModal = useCallback( + async (e: MouseEvent) => { + e.preventDefault(); + toggleModal(MODAL_NAME, false); + setOpened(false); + }, + [toggleModal, setOpened], + ); + + const handleOpenModal = useCallback( + async (e: MouseEvent) => { + e.preventDefault(); + toggleModal(MODAL_NAME, true); + setOpened(true); + }, + [toggleModal, setOpened], + ); + + return ( + <> + + +
+
+
+

Log de Cambios

+ +
+
+ {logEntries.length > 0 ? ( + logEntries.map((entry, index) => ( +
+
+ + {entry.date + ? entry.date.toLocaleDateString('es-ES', { + day: 'numeric', + month: 'long', + year: 'numeric', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }) + : ''} + + {entry.email} +
+
{entry.diff}
+
+ )) + ) : ( +

No hay cambios registrados.

+ )} +
+
+ +
+
+
+
+ + ); +} diff --git a/src/components/CRMNotes.tsx b/src/components/CRMNotes.tsx index 620cc19..91da925 100644 --- a/src/components/CRMNotes.tsx +++ b/src/components/CRMNotes.tsx @@ -4,7 +4,8 @@ import { ChangeEvent, MouseEvent, useCallback, useState } from 'react'; import Modal from '@/components/Modal'; import { useModal } from '@/context/ModalProvider'; import { LimitedTextarea } from '@/components/input/LimitedTextarea'; -import { updateNotesRequest } from '@/lib/actions'; +import { updateNotesRequest, addCRMLog } from '@/lib/actions'; +import { useSession } from '@/context/SessionProvider'; type CRMNotesButtonProps = { helpRequestId: number; @@ -12,6 +13,7 @@ type CRMNotesButtonProps = { }; export default function CRMNotes({ helpRequestId, currentNotes }: CRMNotesButtonProps) { + const { user } = useSession(); const { toggleModal } = useModal(); const [notes, setNotes] = useState(currentNotes || ''); const [newNotes, setNewNotes] = useState(currentNotes || ''); @@ -27,10 +29,18 @@ export default function CRMNotes({ helpRequestId, currentNotes }: CRMNotesButton setError(error); return; } + if (user) { + await addCRMLog( + '---ANTES---\n' + notes + '\n---DESPUES---\n' + newNotes, + helpRequestId, + user.id, + (user.user_metadata.full_name || user.user_metadata.nombre) + ' ' + user.email, + ); + } toggleModal(MODAL_NAME, false); setNotes(newNotes); }, - [newNotes, setNotes, toggleModal], + [newNotes, setNotes, toggleModal, user], ); async function handleOpenModal(e: MouseEvent) { diff --git a/src/components/ChangeCRMStatus.tsx b/src/components/ChangeCRMStatus.tsx index e7aaa9c..171950b 100644 --- a/src/components/ChangeCRMStatus.tsx +++ b/src/components/ChangeCRMStatus.tsx @@ -1,8 +1,9 @@ 'use client'; -import { useState } from 'react'; +import { useCallback, useState } from 'react'; import { CRMStatus, CrmStatusActive, CrmStatusFinished } from '@/helpers/constants'; -import { updateHelpRequestCRMStatus } from '@/lib/actions'; +import { addCRMLog, updateHelpRequestCRMStatus } from '@/lib/actions'; +import { useSession } from '@/context/SessionProvider'; type ChangeCRMStatusRequestButtonProps = { helpRequestId: number; @@ -17,22 +18,33 @@ export default function ChangeCRMStatus({ currentStatus, currentCrmStatus, }: ChangeCRMStatusRequestButtonProps) { + const { user } = useSession(); const [crmStatus, setCrmStatus] = useState(currentCrmStatus || CrmStatusActive); const [error, setError] = useState({}); - const updateStatusRequest = async (newCrmStatus: string) => { - var status = currentStatus || 'active'; - if (newCrmStatus === CrmStatusFinished) { - status = 'finished'; - } else if (newCrmStatus !== CrmStatusFinished && status == 'finished') { - status = 'active'; - } - const { data, error } = await updateHelpRequestCRMStatus(String(helpRequestId), status, newCrmStatus); - - onStatusUpdate(status); + const updateStatusRequest = useCallback( + async (newCrmStatus: string) => { + var status = currentStatus || 'active'; + if (newCrmStatus === CrmStatusFinished) { + status = 'finished'; + } else if (newCrmStatus !== CrmStatusFinished && status == 'finished') { + status = 'active'; + } + const { data, error } = await updateHelpRequestCRMStatus(String(helpRequestId), status, newCrmStatus); + if (user !== null) { + await addCRMLog( + 'Estado cambiado de ' + crmStatus + ' a ' + newCrmStatus, + helpRequestId, + user.id, + (user.user_metadata.full_name || user.user_metadata.nombre) + ' ' + user.email, + ); + } + onStatusUpdate(status); - return { data, error }; - }; + return { data, error }; + }, + [crmStatus, helpRequestId, user], + ); async function handleUpdateSubmit(newStatus: string) { const { data, error } = await updateStatusRequest(newStatus); diff --git a/src/components/SolicitudCard.tsx b/src/components/SolicitudCard.tsx index 30eeb4c..a42f908 100644 --- a/src/components/SolicitudCard.tsx +++ b/src/components/SolicitudCard.tsx @@ -16,6 +16,7 @@ import ChangeStatusButton from './ChangeStatusButton'; import ChangeCRMStatus from './ChangeCRMStatus'; import { UserRoles } from '@/helpers/constants'; import CRMNotes from '@/components/CRMNotes'; +import CRMLog from '@/components/CRMLog'; type SolicitudCardProps = { caso: SelectedHelpData; @@ -209,6 +210,7 @@ export default function SolicitudCard({ /> )} {(isCrmUser || isAdmin) && } + {(isCrmUser || isAdmin) && } {isAdmin && setDeleted(true)} />} diff --git a/src/lib/actions.ts b/src/lib/actions.ts index eb36920..c7be33f 100644 --- a/src/lib/actions.ts +++ b/src/lib/actions.ts @@ -1,6 +1,7 @@ 'use server'; import { + CRMUsersLogRow, helpDataSelectFields, HelpRequestAssignmentInsert, HelpRequestData, @@ -79,6 +80,26 @@ export async function updateNotesRequest(newNotes: string, helpRequestId: string return { data, error }; } +export async function addCRMLog(diff: string, helpRequestId: number, userId: string, email: string) { + const supabase = await createClient(); + const { data, error } = await supabase + .from('crm_users_log') + .insert([{ help_request_id: helpRequestId, user_id: userId, email, diff }]); + return { data, error }; +} + +export async function getCRMLogEntries(helpRequestId: number): Promise { + const supabase = await createClient(); + const { data, error } = await supabase + .from('crm_users_log') + .select('*') + .eq('help_request_id', helpRequestId) + .order('created_at', { ascending: false }); + + if (error) throw error; + return data; +} + export async function deleteHelpRequest(helpRequestId: string) { const supabase = await createClient(); const { data, error } = await supabase.from('help_requests').delete().eq('id', helpRequestId).select(); diff --git a/src/types/Requests.ts b/src/types/Requests.ts index ec9bc2a..0e4046d 100644 --- a/src/types/Requests.ts +++ b/src/types/Requests.ts @@ -16,6 +16,7 @@ export type HelpRequestAssignmentData = Database['public']['Tables']['help_reque export type HelpRequestAssignmentInsert = Database['public']['Tables']['help_request_assignments']['Insert']; export type HelpRequestComment = Database['public']['Tables']['comments']['Row']; +export type CRMUsersLogRow = Database['public']['Tables']['crm_users_log']['Row']; export type HelpRequestAdditionalInfo = { special_situations?: string; diff --git a/src/types/database.ts b/src/types/database.ts index dac3a95..ec0afab 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -424,6 +424,33 @@ export type Database = { }; Relationships: []; }; + crm_users_log: { + Row: { + id: number; + help_request_id: number; + created_at: string | null; + email: string; + user_id: string; + diff: string | null; + }; + Insert: { + id: number | null; + help_request_id: number; + created_at?: string | null; + email: string; + user_id: string; + diff: string; + }; + Relationships: [ + { + foreignKeyName: 'crm_users_log_help_request_id_fkey'; + columns: ['help_request_id']; + isOneToOne: false; + referencedRelation: 'help_requests'; + referencedColumns: ['id']; + }, + ]; + }; }; Views: { distinct_collection_cities: { diff --git a/supabase/migrations/20241114165749_crm_log.sql b/supabase/migrations/20241114165749_crm_log.sql new file mode 100644 index 0000000..4b7811a --- /dev/null +++ b/supabase/migrations/20241114165749_crm_log.sql @@ -0,0 +1,107 @@ +create table "public"."crm_users_log" ( + "help_request_id" bigint not null, + "created_at" timestamp with time zone not null default now(), + "email" character varying, + "user_id" uuid, + "diff" text, + "id" bigint generated by default as identity not null +); + + +alter table "public"."crm_users_log" enable row level security; + +CREATE UNIQUE INDEX crm_users_log_pkey ON public.crm_users_log USING btree (id); + +alter table "public"."crm_users_log" add constraint "crm_users_log_pkey" PRIMARY KEY using index "crm_users_log_pkey"; + +alter table "public"."crm_users_log" add constraint "crm_users_log_help_request_id_fkey" FOREIGN KEY (help_request_id) REFERENCES help_requests(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."crm_users_log" validate constraint "crm_users_log_help_request_id_fkey"; + +alter table "public"."crm_users_log" add constraint "crm_users_log_user_id_fkey" FOREIGN KEY (user_id) REFERENCES auth.users(id) ON UPDATE CASCADE ON DELETE CASCADE not valid; + +alter table "public"."crm_users_log" validate constraint "crm_users_log_user_id_fkey"; + +grant delete on table "public"."crm_users_log" to "anon"; + +grant insert on table "public"."crm_users_log" to "anon"; + +grant references on table "public"."crm_users_log" to "anon"; + +grant select on table "public"."crm_users_log" to "anon"; + +grant trigger on table "public"."crm_users_log" to "anon"; + +grant truncate on table "public"."crm_users_log" to "anon"; + +grant update on table "public"."crm_users_log" to "anon"; + +grant delete on table "public"."crm_users_log" to "authenticated"; + +grant insert on table "public"."crm_users_log" to "authenticated"; + +grant references on table "public"."crm_users_log" to "authenticated"; + +grant select on table "public"."crm_users_log" to "authenticated"; + +grant trigger on table "public"."crm_users_log" to "authenticated"; + +grant truncate on table "public"."crm_users_log" to "authenticated"; + +grant update on table "public"."crm_users_log" to "authenticated"; + +grant delete on table "public"."crm_users_log" to "service_role"; + +grant insert on table "public"."crm_users_log" to "service_role"; + +grant references on table "public"."crm_users_log" to "service_role"; + +grant select on table "public"."crm_users_log" to "service_role"; + +grant trigger on table "public"."crm_users_log" to "service_role"; + +grant truncate on table "public"."crm_users_log" to "service_role"; + +grant update on table "public"."crm_users_log" to "service_role"; + +create policy "Enable insert for admins" +on "public"."crm_users_log" +as permissive +for insert +to authenticated +with check ((EXISTS ( SELECT 1 + FROM user_roles + WHERE ((user_roles.user_id = auth.uid()) AND (user_roles.role = 'admin'::roles))))); + + +create policy "Enable insert for moderators" +on "public"."crm_users_log" +as permissive +for insert +to authenticated +with check ((EXISTS ( SELECT 1 + FROM user_roles + WHERE ((user_roles.user_id = auth.uid()) AND (user_roles.role = 'moderator'::roles))))); + + +create policy "Enable read for moderators" +on "public"."crm_users_log" +as permissive +for select +to authenticated +using ((EXISTS ( SELECT 1 + FROM user_roles + WHERE ((user_roles.user_id = auth.uid()) AND (user_roles.role = 'moderator'::roles))))); + + +create policy "Enable reads for admins" +on "public"."crm_users_log" +as permissive +for select +to authenticated +using ((EXISTS ( SELECT 1 + FROM user_roles + WHERE ((user_roles.user_id = auth.uid()) AND (user_roles.role = 'admin'::roles))))); + + + From 9defe942a9982679bec585a52f282ebcf1cae214 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 15 Nov 2024 09:18:26 +0100 Subject: [PATCH 2/5] update package-lock --- package-lock.json | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/package-lock.json b/package-lock.json index 59bb87b..d6f130c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,13 @@ "leaflet": "^1.9.4", "lucide-react": "^0.454.0", "maplibre-gl": "^4.7.1", + "match-sorter": "^8.0.0", "next": "15.0.2", "react": "^18.3.1", "react-dom": "^18.3.1", "react-google-places-autocomplete": "^4.1.0", "react-map-gl": "^7.1.7", + "react-virtuoso": "^4.12.0", "sonner": "^1.7.0" }, "devDependencies": { @@ -7205,6 +7207,15 @@ "node": ">= 18" } }, + "node_modules/match-sorter": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-8.0.0.tgz", + "integrity": "sha512-bGJ6Zb+OhzXe+ptP5d80OLVx7AkqfRbtGEh30vNSfjNwllu+hHI+tcbMIT/fbkx/FKN1PmKuDb65+Oofg+XUxw==", + "dependencies": { + "@babel/runtime": "^7.23.8", + "remove-accents": "0.5.0" + } + }, "node_modules/md5": { "version": "2.3.0", "license": "BSD-3-Clause", @@ -9058,6 +9069,18 @@ "react-dom": ">=16.6.0" } }, + "node_modules/react-virtuoso": { + "version": "4.12.0", + "resolved": "https://registry.npmjs.org/react-virtuoso/-/react-virtuoso-4.12.0.tgz", + "integrity": "sha512-oHrKlU7xHsrnBQ89ecZoMPAK0tHnI9s1hsFW3KKg5ZGeZ5SWvbGhg/QFJFY4XETAzoCUeu+Xaxn1OUb/PGtPlA==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16 || >=17 || >= 18", + "react-dom": ">=16 || >=17 || >= 18" + } + }, "node_modules/read-cache": { "version": "1.0.0", "dev": true, @@ -9150,6 +9173,11 @@ "integrity": "sha512-caKfSz9rDeSKBQQnlJnVW3mbVdFgxgGWQKq1XlFokqjf+hQD5gxutLGTTY2A/x24UxVyJe9gH5fAkFI63ULw4A==", "dev": true }, + "node_modules/remove-accents": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.5.0.tgz", + "integrity": "sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==" + }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", From 46827ac4371df66084fc1f0b7c965319fe796bff Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 15 Nov 2024 09:21:02 +0100 Subject: [PATCH 3/5] fix: keep used properties in HelpRequestData --- src/app/casos-activos/solicitudes/page.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/casos-activos/solicitudes/page.tsx b/src/app/casos-activos/solicitudes/page.tsx index 5e9c35a..4b584b6 100644 --- a/src/app/casos-activos/solicitudes/page.tsx +++ b/src/app/casos-activos/solicitudes/page.tsx @@ -10,7 +10,7 @@ export const dynamic = 'force-dynamic'; function parseData(data: Database['public']['Tables']['help_requests']['Row'][]): HelpRequestData[] { return data.map((d) => { // Remove unused properties to reduce the payload size - const { coordinates, crm_status, resources, user_id, ...rest } = d; + const { coordinates, location, ...rest } = d; return { ...rest, // Fix the coordinates to 3 decimals so locations have a 100m precision @@ -21,7 +21,7 @@ function parseData(data: Database['public']['Tables']['help_requests']['Row'][]) } const getData = async (supabase: SupabaseClient) => { - const { error, data } = await supabase.from('help_requests').select().eq('type', 'necesita'); + const { error, data } = await supabase.from('help_requests').select('*').eq('type', 'necesita'); if (error) { throw new Error('Error fetching solicita:', error); From 86e622e8b030abcb86c1350c203c87533502fab8 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 15 Nov 2024 09:23:57 +0100 Subject: [PATCH 4/5] query solicitudes sort by created_at --- src/app/casos-activos/solicitudes/page.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/casos-activos/solicitudes/page.tsx b/src/app/casos-activos/solicitudes/page.tsx index 4b584b6..ecf0cea 100644 --- a/src/app/casos-activos/solicitudes/page.tsx +++ b/src/app/casos-activos/solicitudes/page.tsx @@ -21,7 +21,11 @@ function parseData(data: Database['public']['Tables']['help_requests']['Row'][]) } const getData = async (supabase: SupabaseClient) => { - const { error, data } = await supabase.from('help_requests').select('*').eq('type', 'necesita'); + const { error, data } = await supabase + .from('help_requests') + .select('*') + .eq('type', 'necesita') + .order('created_at', { ascending: false }); if (error) { throw new Error('Error fetching solicita:', error); From d1f377b30586e4420957a17d51fc319bb84cb9e0 Mon Sep 17 00:00:00 2001 From: j8seangel Date: Fri, 15 Nov 2024 09:29:03 +0100 Subject: [PATCH 5/5] fix lint --- src/app/casos-activos/solicitudes/types.ts | 1 - src/app/globals.css | 2 +- src/components/layout/Sidebar.tsx | 4 ++-- src/components/map/map.tsx | 3 +-- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/app/casos-activos/solicitudes/types.ts b/src/app/casos-activos/solicitudes/types.ts index dbbc71f..a9e500d 100644 --- a/src/app/casos-activos/solicitudes/types.ts +++ b/src/app/casos-activos/solicitudes/types.ts @@ -3,4 +3,3 @@ import { HelpRequestData } from '@/types/Requests'; export type FilterType = 'search' | 'urgencia' | 'tipoAyuda' | 'pueblo' | 'soloSinAsignar'; export type FiltersData = Record; - diff --git a/src/app/globals.css b/src/app/globals.css index 2267346..8bdd657 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -18,4 +18,4 @@ body { /* .maplibregl-ctrl-attrib { display: none; -} */ \ No newline at end of file +} */ diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 8615d3b..305dbf0 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -25,7 +25,7 @@ import { SelectedHelpData } from '@/types/Requests'; import { getOffersByUser, getRequestsByUser } from '@/lib/actions'; import { useSession } from '../../context/SessionProvider'; -export const SOLICITUDES_PATH = '/casos-activos/solicitudes' +export const SOLICITUDES_PATH = '/casos-activos/solicitudes'; type SidebarProps = { isOpen: boolean; @@ -61,7 +61,7 @@ export default function Sidebar({ isOpen, toggleAction }: SidebarProps) { path: SOLICITUDES_PATH, color: 'text-orange-600', highlight: true, - closeOnClick: true + closeOnClick: true, }, { icon: Inbox, diff --git a/src/components/map/map.tsx b/src/components/map/map.tsx index 819d6a3..250393b 100644 --- a/src/components/map/map.tsx +++ b/src/components/map/map.tsx @@ -55,7 +55,7 @@ const Map: FC = ({ solicitudes, setSelectedMarker }) => { id="solicitudes-circles" type="circle" paint={{ - 'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 3, 12, 7, 20, 200], + 'circle-radius': ['interpolate', ['linear'], ['zoom'], 3, 3, 12, 7, 20, 200], 'circle-color': { property: 'urgency', type: 'categorical', @@ -75,7 +75,6 @@ const Map: FC = ({ solicitudes, setSelectedMarker }) => { ], }, 'circle-stroke-width': 1, - }} />