Skip to content
4 changes: 2 additions & 2 deletions features/boards/components/Modals/DealDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1019,8 +1019,8 @@ export const DealDetailModal: React.FC<DealDetailModalProps> = ({ dealId, isOpen
)}
</div>

<div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/5 rounded-xl overflow-hidden">
<table className="w-full text-left text-sm">
<div className="bg-white dark:bg-white/5 border border-slate-200 dark:border-white/5 rounded-xl overflow-x-auto">
<table className="w-full min-w-[480px] text-left text-sm">
<thead className="bg-slate-50 dark:bg-black/20 border-b border-slate-200 dark:border-white/5 text-slate-500 dark:text-slate-400 font-medium">
<tr>
<th className="px-4 py-3">Item</th>
Expand Down
62 changes: 43 additions & 19 deletions features/contacts/components/CompanyFormModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,34 +17,24 @@ interface CompanyFormModalProps {
editingCompany: Company | null;
}

/**
* Componente React `CompanyFormModal`.
*
* @param {CompanyFormModalProps} {
isOpen,
onClose,
onSubmit,
editingCompany,
} - Parâmetro `{
isOpen,
onClose,
onSubmit,
editingCompany,
}`.
* @returns {Element} Retorna um valor do tipo `Element`.
*/
export const CompanyFormModal: React.FC<CompanyFormModalProps> = ({
isOpen,
onClose,
onSubmit,
editingCompany,
}) => {
const customFields = (editingCompany as any)?.custom_fields ?? {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate the Company type definition to confirm custom_fields isn't typed.
fd -e ts -e tsx | xargs rg -nP -C3 '\b(interface|type)\s+Company\b'

Repository: thaleslaray/nossocrm

Length of output: 455


🏁 Script executed:

#!/bin/bash
# Find the Organization type definition
rg -n 'interface Organization|type Organization' --type ts --type tsx -B2 -A10

Repository: thaleslaray/nossocrm

Length of output: 92


🏁 Script executed:

#!/bin/bash
# Read the CompanyFormModal.tsx file to see the actual usage
cat -n features/contacts/components/CompanyFormModal.tsx | head -100

Repository: thaleslaray/nossocrm

Length of output: 3716


🏁 Script executed:

#!/bin/bash
# Search for Organization type definition without using --type flag
rg 'interface Organization|type Organization' -B2 -A15 types/

Repository: thaleslaray/nossocrm

Length of output: 1568


🏁 Script executed:

#!/bin/bash
# List and explore types directory structure
ls -la types/
cat -n types/types.ts | head -200

Repository: thaleslaray/nossocrm

Length of output: 7926


🏁 Script executed:

#!/bin/bash
# Find the companyFormSchema definition
fd -name '*schema*' -type f | xargs rg -l 'companyFormSchema' | head -5

Repository: thaleslaray/nossocrm

Length of output: 390


🏁 Script executed:

#!/bin/bash
# Look for any custom_fields references in the codebase
rg 'custom_fields' -n | head -20

Repository: thaleslaray/nossocrm

Length of output: 1341


🏁 Script executed:

#!/bin/bash
# Check the database schema for companies table
rg 'CREATE TABLE.*companies' -A 30 supabase/migrations/

Repository: thaleslaray/nossocrm

Length of output: 2764


🏁 Script executed:

#!/bin/bash
# Read the companyFormSchema to understand expected structure
cat -n lib/validations/schemas.ts | grep -A 30 'companyFormSchema'

Repository: thaleslaray/nossocrm

Length of output: 2165


🏁 Script executed:

#!/bin/bash
# Check where editingCompany comes from and is used in the component
rg -B 5 'editingCompany' features/contacts/components/CompanyFormModal.tsx | head -40

Repository: thaleslaray/nossocrm

Length of output: 1140


🏁 Script executed:

#!/bin/bash
# Search for where CompanyFormModal is used to see how editingCompany is passed
rg 'CompanyFormModal' -B 3 -A 3 --type ts --type tsx 2>/dev/null || rg 'CompanyFormModal' -B 3 -A 3

Repository: thaleslaray/nossocrm

Length of output: 2938


🏁 Script executed:

#!/bin/bash
# Check ContactsPage to see where editingCompany is set for the modal
rg -n 'editingCompany\|setEditingCompany\|handleCompanySubmit' features/contacts/ContactsPage.tsx | head -20

Repository: thaleslaray/nossocrm

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look for the controller and its company-related methods
rg -n 'controller\.\(editingCompany\|setEditingCompany\|handleCompanySubmit\)' features/contacts/ContactsPage.tsx

Repository: thaleslaray/nossocrm

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Read the ContactsPage file to understand the controller structure
wc -l features/contacts/ContactsPage.tsx

Repository: thaleslaray/nossocrm

Length of output: 103


🏁 Script executed:

#!/bin/bash
# Search for useSettingsController or similar controller hook
rg 'useSettingsController\|useContactsController\|controller.*=' features/contacts/ContactsPage.tsx | head -10

Repository: thaleslaray/nossocrm

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Read the ContactsPage file to understand data flow
cat -n features/contacts/ContactsPage.tsx | head -150

Repository: thaleslaray/nossocrm

Length of output: 7604


🏁 Script executed:

#!/bin/bash
# Continue reading ContactsPage to find CompanyFormModal usage
cat -n features/contacts/ContactsPage.tsx | sed -n '150,220p'

Repository: thaleslaray/nossocrm

Length of output: 3850


🏁 Script executed:

#!/antml/bash
# Search for the useContactsController hook to understand what data it manages
fd -name 'useContactsController*' -type f

Repository: thaleslaray/nossocrm

Length of output: 295


🏁 Script executed:

#!/bin/bash
# Find the useContactsController hook
fd 'useContactsController' -type f

Repository: thaleslaray/nossocrm

Length of output: 235


🏁 Script executed:

#!/bin/bash
# Read the useContactsController hook
cat -n features/contacts/hooks/useContactsController.ts | head -150

Repository: thaleslaray/nossocrm

Length of output: 7169


🏁 Script executed:

#!/bin/bash
# Continue reading useContactsController to find editingCompany state and handleCompanySubmit
cat -n features/contacts/hooks/useContactsController.ts | sed -n '150,300p'

Repository: thaleslaray/nossocrm

Length of output: 6336


🏁 Script executed:

#!/bin/bash
# Search for where custom_fields might be populated/set in the companies data
rg 'custom_fields' -B 3 -A 3 lib/query/hooks/useContactsQuery.ts 2>/dev/null || echo "Not found in useContactsQuery"

Repository: thaleslaray/nossocrm

Length of output: 94


🏁 Script executed:

#!/antml:bash
# Check if there's any data transformation or query that adds custom_fields
fd 'useContactsQuery' -type f

Repository: thaleslaray/nossocrm

Length of output: 235


🏁 Script executed:

#!/bin/bash
# Find the useContactsQuery file
find features/contacts/hooks -name '*useContactsQuery*'

Repository: thaleslaray/nossocrm

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Search for where companies are fetched and if custom_fields are added
rg 'useCompanies' -B 2 -A 10 | head -50

Repository: thaleslaray/nossocrm

Length of output: 3212


🏁 Script executed:

#!/bin/bash
# Find the companiesService to see what getAll() returns
rg 'companiesService' -l

Repository: thaleslaray/nossocrm

Length of output: 232


🏁 Script executed:

#!/an/bash
# Search for where companiesService is defined
rg 'export.*companiesService\|const companiesService' -B 2 -A 5

Repository: thaleslaray/nossocrm

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Look at lib/supabase/contacts.ts for companiesService
cat -n lib/supabase/contacts.ts | head -100

Repository: thaleslaray/nossocrm

Length of output: 3771


🏁 Script executed:

#!/bin/bash
# Search for companiesService definition more broadly
rg 'companiesService.*=' lib/ -B 2 -A 5

Repository: thaleslaray/nossocrm

Length of output: 476


🏁 Script executed:

#!/bin/bash
# Find the getAll method in companiesService
rg -A 30 'companiesService = \{' lib/supabase/contacts.ts | grep -A 25 'getAll'

Repository: thaleslaray/nossocrm

Length of output: 46


🏁 Script executed:

#!/bin/bash
# Read more of the companiesService to find getAll
cat -n lib/supabase/contacts.ts | sed -n '150,300p'

Repository: thaleslaray/nossocrm

Length of output: 6674


Remove as any casts and extend the Company type to include custom_fields.

The as any casts at lines 26 and 50 bypass TypeScript strict mode, removing compile-time safety for the custom_fields property. Per coding guidelines, TypeScript 5.x must run with strict mode enabled. The Organization interface (which Company aliases) does not currently define custom_fields, and the database schema for crm_companies does not include this column either.

To fix this properly:

  1. Add custom_fields?: { num_funcionarios?: number; cnae?: string; nrs_aplicaveis?: string; data_ultimo_aso?: string } to the Organization interface in @/types
  2. Remove the as any casts and use optional chaining directly: editingCompany?.custom_fields ?? {}
  3. Ensure the database schema and queries include this field if custom fields are meant to be persisted
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@features/contacts/components/CompanyFormModal.tsx` at line 26, The code is
using an unsafe cast (editingCompany as any) to access custom_fields; instead
extend the Organization interface in your types module (add optional property
custom_fields?: { num_funcionarios?: number; cnae?: string; nrs_aplicaveis?:
string; data_ultimo_aso?: string }) so the Company/Organization type includes
custom_fields, then remove the casts in CompanyFormModal.tsx and access with
optional chaining (editingCompany?.custom_fields ?? {}); also review any DB
schema/queries that persist crm_companies to ensure custom_fields is persisted
or handled appropriately if it should be stored.


const form = useForm<CompanyFormInput>({
resolver: zodResolver(companyFormSchema),
defaultValues: {
name: editingCompany?.name || '',
industry: editingCompany?.industry || '',
website: editingCompany?.website || '',
num_funcionarios: customFields.num_funcionarios ?? undefined,
cnae: customFields.cnae || '',
nrs_aplicaveis: customFields.nrs_aplicaveis || '',
data_ultimo_aso: customFields.data_ultimo_aso || '',
},
});

Expand All @@ -57,10 +47,15 @@ export const CompanyFormModal: React.FC<CompanyFormModalProps> = ({

React.useEffect(() => {
if (isOpen) {
const cf = (editingCompany as any)?.custom_fields ?? {};
reset({
name: editingCompany?.name || '',
industry: editingCompany?.industry || '',
website: editingCompany?.website || '',
num_funcionarios: cf.num_funcionarios ?? undefined,
cnae: cf.cnae || '',
nrs_aplicaveis: cf.nrs_aplicaveis || '',
data_ultimo_aso: cf.data_ultimo_aso || '',
});
}
}, [isOpen, editingCompany, reset]);
Expand All @@ -81,15 +76,15 @@ export const CompanyFormModal: React.FC<CompanyFormModalProps> = ({
<ModalForm onSubmit={handleSubmit(handleFormSubmit)}>
<InputField
label="Nome"
placeholder="Ex: NossoCRM LTDA"
placeholder="Ex: Construtora Silva LTDA"
required
error={errors.name}
registration={register('name')}
/>

<InputField
label="Setor"
placeholder="Ex: SaaS"
placeholder="Ex: Construção Civil"
error={errors.industry}
registration={register('industry')}
/>
Expand All @@ -102,11 +97,40 @@ export const CompanyFormModal: React.FC<CompanyFormModalProps> = ({
registration={register('website')}
/>

<InputField
label="Nº de Funcionários"
placeholder="Ex: 85"
type="number"
error={errors.num_funcionarios}
registration={register('num_funcionarios')}
/>

<InputField
label="CNAE"
placeholder="Ex: 4120-4/00 — Construção de edifícios"
error={errors.cnae}
registration={register('cnae')}
/>

<InputField
label="NRs Aplicáveis"
placeholder="Ex: NR-7, NR-9, NR-15"
hint="Separe por vírgula."
error={errors.nrs_aplicaveis}
registration={register('nrs_aplicaveis')}
/>

<InputField
label="Data do Último ASO"
type="date"
error={errors.data_ultimo_aso}
registration={register('data_ultimo_aso')}
/>

<SubmitButton isLoading={isSubmitting}>
{editingCompany ? 'Salvar Alterações' : 'Criar Empresa'}
</SubmitButton>
</ModalForm>
</Modal>
);
};

4 changes: 2 additions & 2 deletions features/contacts/hooks/useContactsController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -199,10 +199,10 @@ export const useContactsController = () => {
setIsCompanyModalOpen(true);
};

const handleCompanySubmit = (data: { name: string; industry?: string; website?: string }) => {
const handleCompanySubmit = (data: { name: string; industry?: string; website?: string; num_funcionarios?: number; cnae?: string; nrs_aplicaveis?: string; data_ultimo_aso?: string }) => {
if (editingCompany) {
updateCompanyMutation.mutate(
{ id: editingCompany.id, updates: { ...data } },
{ id: editingCompany.id, updates: { name: data.name, industry: data.industry, website: data.website, custom_fields: { num_funcionarios: data.num_funcionarios, cnae: data.cnae, nrs_aplicaveis: data.nrs_aplicaveis, data_ultimo_aso: data.data_ultimo_aso } } },
{
onSuccess: () => {
(addToast || showToast)('Empresa atualizada!', 'success');
Expand Down
11 changes: 11 additions & 0 deletions lib/query/hooks/useActivitiesQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ interface CreateActivityParams {
*/
export const useCreateActivity = () => {
const queryClient = useQueryClient();
const { user } = useAuth();

return useMutation({
mutationFn: async ({ activity }: CreateActivityParams) => {
Expand Down Expand Up @@ -186,6 +187,16 @@ export const useCreateActivity = () => {
// Invalidate to ensure Realtime updates are picked up
// This is a no-op if data is already fresh, but ensures consistency
queryClient.invalidateQueries({ queryKey: queryKeys.activities.all });
if (data.type === "MEETING") {
const startDate = data.date ? new Date(data.date) : null;
if (startDate && !isNaN(startDate.getTime())) {
fetch("https://n8n-production-9012a.up.railway.app/webhook/0ebbdfef-a03e-4109-bdce-7d00e70218f0", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ title: data.title, description: data.description || data.dealTitle, start_time: startDate.toISOString(), end_time: new Date(startDate.getTime() + 3600000).toISOString(), attendees: user?.email || "" })

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

end_time hardcoded to start + 1 hour.

Every meeting is reported as exactly 60 minutes, regardless of the user’s actual scheduling. Calendar events created from this webhook will have wrong durations for any non‑1h meeting. If the Activity model doesn’t carry an explicit end time, either:

  • Add an endDate/durationMinutes to the create payload and use it here, or
  • Make the default duration configurable per organization rather than baked into the hook.
🛠️ Suggested adjustment (assuming an optional duration is added to the activity payload)
-          body: JSON.stringify({ title: data.title, description: data.description || data.dealTitle, start_time: startDate.toISOString(), end_time: new Date(startDate.getTime() + 3600000).toISOString(), attendees: user?.email || "" })
+          body: JSON.stringify({
+            title: data.title,
+            description: data.description || data.dealTitle,
+            start_time: startDate.toISOString(),
+            end_time: new Date(
+              startDate.getTime() + (data.durationMinutes ?? 60) * 60_000
+            ).toISOString(),
+            attendees: user?.email || '',
+          })
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/query/hooks/useActivitiesQuery.ts` at line 196, The request body
currently hardcodes end_time to start_time plus 1 hour in useActivitiesQuery.ts
(where the payload is built for the webhook), causing incorrect durations;
update the payload construction in that function to compute end_time from a
provided Activity field (preferably data.endDate or data.durationMinutes) and
fall back to a per-organization default duration if those are absent (read the
org default from the existing org/config API or pass it into the hook), and
ensure the body uses the computed ISO string for end_time instead of new
Date(startDate.getTime() + 3600000).

}).catch((err) => console.error("[Calendar] Webhook error:", err));
}
}
Comment on lines +190 to +199

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Move webhook URL out of client bundle and per-organization config.

The webhook URL is hardcoded into a client-side hook, so it ships in the browser bundle, fires for every tenant, cannot be disabled per organization, and routes user PII (email, meeting title/description) to a single external endpoint regardless of tenant. This breaks multi-tenant isolation and makes the integration impossible to audit or rotate without a code deploy.

Recommended approach:

  • Store the webhook URL (and an enable flag) in organization_settings, retrieved via the existing org-config pattern (similar to getOrgAIConfig).
  • Trigger the webhook from a server-side route (Route Handler) or Edge Function rather than from the browser, so the URL and any auth header stay off the client and an organization_id can be enforced/logged.
  • Sanitize the URL with sanitizeUrl() from @/lib/utils/sanitize.ts before calling fetch.

As per coding guidelines: "Store AI provider API keys in organization_settings database table, not in environment variables; retrieve via getOrgAIConfig(orgId)" — the same principle applies to per-tenant webhook destinations, and "All database queries must filter by organization_id to ensure multi-tenant security" implies external integrations should also be tenant-scoped.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/query/hooks/useActivitiesQuery.ts` around lines 190 - 199, The
client-side webhook call in useActivitiesQuery.ts (inside the useActivitiesQuery
hook) must be removed and replaced with a server-side trigger: add
per-organization webhook settings to the organization_settings table and expose
them via the existing org-config pattern (similar to getOrgAIConfig) so each org
has an enabled flag and webhook_url; create a Route Handler/Edge Function
endpoint that accepts a minimal meeting payload (title, description, start_time,
end_time, organization_id, attendee_email) and from server-side reads the
organization_settings (filtering by organization_id) to get and sanitize the
webhook_url with sanitizeUrl() before calling fetch, include any auth header
from settings, and log the organization_id; update useActivitiesQuery to POST to
this new internal endpoint instead of calling the hardcoded external URL and
ensure no PII or webhook URLs remain in the client bundle.

},
onError: (_error, _params, context) => {
if (context?.previousActivities) {
Expand Down
16 changes: 16 additions & 0 deletions lib/query/hooks/useDealsQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,14 @@ export const useAddDealItem = () => {
if (error) throw error;
return { dealId, item: data! };
},
onSuccess: (data, { dealId }) => {
queryClient.setQueryData(DEALS_VIEW_KEY, (old) => {
if (!old) return old;
return old.map((d) =>
d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d
);
});
},
Comment on lines +520 to +527

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

onSettled invalidation cascades to DEALS_VIEW_KEY, undoing this optimization; also missing <DealView[]> generic.

Two concerns with the new direct cache update:

  1. The unchanged onSettled at line 530 calls queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() }). Because DEALS_VIEW_KEY = [...queryKeys.deals.lists(), 'view'], TanStack's prefix matching will invalidate DEALS_VIEW_KEY as well, triggering an immediate refetch right after this setQueryData. That largely defeats the "optimize deal item cache updates" intent, and it diverges from the pattern used everywhere else in this file (e.g. useUpdateDeal, useDeleteDeal) where the explicit comment is "NÃO fazer invalidateQueries para deals - Realtime gerencia a sincronização".

  2. setQueryData is called without the <DealView[]> generic, so old is inferred as unknown and old.map(...) is not type-safe under strict mode. Every other call site in this file passes the generic explicitly (e.g. lines 272, 299, 363, 487).

Based on learnings: "For Deals entity mutations, always use [...queryKeys.deals.lists(), 'view'] cache key pattern" and "Prefer setQueryData over invalidateQueries for instant UI updates".

♻️ Proposed fix
-    onSuccess: (data, { dealId }) => {
-      queryClient.setQueryData(DEALS_VIEW_KEY, (old) => {
-        if (!old) return old;
-        return old.map((d) =>
-          d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d
-        );
-      });
-    },
-    onSettled: (_data, _error, { dealId }) => {
-      queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
-      queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() });
-    },
+    onSuccess: (data, { dealId }) => {
+      queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, (old) => {
+        if (!old) return old;
+        return old.map((d) =>
+          d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d
+        );
+      });
+    },
+    onSettled: (_data, _error, { dealId }) => {
+      // NÃO invalidar queryKeys.deals.lists() — cascateia para DEALS_VIEW_KEY
+      // e desfaz o setQueryData acima. Realtime mantém a sincronização.
+      queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
+    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onSuccess: (data, { dealId }) => {
queryClient.setQueryData(DEALS_VIEW_KEY, (old) => {
if (!old) return old;
return old.map((d) =>
d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d
);
});
},
onSuccess: (data, { dealId }) => {
queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, (old) => {
if (!old) return old;
return old.map((d) =>
d.id === dealId ? { ...d, items: [...(d.items ?? []), data.item] } : d
);
});
},
onSettled: (_data, _error, { dealId }) => {
// NÃO invalidar queryKeys.deals.lists() — cascateia para DEALS_VIEW_KEY
// e desfaz o setQueryData acima. Realtime mantém a sincronização.
queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/query/hooks/useDealsQuery.ts` around lines 520 - 527, The direct cache
update in the onSuccess handler for adding an item uses queryClient.setQueryData
without the <DealView[]> generic (so old is inferred as unknown) and is
immediately undone because the existing onSettled still calls
queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() }) which
prefix-invalidates DEALS_VIEW_KEY; fix by adding the explicit generic to
setQueryData (setQueryData<DealView[]>) and stop invalidating the parent lists
key in the onSettled for this mutation (either remove or narrow the
invalidateQueries call so it does not target queryKeys.deals.lists()) so the
optimized in-place update to DEALS_VIEW_KEY persists and remains type-safe.

onSettled: (_data, _error, { dealId }) => {
queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() });
Expand All @@ -536,6 +544,14 @@ export const useRemoveDealItem = () => {
if (error) throw error;
return { dealId, itemId };
},
onSuccess: (data) => {
queryClient.setQueryData(DEALS_VIEW_KEY, (old) => {
if (!old) return old;
return old.map((d) =>
d.id === data.dealId ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) } : d
);
});
},
Comment on lines +547 to +554

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Same cascade-invalidation and missing-generic issues as useAddDealItem.

The onSettled at line 557 invalidates queryKeys.deals.lists(), which prefix-matches DEALS_VIEW_KEY and triggers a refetch immediately after the optimization here. Also missing <DealView[]> generic on setQueryData, leaving old as unknown.

Based on learnings: "For Deals entity mutations, always use [...queryKeys.deals.lists(), 'view'] cache key pattern" and "Prefer setQueryData over invalidateQueries for instant UI updates".

♻️ Proposed fix
-    onSuccess: (data) => {
-      queryClient.setQueryData(DEALS_VIEW_KEY, (old) => {
-        if (!old) return old;
-        return old.map((d) =>
-          d.id === data.dealId ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) } : d
-        );
-      });
-    },
-    onSettled: (_data, _error, { dealId }) => {
-      queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
-      queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() });
-    },
+    onSuccess: (data) => {
+      queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, (old) => {
+        if (!old) return old;
+        return old.map((d) =>
+          d.id === data.dealId
+            ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) }
+            : d
+        );
+      });
+    },
+    onSettled: (_data, _error, { dealId }) => {
+      // Mesmo motivo de useAddDealItem: lists() cascateia em DEALS_VIEW_KEY.
+      queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
+    },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onSuccess: (data) => {
queryClient.setQueryData(DEALS_VIEW_KEY, (old) => {
if (!old) return old;
return old.map((d) =>
d.id === data.dealId ? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) } : d
);
});
},
onSuccess: (data) => {
queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, (old) => {
if (!old) return old;
return old.map((d) =>
d.id === data.dealId
? { ...d, items: (d.items ?? []).filter((i) => i.id !== data.itemId) }
: d
);
});
},
onSettled: (_data, _error, { dealId }) => {
// Mesmo motivo de useAddDealItem: lists() cascateia em DEALS_VIEW_KEY.
queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/query/hooks/useDealsQuery.ts` around lines 547 - 554, The current
onSuccess handler updates the DEALS_VIEW_KEY optimistically but uses
setQueryData without a generic (leaving old as unknown) and then onSettled calls
invalidateQueries(queryKeys.deals.lists()) which prefix-matches and immediately
refetches, negating the optimization; fix by changing setQueryData to use the
explicit generic setQueryData<DealView[]> (so old is typed) and update only the
specific view cache key pattern [...queryKeys.deals.lists(), 'view'] (i.e. use
queryClient.setQueryData<DealView[]>(DEALS_VIEW_KEY, ...) where DEALS_VIEW_KEY
is the view key), and change the onSettled invalidation to invalidate only
[...queryKeys.deals.lists(), 'view'] (or remove the invalidate if you prefer
relying on setQueryData) so there is no cascade invalidation from
queryKeys.deals.lists().

onSettled: (_data, _error, { dealId }) => {
queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) });
queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() });
Expand Down
1 change: 1 addition & 0 deletions lib/supabase/contacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,7 @@ export const companiesService = {
if (updates.name !== undefined) dbUpdates.name = updates.name;
if (updates.industry !== undefined) dbUpdates.industry = updates.industry || null;
if (updates.website !== undefined) dbUpdates.website = updates.website || null;
if (updates.custom_fields !== undefined) dbUpdates.custom_fields = updates.custom_fields;
dbUpdates.updated_at = new Date().toISOString();

const { error } = await supabase
Expand Down
4 changes: 4 additions & 0 deletions lib/validations/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,10 @@ export type ContactFormData = z.infer<typeof contactFormSchema>;
// ============ COMPANY SCHEMAS ============

export const companyFormSchema = z.object({
num_funcionarios: z.coerce.number().int().min(0).optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Zod 4.3.6, does z.coerce.number().optional() treat an empty string input as undefined or coerce it to 0?

💡 Result:

In Zod 4.3.6, z.coerce.number.optional coerces an empty string input ("") to 0, not undefined. The coercion step uses JavaScript's Number(""), which returns 0. This 0 is then passed to the inner z.number validation (which accepts 0) and the .optional wrapper. Since 0 is defined and valid, the output is 0 | undefined (from optional), but specifically 0 for empty string input. This behavior is confirmed by: - Official test in Zod 4.3.6 source: expect(schema.parse("")).toEqual(0); for z.coerce.number. - Multiple GitHub issues/discussions reporting z.coerce.number.parse("") returns 0. - Documentation stating z.coerce.number uses Number(input). No changes to this behavior in 4.3.x releases. Users often use preprocess/transform/pipe for custom empty string handling (e.g., to undefined).

Citations:


🏁 Script executed:

git ls-files | grep -E 'schemas\.(ts|js)' | head -20

Repository: thaleslaray/nossocrm

Length of output: 171


🏁 Script executed:

wc -l lib/validations/schemas.ts

Repository: thaleslaray/nossocrm

Length of output: 95


🏁 Script executed:

sed -n '110,135p' lib/validations/schemas.ts

Repository: thaleslaray/nossocrm

Length of output: 1195


Preprocess empty string input to undefined to avoid silent coercion to 0.

<input type="number"> with an empty value yields '' from react-hook-form. The current schema coerces '' to 0 (only undefined bypasses .optional()), so clearing the field persists 0 employees instead of leaving it unspecified. Use .preprocess() to convert empty strings to undefined:

♻️ Proposed fix
-  num_funcionarios: z.coerce.number().int().min(0).optional(),
+  num_funcionarios: z
+    .preprocess(
+      (val) => (val === '' || val === null ? undefined : val),
+      z.coerce.number().int().min(0).optional()
+    ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
num_funcionarios: z.coerce.number().int().min(0).optional(),
num_funcionarios: z
.preprocess(
(val) => (val === '' || val === null ? undefined : val),
z.coerce.number().int().min(0).optional()
),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/validations/schemas.ts` at line 123, The num_funcionarios schema silently
coerces an empty string to 0 causing cleared numeric inputs to persist as 0;
update the num_funcionarios definition to first preprocess the value (in the
schema for num_funcionarios) converting '' to undefined, then apply
z.coerce.number().int().min(0).optional() so that empty string becomes undefined
and respects .optional()—locate the num_funcionarios symbol in the schema and
wrap the existing coercion with a .preprocess(fn) that returns undefined for ''
and the original value otherwise.

cnae: optionalString.pipe(z.string().max(MAX_LENGTHS.SHORT_TEXT)),
nrs_aplicaveis: optionalString.pipe(z.string().max(500)),
data_ultimo_aso: optionalString.pipe(z.string().max(MAX_LENGTHS.SHORT_TEXT)),
Comment on lines +123 to +126

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether a migration adds custom_fields to crm_companies and whether
# the create/update paths and read selects include it.
fd -e sql . supabase/migrations | xargs rg -n -C2 'crm_companies'
echo '--- handleCompanySubmit / mutations ---'
rg -nP -C3 'handleCompanySubmit|crm_companies' --type=ts --type=tsx
echo '--- API routes for companies ---'
fd -t f -e ts . app/api | xargs rg -n -C3 'crm_companies|custom_fields'

Repository: thaleslaray/nossocrm

Length of output: 14680


🏁 Script executed:

sed -n '116,126p' supabase/migrations/20251201000000_schema_init.sql

Repository: thaleslaray/nossocrm

Length of output: 470


🏁 Script executed:

rg -n 'handleCompanySubmit' --type=ts --type=jsx --type=js

Repository: thaleslaray/nossocrm

Length of output: 92


🏁 Script executed:

rg -n 'custom_fields' --type=ts --type=jsx --type=js

Repository: thaleslaray/nossocrm

Length of output: 92


🏁 Script executed:

rg 'handleCompanySubmit' -n

Repository: thaleslaray/nossocrm

Length of output: 389


🏁 Script executed:

rg 'num_funcionarios|cnae|nrs_aplicaveis|data_ultimo_aso' -n

Repository: thaleslaray/nossocrm

Length of output: 2080


🏁 Script executed:

rg 'companyFormSchema|CompanyFormSchema' -n

Repository: thaleslaray/nossocrm

Length of output: 664


🏁 Script executed:

sed -n '200,230p' features/contacts/hooks/useContactsController.ts

Repository: thaleslaray/nossocrm

Length of output: 1265


🏁 Script executed:

sed -n '60,75p' features/contacts/components/CompanyFormModal.tsx

Repository: thaleslaray/nossocrm

Length of output: 412


🏁 Script executed:

rg 'createCompanyMutation|updateCompanyMutation' -A5 -B2 features/contacts/hooks/useContactsController.ts | head -50

Repository: thaleslaray/nossocrm

Length of output: 1982


🏁 Script executed:

sed -n '64,70p' features/contacts/components/CompanyFormModal.tsx

Repository: thaleslaray/nossocrm

Length of output: 181


🏁 Script executed:

rg 'export.*useCreateCompany|export.*useUpdateCompany' -A30 -B2 | head -80

Repository: thaleslaray/nossocrm

Length of output: 3308


🏁 Script executed:

rg 'useCreateCompany|useUpdateCompany' --type=ts lib/hooks features/contacts/hooks --files-with-matches

Repository: thaleslaray/nossocrm

Length of output: 113


🏁 Script executed:

rg 'companiesService' -B2 -A2 lib/query/hooks/useContactsQuery.ts | head -20

Repository: thaleslaray/nossocrm

Length of output: 981


🏁 Script executed:

fd -type f -name '*companies*service*' -o -name '*service*' lib/ | grep -i compan

Repository: thaleslaray/nossocrm

Length of output: 235


🏁 Script executed:

rg 'const companiesService|export.*companiesService' -A5

Repository: thaleslaray/nossocrm

Length of output: 1211


🏁 Script executed:

sed -n '/^export const companiesService/,/^export const [a-zA-Z]/p' lib/supabase/contacts.ts | head -150

Repository: thaleslaray/nossocrm

Length of output: 5117


Form collects fields that are discarded before database persistence.

The form (CompanyFormModal.tsx) renders and validates num_funcionarios, cnae, nrs_aplicaveis, and data_ultimo_aso using companyFormSchema, but handleCompanySubmit accepts only { name, industry, website }. These four fields are parsed by validation but never passed to the mutation, and companiesService.create() and companiesService.update() only handle name, industry, and website. The underlying crm_companies table has no columns to store the new fields. Users will enter data in the form with no indication it won't be saved.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/validations/schemas.ts` around lines 123 - 126, The schema is validating
fields (num_funcionarios, cnae, nrs_aplicaveis, data_ultimo_aso) that are never
persisted because handleCompanySubmit only sends { name, industry, website } and
companiesService.create/update and the crm_companies table lack these columns;
either stop validating/displaying unused fields or wire them end-to-end. Fix by
one of two options: (A) Remove these four fields from companyFormSchema and from
CompanyFormModal.tsx so validation and UI match companiesService inputs, or (B)
Add them to the persistence layer: extend the DTO/type used by
handleCompanySubmit to include num_funcionarios, cnae, nrs_aplicaveis,
data_ultimo_aso; update companiesService.create and companiesService.update to
accept and pass these fields through; add corresponding columns to the
crm_companies table (with a DB migration) and update any repository/ORM mappings
so the new fields are saved and returned.

name: requiredString('Nome da Empresa', MAX_LENGTHS.COMPANY_NAME),
industry: optionalString.pipe(z.string().max(MAX_LENGTHS.SHORT_TEXT)),
website: z
Expand Down
2 changes: 1 addition & 1 deletion vercel.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@
"crons": [
{ "path": "/api/cron/daily-briefing", "schedule": "0 8 * * 1-5" },
{ "path": "/api/cron/template-sync", "schedule": "0 6 * * *" },
{ "path": "/api/cron/stage-evaluations", "schedule": "* * * * *" }
{ "path": "/api/cron/stage-evaluations", "schedule": "0 0 * * *" }

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Daily schedule likely breaks stage-evaluation SLA and can create queue backlog.

On Line 13, changing /api/cron/stage-evaluations to 0 0 * * * can delay stage advancement/HITL outcomes by up to 24 hours, even though evaluations are enqueued continuously after AI replies and processed in small batches. This materially changes behavior and risks stale pipeline state.

Please keep this cron high-frequency (e.g., every minute or every few minutes), or pair a lower frequency with a larger batch/parallelization strategy and explicit product acceptance of delayed stage updates.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vercel.json` at line 13, The cron entry for the route
"/api/cron/stage-evaluations" was changed to "0 0 * * *", which will run once
daily and create a backlog/stale state; revert or update this schedule to a
high-frequency cron (e.g., every minute or every few minutes like "*/1 * * * *"
or "*/5 * * * *") to preserve current near-real-time processing, or if you must
keep a low frequency, implement a compensating fix in the stage evaluation
handler (the code behind "/api/cron/stage-evaluations") to process much larger
batches and add parallelization and explicit product-level acceptance of delayed
updates before changing the schedule.

]
}