-
Notifications
You must be signed in to change notification settings - Fork 408
Feat/company custom fields #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
ea2c4dc
b69db90
ce7336c
bf9aae4
61f74d3
273154e
1bf8170
6794ce9
5c8b7de
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -136,6 +136,7 @@ interface CreateActivityParams { | |
| */ | ||
| export const useCreateActivity = () => { | ||
| const queryClient = useQueryClient(); | ||
| const { user } = useAuth(); | ||
|
|
||
| return useMutation({ | ||
| mutationFn: async ({ activity }: CreateActivityParams) => { | ||
|
|
@@ -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 || "" }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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
🛠️ 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 |
||
| }).catch((err) => console.error("[Calendar] Webhook error:", err)); | ||
| } | ||
| } | ||
|
Comment on lines
+190
to
+199
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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:
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 🤖 Prompt for AI Agents |
||
| }, | ||
| onError: (_error, _params, context) => { | ||
| if (context?.previousActivities) { | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Two concerns with the new direct cache update:
Based on learnings: "For Deals entity mutations, always use ♻️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| onSettled: (_data, _error, { dealId }) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) }); | ||||||||||||||||||||||||||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same cascade-invalidation and missing-generic issues as The Based on learnings: "For Deals entity mutations, always use ♻️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||
| onSettled: (_data, _error, { dealId }) => { | ||||||||||||||||||||||||||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: queryKeys.deals.detail(dealId) }); | ||||||||||||||||||||||||||||||||||||||||||||||
| queryClient.invalidateQueries({ queryKey: queryKeys.deals.lists() }); | ||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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(), | ||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🌐 Web query:
💡 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 -20Repository: thaleslaray/nossocrm Length of output: 171 🏁 Script executed: wc -l lib/validations/schemas.tsRepository: thaleslaray/nossocrm Length of output: 95 🏁 Script executed: sed -n '110,135p' lib/validations/schemas.tsRepository: thaleslaray/nossocrm Length of output: 1195 Preprocess empty string input to
♻️ 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 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.sqlRepository: thaleslaray/nossocrm Length of output: 470 🏁 Script executed: rg -n 'handleCompanySubmit' --type=ts --type=jsx --type=jsRepository: thaleslaray/nossocrm Length of output: 92 🏁 Script executed: rg -n 'custom_fields' --type=ts --type=jsx --type=jsRepository: thaleslaray/nossocrm Length of output: 92 🏁 Script executed: rg 'handleCompanySubmit' -nRepository: thaleslaray/nossocrm Length of output: 389 🏁 Script executed: rg 'num_funcionarios|cnae|nrs_aplicaveis|data_ultimo_aso' -nRepository: thaleslaray/nossocrm Length of output: 2080 🏁 Script executed: rg 'companyFormSchema|CompanyFormSchema' -nRepository: thaleslaray/nossocrm Length of output: 664 🏁 Script executed: sed -n '200,230p' features/contacts/hooks/useContactsController.tsRepository: thaleslaray/nossocrm Length of output: 1265 🏁 Script executed: sed -n '60,75p' features/contacts/components/CompanyFormModal.tsxRepository: thaleslaray/nossocrm Length of output: 412 🏁 Script executed: rg 'createCompanyMutation|updateCompanyMutation' -A5 -B2 features/contacts/hooks/useContactsController.ts | head -50Repository: thaleslaray/nossocrm Length of output: 1982 🏁 Script executed: sed -n '64,70p' features/contacts/components/CompanyFormModal.tsxRepository: thaleslaray/nossocrm Length of output: 181 🏁 Script executed: rg 'export.*useCreateCompany|export.*useUpdateCompany' -A30 -B2 | head -80Repository: thaleslaray/nossocrm Length of output: 3308 🏁 Script executed: rg 'useCreateCompany|useUpdateCompany' --type=ts lib/hooks features/contacts/hooks --files-with-matchesRepository: thaleslaray/nossocrm Length of output: 113 🏁 Script executed: rg 'companiesService' -B2 -A2 lib/query/hooks/useContactsQuery.ts | head -20Repository: thaleslaray/nossocrm Length of output: 981 🏁 Script executed: fd -type f -name '*companies*service*' -o -name '*service*' lib/ | grep -i companRepository: thaleslaray/nossocrm Length of output: 235 🏁 Script executed: rg 'const companiesService|export.*companiesService' -A5Repository: thaleslaray/nossocrm Length of output: 1211 🏁 Script executed: sed -n '/^export const companiesService/,/^export const [a-zA-Z]/p' lib/supabase/contacts.ts | head -150Repository: thaleslaray/nossocrm Length of output: 5117 Form collects fields that are discarded before database persistence. The form (CompanyFormModal.tsx) renders and validates 🤖 Prompt for AI Agents |
||||||||||||||
| name: requiredString('Nome da Empresa', MAX_LENGTHS.COMPANY_NAME), | ||||||||||||||
| industry: optionalString.pipe(z.string().max(MAX_LENGTHS.SHORT_TEXT)), | ||||||||||||||
| website: z | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 * * *" } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Daily schedule likely breaks stage-evaluation SLA and can create queue backlog. On Line 13, changing 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 |
||
| ] | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 455
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 92
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 3716
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 1568
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 7926
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 390
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 1341
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 2764
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 2165
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 1140
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 2938
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 103
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 7604
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 3850
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 295
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 235
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 7169
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 6336
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 94
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 235
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 3212
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 232
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 3771
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 476
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 46
🏁 Script executed:
Repository: thaleslaray/nossocrm
Length of output: 6674
Remove
as anycasts and extend theCompanytype to includecustom_fields.The
as anycasts at lines 26 and 50 bypass TypeScript strict mode, removing compile-time safety for thecustom_fieldsproperty. Per coding guidelines, TypeScript 5.x must run with strict mode enabled. TheOrganizationinterface (whichCompanyaliases) does not currently definecustom_fields, and the database schema forcrm_companiesdoes not include this column either.To fix this properly:
custom_fields?: { num_funcionarios?: number; cnae?: string; nrs_aplicaveis?: string; data_ultimo_aso?: string }to theOrganizationinterface in@/typesas anycasts and use optional chaining directly:editingCompany?.custom_fields ?? {}🤖 Prompt for AI Agents