Commit inicial - upload de todos os arquivos da pasta

This commit is contained in:
2026-06-13 17:32:41 -03:00
commit 759e2663ec
311 changed files with 31868 additions and 0 deletions

View File

@@ -0,0 +1,232 @@
import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Pencil, XCircle, AlertTriangle, Info, DollarSign } from 'lucide-react';
import { PageContainer, PageHeader } from '../../components/layout';
import { Button, Badge } from '../../components/ui';
import { Tabs, type TabItem } from '../../components/ui/Tabs';
import { useToast } from '../../components/ui/Toast';
import { useBreadcrumbs } from '../../hooks/useBreadcrumbs';
import { useReadOnly } from '../../hooks/useReadOnly';
import { useFieldVisibility } from '../../hooks/useFieldVisibility';
import { useWorkOrder, useWorkOrderSummary, useCancelWorkOrder } from '../../hooks/useWorkOrders';
import { getWorkOrderStatusConfig } from '../../constants/work-order-status';
import { OrdemServicoSummaryTab } from './components/OrdemServicoSummaryTab';
import { OrdemServicoDeliverablesTab } from './components/OrdemServicoDeliverablesTab';
import { OrdemServicoStatusHistoryTab } from './components/OrdemServicoStatusHistoryTab';
import { CancelWorkOrderModal } from './components/CancelWorkOrderModal';
function formatNumber(value: number) {
return new Intl.NumberFormat('pt-BR', { maximumFractionDigits: 2 }).format(value);
}
function formatCurrency(value: number) {
return new Intl.NumberFormat('pt-BR', { style: 'currency', currency: 'BRL' }).format(value);
}
function formatPercent(value: number) {
return `${value.toFixed(1)}%`;
}
export function OrdemServicoDetailPage() {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { showToast } = useToast();
const { isReadOnly } = useReadOnly();
const isVisible = useFieldVisibility();
const [cancelOpen, setCancelOpen] = useState(false);
const { data: workOrder, isLoading, isError } = useWorkOrder(id ?? '');
const { data: summary } = useWorkOrderSummary(id ?? '');
const cancelMutation = useCancelWorkOrder();
const breadcrumbs = useBreadcrumbs({ ':name': workOrder?.code ?? '' });
if (isLoading) {
return (
<PageContainer>
<div className="flex items-center justify-center py-20">
<span className="h-8 w-8 animate-spin rounded-full border-4 border-isis-blue border-t-transparent" />
</div>
</PageContainer>
);
}
if (isError || !workOrder) {
return (
<PageContainer>
<div className="flex flex-col items-center justify-center gap-3 py-20 text-text-secondary">
<Info className="h-10 w-10" />
<p className="text-lg font-medium">Ordem de serviço não encontrada</p>
<Button variant="secondary" onClick={() => navigate('/ordens-servico')}>
Voltar para lista
</Button>
</div>
</PageContainer>
);
}
const statusConfig = getWorkOrderStatusConfig(workOrder.status);
const isEditable = workOrder.status === 'RASCUNHO' || workOrder.status === 'EMITIDA';
const canCancel = workOrder.status === 'RASCUNHO' && (workOrder._count.deliverables ?? 0) === 0;
const tabs: TabItem[] = [
{ id: 'summary', label: 'Resumo' },
{ id: 'deliverables', label: 'Entregáveis' },
{ id: 'history', label: 'Histórico de status' },
];
async function handleCancel(observation: string) {
if (!id) return;
try {
await cancelMutation.mutateAsync({ id, observation });
showToast('Ordem de serviço cancelada', 'success');
setCancelOpen(false);
} catch {
showToast('Erro ao cancelar ordem de serviço', 'error');
}
}
return (
<PageContainer>
<PageHeader
title={`${workOrder.code}${workOrder.name}`}
breadcrumbs={breadcrumbs}
actions={
!isReadOnly ? (
<div className="flex gap-2">
{isEditable && (
<Button
variant="secondary"
icon={<Pencil size={14} />}
onClick={() => navigate(`/ordens-servico/${workOrder.id}/editar`)}
>
Editar
</Button>
)}
{canCancel && (
<Button
variant="danger"
icon={<XCircle size={14} />}
onClick={() => setCancelOpen(true)}
>
Cancelar
</Button>
)}
</div>
) : undefined
}
/>
<div className="mb-4 flex flex-wrap items-center gap-3">
<Badge variant={statusConfig.variant}>{statusConfig.label}</Badge>
<span className="text-small text-text-secondary">Contrato: {workOrder.contract.name}</span>
<span className="text-small text-text-secondary">
Item: {workOrder.contractItem.code} {workOrder.contractItem.name}
</span>
</div>
{workOrder.description && <p className="mb-6 text-text-secondary">{workOrder.description}</p>}
{summary?.lowBalanceAlert && (
<div className="mb-4 flex items-center gap-2 rounded border border-warning/30 bg-warning/10 px-4 py-3 text-small text-warning">
<AlertTriangle size={16} />
<span>
Saldo baixo: menos de 20% disponível ({formatNumber(Number(summary.ustAvailable))} de{' '}
{formatNumber(Number(summary.ustReserved))} UST).
</span>
</div>
)}
{/* Card de Valoração — visível apenas para perfis com acesso financeiro */}
{isVisible('totalValue') && (
<div className="mb-6 rounded-lg border border-border bg-card p-5">
<div className="flex items-start gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-isis-blue/10">
<DollarSign className="h-5 w-5 text-isis-blue" />
</div>
<div className="flex flex-1 flex-col gap-1">
<p className="text-[11px] font-medium uppercase tracking-wider text-text-muted">
Valor da Ordem de Serviço
</p>
<p className="text-2xl font-bold text-isis-blue">
{workOrder.totalValue != null
? formatCurrency(Number(workOrder.totalValue))
: 'Não calculado'}
</p>
{workOrder.contractItem.itemType === 'SAAS_LICENSE' &&
workOrder.contractItem.ustValue != null && (
<p className="text-small text-text-secondary">
{formatNumber(Number(workOrder.reservedUst))} licenças × R${' '}
{Number(workOrder.contractItem.ustValue).toLocaleString('pt-BR', {
minimumFractionDigits: 2,
})}
/unidade faturamento anual
</p>
)}
{workOrder.contractItem.itemType === 'UST' && (
<p className="text-small text-text-secondary">
Soma dos Entregáveis vinculados ({workOrder._count?.deliverables ?? 0} ativos)
</p>
)}
</div>
</div>
</div>
)}
{isVisible('reservedUst') && (
<div className="mb-6 grid grid-cols-1 gap-4 md:grid-cols-4">
<div className="rounded border border-border-default p-4">
<p className="text-small text-text-muted">
{workOrder.contractItem.itemType === 'SAAS_LICENSE'
? 'Licenças reservadas'
: 'Pool reservado'}
</p>
<p className="text-lg font-semibold text-primary">
{formatNumber(Number(workOrder.reservedUst))}{' '}
{workOrder.contractItem.itemType === 'SAAS_LICENSE' ? 'licenças' : 'UST'}
</p>
</div>
<div className="rounded border border-border-default p-4">
<p className="text-small text-text-muted">Pool consumido</p>
<p className="text-lg font-semibold text-primary">
{summary ? formatNumber(Number(summary.ustConsumed)) : '—'} UST
</p>
</div>
<div className="rounded border border-border-default p-4">
<p className="text-small text-text-muted">Saldo disponível</p>
<p className="text-lg font-semibold text-primary">
{summary ? formatNumber(Number(summary.ustAvailable)) : '—'} UST
</p>
</div>
<div className="rounded border border-border-default p-4">
<p className="text-small text-text-muted">% de consumo</p>
<p className="text-lg font-semibold text-primary">
{summary ? formatPercent(Number(summary.consumptionPercent)) : '—'}
</p>
</div>
</div>
)}
<Tabs tabs={tabs} defaultTab="summary">
{(activeTabId: string) => (
<div className="pt-4">
{activeTabId === 'summary' && <OrdemServicoSummaryTab workOrderId={workOrder.id} />}
{activeTabId === 'deliverables' && (
<OrdemServicoDeliverablesTab workOrderId={workOrder.id} />
)}
{activeTabId === 'history' && (
<OrdemServicoStatusHistoryTab history={workOrder.statusHistory} />
)}
</div>
)}
</Tabs>
<CancelWorkOrderModal
open={cancelOpen}
loading={cancelMutation.isPending}
onConfirm={(obs) => void handleCancel(obs)}
onCancel={() => setCancelOpen(false)}
/>
</PageContainer>
);
}