[MVPTN-37] feat(Subview): Ajusta diversos pontos da aplicação para trabalhar com subviews sem sobrecarga

This commit is contained in:
Keven 2025-11-17 14:46:39 -03:00
parent b2e0d50dd6
commit bc2c2ef3dd
32 changed files with 728 additions and 396 deletions

166
package-lock.json generated
View file

@ -23,7 +23,7 @@
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
@ -1436,6 +1436,24 @@
}
}
},
"node_modules/@radix-ui/react-alert-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-arrow": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.7.tgz",
@ -1572,6 +1590,24 @@
}
}
},
"node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@ -1638,6 +1674,24 @@
}
}
},
"node_modules/@radix-ui/react-dialog/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
@ -1830,6 +1884,24 @@
}
}
},
"node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popover": {
"version": "1.1.15",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.15.tgz",
@ -1867,6 +1939,24 @@
}
}
},
"node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-popper": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.8.tgz",
@ -1970,6 +2060,24 @@
}
}
},
"node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-progress": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
@ -2131,6 +2239,24 @@
}
}
},
"node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-separator": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.8.tgz",
@ -2177,7 +2303,7 @@
}
}
},
"node_modules/@radix-ui/react-separator/node_modules/@radix-ui/react-slot": {
"node_modules/@radix-ui/react-slot": {
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.4.tgz",
"integrity": "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==",
@ -2195,24 +2321,6 @@
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-switch": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.6.tgz",
@ -2306,6 +2414,24 @@
}
}
},
"node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
"integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",

View file

@ -25,7 +25,7 @@
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",

BIN
public/sounds/success.mp3 Normal file

Binary file not shown.

View file

@ -0,0 +1,83 @@
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",
{
variants: {
orientation: {
horizontal:
"[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
vertical:
"flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
},
},
defaultVariants: {
orientation: "horizontal",
},
}
)
function ButtonGroup({
className,
orientation,
...props
}: React.ComponentProps<"div"> & VariantProps<typeof buttonGroupVariants>) {
return (
<div
role="group"
data-slot="button-group"
data-orientation={orientation}
className={cn(buttonGroupVariants({ orientation }), className)}
{...props}
/>
)
}
function ButtonGroupText({
className,
asChild = false,
...props
}: React.ComponentProps<"div"> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot : "div"
return (
<Comp
className={cn(
"bg-muted flex items-center gap-2 rounded-md border px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function ButtonGroupSeparator({
className,
orientation = "vertical",
...props
}: React.ComponentProps<typeof Separator>) {
return (
<Separator
data-slot="button-group-separator"
orientation={orientation}
className={cn(
"bg-input relative !m-0 self-stretch data-[orientation=vertical]:h-auto",
className
)}
{...props}
/>
)
}
export {
ButtonGroup,
ButtonGroupSeparator,
ButtonGroupText,
buttonGroupVariants,
}

View file

@ -1,131 +1,126 @@
import { FingerprintIcon, WebcamIcon } from 'lucide-react';
import {
memo,
useCallback,
useMemo,
useState
} from 'react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import { Button } from '@/components/ui/button';
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle
} from '@/components/ui/item';
import TPessoaCartaoForm from '@/packages/servicos/components/TPessoaCartao/TPessoaCartaoForm';
import GetNameInitials from '@/shared/actions/text/GetNameInitials';
import WebCamDialog from '@/shared/components/webcam/WebCamDialog';
import { useFingerTechIndexHook } from '@/shared/hooks/FingerTech/useFingerTechIndexHook';
import { FingerprintIcon, WebcamIcon } from "lucide-react";
import { useCallback, useEffect, useState } from "react";
function TPessoaTableFormSubview({
item_index,
data,
params,
form,
}: any) {
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Item, ItemActions, ItemContent, ItemDescription, 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 WebCamDialog from "@/shared/components/webcam/WebCamDialog";
import { useFingerTechIndexHook } from "@/shared/hooks/FingerTech/useFingerTechIndexHook";
const [isWebCamOpenDialog, setIsWebCamOpenDialog] = useState(false)
const { base64, captureFingerTech } = useFingerTechIndexHook();
const handleBiometria = useCallback(() => {
export default function TPessoaTableFormSubview({ params, servico, selectedTPessoa, form, index }: TPessoaTableFormSubviewInterface) {
console.log(captureFingerTech())
const [isOpenWebcamDialog, setIsOpenWebcamDialog] = useState(false)
const [base64, setBase64] = useState('')
const [statusBiometria, setStatusBiometria] = useState(0)
const [classBiometriaButton, setClassBiometriaButton] = useState('')
const { base64: base64Biometria, captureFingerTech } = useFingerTechIndexHook()
})
const handleCapture = (data: string) => {
setBase64(data)
};
const handleCaptureBiometria = useCallback(async () => {
const response = await captureFingerTech()
if (response) {
setStatusBiometria(1)
}
}, [])
const handleBiometriaButton = useCallback(async () => {
switch (statusBiometria) {
// Define a classe do botão de biometria com base no status, sem estado extra
const biometriaButtonClass = useMemo(() => {
switch (1) {
case 0:
// Amarelo (aviso)
setClassBiometriaButton('bg-amber-100 text-amber-700 border border-amber-300 hover:bg-amber-200 hover:text-amber-800');
break;
return 'bg-amber-100 text-amber-700 border border-amber-300 hover:bg-amber-200 hover:text-amber-800';
case 1:
// Verde discreto
setClassBiometriaButton('bg-green-100 text-green-700 border border-green-300 hover:bg-green-200 hover:text-green-800');
break;
return 'bg-green-100 text-green-700 border border-green-300 hover:bg-green-200 hover:text-green-800';
case 2:
// Vermelho (erro)
setClassBiometriaButton('bg-red-100 text-red-700 border border-red-300 hover:bg-red-200 hover:text-red-800');
break;
return 'bg-red-100 text-red-700 border border-red-300 hover:bg-red-200 hover:text-red-800';
default:
break;
return '';
}
}, [statusBiometria])
useEffect(() => {
handleBiometriaButton()
}, [statusBiometria])
}, []);
return (
<div className="p-5" key={index}>
<div>
<Item variant="outline">
<ItemMedia>
<Avatar className="size-10">
<AvatarImage src={`data:image/png;base64,${base64}`} />
<AvatarImage src={``} />
<AvatarFallback>
{GetNameInitials(selectedTPessoa.nome)}
{GetNameInitials(data.pessoa?.nome)}
</AvatarFallback>
</Avatar>
</ItemMedia>
<ItemContent>
<ItemTitle>
{selectedTPessoa.cpf_cnpj} - {selectedTPessoa.nome}
{data.pessoa?.cpf_cnpj} - {data.pessoa?.nome}
</ItemTitle>
<ItemDescription>
{selectedTPessoa.email ? selectedTPessoa.email : 'Email não informado'}
{data.pessoa?.email || 'Email não informado'}
</ItemDescription>
{/* Verifica se existe formulário de acordo com parâmetros */}
{params.map((param) => (
// Verifica se dfeve aparecer os dados do cartão
Number(param.valor) == Number(servico.servico_tipo_id) && (
{params
.filter((param) => Number(param.valor) === data.servico.servico_tipo_id)
.map((param) => (
<TPessoaCartaoForm
index={index}
form={form}
index={item_index}
key={param.config_id}
/>
)
))}
))}
</ItemContent>
<ItemActions>
{servico.requer_biometria === 'S' && (
{data.servico.requer_biometria === 'S' && (
<Button
type="button"
size="icon-lg"
variant="outline"
className={`rounded-full cursor-pointer ` + classBiometriaButton}
aria-label="Invite"
onClick={handleCaptureBiometria}
className={`rounded-full cursor-pointer ${biometriaButtonClass}`}
aria-label="Capturar biometria"
onClick={() => { handleBiometria() }}
>
<FingerprintIcon />
</Button>
)}
{servico.requer_biometria === 'S' && (
{data.servico.requer_biometria && (
<Button
type="button"
size="icon-lg"
variant="outline"
className="rounded-full cursor-pointer"
aria-label="Invite"
onClick={() => { setIsOpenWebcamDialog(true) }}
aria-label="Capturar imagem"
onClick={() => { setIsWebCamOpenDialog(true) }}
>
<WebcamIcon />
</Button>
)}
</ItemActions>
</Item>
{isOpenWebcamDialog && (
{isWebCamOpenDialog && (
<WebCamDialog
isOpen={isOpenWebcamDialog}
onClose={() => { setIsOpenWebcamDialog(false) }}
onSave={handleCapture}
isOpen={isWebCamOpenDialog}
onClose={() => { setIsWebCamOpenDialog(false) }}
onSave={() => { }}
key={1}
/>
)}
</div>
);
}
// Memo para evitar re-renderizações desnecessárias da subview
export default memo(TPessoaTableFormSubview);

View file

@ -6,7 +6,7 @@ import { withClientErrorHandler } from "@/shared/actions/withClientErrorHandler/
async function executeGCalculoServicoService(payload: GCalculoServicoInterface, data) {
const response = await GCalculoServico(payload);
const item = PrepareTServicoItemPedidoCalculoResponse(response.data, data)
const item = PrepareTServicoItemPedidoCalculoResponse(response, data)
return item;
}

View file

@ -1,60 +1,37 @@
import TServicoItemPedidoAddInterface from "../../interfaces/TServicoItemPedido/TServicoItemPedidoAddInterface";
import TServicoItemPedidoAddResponseInterface from "../../interfaces/TServicoItemPedido/TServicoItemPedidoAddResponseInterface";
import TServicoItemPedidoAddInterface from "@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddInterface";
import TServicoItemPedidoAddResponseInterface from "@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddResponseInterface";
export function PrepareTServicoItemPedidoCalculoResponse(
result: any,
data: TServicoItemPedidoAddInterface
) {
// Valida resposta básica do cálculo de emolumento
if (!result) {
if (result.status == 404 || result.status == 400) {
return {
status: 400,
message: "Resposta inválida ao calcular emolumento.",
'status': result.status,
'message': result.detail
};
}
// Emolumento obrigatório
if (!result.emolumento_id) {
return {
status: 400,
message: "Emolumento não informado na resposta.",
};
}
// Valida campos numéricos da resposta (não podem ser NaN)
const numericFields = [
"valor_total",
"valor_emolumento",
"valor_fundos",
"taxa_judiciaria",
"valor_iss",
] as const;
for (const field of numericFields) {
const value = result[field];
if (value !== null && value !== undefined && isNaN(Number(value))) {
return {
status: 400,
message: `Valor numérico inválido em '${field}'.`,
};
}
}
const item: TServicoItemPedidoAddResponseInterface = {
emolumento_id: result.emolumento_id,
emolumento_item_id: result.emolumento_item_id ?? null,
emolumento_id: result.data.emolumento_id,
emolumento_item_id: result.data.emolumento_item_id ?? null,
servico_tipo_id: data.servico_tipo.servico_tipo_id ?? 0,
tipo_item: data.servico_tipo.tipo_item ?? "",
descricao: data.servico_tipo.descricao ?? "",
tabela: data.servico_tipo?.descricao ?? "",
situacao: "F",
qtd: 1,
valor: result.valor_total ?? 0,
emolumento: result.valor_emolumento ?? 0,
fundesp: result.valor_fundos ?? 0,
taxa_judiciaria: result.taxa_judiciaria ?? 0,
valor_iss: result.valor_iss ?? 0,
valor: result.data.valor_total ?? 0,
emolumento: result.data.valor_emolumento ?? 0,
fundesp: result.data.valor_fundos ?? 0,
taxa_judiciaria: result.data.taxa_judiciaria ?? 0,
valor_iss: result.data.valor_iss ?? 0,
pessoa_id: data.pessoa.pessoa_id ?? null,
subview: {
servico: data.servico_tipo,
pessoa: data.pessoa,
}
};
return item;

View file

@ -53,7 +53,9 @@ export default function TPessoaCartaoForm({ index, form }: TPessoaCartaoFormInte
<FormControl>
<Input {...field}
type="number"
onChange={(e) => field.onChange(parseNumberInput(e))} />
onChange={(e) => field.onChange(parseNumberInput(e))}
defaultValue={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>
@ -69,7 +71,10 @@ export default function TPessoaCartaoForm({ index, form }: TPessoaCartaoFormInte
<FormItem>
<FormLabel>Data de Abertura</FormLabel>
<FormControl>
<Input {...field} type="date" />
<Input {...field}
type="date"
defaultValue={field.value ?? ""}
/>
</FormControl>
<FormMessage />
</FormItem>

View file

@ -1,122 +0,0 @@
import { ColumnDef } from '@tanstack/react-table';
import { Minus, Plus } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import TServicoItemPedidoInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoIntefarce';
import GetCapitalize from '@/shared/actions/text/GetCapitalize';
import { SortableHeader } from '@/shared/components/dataTable/SortableHeader';
/** Permite atualizar a linha externamente, caso o DataTable forneça meta.updateData */
type TableMeta = {
updateData?: (rowIndex: number, columnId: string, value: unknown) => void;
};
export default function TServicoItemPedidoFormColumns(): ColumnDef<TServicoItemPedidoInterface>[] {
return [
// servico
{
accessorKey: 'servico',
header: ({ column }) => SortableHeader('Serviço / Tabela', column),
cell: ({ row }) => {
const data = row.original;
return (
<div className="flex items-center gap-3">
<div>
<div className="font-semibold text-gray-900 capitalize">
{GetCapitalize(data.descricao)}
</div>
<div className="text-sm text-gray-500">
{GetCapitalize(data.tabela)}
</div>
</div>
</div>
);
},
sortingFn: (a, b) =>
(a.original.descricao?.toLowerCase() || '').localeCompare(
b.original.descricao?.toLowerCase() || '',
),
},
// emolumento
{
accessorKey: 'emolumento',
header: ({ column }) => SortableHeader('Emolumento', column),
cell: ({ row }) => <div>R$ {row.getValue('emolumento') || '---'}</div>,
},
// taxa_judiciaria
{
accessorKey: 'taxa_judiciaria',
header: ({ column }) => SortableHeader('Tx. Judiciária', column),
cell: ({ row }) => <div>R$ {row.getValue('taxa_judiciaria') || '---'}</div>,
},
// fundesp
{
accessorKey: 'fundesp',
header: ({ column }) => SortableHeader('Fundesp 21%', column),
cell: ({ row }) => <div>R$ {row.getValue('fundesp') || '---'}</div>,
},
// valor_iss
{
accessorKey: 'valor_iss',
header: ({ column }) => SortableHeader('ISS 5%', column),
cell: ({ row }) => <div>R$ {row.getValue('valor_iss') || '---'}</div>,
},
// total
{
accessorKey: 'valor',
header: ({ column }) => SortableHeader('Total', column),
cell: ({ row }) => <div>R$ {row.getValue('valor') || '---'}</div>,
},
{
accessorKey: 'quantidade',
header: ({ column }) => SortableHeader('Qtd.', column),
enableSorting: true,
cell: ({ row, table }) => {
return (
<div className="flex items-center gap-1">
{/* Botão diminuir */}
<Button
type="button"
size="icon"
variant="outline"
aria-label="Diminuir quantidade"
className="bg-white border border-gray-300 dark:bg-gray-700 dark:border-gray-700 ring-primary dark:ring-white hover:border-primary dark:hover:border-white hover:ring-1 hover:text-primary dark:hover:text-white dark:hover:bg-transparent text-gray-600 dark:text-gray-100 h-8 rounded-lg w-8 inline-flex items-center justify-center text-base button-press-feedback"
>
<Minus className="h-4 w-4" />
</Button>
{/* Campo de entrada */}
<div className="w-12">
<Input
type="number"
className="h-8 text-center px-1 bg-white dark:bg-gray-700
border-gray-300 dark:border-gray-700
text-gray-600 dark:text-gray-100"
/>
</div>
{/* Botão aumentar */}
<Button
type="button"
size="icon"
variant="outline"
aria-label="Aumentar quantidade"
className="bg-white border border-gray-300 dark:bg-gray-700 dark:border-gray-700 ring-primary dark:ring-white hover:border-primary dark:hover:border-white hover:ring-1 hover:text-primary dark:hover:text-white dark:hover:bg-transparent text-gray-600 dark:text-gray-100 h-8 rounded-lg w-8 inline-flex items-center justify-center text-base button-press-feedback"
>
<Plus className="h-4 w-4" />
</Button>
</div>
);
},
}
];
}

View file

@ -1,24 +1,128 @@
'use client';
import TServicoItemPedidoTableInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoTableInterface';
import { DataTable } from '@/shared/components/dataTable/DataTable';
import { Minus, Plus } from 'lucide-react';
import React, { memo } from 'react';
import TServicoItemPedidoFormColumns from './TServicoItemPedidoFormColumns';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import TPessoaTableFormSubview from '@/packages/administrativo/components/TPessoa/TPessoaTableFormSubview';
import TServicoItemPedidoFormTableInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoFormTableInterface';
import GetCapitalize from '@/shared/actions/text/GetCapitalize';
/**
* Componente principal da tabela de Naturezas
*/
export default function TServicoItemPedidoFormTable({ data }: TServicoItemPedidoTableInterface) {
const columns = TServicoItemPedidoFormColumns();
function TServicoItemPedidoFormTableComponent({
data,
form,
params
}: TServicoItemPedidoFormTableInterface) {
return (
<div>
<DataTable
data={data}
columns={columns}
filterColumn="servico"
filterPlaceholder="Buscar por serviço..."
/>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>
Serviço / Tabela
</TableHead>
<TableHead>
Emolumento
</TableHead>
<TableHead>
Tx. Judiciária
</TableHead>
<TableHead>
Fundesp 21%
</TableHead>
<TableHead>
ISS 5%
</TableHead>
<TableHead>
Total
</TableHead>
<TableHead className="text-center">Qtd.</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{data?.length ? (
data.map((item, index) => {
return (
<React.Fragment key={`fragment-${index}`}>
{/* Linha principal */}
<TableRow key={`row-${index}`} className="cursor-pointer hover:bg-gray-50">
<TableCell>
<div className="flex items-center gap-3">
<div>
<div className="font-semibold text-gray-900 capitalize">
{GetCapitalize(item.descricao)}
</div>
<div className="text-sm text-gray-500">
{GetCapitalize(item.tabela)}
</div>
</div>
</div>
</TableCell>
<TableCell>R$ {item.emolumento ?? '---'}</TableCell>
<TableCell>R$ {item.taxa_judiciaria ?? '---'}</TableCell>
<TableCell>R$ {item.fundesp ?? '---'}</TableCell>
<TableCell>R$ {item.valor_iss ?? '---'}</TableCell>
<TableCell>R$ {item.valor ?? '---'}</TableCell>
<TableCell>
<div className="flex items-center gap-1 justify-center">
<Button
type="button"
size="icon"
variant="outline"
className="bg-white border h-8 w-8 rounded-lg"
>
<Minus className="h-4 w-4" />
</Button>
<Input type="number" className="h-8 text-center px-1 w-12" />
<Button
type="button"
size="icon"
variant="outline"
className="bg-white border h-8 w-8 rounded-lg"
>
<Plus className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
{/* SubView */}
{item.subview && (
<TableRow className="bg-gray-50">
<TableCell colSpan={7} className="p-4">
<TPessoaTableFormSubview
item_index={item.index}
data={item.subview}
params={params}
form={form}
/>
</TableCell>
</TableRow>
)}
</React.Fragment>
);
})
) : (
<TableRow>
<TableCell colSpan={7} className="text-center py-4">
Nenhum item encontrado.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
);
}
export const TServicoItemPedidoFormTable = memo(TServicoItemPedidoFormTableComponent);

View file

@ -1,61 +1,105 @@
'use client';
import { BookmarkX, IdCardIcon, MoreHorizontalIcon, TicketIcon } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { ButtonGroup } from '@/components/ui/button-group';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import TServicoItemPedidoListInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoListInterface';
import FormatMoney from '@/shared/actions/money/FormatMoney';
import { ServicosPedidosSituacoesBadge } from '@/shared/components/servicosPedidosSituacoes/ServicosPedidosSituacoesBadge';
export default function TServicoItemPedidoList({ items }: TServicoItemPedidoListInterface) {
const money = (v: unknown) =>
new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(v || 0));
return (
<Card className="card-border">
<CardHeader>
<CardTitle className="text-2xl font-semibold">Itens: {items?.length}</CardTitle>
<CardTitle className="text-2xl font-semibold">
Itens: {items?.length}
</CardTitle>
</CardHeader>
<CardContent>
{/* Altura máxima + scroll vertical */}
<div className="space-y-4 max-h-[60vh] overflow-y-auto pr-1">
{items?.map((item) => (
<div
key={item.servico_itempedido_id}
className="bg-cart-item border-cart-border flex items-start gap-4 rounded-lg border p-4"
>
<div key={item.servico_itempedido_id} className="bg-cart-item border-cart-border rounded-lg border p-4">
{/* Descrição */}
<div className="min-w-0 flex-1">
<div className="flex-1">
<h3 className="text-foreground line-clamp-2 text-sm lg:text-base font-bold">
{item.descricao}
{item.descricao} de {item.nome} - <ServicosPedidosSituacoesBadge situacao={item.situacao} />
</h3>
<h6 className="text-foreground line-clamp-2 text-sm lg:text-base">
# {item.servico_itempedido_id}
</h6>
</div>
{/* Valores (grid compacto) */}
<div className="grid grid-cols-5 gap-3 min-w-[520px] text-right">
<div className="grid grid-cols-6 gap-3 mt-4">
<div>
<div className="text-xs text-muted-foreground">Emolumento</div>
<div className="font-semibold">{money(item.emolumento)}</div>
<div className="font-semibold">{FormatMoney(item.emolumento)}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Tx. Judiciária</div>
<div className="font-semibold">{money(item.taxa_judiciaria)}</div>
<div className="font-semibold">{FormatMoney(item.taxa_judiciaria)}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">ISS</div>
<div className="font-semibold">{money(item.valor_iss)}</div>
<div className="font-semibold">{FormatMoney(item.valor_iss)}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Fundesp</div>
<div className="font-semibold">{money(item.fundesp)}</div>
<div className="font-semibold">{FormatMoney(item.fundesp)}</div>
</div>
<div>
<div className="text-xs text-muted-foreground">Total</div>
<div className="font-semibold">{money(item.valor)}</div>
<div className="font-semibold">{FormatMoney(item.valor)}</div>
</div>
<div className="text-end">
<ButtonGroup>
<ButtonGroup>
<Button variant="outline">
Ações
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" aria-label="More Options">
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-52">
<DropdownMenuGroup>
<DropdownMenuItem className='cursor-pointer'>
<TicketIcon />
Imprimir Etiqueta
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuGroup>
<DropdownMenuItem className='cursor-pointer'>
<IdCardIcon />
Imprimir Cartão
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className='cursor-pointer' variant="destructive">
<BookmarkX /> Estornar Item
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</ButtonGroup>
</div>
</div>
</div>
))}
</div>

View file

@ -2,8 +2,8 @@
import { DataTable } from '@/shared/components/dataTable/DataTable';
import TServicoItemPedidoColumns from './TServicoItemPedidoColumns';
import TServicoItemPedidoTableInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoTableInterface';
import TServicoItemPedidoColumns from './TServicoItemPedidoColumns';
/**
* Componente principal da tabela de Naturezas

View file

@ -0,0 +1,28 @@
import { Input } from "@/components/ui/input";
export function PessoaSubviewForm({ form, index, pessoa }) {
return (
<div className="space-y-3 p-4 border rounded-md bg-gray-50">
<h3 className="font-semibold text-gray-700">
Dados da Pessoa Selecionada
</h3>
<div className="grid grid-cols-2 gap-3">
<div>
<label className="text-sm font-medium">Nome</label>
<Input {...form.register(`itens.${index}.pessoa_nome`)} defaultValue={pessoa?.nome} />
</div>
<div>
<label className="text-sm font-medium">CPF/CNPJ</label>
<Input {...form.register(`itens.${index}.pessoa_documento`)} defaultValue={pessoa?.cpfcnpj}
/>
</div>
</div>
</div>
);
}

View file

@ -1,8 +1,8 @@
'use client';
import { CalendarIcon, ClockIcon, Pencil } from 'lucide-react';
import { useCallback, useEffect } from 'react';
import { BookmarkX, CalendarIcon, ReceiptText } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
@ -16,14 +16,13 @@ import { FormatCPF } from '@/shared/actions/CPF/FormatCPF';
import { FormatDateTime } from '@/shared/actions/dateTime/FormatDateTime';
import GetCapitalize from '@/shared/actions/text/GetCapitalize';
import GetNameInitials from '@/shared/actions/text/GetNameInitials';
import ConfirmDialog from '@/shared/components/confirmDialog/ConfirmDialog';
export default function TServicoPedidoDetails({ servico_pedido_id }: TServicoPedidoInterface) {
const { TServicoItemPedido, indexTServicoItemPedido } = useTServicoItemPedidoIndexHook()
const { TServicoPedido, showTServicoPedido } = useTServicoPedidoShowHook()
const [isCancelServicoPedidoDialogOpen, setIsCancelServicoPedidoDialogOpen] = useState(false)
const TServicoPedidoShowData = useCallback(async () => {
const servicoPedido: TServicoPedidoInterface = {
@ -42,23 +41,19 @@ export default function TServicoPedidoDetails({ servico_pedido_id }: TServicoPed
await indexTServicoItemPedido(servicoPedido)
})
const handleCancelServicoPedidoOpenDialog = useCallback((state: boolean) => {
setIsCancelServicoPedidoDialogOpen(state)
}, [])
useEffect(() => {
TServicoPedidoShowData()
}, [])
return (
<div className="relative h-full flex flex-col px-2 sm:px-4 py-2 sm:py-4 md:px-6 container mx-auto">
<div className="container mx-auto flex items-center justify-between mb-4">
<h3 className="text-2xl font-semibold">
Pedido: #{TServicoPedido?.servico_pedido_id}
</h3>
<div className="flex items-center gap-2 print:hidden">
<Button>
<Pencil className="mr-2 h-4 w-4" />
Edit
</Button>
</div>
</div>
<h3 className='text-4xl font-bold mb-4'>
Pedido: #{TServicoPedido?.servico_pedido_id}
</h3>
{/* Main */}
<div className="container mx-auto h-full">
<div className="flex flex-col lg:flex-row gap-4">
@ -99,18 +94,12 @@ export default function TServicoPedidoDetails({ servico_pedido_id }: TServicoPed
</div>
</div>
<Separator className="my-5" />
<div className="mb-4 flex items-center gap-2">
<CalendarIcon className="opacity-70" />
<div className="flex items-center gap-2">
<CalendarIcon className="opacity-70 size-5" />
<span className="truncate">
{FormatDateTime(TServicoPedido?.data_pedido)}
</span>
</div>
<div className="flex items-center gap-2">
<ClockIcon className="opacity-70" />
<span className="truncate">
14:50:31
</span>
</div>
</CardContent>
</Card>
<Card role="presentation" className="card-border">
@ -141,17 +130,34 @@ export default function TServicoPedidoDetails({ servico_pedido_id }: TServicoPed
<Card className="card-border">
<CardHeader>
<CardTitle className="text-2xl font-semibold">
Impressões
Controles
</CardTitle>
</CardHeader>
<CardContent className="flex flex-col gap-2">
<Button className="w-full" variant="outline">Recibo</Button>
<Button className="w-full">Etiqueta</Button>
<Button className="w-full cursor-pointer" variant={`outline`} onClick={() => { handleCancelServicoPedidoOpenDialog(true) }}>
<BookmarkX /> Estornar Pedido
</Button>
<Button className="w-full cursor-pointer">
<ReceiptText /> Imprimir Recibo
</Button>
</CardContent>
</Card>
</div>
</div>
</div>
{/* Confirma o cancelamento do pedido */}
{isCancelServicoPedidoDialogOpen && (
<ConfirmDialog
isOpen={isCancelServicoPedidoDialogOpen}
title="Estorno do pedido"
description="Atenção"
message="Deseja estornar o pedido?"
confirmText="Sim, estornar"
cancelText="Fechar"
onConfirm={() => { handleCancelServicoPedidoOpenDialog(false) }}
onCancel={() => { handleCancelServicoPedidoOpenDialog(false) }}
/>
)}
</div>
)
}

View file

@ -4,6 +4,7 @@ import * as React from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Separator } from '@/components/ui/separator';
import FormatMoney from '@/shared/actions/money/FormatMoney';
import { ServicosPedidosSituacoesBadge } from '@/shared/components/servicosPedidosSituacoes/ServicosPedidosSituacoesBadge';
import TServicoPedidoDetailsPagamentoInterface from '../../interfaces/TServicoPedido/TServicoPedidoDetailsPagamentoInterface';
@ -13,28 +14,29 @@ export default function TServicoPedidoDetailsPagamento({
items,
}: TServicoPedidoDetailsPagamentoInterface) {
// Formatação monetária (BRL)
const fmt = React.useMemo(
() => new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }),
[]
);
// Helper para evitar NaN/undefined
const safe = (n: number | undefined | null) =>
typeof n === 'number' && Number.isFinite(n) ? n : 0;
// Somas por tipo de valor
const { emolumento, taxa_judiciaria, valor_iss, fundesp } = React.useMemo(() => {
return (items ?? []).reduce(
(acc, item) => {
acc.emolumento += safe(item.emolumento);
acc.taxa_judiciaria += safe(item.taxa_judiciaria);
acc.valor_iss += safe(item.valor_iss);
acc.fundesp += safe(item.fundesp);
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
@ -47,35 +49,57 @@ export default function TServicoPedidoDetailsPagamento({
Pagamento <ServicosPedidosSituacoesBadge situacao={situacao} />
</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Emolumento</span>
<span className="font-medium">{fmt.format(emolumento)}</span>
<span className="font-medium">{FormatMoney(emolumento)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Tx. Judiciária</span>
<span className="font-medium">{fmt.format(taxa_judiciaria)}</span>
<span className="font-medium">{FormatMoney(taxa_judiciaria)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">ISS</span>
<span className="font-medium">{fmt.format(valor_iss)}</span>
<span className="font-medium">{FormatMoney(valor_iss)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Fundesp</span>
<span className="font-medium">{fmt.format(fundesp)}</span>
<span className="font-medium">{FormatMoney(fundesp)}</span>
</div>
<Separator className="border-cart-border" />
<div className="flex items-center justify-between text-xl">
<span className="text-muted-foreground font-semibold">Total</span>
<span className="font-semibold">{FormatMoney(total)}</span>
</div>
<Separator className="border-cart-border" />
<div className="flex items-center justify-between">
<span className="text-muted-foreground font-semibold">Total</span>
<span className="font-semibold">{fmt.format(total)}</span>
<span className="text-muted-foreground">Emolumento</span>
<span className="font-medium">{FormatMoney(emolumento)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Tx. Judiciária</span>
<span className="font-medium">{FormatMoney(taxa_judiciaria)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">ISS</span>
<span className="font-medium">{FormatMoney(valor_iss)}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Fundesp</span>
<span className="font-medium">{FormatMoney(fundesp)}</span>
</div>
</div>
</CardContent>
</Card>

View file

@ -25,10 +25,9 @@ import { Switch } from '@/components/ui/switch';
import GEmolumentoServicoSelect from '@/packages/administrativo/components/GEmolumento/GEmolumentoServicoSelect';
import GUsuarioSelect from '@/packages/administrativo/components/GUsuario/GUsuarioSelect';
import TPessoaTableFormDialog from '@/packages/administrativo/components/TPessoa/TPessoaTableFormDialog';
import TPessoaTableFormSubview from '@/packages/administrativo/components/TPessoa/TPessoaTableFormSubview';
import TServicoTipoSelect from '@/packages/administrativo/components/TServicoTipo/TServicoTipoSelect';
import TPessoaInterface from '@/packages/administrativo/interfaces/TPessoa/TPessoaInterface';
import HandleSelectTServicoTipoAction from '@/packages/servicos/actions/TServicoPedido/HandleSelectTServicoTipoAction';
import TServicoItemPedidoFormTable from '@/packages/servicos/components/TServicoItemPedido/TServicoItemPedidoFormTable';
import { useTServicoItemPedidoAddHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoAddHook';
import { useTServicoItemPedidoIndexHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoIndexHook';
import { useTServicoItemPedidoLocalAddHook } from '@/packages/servicos/hooks/TServicoItemPedido/useTServicoItemPedidoLocalAddHook';
@ -51,7 +50,9 @@ import {
} from '@/shared/components/step/stepNavigator';
import TipoPagamentoSelect from '@/shared/components/tipoPagamento/TipoPagamentoSelect';
import TServicoSubviewInterface from '../../interfaces/TServico/TServicoSubviewInterface';
import TServicoItemPedidoAddInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoAddInterface';
import TServicoItemPedidoAddResponseInterface from '../../interfaces/TServicoItemPedido/TServicoItemPedidoAddResponseInterface';
import { TServicoItemPedidoFormTable } from '../TServicoItemPedido/TServicoItemPedidoFormTable';
export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedidoFormInterface) {
@ -76,7 +77,8 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
const handleCloseSaveConfirm = useCallback(() => setIsSaveConfirmOpen(false), []);
// Hooks
const { response, setResponse } = useResponse()
// const playSuccess = useSoundHook("/sounds/success.mp3");
const { setResponse } = useResponse()
const { saveTServicoPedido } = useTServicoPedidoSaveHook();
const { showTServicoPedido } = useTServicoPedidoShowHook();
const { TServicoItemPedidoLocal, localAddTServicoItemPedido, setLocalTServicoItemPedido } = useTServicoItemPedidoLocalAddHook(setValue);
@ -107,6 +109,14 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
// Desativa o botão de loading
setIsSaving(false);
// Verifica se devo redirecionar a pagina
if (response?.servico_pedido_id > 0) {
// Toca o som do sistema
// playSuccess()
}
// Verifica se devo redirecionar a pagina
if (response?.servico_pedido_id > 0 && !shouldKeepFormOpen) {
@ -127,8 +137,12 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
// Busca os itens do Pedido
const fetchPedidoItens = useCallback(async (id: number) => {
const pedidoItens = {
servico_pedido_id: id
}
// Busca os itens do pedido
const response = await indexTServicoItemPedido({ servico_pedido_id: id });
const response = await indexTServicoItemPedido(pedidoItens);
// Verifica se os dados foram localizados
if (response?.data?.length) {
@ -161,14 +175,16 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
}, [servico_pedido_id, showTServicoPedido, reset, fetchPedidoItens]);
const handleAddItemWithPessoa = useCallback(async (selectedTPessoa: any) => {
const handleAddItemWithPessoa = useCallback(async (selectedTPessoa: TPessoaInterface) => {
// Habilita o loading
setIsAdding(true)
// Constroi um novo item
const newItem: TServicoSubviewInterface = await addTServicoItemPedido({
const newItem: TServicoItemPedidoAddResponseInterface = await addTServicoItemPedido({
servico_tipo: selectedServicoTipo,
emolumento: selectedEmolumento
emolumento: selectedEmolumento,
pessoa: selectedTPessoa
});
// Verifica se existe um novo item
@ -177,19 +193,8 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
// Obtem o indice atual
const index = TServicoItemPedidoLocal.length;
// Guarda o ID da pessoa
newItem.pessoa_id = selectedTPessoa.pessoa_id;
// Cria a subview se necessário
newItem.subview = (
<TPessoaTableFormSubview
form={form}
servico={selectedServicoTipo}
params={TServicoPedidoParams}
selectedTPessoa={selectedTPessoa}
index={index}
/>
);
// Define a posição do item
newItem.index = index
// Atualiza o estado
localAddTServicoItemPedido(newItem);
@ -197,6 +202,7 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
// Atualiza os itens do formulário
form.setValue(`itens.${index}`, newItem);
// Desabilita o loading
setIsAdding(false)
}, [
@ -206,7 +212,6 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
TServicoItemPedidoLocal.length,
localAddTServicoItemPedido,
form,
TServicoPedidoParams
]);
// Controla o formulário de cancelamento de pedido
@ -251,7 +256,7 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
setIsAdding(true)
// Prepara e valida os dados de item do pedido
const payload = {
const payload: TServicoItemPedidoAddInterface = {
servico_tipo: selectedServicoTipo,
emolumento: selectedEmolumento
}
@ -303,7 +308,7 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
}
const total = TServicoItemPedidoLocal.reduce((acc, item) => {
const valor = Number(item.valor_total ?? item.valor ?? 0);
const valor = Number(item.valor ?? 0);
return acc + valor;
}, 0);
@ -522,7 +527,10 @@ export default function TServicoPedidoForm({ servico_pedido_id }: TServicoPedido
/>
</div>
<div className="col-span-12">
<TServicoItemPedidoFormTable data={TServicoItemPedidoLocal} />
<TServicoItemPedidoFormTable
form={form}
params={TServicoPedidoParams}
data={TServicoItemPedidoLocal} />
</div>
</div>
</CardContent>

View file

@ -1,7 +1,8 @@
'use client';
import { useState } from 'react';
import { UseFormSetValue } from 'react-hook-form';
import { FieldValues, UseFormSetValue } from 'react-hook-form';
import { GCalculoServicoService } from '@/packages/administrativo/services/GCalculo/GCalculoServicoService';
import PrepareTServicoItemPedidoPayload from '@/packages/servicos/actions/TServicoPedido/PrepareTServicoItemPedidoPayload';
@ -9,8 +10,7 @@ import TServicoItemPedidoAddInterface from '@/packages/servicos/interfaces/TServ
import { default as TServicoItemPedidoIndexResponseInterface } from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddResponseInterface';
import { useResponse } from '@/shared/components/response/ResponseContext';
export const useTServicoItemPedidoAddHook = (setValue?: UseFormSetValue<TServicoItemPedidoAddInterface>) => {
export function useTServicoItemPedidoAddHook<TFormValues extends FieldValues>(setValue?: UseFormSetValue<TFormValues>) {
const { setResponse } = useResponse();
@ -25,13 +25,23 @@ export const useTServicoItemPedidoAddHook = (setValue?: UseFormSetValue<TServico
setResponse(payload)
return null;
return;
}
// Realiza a busca do item
const response = await GCalculoServicoService(payload, data);
// Verifico se tenho código de resposta
if (response.status) {
// Exibo a resposta em tela
setResponse(response)
return;
}
// Obtem o resultado da busca
const item = response;

View file

@ -1,16 +1,16 @@
'use client';
import { useState } from 'react';
import { UseFormSetValue } from 'react-hook-form';
import { FieldValues, UseFormSetValue } from 'react-hook-form';
import TServicoItemPedidoAddInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddInterface';
import TServicoItemPedidoIndexResponseInterface from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddResponseInterface';
import { default as TServicoItemPedidoAddResponseInterface, default as TServicoItemPedidoIndexResponseInterface } from '@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddResponseInterface';
export const useTServicoItemPedidoLocalAddHook = (setValue?: UseFormSetValue<TServicoItemPedidoAddInterface>) => {
export function useTServicoItemPedidoLocalAddHook<TFormValues extends FieldValues>(setValue?: UseFormSetValue<TFormValues>) {
const [TServicoItemPedidoLocal, setLocalTServicoItemPedido] = useState<TServicoItemPedidoIndexResponseInterface[]>([]);
const localAddTServicoItemPedido = (item: any) => {
const localAddTServicoItemPedido = (item: TServicoItemPedidoAddResponseInterface) => {
setLocalTServicoItemPedido((prev) => {

View file

@ -19,6 +19,7 @@ export function useTServicoPedidoFormHook(defaults?: Partial<TServicoPedidoFormV
pagador_cpfcnpj: "",
valor_pedido: 0,
valor_pago: 0,
situacao: "F",
tipo_pagamento: null,
...defaults,
},

View file

@ -10,7 +10,7 @@ export const useTServicoPedidoShowHook = () => {
const { setResponse } = useResponse();
const [TServicoPedido, setTServicoPedido] = useState<TServicoPedidoInterface>(null);
const [TServicoPedido, setTServicoPedido] = useState<TServicoPedidoInterface>();
const showTServicoPedido = async (data: TServicoPedidoInterface) => {

View file

@ -0,0 +1,7 @@
import TServicoTipoInterface from "@/app/(protected)/(cadastros)/cadastros/_interfaces/TServicoTipoInterface";
import TPessoaInterface from "@/packages/administrativo/interfaces/TPessoa/TPessoaInterface";
export default interface TServicoItemPedidoSubviewInterface {
servico: TServicoTipoInterface,
pessoa: TPessoaInterface
}

View file

@ -1,5 +0,0 @@
export default interface TServicoSubviewInterface {
pessoa_id: number;
subview: any;
}

View file

@ -4,7 +4,4 @@ import GEmolumentoInterface from "@/packages/administrativo/interfaces/GEmolumen
export default interface TServicoItemPedidoAddInterface {
emolumento: GEmolumentoInterface;
servico_tipo: TServicoTipoInterface;
sistema_id?: number;
valor_documento?: number;
quantidade?: number;
}

View file

@ -1,4 +1,7 @@
import TServicoItemPedidoSubviewInterface from "../TServico/TServicoItemPedidoSubviewInterface";
export default interface TServicoItemPedidoAddResponseInterface {
index?: number;
emolumento_id: number;
emolumento_item_id: number;
servico_tipo_id: number;
@ -12,4 +15,6 @@ export default interface TServicoItemPedidoAddResponseInterface {
fundesp: number;
taxa_judiciaria: number;
valor_iss: number;
pessoa_id?: number;
subview?: TServicoItemPedidoSubviewInterface;
}

View file

@ -1,9 +0,0 @@
import { TServicoItemPedidoFormValues } from '../../schemas/TServicoItemPedido/TServicoItemPedidoSchema';
export interface TServicoItemPedidoFormInterface {
isOpen: boolean;
data: TServicoItemPedidoFormValues | null;
onClose: (item: null, isFormStatus: boolean) => void;
onSave: (data: TServicoItemPedidoFormValues) => void;
buttonIsLoading: boolean;
}

View file

@ -0,0 +1,10 @@
import TServicoItemPedidoAddResponseInterface from "@/packages/servicos/interfaces/TServicoItemPedido/TServicoItemPedidoAddResponseInterface";
import { TServicoPedidoFormInterface } from "@/packages/servicos/interfaces/TServicoPedido/TServicoPedidoFormInterface";
import GConfigInterface from "@/shared/interfaces/GConfigInterface";
export default interface TServicoItemPedidoFormTableInterface {
data: TServicoItemPedidoAddResponseInterface;
form: TServicoPedidoFormInterface;
params: GConfigInterface;
}

View file

@ -53,6 +53,10 @@ export const TServicoPedidoFormSchema = z.object({
tipo_pagamento: z
.any()
.refine((v) => !!v, "Selecione a forma de pagamento."),
situacao: z
.string()
.min(1, "Campo situação deve ser informado"),
});
export type TServicoPedidoFormValues = z.infer<typeof TServicoPedidoFormSchema>;

View file

@ -0,0 +1,5 @@
export default function FormatMoney(data) {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(Number(data || 0));
}

View file

@ -34,6 +34,7 @@ import {
} from '@/components/ui/table';
import DataTableInterface from './interfaces/DataTableInterface';
import DataTableSubview from './interfaces/DataTableSubview';
/**
* DataTable genérico com suporte a subvisões dinâmicas (subtabelas ou detalhes).
@ -210,7 +211,7 @@ export function DataTable<TData extends { subview?: React.ReactNode | (() => Rea
colSpan={row.getVisibleCells().length}
className="p-0"
>
{subview}
<DataTableSubview subview={subview} />
</TableCell>
</TableRow>
)}

View file

@ -0,0 +1,11 @@
'use client';
export default function DataTableSubview({ subview }) {
if (!subview) return null;
if (!subview.component) return null;
const Component = subview.component;
const props = subview.props || {};
return <Component {...props} />;
}

View file

@ -4,10 +4,9 @@ import { useState } from 'react';
import { FingerTechCaptureService } from '@/shared/services/FingerTech/FingerTechCaptureService';
export const useFingerTechIndexHook = () => {
const [base64, setBase64] = useState<any>('');
const [base64, setBase64] = useState<string>('');
const captureFingerTech = async () => {

View file

@ -0,0 +1,18 @@
'use client'
import { useCallback, useRef } from "react";
export function useSoundHook(soundPath: string) {
const audioRef = useRef<HTMLAudioElement | null>(null);
if (!audioRef.current) {
audioRef.current = new Audio(soundPath);
}
const play = useCallback(() => {
audioRef.current?.play().catch(() => { });
}, []);
return play;
}