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
162 changes: 97 additions & 65 deletions src/hooks/quotes/useQuotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,80 +8,104 @@ import { createClientLogger } from '@/lib/telemetry/structuredLogger';
import { toast } from 'sonner';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { quoteService } from '@/services/quoteService';
import type { Quote, QuoteItem, PersonalizationTechnique } from "@/hooks/quotes/quoteTypes";
import type { Quote, QuoteItem } from '@/hooks/quotes/quoteTypes';
import { supabase } from '@/integrations/supabase/client';

export type {
Quote,
QuoteItem,
QuoteItemPersonalization,
PersonalizationTechnique,
} from "@/hooks/quotes/quoteTypes";
} from '@/hooks/quotes/quoteTypes';

type QuoteHistoryOptions = {
fieldChanged?: string;
oldValue?: unknown;
newValue?: unknown;
metadata?: Record<string, unknown>;
};

type QuoteSyncResponse = {
error?: string;
bitrix_deal_id?: string | number | null;
success?: boolean;
};

function getErrorMessage(err: unknown): string {
return err instanceof Error ? err.message : 'Erro desconhecido';
}

export function useQuotes() {
const { user } = useAuth();
const userId = user?.id ?? null;
const { currentOrg } = useOrganization();
const orgId = currentOrg?.id || null;
const scope = useSalesScope();
const queryClient = useQueryClient();

// Queries
const {
data: quotes = [],
isLoading,
error,
refetch: fetchQuotes
const {
data: quotes = [],
isLoading,
error,
refetch: fetchQuotes,
} = useQuery({
queryKey: ['quotes', user?.id, scope],
queryFn: () => quoteService.fetchQuotes(user!.id, scope),
enabled: !!user,
queryKey: ['quotes', userId, scope],
queryFn: () => quoteService.fetchQuotes(userId ?? '', scope),
enabled: !!userId,
});

const {
data: techniques = [],
refetch: fetchTechniques
} = useQuery({
const { data: techniques = [], refetch: fetchTechniques } = useQuery({
queryKey: ['techniques'],
queryFn: () => quoteService.fetchTechniques(),
enabled: !!user,
enabled: !!userId,
staleTime: 60 * 60 * 1000, // 1 hour
});

// Mutations
const createMutation = useMutation({
mutationFn: ({ quote, items }: { quote: Partial<Quote>; items: QuoteItem[] }) =>
quoteService.createQuote(quote, items, user!.id, orgId),
mutationFn: ({ quote, items }: { quote: Partial<Quote>; items: QuoteItem[] }) => {
if (!userId) throw new Error('Usuario nao autenticado');
return quoteService.createQuote(quote, items, userId, orgId);
},
onSuccess: (newQuote) => {
queryClient.invalidateQueries({ queryKey: ['quotes'] });
toast.success('Orçamento criado!', { description: `Número: ${newQuote.quote_number}` });
},
onError: (err: any) => {
toast.error('Erro ao criar orçamento', { description: err.message });
}
onError: (err: unknown) => {
toast.error('Erro ao criar orçamento', { description: getErrorMessage(err) });
},
});

const updateMutation = useMutation({
mutationFn: ({ quoteId, quote, items }: { quoteId: string; quote: Partial<Quote>; items: QuoteItem[] }) =>
quoteService.updateQuote(quoteId, quote, items),
mutationFn: ({
quoteId,
quote,
items,
}: {
quoteId: string;
quote: Partial<Quote>;
items: QuoteItem[];
}) => quoteService.updateQuote(quoteId, quote, items),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['quotes'] });
toast.success('Orçamento atualizado!');
},
onError: (err: any) => {
toast.error('Erro ao atualizar orçamento', { description: err.message });
}
onError: (err: unknown) => {
toast.error('Erro ao atualizar orçamento', { description: getErrorMessage(err) });
},
});

const statusMutation = useMutation({
mutationFn: ({ quoteId, status }: { quoteId: string; status: Quote['status'] }) =>
mutationFn: ({ quoteId, status }: { quoteId: string; status: Quote['status'] }) =>
quoteService.updateQuoteStatus(quoteId, status),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['quotes'] });
toast.success('Status atualizado');
},
onError: () => {
toast.error('Erro ao atualizar status');
}
},
});

const deleteMutation = useMutation({
Expand All @@ -92,15 +116,15 @@ export function useQuotes() {
},
onError: () => {
toast.error('Erro ao excluir orçamento');
}
},
});

// Actions
const fetchQuote = async (quoteId: string) => {
try {
return await quoteService.fetchQuote(quoteId);
} catch (err: any) {
toast.error('Erro ao carregar orçamento', { description: err.message });
} catch (err: unknown) {
toast.error('Erro ao carregar orçamento', { description: getErrorMessage(err) });
return null;
}
};
Expand Down Expand Up @@ -139,30 +163,31 @@ export function useQuotes() {
const original = await fetchQuote(quoteId);
if (!original) throw new Error('Orçamento não encontrado');

const items: QuoteItem[] = original.items?.map((item) => ({
product_id: item.product_id,
product_name: item.product_name,
product_sku: item.product_sku,
product_image_url: item.product_image_url,
quantity: item.quantity,
unit_price: item.unit_price,
color_name: item.color_name,
color_hex: item.color_hex,
notes: item.notes,
personalizations: item.personalizations?.map((p) => ({
technique_id: p.technique_id,
technique_name: p.technique_name,
colors_count: p.colors_count,
positions_count: p.positions_count,
area_cm2: p.area_cm2,
width_cm: p.width_cm,
height_cm: p.height_cm,
setup_cost: p.setup_cost,
unit_cost: p.unit_cost,
total_cost: p.total_cost,
notes: p.notes,
})),
})) || [];
const items: QuoteItem[] =
original.items?.map((item) => ({
product_id: item.product_id,
product_name: item.product_name,
product_sku: item.product_sku,
product_image_url: item.product_image_url,
quantity: item.quantity,
unit_price: item.unit_price,
color_name: item.color_name,
color_hex: item.color_hex,
notes: item.notes,
personalizations: item.personalizations?.map((p) => ({
technique_id: p.technique_id,
technique_name: p.technique_name,
colors_count: p.colors_count,
positions_count: p.positions_count,
area_cm2: p.area_cm2,
width_cm: p.width_cm,
height_cm: p.height_cm,
setup_cost: p.setup_cost,
unit_cost: p.unit_cost,
total_cost: p.total_cost,
notes: p.notes,
})),
})) || [];

const newQuote = await createQuote(
{
Expand All @@ -186,8 +211,8 @@ export function useQuotes() {
);

return newQuote;
} catch (err: any) {
toast.error('Erro ao duplicar', { description: err.message });
} catch (err: unknown) {
toast.error('Erro ao duplicar', { description: getErrorMessage(err) });
return null;
}
};
Expand All @@ -200,14 +225,15 @@ export function useQuotes() {
headers: log.headers(),
});
if (fnError) throw new Error(fnError.message);
if (data.error) throw new Error(data.error);
const syncData = data as QuoteSyncResponse | null;
if (syncData?.error) throw new Error(syncData.error);
toast.success('Sincronizado com Bitrix!', {
description: `Deal ID: ${data.bitrix_deal_id || 'N/A'}`,
description: `Deal ID: ${syncData?.bitrix_deal_id || 'N/A'}`,
});
queryClient.invalidateQueries({ queryKey: ['quotes'] });
return true;
} catch (err: any) {
toast.error('Erro ao sincronizar', { description: err.message });
} catch (err: unknown) {
toast.error('Erro ao sincronizar', { description: getErrorMessage(err) });
return false;
}
};
Expand All @@ -218,19 +244,25 @@ export function useQuotes() {
body: { action: 'test_webhook', data: {} },
});
if (fnError) throw new Error(fnError.message);
if (data.success) {
const testData = data as QuoteSyncResponse | null;
if (testData?.success) {
toast.success('Conexão com N8N estabelecida!');
return true;
}
toast.error('Falha na conexão com N8N');
return false;
} catch (err: any) {
toast.error('Erro ao testar webhook', { description: err.message });
} catch (err: unknown) {
toast.error('Erro ao testar webhook', { description: getErrorMessage(err) });
return false;
}
};

const logQuoteHistory = async (quoteId: string, action: string, description: string, options?: any) => {
const logQuoteHistory = async (
quoteId: string,
action: string,
description: string,
options?: QuoteHistoryOptions,
) => {
if (!user) return;
try {
await quoteService.logHistory(quoteId, user.id, action, description, options);
Expand All @@ -243,7 +275,7 @@ export function useQuotes() {
quotes,
techniques,
isLoading: isLoading || createMutation.isPending || updateMutation.isPending,
error: error ? (error as any).message : null,
error: error ? getErrorMessage(error) : null,
fetchQuotes,
fetchQuote,
createQuote,
Expand Down
28 changes: 21 additions & 7 deletions src/lib/external-db/price-tables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,15 @@ export async function fetchPromobrindPriceTables(options?: {
if (options?.techniqueCode) filters.table_code = options.techniqueCode;

const result = await invokeExternalDb<Record<string, unknown>>({
table: 'customization_price_tables', operation: 'select',
filters, select: '*', limit: 500,
table: 'customization_price_tables',
operation: 'select',
filters,
select: '*',
limit: 500,
orderBy: { column: 'tier_1_min_qty', ascending: true },
});

let tables: PromobrindPriceTable[] = result.records.map(r => ({
let tables: PromobrindPriceTable[] = result.records.map((r) => ({
id: r.id as string,
table_code: r.table_code as string,
table_code_option: r.table_code_option as string,
Expand All @@ -63,10 +66,21 @@ export async function fetchPromobrindPriceTables(options?: {
technique_name: r.customization_type_name as string,
}));

if (options?.quantity) tables = tables.filter(t => t.min_quantity <= options.quantity! && (t.max_quantity === null || t.max_quantity >= options.quantity!));
if (options?.colors) tables = tables.filter(t => (t.min_colors === null || t.min_colors <= options.colors!) && (t.max_colors === null || t.max_colors >= options.colors!));
if (options?.width) tables = tables.filter(t => t.max_area_width_cm === null || t.max_area_width_cm >= options.width!);
if (options?.height) tables = tables.filter(t => t.max_area_height_cm === null || t.max_area_height_cm >= options.height!);
const { quantity, colors, width, height } = options ?? {};
if (quantity)
tables = tables.filter(
(t) => t.min_quantity <= quantity && (t.max_quantity === null || t.max_quantity >= quantity),
);
if (colors)
tables = tables.filter(
(t) =>
(t.min_colors === null || t.min_colors <= colors) &&
(t.max_colors === null || t.max_colors >= colors),
);
if (width)
tables = tables.filter((t) => t.max_area_width_cm === null || t.max_area_width_cm >= width);
if (height)
tables = tables.filter((t) => t.max_area_height_cm === null || t.max_area_height_cm >= height);

return tables;
}
Expand Down
Loading
Loading