refactor(): Ajustes diversos na tela de pedidos

This commit is contained in:
Keven 2025-12-15 19:45:29 -03:00
parent 32937c9501
commit fca1d0c293
16 changed files with 206 additions and 233 deletions

View file

@ -1,5 +1,5 @@
import { FingerprintIcon, WebcamIcon } from 'lucide-react';
import { memo, useCallback, useMemo, useState } from 'react';
import { WebcamIcon } from 'lucide-react';
import { memo, useCallback, useState } from 'react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
@ -11,12 +11,11 @@ import {
ItemMedia,
ItemTitle,
} from '@/components/ui/item';
import TPessoaTableFormSubviewInterface from '@/packages/administrativo/interfaces/TPessoa/TPessoaTableFormSubviewInterface';
import TPessoaCartaoForm from '@/packages/servicos/components/TPessoaCartao/TPessoaCartaoForm';
import GetNameInitials from '@/shared/actions/text/GetNameInitials';
import BiometriaButton from '@/shared/components/biometria/BiometriaButton';
import WebCamDialog from '@/shared/components/webcam/WebCamDialog';
import { useFingerTechCaptureHook } from '@/shared/hooks/FingerTech/useFingerTechCaptureHook';
import TPessoaTableFormSubviewInterface from '@/packages/administrativo/interfaces/TPessoa/TPessoaTableFormSubviewInterface';
function TPessoaTableFormSubview({
item_index,
@ -24,28 +23,17 @@ function TPessoaTableFormSubview({
params,
form,
}: TPessoaTableFormSubviewInterface) {
const [isWebCamOpenDialog, setIsWebCamOpenDialog] = useState(false);
const { base64, captureFingerTech } = useFingerTechCaptureHook();
const [statusBiometria, setStatusBiometria] = useState(0)
// Chama o leitor biométrico
const handleBiometria = useCallback(() => {
console.log(captureFingerTech());
}, []);
const handleCaptureSuccess = useCallback(async (base64: string) => {
const biometriaButtonClass = useMemo(() => {
const status = 1 as number; // força tipo number
console.log(base64)
switch (status) {
case 0:
return 'bg-amber-100 text-amber-700 border border-amber-300 hover:bg-amber-200 hover:text-amber-800';
case 1:
return 'bg-green-100 text-green-700 border border-green-300 hover:bg-green-200 hover:text-green-800';
case 2:
return 'bg-red-100 text-red-700 border border-red-300 hover:bg-red-200 hover:text-red-800';
default:
return '';
}
}, []);
setStatusBiometria(1)
}, [])
return (
<div>
@ -69,18 +57,10 @@ function TPessoaTableFormSubview({
</ItemContent>
<ItemActions>
{data?.servico?.requer_biometria === 'S' && (
<Button
type="button"
size="icon-lg"
variant="outline"
className={`cursor-pointer rounded-full ${biometriaButtonClass}`}
aria-label="Capturar biometria"
onClick={() => {
handleBiometria();
}}
>
<FingerprintIcon />
</Button>
<BiometriaButton
status={statusBiometria}
onCaptureSuccess={handleCaptureSuccess}
/>
)}
{data?.servico?.requer_biometria && (
<Button
@ -104,7 +84,7 @@ function TPessoaTableFormSubview({
onClose={() => {
setIsWebCamOpenDialog(false);
}}
onSave={() => {}}
onSave={() => { }}
key={item_index}
/>
)}

View file

@ -26,6 +26,7 @@ export default function TServicoItemPedidoList({
items,
openConfirmDialog,
}: TServicoItemPedidoListInterface) {
const { setResponse } = useResponse();
const { cancelarTServicoItemPedido } = useTServicoItemPedidoCancelarHook();
const { ativarTServicoItemPedido } = useTServicoItemPedidoAtivarHook();
@ -37,6 +38,7 @@ export default function TServicoItemPedidoList({
}, [items]);
const handleSituacaoTServicoItemPedido = useCallback(
async (item: TServicoItemPedidoInterface) => {
const servicoItemPedido: TServicoItemPedidoInterface = {
servico_itempedido_id: item.servico_itempedido_id,
@ -163,9 +165,8 @@ export default function TServicoItemPedidoList({
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
className={`cursor-pointer ${
isCancelado ? 'text-green-600' : 'text-destructive'
}`}
className={`cursor-pointer ${isCancelado ? 'text-green-600' : 'text-destructive'
}`}
onClick={() =>
openConfirmDialog({
title: confirmTitle,

View file

@ -1,83 +0,0 @@
'use client';
import { Badge } from '@/components/ui/badge';
import { Card, CardContent, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Separator } from '@/components/ui/separator';
interface PedidoItem {
id: number;
descricao: string;
valor: number;
}
interface PedidoResumoProps {
numeroPedido: number;
dataPedido: string;
itens: PedidoItem[];
}
/**
* Componente de resumo do pedido
* Exibe número do pedido, data formatada, lista de itens e total.
*/
export default function TServicoItemPedidoResumo({
numeroPedido,
dataPedido,
itens,
}: PedidoResumoProps) {
const total = itens.reduce((acc, item) => acc + item.valor, 0);
return (
<Card className="flex w-full max-w-md flex-col rounded-xl border border-slate-200 bg-white/80 shadow-sm backdrop-blur-sm">
{/* Cabeçalho */}
<CardHeader className="flex flex-row items-center justify-between border-b border-slate-100 pb-3">
<CardTitle className="text-sm font-semibold text-slate-800">
Pedido {numeroPedido}
</CardTitle>
<Badge
variant="outline"
className="border-none bg-[#F36F28]/10 text-xs font-medium text-[#F36F28]"
>
{dataPedido}
</Badge>
</CardHeader>
{/* Conteúdo - Lista de Itens */}
<CardContent className="flex-1 px-4 py-3">
<ScrollArea className="h-[220px] pr-2 sm:h-[260px]">
{itens.length > 0 ? (
itens.map((item) => (
<div
key={item.id}
className="flex items-center justify-between border-b border-slate-100 py-2 last:border-0"
>
<span className="max-w-[65%] truncate text-sm text-slate-700">
{item.descricao}
</span>
<span className="text-sm font-semibold text-slate-900">
R$ {item.valor.toFixed(2).replace('.', ',')}
</span>
</div>
))
) : (
<div className="flex h-full items-center justify-center text-sm text-slate-400">
Nenhum item adicionado
</div>
)}
</ScrollArea>
</CardContent>
<Separator className="my-1" />
{/* Rodapé - Total */}
<CardFooter className="flex items-center justify-between rounded-b-xl bg-slate-50 px-4 py-3">
<span className="text-sm font-semibold text-slate-800">Total</span>
<span className="text-lg font-bold text-[#F36F28]">
R$ {total.toFixed(2).replace('.', ',')}
</span>
</CardFooter>
</Card>
);
}

View file

@ -24,6 +24,7 @@ import { useResponse } from '@/shared/components/response/ResponseContext';
export default function TServicoPedidoDetails({
servico_pedido_id,
}: TServicoPedidoDetailsInterface) {
const { setResponse } = useResponse();
const { ativarTServicoPedido } = useTServicoPedidoAtivarHook();
const { cancelarTServicoPedido } = useTServicoPedidoCancelarHook();
@ -83,8 +84,8 @@ export default function TServicoPedidoDetails({
message: '',
confirmText: 'Confirmar',
cancelText: 'Cancelar',
onConfirm: () => {},
onCancel: () => {},
onConfirm: () => { },
onCancel: () => { },
});
// Função utilitária para abrir o dialog dinamicamente
@ -141,10 +142,10 @@ export default function TServicoPedidoDetails({
const actionIcon = isCancelado ? <RotateCcwIcon /> : <BookmarkX />;
return (
<div className="relative container mx-auto flex h-full flex-col px-2 py-2 sm:px-4 sm:py-4 md:px-6">
<div>
<h3 className="mb-4 text-4xl font-bold">Pedido: #{TServicoPedido?.servico_pedido_id}</h3>
{/* Main */}
<div className="container mx-auto h-full">
<div className="mx-auto h-full">
<div className="flex flex-col gap-4 lg:flex-row">
{/* Left column */}
<div className="flex flex-auto flex-col gap-4">

View file

@ -1,45 +1,29 @@
'use client';
import * as React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import { useTServicoPedidoDetailsPagamentoController } from '@/packages/servicos/hooks/TServicoPedido/useTServicoPedidoDetailsControllerHook';
import TServicoPedidoDetailsPagamentoInterface from '@/packages/servicos/interfaces/TServicoPedido/TServicoPedidoDetailsPagamentoInterface';
import FormatMoney from '@/shared/actions/money/FormatMoney';
import { ServicosPedidosSituacoesBadge } from '@/shared/components/servicosPedidosSituacoes/ServicosPedidosSituacoesBadge';
import { ServicosPedidosSituacoesEnum } from '@/shared/enums/ServicosPedidosSituacoesEnum';
import TServicoPedidoDetailsPagamentoInterface from '../../interfaces/TServicoPedido/TServicoPedidoDetailsPagamentoInterface';
export default function TServicoPedidoDetailsPagamento({
situacao,
items,
}: TServicoPedidoDetailsPagamentoInterface) {
// Somas por tipo de valor
const { emolumento, taxa_judiciaria, valor_iss, fundesp } = React.useMemo(() => {
return (items ?? []).reduce(
(acc, item) => {
if (item.situacao === 'F') {
acc.emolumento += item.emolumento;
acc.taxa_judiciaria += item.taxa_judiciaria;
acc.valor_iss += item.valor_iss;
acc.fundesp += item.fundesp;
}
return acc;
},
{ emolumento: 0, taxa_judiciaria: 0, valor_iss: 0, fundesp: 0 },
);
}, [items]);
// Total exibido = soma dos quatro componentes
const total = emolumento + taxa_judiciaria + valor_iss + fundesp;
const { emolumento, taxa_judiciaria, valor_iss, fundesp, total } = useTServicoPedidoDetailsPagamentoController(items);
return (
<Card className="card-border">
<CardHeader>
<CardTitle className="text-2xl font-semibold">
Pagamento{' '}
Pagamento -
<ServicosPedidosSituacoesBadge
situacao={situacao as 'A' | 'C' | 'F' | null | undefined}
situacao={situacao as ServicosPedidosSituacoesEnum}
/>
</CardTitle>
</CardHeader>

View file

@ -0,0 +1,37 @@
'use client';
import * as React from 'react';
import TServicoPedidoDetailsPagamentoInterface from '@/packages/servicos/interfaces/TServicoPedido/TServicoPedidoDetailsPagamentoInterface';
export const useTServicoPedidoDetailsPagamentoController = (
items: TServicoPedidoDetailsPagamentoInterface['items']
) => {
// Somas por tipo de valor
const { emolumento, taxa_judiciaria, valor_iss, fundesp } = React.useMemo(() => {
return (items ?? []).reduce(
(acc, item) => {
if (item.situacao === 'F') {
acc.emolumento += item.emolumento;
acc.taxa_judiciaria += item.taxa_judiciaria;
acc.valor_iss += item.valor_iss;
acc.fundesp += item.fundesp;
}
return acc;
},
{ emolumento: 0, taxa_judiciaria: 0, valor_iss: 0, fundesp: 0 },
);
}, [items]);
// Total exibido = soma dos quatro componentes
const total = emolumento + taxa_judiciaria + valor_iss + fundesp;
return {
emolumento,
taxa_judiciaria,
valor_iss,
fundesp,
total,
};
};

View file

@ -23,7 +23,7 @@ export function useTServicoPedidoFormHook(defaults?: Partial<TServicoPedidoFormV
pagador_cpfcnpj: '',
valor_pedido: 0,
valor_pago: 0,
situacao: ServicosPedidosSituacoesEnum.F,
situacao: ServicosPedidosSituacoesEnum.FECHADO,
tipo_pagamento: null,
...defaults,
},

View file

@ -0,0 +1,48 @@
import { FingerprintIcon } from 'lucide-react';
import { memo, useCallback, useMemo } from 'react';
import { Button } from '@/components/ui/button';
import { useFingerTechCaptureHook } from '@/shared/hooks/FingerTech/useFingerTechCaptureHook';
import BiometriaButtonInterface from './interfaces/BiometriaButtonInterface';
const BiometriaButton = ({ status = 0, onCaptureSuccess }: BiometriaButtonInterface) => {
const { base64, captureFingerTech } = useFingerTechCaptureHook();
const handleCapture = useCallback(async () => {
await captureFingerTech();
onCaptureSuccess('2123123')
}, [captureFingerTech]);
const statusClasses = useMemo(() => {
switch (status) {
case 0: // Pendente/Aguardando (Amarelo)
return 'bg-amber-100 text-amber-700 border-amber-300 hover:bg-amber-200 hover:text-amber-800';
case 1: // Sucesso (Verde)
return 'bg-green-100 text-green-700 border-green-300 hover:bg-green-200 hover:text-green-800';
case 2: // Erro (Vermelho)
return 'bg-red-100 text-red-700 border-red-300 hover:bg-red-200 hover:text-red-800';
default:
return '';
}
}, [status]);
return (
<Button
type="button"
size="icon-lg"
variant="outline"
className={`cursor-pointer rounded-full transition-colors ${statusClasses}`}
aria-label="Capturar biometria"
onClick={handleCapture}
>
<FingerprintIcon size={24} />
</Button>
);
};
export default memo(BiometriaButton);

View file

@ -0,0 +1,4 @@
export default interface BiometriaButtonInterface {
status?: number; // 0: pendente, 1: sucesso, 2: erro
onCaptureSuccess: (base64: string) => void;
}

View file

@ -1,100 +1,31 @@
'use client';
import { AlertCircleIcon, BadgeCheckIcon, CheckIcon } from 'lucide-react';
import * as React from 'react';
import { Badge } from '@/components/ui/badge';
import { cn } from '@/lib/utils';
import { ServicosPedidosSituacoesEnum } from '@/shared/enums/ServicosPedidosSituacoesEnum';
import ServicosPedidosSituacoesBadgeInterface from '@/shared/components/servicosPedidosSituacoes/interfaces/ServicosPedidosSituacoesBadgeInterface';
type Situacao = keyof typeof ServicosPedidosSituacoesEnum;
type Props = {
situacao?: Situacao | null; // 'A' | 'F' | 'C'
showLabel?: boolean; // exibir texto? (default: true)
compact?: boolean; // menor altura/tamanho (default: false)
className?: string; // classes extras
};
type Cfg = {
label: string;
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
bgClass: string; // fundo "subtle"
textClass: string; // cor do texto/ícone
};
const CONFIG: Record<Situacao, Cfg> = {
A: {
label: ServicosPedidosSituacoesEnum.A, // Aberto
icon: AlertCircleIcon,
bgClass: 'bg-blue-50 dark:bg-blue-900/20',
textClass: 'text-blue-700 dark:text-blue-300',
},
F: {
label: ServicosPedidosSituacoesEnum.F, // Fechado
icon: BadgeCheckIcon,
bgClass: 'bg-emerald-50 dark:bg-emerald-900/20',
textClass: 'text-emerald-700 dark:text-emerald-300',
},
C: {
label: ServicosPedidosSituacoesEnum.C, // Cancelado
icon: CheckIcon, // troque por XIcon se preferir
bgClass: 'bg-red-50 dark:bg-red-900/20',
textClass: 'text-red-700 dark:text-red-300',
},
};
import { useServicosPedidosSituacoesBadgeControllerHook } from './hooks/useServicosPedidosSituacoesBadgeControllerHook';
export function ServicosPedidosSituacoesBadge({
situacao,
showLabel = true,
compact = false,
className,
}: Props) {
if (!situacao || !(situacao in CONFIG)) {
return (
<Badge
className={cn(
// base semelhante ao exemplo: tag + borda + textos neutros
'border border-gray-100 text-gray-900 dark:border-gray-700 dark:text-gray-50',
'gap-1.5 rounded-md bg-gray-50 dark:bg-gray-800/50',
compact ? 'h-5 px-2 text-xs' : 'h-6 px-2.5 text-xs',
className,
)}
variant="secondary"
>
<AlertCircleIcon
className={cn(compact ? 'h-3.5 w-3.5' : 'h-4 w-4', 'text-gray-500 dark:text-gray-300')}
/>
{showLabel ? (
<span className="font-semibold text-gray-700 capitalize dark:text-gray-200">
Indefinido
</span>
) : (
<span className="sr-only">Indefinido</span>
)}
</Badge>
);
}
}: ServicosPedidosSituacoesBadgeInterface) {
const { label, icon: Icon, bgClass, textClass } = CONFIG[situacao];
const { label, Icon, color, bg, size, iconSize } = useServicosPedidosSituacoesBadgeControllerHook(situacao, compact);
return (
<Badge
className={cn(
// wrapper estilo "<div class='tag ... bg-success-subtle ...'>"
'gap-1.5 rounded-md border border-gray-100 text-gray-900 dark:border-gray-700 dark:text-gray-50',
bgClass,
compact ? 'h-5 px-2 text-[11px]' : 'h-6 px-2.5 text-xs',
'gap-1.5 rounded-md border border-gray-100 dark:border-gray-700',
bg,
size,
className,
)}
>
{/* Ícone e label com cor semântica, como "<span class='text-success'>" */}
<Icon className={cn(compact ? 'h-3.5 w-3.5' : 'h-4 w-4', textClass)} />
{showLabel ? (
<span className={cn('font-semibold capitalize', textClass)}>{label}</span>
) : (
<span className="sr-only">{label}</span>
)}
<Icon className={cn(iconSize, color)} />
{showLabel && <span className={cn('font-semibold capitalize', color)}>{label}</span>}
</Badge>
);
}

View file

@ -0,0 +1,24 @@
import { AlertCircleIcon, BadgeCheckIcon, XIcon } from "lucide-react";
import { ServicosPedidosSituacoesEnum } from "@/shared/enums/ServicosPedidosSituacoesEnum";
export const ServicosPedidosSituacoesBadgeMap = {
[ServicosPedidosSituacoesEnum.ABERTO]: {
label: 'Aberto',
icon: AlertCircleIcon,
color: 'text-blue-700 dark:text-blue-300',
bg: 'bg-blue-50 dark:bg-blue-900/20',
},
[ServicosPedidosSituacoesEnum.FECHADO]: {
label: 'Fechado',
icon: BadgeCheckIcon,
color: 'text-emerald-700 dark:text-emerald-300',
bg: 'bg-emerald-50 dark:bg-emerald-900/20',
},
[ServicosPedidosSituacoesEnum.CANCELADO]: {
label: 'Cancelado',
icon: XIcon,
color: 'text-red-700 dark:text-red-300',
bg: 'bg-red-50 dark:bg-red-900/20',
},
} as const;

View file

@ -0,0 +1,35 @@
'use client';
import { AlertCircleIcon } from 'lucide-react';
import { ServicosPedidosSituacoesEnum } from '@/shared/enums/ServicosPedidosSituacoesEnum';
import { ServicosPedidosSituacoesBadgeMap } from '../enums/ServicosPedidosSituacoesBadgeMap';
export const useServicosPedidosSituacoesBadgeControllerHook = (
situacao?: ServicosPedidosSituacoesEnum | null,
compact?: boolean,
) => {
const config =
situacao && ServicosPedidosSituacoesBadgeMap[situacao]
? ServicosPedidosSituacoesBadgeMap[situacao]
: {
label: 'Indefinido',
icon: AlertCircleIcon,
color: 'text-gray-600 dark:text-gray-200',
bg: 'bg-gray-50 dark:bg-gray-800/50',
};
const Icon = config.icon;
const size = compact ? 'h-5 px-2 text-[11px]' : 'h-6 px-2.5 text-xs';
const iconSize = compact ? 'h-3.5 w-3.5' : 'h-4 w-4';
return {
label: config.label,
Icon,
color: config.color,
bg: config.bg,
size,
iconSize,
};
};

View file

@ -0,0 +1,8 @@
import { ServicosPedidosSituacoesEnum } from "@/shared/enums/ServicosPedidosSituacoesEnum";
export default interface ServicosPedidosSituacoesBadgeInterface {
situacao?: ServicosPedidosSituacoesEnum | null;
showLabel?: boolean;
compact?: boolean;
className?: string;
}

View file

@ -13,6 +13,8 @@ async function executeFingerTechCaptureData() {
method: 'GET',
});
console.log(response)
return await response.text();
}

View file

@ -1,5 +1,5 @@
export enum ServicosPedidosSituacoesEnum {
A = 'Aberto',
F = 'Fechado',
C = 'Cancelado',
ABERTO = 'A',
FECHADO = 'F',
CANCELADO = 'C',
}

View file

@ -5,16 +5,17 @@ import { useState } from 'react';
import { FingerTechCaptureService } from '@/shared/services/FingerTech/FingerTechCaptureService';
export const useFingerTechCaptureHook = () => {
const [base64, setBase64] = useState<string>('');
const captureFingerTech = async () => {
const response = await FingerTechCaptureService();
const base64Data = response?.data ?? '';
setBase64(base64Data);
setBase64('123123');
return response;
};
return {