atualizacoes modulo Score e Commander

This commit is contained in:
Vitex Tecnologia
2026-05-20 10:53:17 -03:00
parent 6acfee2c76
commit 0b5c7350c5
14 changed files with 265 additions and 44 deletions
BIN
View File
Binary file not shown.
@@ -8,12 +8,26 @@ import {
ExternalLink,
Landmark,
Loader2,
Trash2,
Wallet,
} from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { Textarea } from "@/components/ui/textarea";
import { Label } from "@/components/ui/label";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { useAuthAccess } from "@/contexts/AuthAccessContext";
import {
fechamentoBancoPontosService,
type BancoPontosExtratoItem,
@@ -59,6 +73,8 @@ function nomeExibicaoParceiro(p: BancoPontosParceiroExtrato): string {
export default function BancoPontosExtrato() {
const navigate = useNavigate();
const location = useLocation();
const { me } = useAuthAccess();
const podeExcluirTransacao = me?.capabilities.bancoPontos.excluirTransacao ?? false;
const { parceiroId = "" } = useParams();
const nomeDoState = (location.state as ExtratoLocationState | null)?.nomeExibicao?.trim();
const [loading, setLoading] = useState(true);
@@ -67,6 +83,9 @@ export default function BancoPontosExtrato() {
const [linhas, setLinhas] = useState<BancoPontosExtratoItem[]>([]);
const [meta, setMeta] = useState({ total: 0, paginaAtual: 1, totalPaginas: 1 });
const [page, setPage] = useState(1);
const [linhaParaExcluir, setLinhaParaExcluir] = useState<BancoPontosExtratoItem | null>(null);
const [motivoExclusao, setMotivoExclusao] = useState("");
const [excluindo, setExcluindo] = useState(false);
const load = useCallback(async () => {
if (!parceiroId) return;
@@ -125,6 +144,28 @@ export default function BancoPontosExtrato() {
return `Mostrando ${inicio}${fim} de ${meta.total}`;
})();
const handleExcluirTransacao = async () => {
if (!linhaParaExcluir) return;
const motivo = motivoExclusao.trim();
if (motivo.length < 3) {
toast.error("Informe o motivo da exclusão (mínimo 3 caracteres).");
return;
}
try {
setExcluindo(true);
await fechamentoBancoPontosService.excluirTransacao(linhaParaExcluir.id, motivo);
toast.success("Lançamento excluído do extrato.");
setLinhaParaExcluir(null);
setMotivoExclusao("");
await load();
} catch (e) {
const msg = e instanceof Error ? e.message : "Erro ao excluir lançamento.";
toast.error(msg);
} finally {
setExcluindo(false);
}
};
const handleExportarXlsx = async () => {
if (!parceiroId) return;
try {
@@ -220,19 +261,28 @@ export default function BancoPontosExtrato() {
<TableHead className="min-w-[100px]">Tipo</TableHead>
<TableHead className="min-w-[130px] text-right">Valor (Pontos)</TableHead>
<TableHead className="min-w-[120px] text-center">Fechamento</TableHead>
{podeExcluirTransacao ? (
<TableHead className="min-w-[80px] text-center">Ações</TableHead>
) : null}
</TableRow>
</TableHeader>
<TableBody>
{loading ? (
<TableRow>
<TableCell colSpan={6} className="py-14 text-center text-muted-foreground">
<TableCell
colSpan={podeExcluirTransacao ? 7 : 6}
className="py-14 text-center text-muted-foreground"
>
<Loader2 className="mx-auto mb-2 h-8 w-8 animate-spin text-primary" />
Carregando extrato...
</TableCell>
</TableRow>
) : linhasOrdenadas.length === 0 ? (
<TableRow>
<TableCell colSpan={6} className="py-12 text-center text-muted-foreground">
<TableCell
colSpan={podeExcluirTransacao ? 7 : 6}
className="py-12 text-center text-muted-foreground"
>
Nenhuma movimentação registrada para este parceiro.
</TableCell>
</TableRow>
@@ -287,6 +337,23 @@ export default function BancoPontosExtrato() {
<span className="text-sm text-muted-foreground"></span>
)}
</TableCell>
{podeExcluirTransacao ? (
<TableCell className="text-center">
<Button
type="button"
variant="ghost"
size="icon"
title="Excluir lançamento"
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
onClick={() => {
setLinhaParaExcluir(linha);
setMotivoExclusao("");
}}
>
<Trash2 className="h-4 w-4" />
</Button>
</TableCell>
) : null}
</TableRow>
);
})
@@ -330,6 +397,50 @@ export default function BancoPontosExtrato() {
</div>
</div>
</div>
<AlertDialog
open={linhaParaExcluir != null}
onOpenChange={(open) => {
if (!open) {
setLinhaParaExcluir(null);
setMotivoExclusao("");
}
}}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Excluir lançamento</AlertDialogTitle>
<AlertDialogDescription>
O lançamento será removido do extrato e do saldo, mas permanece registrado no banco com motivo e
auditoria. Esta ação não pode ser desfeita pela interface.
</AlertDialogDescription>
</AlertDialogHeader>
<div className="space-y-2 py-2">
<Label htmlFor="motivo-exclusao-banco">Motivo da exclusão</Label>
<Textarea
id="motivo-exclusao-banco"
value={motivoExclusao}
onChange={(e) => setMotivoExclusao(e.target.value)}
placeholder="Descreva o motivo (mínimo 3 caracteres)"
rows={3}
disabled={excluindo}
/>
</div>
<AlertDialogFooter>
<AlertDialogCancel disabled={excluindo}>Cancelar</AlertDialogCancel>
<AlertDialogAction
disabled={excluindo || motivoExclusao.trim().length < 3}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
onClick={(e) => {
e.preventDefault();
void handleExcluirTransacao();
}}
>
{excluindo ? "Excluindo..." : "Confirmar exclusão"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
);
}
@@ -356,9 +356,10 @@ export default function CompetenciaFechamentos() {
{podeImportarAsana ? (
<Button
size="sm"
variant="secondary"
variant="outline"
onClick={() => setIsReprocessModalOpen(true)}
disabled={importing}
className="hover:bg-muted/60 hover:text-foreground"
>
<RefreshCcw className="mr-2 h-4 w-4" />
Reprocessar Asana
@@ -367,7 +368,7 @@ export default function CompetenciaFechamentos() {
{podeConcluirCompetencia ? (
<Button
size="sm"
variant="outline"
variant="default"
onClick={() => setIsConcluirModalOpen(true)}
disabled={concluindoCompetencia}
>
@@ -689,8 +689,8 @@ export default function Configuracoes() {
</div>
<Button
type="button"
variant="secondary"
className="h-11 shrink-0 whitespace-nowrap px-5 lg:self-stretch"
variant="outline"
className="h-11 shrink-0 whitespace-nowrap bg-background px-5 hover:bg-muted/60 hover:text-foreground dark:bg-muted/30 dark:hover:bg-muted/50 lg:self-stretch"
onClick={handleBuscarWorkspaces}
disabled={!podeSalvarConfig || !podeBuscarWorkspaces || Boolean(configAsanaError)}
>
@@ -1076,7 +1076,7 @@ export default function FechamentoDetalhes() {
) : podeConcluirFechamento ? (
<Button
size="sm"
variant="secondary"
variant="default"
onClick={handleAbrirConcluir}
disabled={tarefas.length === 0 || concluindo || isCompetenciaConcluida}
className="min-w-[152px]"
@@ -1713,18 +1713,18 @@ export default function FechamentoDetalhes() {
<div className="flex flex-col gap-2 sm:flex-row sm:flex-wrap">
<Button
type="button"
variant="secondary"
variant="outline"
size="sm"
className="h-9 justify-center font-normal"
className="h-9 justify-center bg-background font-normal hover:bg-muted/60 hover:text-foreground dark:bg-muted/30 dark:hover:bg-muted/50"
onClick={() => setPontuacaoPagaInput(pontuacaoTotalLabel)}
>
Igual aos aprovados
</Button>
<Button
type="button"
variant="secondary"
variant="outline"
size="sm"
className="h-9 justify-center font-normal"
className="h-9 justify-center bg-background font-normal hover:bg-muted/60 hover:text-foreground dark:bg-muted/30 dark:hover:bg-muted/50"
onClick={() => setPontuacaoPagaInput(String(pontuacaoMeta ?? 0))}
>
Igual à meta
@@ -782,7 +782,13 @@ export default function Parceiros() {
<TableCell>{parceiro.email}</TableCell>
<TableCell>{getTipoPessoaLabel(parceiro.tipoPessoa)}</TableCell>
<TableCell>
<Badge variant={parceiro.estaAtivo ? "secondary" : "outline"}>
<Badge
className={
parceiro.estaAtivo
? "border-emerald-600/30 bg-emerald-50 font-normal text-emerald-800 hover:bg-emerald-50 dark:bg-emerald-950/40 dark:text-emerald-200"
: "border-border bg-muted/60 font-normal text-muted-foreground hover:bg-muted/60 dark:bg-muted/40 dark:text-muted-foreground"
}
>
{parceiro.estaAtivo ? "Ativo" : "Inativo"}
</Badge>
</TableCell>
@@ -1,5 +1,5 @@
import { useEffect, useMemo, useState } from "react";
import { Edit, Plus, Power, Users, UserCheck, UserX } from "lucide-react";
import { Edit, Plus, Power, Trash2, Users, UserCheck, UserX } from "lucide-react";
import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
@@ -84,6 +84,7 @@ function getEmailUsuario(usuario: UsuarioItem | null | undefined): string {
export default function Usuarios() {
const { me } = useAuthAccess();
const podeEditarUsuarios = me?.capabilities.usuarios.criarEditarToggle ?? false;
const podeExcluirUsuario = me?.capabilities.usuarios.excluir ?? false;
const [usuarios, setUsuarios] = useState<UsuarioItem[]>([]);
const [parceiros, setParceiros] = useState<ParceiroItem[]>([]);
@@ -92,8 +93,10 @@ export default function Usuarios() {
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false);
const [isToggleOpen, setIsToggleOpen] = useState(false);
const [isDeleteOpen, setIsDeleteOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [toggling, setToggling] = useState(false);
const [deleting, setDeleting] = useState(false);
const [selectedUsuario, setSelectedUsuario] = useState<UsuarioItem | null>(null);
const [form, setForm] = useState<UsuarioForm>(formInicial);
@@ -279,6 +282,30 @@ export default function Usuarios() {
}
};
const openDeleteDialog = (usuario: UsuarioItem) => {
setSelectedUsuario(usuario);
setIsDeleteOpen(true);
};
const handleDeleteUsuario = async () => {
if (!selectedUsuario) {
return;
}
try {
setDeleting(true);
await fechamentoUsuariosService.excluirUsuario(selectedUsuario.id);
toast.success("Usuário excluído com sucesso.");
setIsDeleteOpen(false);
setSelectedUsuario(null);
setReloadNonce((prev) => prev + 1);
} catch (error) {
const message = error instanceof Error ? error.message : "Erro ao excluir usuário.";
toast.error(message);
} finally {
setDeleting(false);
}
};
const handleToggleStatus = async () => {
if (!selectedUsuario) {
return;
@@ -459,6 +486,8 @@ export default function Usuarios() {
</TableCell>
<TableCell>
<div className="flex items-center justify-center gap-1">
{podeEditarUsuarios || podeExcluirUsuario ? (
<>
{podeEditarUsuarios ? (
<>
<Button
@@ -478,6 +507,20 @@ export default function Usuarios() {
<Power className="h-4 w-4" />
</Button>
</>
) : null}
{podeExcluirUsuario ? (
<Button
variant="ghost"
size="icon"
onClick={() => openDeleteDialog(usuario)}
title="Excluir usuário"
disabled={me?.id === usuario.id}
className="text-destructive hover:bg-destructive/10 hover:text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
) : null}
</>
) : (
<span className="text-xs text-muted-foreground">Somente leitura</span>
)}
@@ -741,6 +784,37 @@ export default function Usuarios() {
</DialogContent>
</Dialog>
<AlertDialog open={isDeleteOpen} onOpenChange={setIsDeleteOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Excluir usuário</AlertDialogTitle>
<AlertDialogDescription className="space-y-2">
<span className="block">
Deseja excluir o usuário <strong>{getNomeUsuario(selectedUsuario)}</strong> (
{getEmailUsuario(selectedUsuario)})?
</span>
<span className="block">
O usuário deixará de aparecer na lista e não poderá mais acessar o sistema. Se houver parceiro
vinculado, apenas o vínculo será removido os dados do parceiro e dos fechamentos permanecem
intactos.
</span>
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel className="hover:bg-muted/60 hover:text-foreground" disabled={deleting}>
Cancelar
</AlertDialogCancel>
<AlertDialogAction
onClick={handleDeleteUsuario}
disabled={deleting || !selectedUsuario || me?.id === selectedUsuario?.id}
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
>
{deleting ? "Excluindo..." : "Excluir usuário"}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={isToggleOpen} onOpenChange={setIsToggleOpen}>
<AlertDialogContent>
<AlertDialogHeader>
+3
View File
@@ -1,5 +1,8 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
/** Operações de IA (imagem, áudio, transcrição) podem levar vários minutos. */
export const LONG_RUNNING_REQUEST_TIMEOUT_MS = 300_000;
class ApiService {
private readonly axiosInstance: AxiosInstance;
private readonly apiKey: string;
+3 -2
View File
@@ -1,5 +1,5 @@
import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
import { apiService } from './api';
import { apiService, LONG_RUNNING_REQUEST_TIMEOUT_MS } from './api';
export type VoiceType = 'alloy' | 'echo' | 'fable' | 'nova' | 'onyx' | 'shimmer';
@@ -135,7 +135,8 @@ class AudioGenerationService {
try {
const response = await apiService.post<AudioGenerationResponse>(
this.AUDIO_GENERATION_ENDPOINT,
{ estabelecimento_id: estabId, user_email: email, message, voice }
{ estabelecimento_id: estabId, user_email: email, message, voice },
{ timeout: LONG_RUNNING_REQUEST_TIMEOUT_MS }
);
return response.data;
} catch (error: unknown) {
+13
View File
@@ -303,6 +303,19 @@ class FechamentoBancoPontosService {
throw error instanceof Error ? error : new Error("Erro ao exportar extrato do banco de pontos.");
}
}
async excluirTransacao(transacaoId: string, motivo: string): Promise<void> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
await axios.delete(`${baseUrl}/banco-pontos/transacoes/${transacaoId}`, {
headers,
data: { motivo: motivo.trim() },
});
} catch (error) {
this.handleAxiosError(error, "Erro ao excluir lançamento do banco de pontos.");
}
}
}
export const fechamentoBancoPontosService = new FechamentoBancoPontosService();
@@ -22,7 +22,7 @@ export type Capabilities = {
tarefaEditarRevisao: boolean;
tarefaExcluirManual: boolean;
};
bancoPontos: { acessoAdmin: boolean };
bancoPontos: { acessoAdmin: boolean; excluirTransacao: boolean };
parceiros: {
listar: boolean;
criar: boolean;
@@ -31,7 +31,7 @@ export type Capabilities = {
verFator: boolean;
editarFator: boolean;
};
usuarios: { listar: boolean; criarEditarToggle: boolean };
usuarios: { listar: boolean; criarEditarToggle: boolean; excluir: boolean };
configuracoes: { salvar: boolean };
};
@@ -57,7 +57,7 @@ function adminCapabilities(): Capabilities {
tarefaEditarRevisao: true,
tarefaExcluirManual: true,
},
bancoPontos: { acessoAdmin: true },
bancoPontos: { acessoAdmin: true, excluirTransacao: true },
parceiros: {
listar: true,
criar: true,
@@ -66,7 +66,7 @@ function adminCapabilities(): Capabilities {
verFator: true,
editarFator: true,
},
usuarios: { listar: true, criarEditarToggle: true },
usuarios: { listar: true, criarEditarToggle: true, excluir: true },
configuracoes: { salvar: true },
};
}
@@ -93,7 +93,7 @@ function parceiroCapabilities(): Capabilities {
tarefaEditarRevisao: false,
tarefaExcluirManual: false,
},
bancoPontos: { acessoAdmin: false },
bancoPontos: { acessoAdmin: false, excluirTransacao: false },
parceiros: {
listar: false,
criar: false,
@@ -102,7 +102,7 @@ function parceiroCapabilities(): Capabilities {
verFator: false,
editarFator: false,
},
usuarios: { listar: false, criarEditarToggle: false },
usuarios: { listar: false, criarEditarToggle: false, excluir: false },
configuracoes: { salvar: false },
};
}
@@ -129,7 +129,7 @@ function supervisorCapabilities(): Capabilities {
tarefaEditarRevisao: false,
tarefaExcluirManual: false,
},
bancoPontos: { acessoAdmin: true },
bancoPontos: { acessoAdmin: true, excluirTransacao: false },
parceiros: {
listar: true,
criar: true,
@@ -138,7 +138,7 @@ function supervisorCapabilities(): Capabilities {
verFator: false,
editarFator: false,
},
usuarios: { listar: true, criarEditarToggle: false },
usuarios: { listar: true, criarEditarToggle: false, excluir: false },
configuracoes: { salvar: false },
};
}
+10
View File
@@ -127,6 +127,16 @@ class FechamentoUsuariosService {
this.handleAxiosError(error, "Erro ao atualizar status do usuário.");
}
}
async excluirUsuario(id: string): Promise<void> {
try {
const headers = await buildCommanderHeaders();
const baseUrl = resolveCommanderBaseUrl();
await axios.delete(`${baseUrl}/usuarios/${id}`, { headers });
} catch (error) {
this.handleAxiosError(error, "Erro ao excluir usuário.");
}
}
}
export const fechamentoUsuariosService = new FechamentoUsuariosService();
+3 -2
View File
@@ -1,5 +1,5 @@
import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
import { apiService } from './api';
import { apiService, LONG_RUNNING_REQUEST_TIMEOUT_MS } from './api';
export type ImageSize = '1024x1024' | '1024x1792' | '1792x1024';
@@ -127,7 +127,8 @@ class ImageGenerationService {
try {
const response = await apiService.post<ImageGenerationResponse>(
this.IMAGE_GENERATION_ENDPOINT,
{ estabelecimento_id: estabId, user_email: email, description, size }
{ estabelecimento_id: estabId, user_email: email, description, size },
{ timeout: LONG_RUNNING_REQUEST_TIMEOUT_MS }
);
if (!response.data.success) {
+3 -2
View File
@@ -1,5 +1,5 @@
import { GlobalFunctions, TransferAreaProperties } from '@/GlobalFunctions';
import { apiService } from './api';
import { apiService, LONG_RUNNING_REQUEST_TIMEOUT_MS } from './api';
export interface TranscriptionResponse {
success: boolean;
@@ -82,7 +82,8 @@ class TranscriptionService {
try {
const response = await apiService.postFormData<TranscriptionResponse>(
this.TRANSCRIPTION_ENDPOINT,
formData
formData,
{ timeout: LONG_RUNNING_REQUEST_TIMEOUT_MS }
);
return response.data;
} catch (error: unknown) {