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
125 changes: 74 additions & 51 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/components/admin/DiscountApprovalQueue.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export function DiscountApprovalQueue() {
<CardContent className="space-y-3">
<p className="text-sm text-muted-foreground">
Cliente: <strong>{quote?.client_name || quote?.client_company || "—"}</strong>
{quote?.total !== null && <> · Total: <strong>R$ {Number(quote.total).toFixed(2)}</strong></>}
{quote !== undefined && quote.total !== null && <> · Total: <strong>R$ {Number(quote.total).toFixed(2)}</strong></>}
</p>
{hasMarkup && (
<div className="text-xs bg-warning/5 border border-warning/20 rounded-md p-2 space-y-0.5">
Expand Down
375 changes: 287 additions & 88 deletions src/components/admin/MockupPromptManager.tsx

Large diffs are not rendered by default.

5 changes: 3 additions & 2 deletions src/components/admin/SellerDiscountLimitsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ export function SellerDiscountLimitsPanel() {
mutationFn: async ({ userId, percent }: { userId: string; percent: number }) => {
const { data: u } = await supabase.auth.getUser();
if (!u.user) throw new Error("Não autenticado");
const { error } = await supabase
.from("seller_discount_limits" as never)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const { error } = await (supabase as any)
.from("seller_discount_limits")
.upsert({ user_id: userId, max_discount_percent: percent, set_by: u.user.id }, { onConflict: "user_id" });
if (error) throw error;
},
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/TechniquesManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export function TechniquesManager() {
isLoading={isLoading}
isRemoving={isRemoving}
onToggleStatus={toggleStatus}
onUpdate={update}
onUpdate={update as (params: Record<string, unknown>) => void}
onRemove={remove}
/>
<div className="mt-4 pt-4 border-t text-xs text-muted-foreground flex items-center gap-2">
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/connections/Bitrix24Tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function Bitrix24Tab() {
{isTesting ? "Testando…" : "Testar conexão (crm.contact.fields)"}
</Button>
<ConnectionTimelineDrawer type="bitrix24" label="Bitrix24" open={timelineOpen} onOpenChange={setTimelineOpen} />
<RefreshFromDbButton onRefreshed={list} />
<RefreshFromDbButton onRefreshed={() => void list()} />
<RetestCooldownSelector className="ml-auto" />
</div>
<TestProgressIndicator
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/connections/DataSourceDebugTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const FIELD_MAP: DataSourceMap[] = [
];

export function DataSourceDebugTab() {
const { secrets, list, loading: secretsLoading } = useSecretsManager();
const { secrets, list, isLoading: secretsLoading } = useSecretsManager();
const [extConns, setExtConns] = useState<ExternalConnRow[] | null>(null);
const [extLoading, setExtLoading] = useState(false);
const [extError, setExtError] = useState<string | null>(null);
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/connections/KeysValidationTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ const STATUS_META: Record<
};

export function KeysValidationTab() {
const { secrets, list, loading } = useSecretsManager();
const { secrets, list, isLoading: loading } = useSecretsManager();
const [filter, setFilter] = useState('');
const [onlyIssues, setOnlyIssues] = useState(false);
const [collapsed, setCollapsed] = useState<Set<string>>(new Set());
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/connections/N8nTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ export function N8nTab() {
{isTesting ? "Testando…" : "Testar /healthz"}
</Button>
<ConnectionTimelineDrawer type="n8n" label="n8n" open={timelineOpen} onOpenChange={setTimelineOpen} />
<RefreshFromDbButton onRefreshed={list} />
<RefreshFromDbButton onRefreshed={() => void list()} />
<RetestCooldownSelector className="ml-auto" />
</div>
<TestProgressIndicator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function useSeverityChangeNotifier() {
const toastId = toast(SEV_TITLE[sev], {
description: `${headline}\n${reason}`,
duration: sev === "P0" ? Infinity : 30_000,
important: sev === "P0",
// important: sev === "P0", // Not in ExternalToast type
action: {
label: "Reconhecer",
onClick: () => {
Expand Down
20 changes: 10 additions & 10 deletions src/components/admin/products/ProductFormStepContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { Switch } from '@/components/ui/switch';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
import { Loader2, Package, Layers, Info, Wand2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import { SectionCard } from './ProductFormHelpers';
import { SectionCard, type FormSectionProps } from './ProductFormHelpers';
import { CategoryCascadeSelector } from './CategoryCascadeSelector';
import { ProductSupplierSection } from './sections/ProductSupplierSection';
import { ProductInfoSection } from './sections/ProductInfoSection';
Expand Down Expand Up @@ -53,7 +53,7 @@ interface FormProps {
setValue: UseFormSetValue<ProductFormData>;
watch: UseFormWatch<ProductFormData>;
errors: FieldErrors<ProductFormData>;
numericProps: (name: keyof ProductFormData) => object;
numericProps: (name: keyof ProductFormData) => Record<string, unknown>;
}

interface StepContentProps {
Expand Down Expand Up @@ -127,13 +127,13 @@ export function ProductFormStepContent({
primarySupplierName={formValues.brand || ''}
/>
<ProductInfoSection
{...formProps}
skuStatus={skuStatus}
duplicateName={duplicateName}
{...(formProps as unknown as Parameters<typeof ProductInfoSection>[0])}
skuStatus={skuStatus as 'idle' | 'valid' | 'duplicate' | 'checking'}
duplicateName={duplicateName ?? ''}
skuManuallyEdited={skuManuallyEdited}
onSkuManualEdit={onSkuManualEdit}
/>
<ProductDimensionsSection {...formProps} isBoxProduct={isBoxProduct} />
<ProductDimensionsSection {...(formProps as unknown as FormSectionProps)} isBoxProduct={isBoxProduct} />
</>
);
case 'commercial':
Expand All @@ -155,20 +155,20 @@ export function ProductFormStepContent({
</>
);
case 'packaging':
return <ProductPackagingSection {...formProps} />;
return <ProductPackagingSection {...(formProps as unknown as FormSectionProps)} />;
case 'fiscal':
return (
<>
<ProductPriceSection
{...formProps}
{...(formProps as unknown as FormSectionProps)}
supplierMarkup={supplierMarkup}
costPriceDisplay={costPriceDisplay}
salePriceDisplay={salePriceDisplay}
onCostPriceDisplayChange={onCostPriceDisplayChange}
onSalePriceDisplayChange={onSalePriceDisplayChange}
onSalePriceManualEdit={onSalePriceManualEdit}
/>
<ProductFiscalSection {...formProps} />
<ProductFiscalSection {...(formProps as unknown as FormSectionProps)} />
</>
);
case 'content':
Expand All @@ -191,7 +191,7 @@ export function ProductFormStepContent({
{isSeoGenerating ? 'Gerando...' : 'Preencher com IA'}
</Button>
</div>
<ProductSeoSection {...formProps} />
<ProductSeoSection {...(formProps as unknown as FormSectionProps)} />
<ProductMarketingTextsSection register={register} />
</>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/products/hooks/useProductFormDraft.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import { type UseFormSetValue } from 'react-hook-form';
import { toast } from 'sonner';
import type { ProductFormData } from './ProductFormSchema';
import type { ProductFormData } from '../ProductFormSchema';

export function useProductFormDraft(
productId: string | undefined,
Expand Down
6 changes: 3 additions & 3 deletions src/components/admin/products/hooks/useSkuValidation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,16 @@ export function useSkuValidation(currentSku: string, isEdit: boolean, originalSk
try {
const { fetchPromobrindProducts } = await import('@/lib/external-db');
const existing = await fetchPromobrindProducts({ search: currentSku, limit: 5 });
const products = Array.isArray(existing)
const products = (Array.isArray(existing)
? existing
: (existing as Record<string, unknown>).products || [];
: (existing as Record<string, unknown>).products || []) as Array<Record<string, unknown>>;
const dup = products.find(
(p: Record<string, unknown>) =>
(p.sku as string | undefined)?.toLowerCase() === currentSku.toLowerCase(),
);
if (dup) {
setStatus('duplicate');
setDuplicateName(dup.name || '');
setDuplicateName(String(dup.name || ''));
} else {
setStatus('valid');
setDuplicateName('');
Expand Down
155 changes: 131 additions & 24 deletions src/components/admin/products/new-supplier/tabs/BasicDataTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Switch } from '@/components/ui/switch';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Loader2, ImagePlus, X, Search } from 'lucide-react';
import { maskCnpj, maskPhone, ESTADOS_BR } from '@/utils/masks';
import type { NewSupplierForm } from '../useNewSupplierForm';

const fieldClass = "mt-1.5 h-9";
const fieldClass = 'mt-1.5 h-9';

interface BasicDataTabProps {
form: NewSupplierForm;
Expand All @@ -20,63 +26,156 @@ export function BasicDataTab({ form }: BasicDataTabProps) {
<div className="flex items-center gap-4">
<div className="relative shrink-0">
{form.logoUrl ? (
<div className="relative w-20 h-20 rounded-lg border border-border overflow-hidden bg-muted">

<img src={form.logoUrl} alt="Logo" className="w-full h-full object-contain" loading="lazy" />
<button type="button" onClick={() => form.setLogoUrl('')} className="absolute -top-1 -right-1 rounded-full bg-destructive text-destructive-foreground p-0.5">
<div className="relative h-20 w-20 overflow-hidden rounded-lg border border-border bg-muted">
<img
src={form.logoUrl}
alt="Logo"
className="h-full w-full object-contain"
loading="lazy"
/>
<button
type="button"
onClick={() => form.setLogoUrl('')}
className="absolute -right-1 -top-1 rounded-full bg-destructive p-0.5 text-destructive-foreground"
>
<X className="h-3 w-3" />
</button>
</div>
) : (
<button type="button" onClick={() => form.logoInputRef.current?.click()} disabled={form.uploadingLogo} className="w-20 h-20 rounded-lg border-2 border-dashed border-border hover:border-primary/50 flex flex-col items-center justify-center gap-1 text-muted-foreground hover:text-primary transition-colors">
{form.uploadingLogo ? <Loader2 className="h-5 w-5 animate-spin" /> : <><ImagePlus className="h-5 w-5" /><span className="text-[10px]">Logo</span></>}
<button
type="button"
onClick={() => form.logoInputRef.current?.click()}
disabled={form.uploadingLogo}
className="flex h-20 w-20 flex-col items-center justify-center gap-1 rounded-lg border-2 border-dashed border-border text-muted-foreground transition-colors hover:border-primary/50 hover:text-primary"
>
{form.uploadingLogo ? (
<Loader2 className="h-5 w-5 animate-spin" />
) : (
<>
<ImagePlus className="h-5 w-5" />
<span className="text-[10px]">Logo</span>
</>
)}
</button>
)}
<input ref={form.logoInputRef} type="file" accept="image/*" className="hidden" onChange={form.handleLogoUpload} />
<input
ref={form.logoInputRef}
type="file"
accept="image/*"
className="hidden"
onChange={form.handleLogoUpload}
/>
</div>
<div className="flex-1">
<Label className="text-xs font-semibold">Nome Fantasia</Label>
<Input value={form.tradingName} onChange={(e: React.ChangeEvent<HTMLInputElement>) => form.setTradingName(e.target.value)} placeholder="Ex: Asia Import" className={fieldClass} autoFocus />
<Input
value={form.tradingName}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
form.setTradingName(e.target.value)
}
placeholder="Ex: Asia Import"
className={fieldClass}
autoFocus
/>
</div>
<div className="w-40 shrink-0">
<Label className="text-xs font-semibold">Código <span className="text-destructive">*</span></Label>
<Input value={form.code} onChange={(e: React.ChangeEvent<HTMLInputElement>) => form.setCode(e.target.value)} placeholder="Auto-gerado" className={`${fieldClass} font-mono uppercase`} />
<Label className="text-xs font-semibold">
Código <span className="text-destructive">*</span>
</Label>
<Input
value={form.code}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => form.setCode(e.target.value)}
placeholder="Auto-gerado"
className={`${fieldClass} font-mono uppercase`}
/>
</div>
</div>

{/* Razão Social */}
<div>
<Label className="text-xs font-semibold">Razão Social <span className="text-destructive">*</span></Label>
<Input value={form.name} onChange={(e: React.ChangeEvent<HTMLInputElement>) => form.setName(e.target.value)} placeholder="Ex: Asia Import Comércio LTDA" className={fieldClass} />
<Label className="text-xs font-semibold">
Razão Social <span className="text-destructive">*</span>
</Label>
<Input
value={form.name}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => form.setName(e.target.value)}
placeholder="Ex: Asia Import Comércio LTDA"
className={fieldClass}
/>
</div>

{/* CNPJ + Inscrição Estadual */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-semibold">CNPJ</Label>
<div className="flex gap-1.5">
<Input value={form.cnpj} onChange={(e: React.ChangeEvent<HTMLInputElement>) => { form.setCnpj(maskCnpj(e.target.value)); form.setCnpjError(''); }} placeholder="00.000.000/0000-00" className={`${fieldClass} font-mono flex-1 ${form.cnpjError ? 'border-destructive' : ''}`} maxLength={18} />
<Button type="button" variant="outline" size="sm" className="h-9 px-2.5 shrink-0" disabled={form.fetchingCnpj || form.cnpj.replace(/\D/g, '').length !== 14} onClick={form.handleCnpjLookup}>
{form.fetchingCnpj ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Search className="h-3.5 w-3.5" />}
<Input
value={form.cnpj}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
form.setCnpj(maskCnpj(e.target.value));
form.setCnpjError('');
}}
placeholder="00.000.000/0000-00"
className={`${fieldClass} flex-1 font-mono ${form.cnpjError ? 'border-destructive' : ''}`}
maxLength={18}
/>
<Button
type="button"
variant="outline"
size="sm"
className="h-9 shrink-0 px-2.5"
disabled={form.fetchingCnpj || form.cnpj.replace(/\D/g, '').length !== 14}
onClick={form.handleCnpjLookup}
>
{form.fetchingCnpj ? (
<Loader2 className="h-3.5 w-3.5 animate-spin" />
) : (
<Search className="h-3.5 w-3.5" />
)}
</Button>
</div>
{form.cnpjError && <p className="text-[10px] text-destructive mt-0.5">{form.cnpjError}</p>}
{form.cnpjError && (
<p className="mt-0.5 text-[10px] text-destructive">{form.cnpjError}</p>
)}
</div>
<div>
<Label className="text-xs font-semibold">Inscrição Estadual</Label>
<Input value={form.inscricaoEstadual} onChange={(e: React.ChangeEvent<HTMLInputElement>) => form.setInscricaoEstadual(e.target.value)} placeholder="Ex: 123.456.789.000" className={fieldClass} />
<Input
value={form.inscricaoEstadual}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
form.setInscricaoEstadual(e.target.value)
}
placeholder="Ex: 123.456.789.000"
className={fieldClass}
/>
</div>
</div>

{/* Fone Fixo 01 + 02 */}
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-xs font-semibold">Fone Fixo 01</Label>
<Input value={form.foneFixo1} onChange={(e: React.ChangeEvent<HTMLInputElement>) => form.setFoneFixo1(maskPhone(e.target.value))} placeholder="(00) 0000-0000" className={fieldClass} maxLength={15} />
<Input
value={form.foneFixo1}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
form.setFoneFixo1(maskPhone(e.target.value))
}
placeholder="(00) 0000-0000"
className={fieldClass}
maxLength={15}
/>
</div>
<div>
<Label className="text-xs font-semibold">Fone Fixo 02</Label>
<Input value={form.foneFixo2} onChange={(e: React.ChangeEvent<HTMLInputElement>) => form.setFoneFixo2(maskPhone(e.target.value))} placeholder="(00) 0000-0000" className={fieldClass} maxLength={15} />
<Input
value={form.foneFixo2}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
form.setFoneFixo2(maskPhone(e.target.value))
}
placeholder="(00) 0000-0000"
className={fieldClass}
maxLength={15}
/>
</div>
</div>

Expand All @@ -85,7 +184,9 @@ export function BasicDataTab({ form }: BasicDataTabProps) {
<div>
<Label className="text-xs font-semibold">Regime Tributário</Label>
<Select value={form.regimeTributario} onValueChange={form.setRegimeTributario}>
<SelectTrigger className={fieldClass}><SelectValue placeholder="Selecione o regime" /></SelectTrigger>
<SelectTrigger className={fieldClass}>
<SelectValue placeholder="Selecione o regime" />
</SelectTrigger>
<SelectContent>
<SelectItem value="MEI">MEI</SelectItem>
<SelectItem value="Simples Nacional">Simples Nacional</SelectItem>
Expand All @@ -97,9 +198,15 @@ export function BasicDataTab({ form }: BasicDataTabProps) {
<div>
<Label className="text-xs font-semibold">Estado de Faturamento</Label>
<Select value={form.estadoFaturamento} onValueChange={form.setEstadoFaturamento}>
<SelectTrigger className={fieldClass}><SelectValue placeholder="Selecione o estado" /></SelectTrigger>
<SelectTrigger className={fieldClass}>
<SelectValue placeholder="Selecione o estado" />
</SelectTrigger>
<SelectContent>
{ESTADOS_BR.map(uf => <SelectItem key={uf} value={uf}>{uf}</SelectItem>)}
{ESTADOS_BR.map((uf) => (
<SelectItem key={uf} value={uf}>
{uf}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
Expand Down
Loading
Loading