Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/hooks/crm/useRamoAtividade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ export function useRamosAtividadeGroups() {
});
}

export const useRamoAtividadeGroups = useRamosAtividadeGroups;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Adapt legacy hook alias to return expected groups array

Exporting useRamoAtividadeGroups as a direct alias of useRamosAtividadeGroups changes the runtime shape seen by existing callers: the aliased hook returns data as an object ({ groups, totalGroups, totalSegmentos }), while downstream code still treats groups as an array (for example, it calls array methods/spread in the ramo filter flow). After this alias is used and the query resolves, those consumers can hit runtime failures like non-iterable/non-function errors. The alias should preserve the old contract (array data) or all consumers should be updated atomically.

Useful? React with 👍 / 👎.


// Buscar ramo por ID
export function useRamoAtividade(id: string | undefined) {
return useQuery({
Expand Down
137 changes: 88 additions & 49 deletions src/hooks/crm/useRamoAtividadeFilter.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { useState, useMemo, useCallback } from 'react';
import { useRamoAtividadeGroups, useSegmentosCompletos } from '@/hooks/crm';
import type { RamoAtividadeGroup, SegmentoComplete, RamoAtividadeFilterState } from "@/types/ramo-atividade";
import type {
RamoAtividadeGroup,
SegmentoComplete,
RamoAtividadeFilterState,
} from '@/types/ramo-atividade';

export type { RamoAtividadeFilterState };

Expand All @@ -24,11 +28,19 @@ export interface UseRamoAtividadeFilterReturn {
}

export function useRamoAtividadeFilter(): UseRamoAtividadeFilterReturn {
const { data: groups = [], isLoading: groupsLoading, error: groupsError } = useRamoAtividadeGroups();
const { data: segmentosData, isLoading: segmentosLoading, error: segmentosError } = useSegmentosCompletos();

const segmentos = segmentosData?.segmentos || [];
const byRamo = segmentosData?.byRamo || new Map();
const {
data: groups = [],
isLoading: groupsLoading,
error: groupsError,
} = useRamoAtividadeGroups();
const {
data: segmentosData,
isLoading: segmentosLoading,
error: segmentosError,
} = useSegmentosCompletos();

const segmentos = useMemo(() => segmentosData?.segmentos || [], [segmentosData?.segmentos]);
const byRamo = useMemo(() => segmentosData?.byRamo || new Map(), [segmentosData?.byRamo]);

const [filterState, setFilterState] = useState<RamoAtividadeFilterState>({
selectedRamos: [],
Expand All @@ -39,31 +51,38 @@ export function useRamoAtividadeFilter(): UseRamoAtividadeFilterReturn {
const error = groupsError || segmentosError;

// Toggle ramo
const toggleRamo = useCallback((ramoSlug: string) => {
setFilterState(prev => {
const isSelected = prev.selectedRamos.includes(ramoSlug);
if (isSelected) {
// Remove ramo e todos os segmentos desse ramo
const segmentosNoRamo = byRamo.get(ramoSlug)?.map((s: SegmentoComplete) => s.segmento_slug) || [];
const toggleRamo = useCallback(
(ramoSlug: string) => {
setFilterState((prev) => {
const isSelected = prev.selectedRamos.includes(ramoSlug);
if (isSelected) {
// Remove ramo e todos os segmentos desse ramo
const segmentosNoRamo =
byRamo.get(ramoSlug)?.map((s: SegmentoComplete) => s.segmento_slug) || [];
return {
...prev,
selectedRamos: prev.selectedRamos.filter((r) => r !== ramoSlug),
selectedSegmentos: prev.selectedSegmentos.filter(
(s: string) => !segmentosNoRamo.includes(s),
),
};
}
// Add ramo + all its segmentos
const segmentosDoRamo =
byRamo.get(ramoSlug)?.map((s: SegmentoComplete) => s.segmento_slug) || [];
return {
...prev,
selectedRamos: prev.selectedRamos.filter(r => r !== ramoSlug),
selectedSegmentos: prev.selectedSegmentos.filter((s: string) => !segmentosNoRamo.includes(s)),
selectedRamos: [...prev.selectedRamos, ramoSlug],
selectedSegmentos: [...new Set([...prev.selectedSegmentos, ...segmentosDoRamo])],
};
}
// Add ramo + all its segmentos
const segmentosDoRamo = byRamo.get(ramoSlug)?.map((s: SegmentoComplete) => s.segmento_slug) || [];
return {
...prev,
selectedRamos: [...prev.selectedRamos, ramoSlug],
selectedSegmentos: [...new Set([...prev.selectedSegmentos, ...segmentosDoRamo])],
};
});
}, [byRamo]);
});
},
[byRamo],
);

// Toggle segmento
const toggleSegmento = useCallback((segmentoSlug: string) => {
setFilterState(prev => {
setFilterState((prev) => {
const isSelected = prev.selectedSegmentos.includes(segmentoSlug);
if (isSelected) {
return {
Expand All @@ -82,41 +101,61 @@ export function useRamoAtividadeFilter(): UseRamoAtividadeFilterReturn {
setFilterState({ selectedRamos: [], selectedSegmentos: [] });
}, []);

const hasActiveFilters = filterState.selectedRamos.length > 0 || filterState.selectedSegmentos.length > 0;
const hasActiveFilters =
filterState.selectedRamos.length > 0 || filterState.selectedSegmentos.length > 0;
const selectedCount = filterState.selectedRamos.length + filterState.selectedSegmentos.length;

const getSegmentosForRamo = useCallback((ramoSlug: string) => {
return byRamo.get(ramoSlug) || [];
}, [byRamo]);

const getSelectedSegmentosForRamo = useCallback((ramoSlug: string) => {
const segmentosNoRamo = byRamo.get(ramoSlug) || [];
return segmentosNoRamo.filter((s: SegmentoComplete) => filterState.selectedSegmentos.includes(s.segmento_slug));
}, [byRamo, filterState.selectedSegmentos]);
const getSegmentosForRamo = useCallback(
(ramoSlug: string) => {
return byRamo.get(ramoSlug) || [];
},
[byRamo],
);

const getSelectedSegmentosForRamo = useCallback(
(ramoSlug: string) => {
const segmentosNoRamo = byRamo.get(ramoSlug) || [];
return segmentosNoRamo.filter((s: SegmentoComplete) =>
filterState.selectedSegmentos.includes(s.segmento_slug),
);
},
[byRamo, filterState.selectedSegmentos],
);

// Segmentos filtrados
const filteredSegmentos = useMemo(() => {
let result = segmentos;
if (filterState.selectedRamos.length > 0) {
result = result.filter(s => filterState.selectedRamos.includes(s.ramo_slug));
result = result.filter((s) => filterState.selectedRamos.includes(s.ramo_slug));
}
return result;
}, [segmentos, filterState.selectedRamos]);

const isRamoSelected = useCallback((ramoSlug: string) => {
return filterState.selectedRamos.includes(ramoSlug);
}, [filterState.selectedRamos]);

const isSegmentoSelected = useCallback((segmentoSlug: string) => {
return filterState.selectedSegmentos.includes(segmentoSlug);
}, [filterState.selectedSegmentos]);

const isRamoPartiallySelected = useCallback((ramoSlug: string) => {
const segmentosDoRamo = getSegmentosForRamo(ramoSlug);
if (segmentosDoRamo.length === 0) return false;
const selectedCount = segmentosDoRamo.filter(s => filterState.selectedSegmentos.includes(s.segmento_slug)).length;
return selectedCount > 0 && selectedCount < segmentosDoRamo.length;
}, [getSegmentosForRamo, filterState.selectedSegmentos]);
const isRamoSelected = useCallback(
(ramoSlug: string) => {
return filterState.selectedRamos.includes(ramoSlug);
},
[filterState.selectedRamos],
);

const isSegmentoSelected = useCallback(
(segmentoSlug: string) => {
return filterState.selectedSegmentos.includes(segmentoSlug);
},
[filterState.selectedSegmentos],
);

const isRamoPartiallySelected = useCallback(
(ramoSlug: string) => {
const segmentosDoRamo = getSegmentosForRamo(ramoSlug);
if (segmentosDoRamo.length === 0) return false;
const selectedCount = segmentosDoRamo.filter((s) =>
filterState.selectedSegmentos.includes(s.segmento_slug),
).length;
return selectedCount > 0 && selectedCount < segmentosDoRamo.length;
},
[getSegmentosForRamo, filterState.selectedSegmentos],
);

return {
groups,
Expand Down
36 changes: 28 additions & 8 deletions src/lib/personalization/repositories/technique.repository.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import type { TecnicaUnificada } from '@/types/tecnica-unificada';
import { fetchExternalData } from '@/lib/external-db';

export interface TechniqueQueryOptions {
search?: string;
Expand Down Expand Up @@ -39,6 +38,23 @@ interface PaginatedResponse<T> {
status: number;
}

interface FetchExternalDataOptions {
url: string;
headers?: HeadersInit;
}
Comment on lines +41 to +44
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve cache semantics in inlined external fetch helper

The new local fetchExternalData helper only accepts url and headers, so the cache metadata still passed by findAll/findById is silently dropped and no TTL caching is applied anymore. This regresses behavior from cached reads to always hitting the external Gravacao API, which can increase latency and API pressure/rate-limit risk in production paths that repeatedly query techniques. The helper should either support the existing cache options or callers should be rewritten to a cache-aware API.

Useful? React with 👍 / 👎.


async function fetchExternalData<T>({ url, headers }: FetchExternalDataOptions): Promise<T> {
const response = await fetch(url, { headers });

if (!response.ok) {
throw new Error(
`External technique API request failed: ${response.status} ${response.statusText}`,
);
}

return response.json() as Promise<T>;
}

function externalToTecnicaUnificada(row: TecnicaGravacaoExterno): TecnicaUnificada {
return {
id: row.id,
Expand Down Expand Up @@ -77,12 +93,13 @@ export async function findAll(options: TechniqueQueryOptions = {}): Promise<Tecn
let tecnicas = (data.data?.records || []).map(externalToTecnicaUnificada);

// Filtros pós-query
if (filters?.search) {
const search = filters.search.toLowerCase();
tecnicas = tecnicas.filter((t: TecnicaUnificada) =>
t.nome.toLowerCase().includes(search) ||
t.codigo.toLowerCase().includes(search) ||
t.descricao?.toLowerCase().includes(search)
if (search) {
const normalizedSearch = search.toLowerCase();
tecnicas = tecnicas.filter(
(t: TecnicaUnificada) =>
t.nome.toLowerCase().includes(normalizedSearch) ||
t.codigo.toLowerCase().includes(normalizedSearch) ||
t.descricao?.toLowerCase().includes(normalizedSearch),
);
}

Expand Down Expand Up @@ -121,7 +138,10 @@ export async function create(tecnica: Omit<TecnicaUnificada, 'id'>): Promise<Tec
return response.json();
}

export async function update(id: string, updates: Partial<TecnicaUnificada>): Promise<TecnicaUnificada> {
export async function update(
id: string,
updates: Partial<TecnicaUnificada>,
): Promise<TecnicaUnificada> {
const response = await fetch(`${GRAVACAO_API}/tecnicas/${id}`, {
method: 'PATCH',
headers: {
Expand Down
1 change: 1 addition & 0 deletions src/pages/kit-builder/useKitBuilderQuote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function useKitBuilderQuote() {

// Create quote
const { data: quote, error: quoteError } = await supabase
// rls-allow: insert cria orçamento do usuário atual; RLS valida user_id
.from('quotes')
.insert({
user_id: user.id,
Expand Down
Loading