Skip to content
17 changes: 14 additions & 3 deletions .eslint-baseline.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"generatedAt": "2026-05-24T19:38:14.033Z",
"totalErrors": 128,
"generatedAt": "2026-05-25T02:40:18.480Z",
"totalErrors": 133,
"counts": {
"src/components/access/DevAccessDeniedPage.tsx": {
"react-hooks/exhaustive-deps": 1
Expand Down Expand Up @@ -123,7 +123,6 @@
"@typescript-eslint/naming-convention": 2
},
"src/components/expert/chat/useExpertChat.ts": {
"@typescript-eslint/no-unused-expressions": 2,
"react-hooks/exhaustive-deps": 3
},
"src/components/filters/CommemorativeDateFilter.tsx": {
Expand Down Expand Up @@ -424,6 +423,12 @@
"src/hooks/admin/useDevGate.ts": {
"react-hooks/exhaustive-deps": 1
},
"src/hooks/admin/useKillSwitchObservability.ts": {
"@typescript-eslint/no-explicit-any": 1
},
"src/hooks/admin/useSmokeTests.ts": {
"@typescript-eslint/no-explicit-any": 2
},
"src/hooks/auth/useAccessSecurity.ts": {
"@typescript-eslint/naming-convention": 5
},
Expand Down Expand Up @@ -491,6 +496,9 @@
"@typescript-eslint/naming-convention": 1,
"@typescript-eslint/no-non-null-assertion": 1
},
"src/lib/external-db/rest-native.ts": {
"@typescript-eslint/no-explicit-any": 3
},
"src/lib/feature-flags.ts": {
"@typescript-eslint/no-non-null-assertion": 1
},
Expand Down Expand Up @@ -529,6 +537,9 @@
"src/pages/admin/AdminExternalDbPage.tsx": {
"react-hooks/exhaustive-deps": 1
},
"src/pages/admin/ObservabilityDashboard.tsx": {
"eqeqeq": 1
},
"src/pages/admin/PermissionsPage.tsx": {
"react-hooks/exhaustive-deps": 1
},
Expand Down
75 changes: 43 additions & 32 deletions src/components/admin/SellerDiscountLimitsPanel.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/**
* SellerDiscountLimitsPanel — gestão administrativa do limite máximo de desconto por vendedor.
*/
import { useState } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { supabase } from "@/integrations/supabase/client";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Percent, Save } from "lucide-react";
import { toast } from "sonner";
import { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';
import { Skeleton } from '@/components/ui/skeleton';
import { Percent, Save } from 'lucide-react';
import { toast } from 'sonner';

interface SellerRow {
user_id: string;
Expand All @@ -23,26 +23,30 @@ export function SellerDiscountLimitsPanel() {
const [edits, setEdits] = useState<Record<string, number>>({});

const { data, isLoading } = useQuery({
queryKey: ["seller-discount-limits"],
queryKey: ['seller-discount-limits'],
queryFn: async (): Promise<SellerRow[]> => {
const { data: profiles, error: pErr } = await supabase
.from("profiles")
.select("user_id, full_name, email, role")
.eq("role", "vendedor");
.from('profiles')
.select('user_id, full_name, email, role')
.eq('role', 'vendedor');
if (pErr) throw pErr;

const ids = (profiles || []).map((p) => p.user_id);
const sellers = (profiles || []).filter(
(p): p is typeof p & { user_id: string } => p.user_id !== null,
);
const ids = sellers.map((p) => p.user_id);
const { data: limits } = await supabase
.from("seller_discount_limits" as never)
.select("user_id, max_discount_percent")
.in("user_id", ids);
.from('seller_discount_limits' as never)
.select('user_id, max_discount_percent')
Comment on lines +39 to +40
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 | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the exact file
fd -i "SellerDiscountLimitsPanel.tsx" .

# Print relevant sections with line numbers
FILE="$(fd -i "SellerDiscountLimitsPanel.tsx" . | head -n 1)"
echo "Using file: $FILE"
nl -ba "$FILE" | sed -n '1,140p'

Repository: adm01-debug/promo-gifts-v4

Length of output: 250


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="$(fd -i "SellerDiscountLimitsPanel.tsx" . | head -n 1)"
# Narrow search for casts
rg -n "as\s+(never|any)" "$FILE" || true
# Narrow search for supabase usage
rg -n "supabase|\.from\(|\.select\(|\.upsert\(|\.in\(" "$FILE"

Repository: adm01-debug/promo-gifts-v4

Length of output: 740


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="./src/components/admin/SellerDiscountLimitsPanel.tsx"
# show the relevant area with line numbers (no nl in this env)
sed -n '1,120p' "$FILE" | cat -n

Repository: adm01-debug/promo-gifts-v4

Length of output: 5452


Remover as never/as any das queries Supabase para voltar a ter segurança de tipos.

  • Em src/components/admin/SellerDiscountLimitsPanel.tsx, o .from('seller_discount_limits' as never) (linhas 39-40) e o (supabase as any) no upsert (linhas 63-68) desativam garantias de tipo justamente onde o schema/payload são usados — isso abre margem pra regressões silenciosas.
💡 Sugestão de ajuste
-      const { data: limits } = await supabase
-        .from('seller_discount_limits' as never)
+      const { data: limits } = await supabase
+        .from('seller_discount_limits')
         .select('user_id, max_discount_percent')
         .in('user_id', ids);

-      // eslint-disable-next-line `@typescript-eslint/no-explicit-any`
-      const { error } = await (supabase as any)
-        .from('seller_discount_limits')
+      const { error } = await supabase
+        .from('seller_discount_limits')
         .upsert(
           { user_id: userId, max_discount_percent: percent, set_by: u.user.id },
           { onConflict: 'user_id' },
         );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/admin/SellerDiscountLimitsPanel.tsx` around lines 39 - 40, In
SellerDiscountLimitsPanel, remove the unsafe casts (the
".from('seller_discount_limits' as never)" and the "(supabase as any)" used for
upsert) and restore type safety by declaring a typed row/interface (e.g.,
SellerDiscountLimit with user_id and max_discount_percent) and using the
Supabase client's generics: call
supabase.from<SellerDiscountLimit>('seller_discount_limits') and perform upsert
with the correctly typed payload via the properly typed supabase client instance
instead of casting to any; ensure the local payload variable matches the
SellerDiscountLimit shape so TypeScript will validate reads and writes.

.in('user_id', ids);

Comment on lines 38 to 42
const byId = new Map<string, number>(
((limits as Array<{ user_id: string; max_discount_percent: number }>) || []).map(
(l) => [l.user_id, Number(l.max_discount_percent)]
)
((limits as Array<{ user_id: string; max_discount_percent: number }>) || []).map((l) => [
l.user_id,
Number(l.max_discount_percent),
]),
);
return (profiles || []).map((p) => ({
return sellers.map((p) => ({
user_id: p.user_id,
full_name: p.full_name,
email: p.email,
Expand All @@ -54,40 +58,47 @@ export function SellerDiscountLimitsPanel() {
const save = useMutation({
mutationFn: async ({ userId, percent }: { userId: string; percent: number }) => {
const { data: u } = await supabase.auth.getUser();
if (!u.user) throw new Error("Não autenticado");
if (!u.user) throw new Error('Não autenticado');
// 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" });
.from('seller_discount_limits')
.upsert(
Comment on lines 62 to +65
{ user_id: userId, max_discount_percent: percent, set_by: u.user.id },
{ onConflict: 'user_id' },
);
if (error) throw error;
},
onSuccess: () => {
toast.success("Limite atualizado");
qc.invalidateQueries({ queryKey: ["seller-discount-limits"] });
toast.success('Limite atualizado');
qc.invalidateQueries({ queryKey: ['seller-discount-limits'] });
},
onError: (e: Error) => toast.error(e.message),
});

return (
<Card>
<CardHeader>
<CardTitle className="text-base flex items-center gap-2">
<CardTitle className="flex items-center gap-2 text-base">
<Percent className="h-4 w-4 text-primary" /> Limites de desconto por vendedor
</CardTitle>
</CardHeader>
<CardContent>
{isLoading ? (
<div className="space-y-2">{[0, 1, 2].map((i) => <Skeleton key={i} className="h-12" />)}</div>
<div className="space-y-2">
{[0, 1, 2].map((i) => (
<Skeleton key={i} className="h-12" />
))}
</div>
) : (
<div className="space-y-2">
{(data ?? []).map((row) => {
const current = edits[row.user_id] ?? row.max_discount_percent;
const dirty = current !== row.max_discount_percent;
return (
<div key={row.user_id} className="flex items-center gap-3 rounded-lg border p-2">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{row.full_name || "Sem nome"}</p>
<p className="text-xs text-muted-foreground truncate">{row.email}</p>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{row.full_name || 'Sem nome'}</p>
<p className="truncate text-xs text-muted-foreground">{row.email}</p>
</div>
<Input
type="number"
Expand All @@ -104,7 +115,7 @@ export function SellerDiscountLimitsPanel() {
disabled={!dirty || save.isPending}
onClick={() => save.mutate({ userId: row.user_id, percent: current })}
>
<Save className="h-3.5 w-3.5 mr-1" /> Salvar
<Save className="mr-1 h-3.5 w-3.5" /> Salvar
</Button>
</div>
);
Expand Down
2 changes: 1 addition & 1 deletion src/components/admin/security/ActiveIpsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ interface IpEntry {
reason: string | null;
expires_at: string | null;
created_at: string;
created_by: string;
created_by: string | null;
}

type Filter = 'all' | 'allow' | 'block' | 'active' | 'expired';
Expand Down
Loading
Loading