feat(web+api): cockpit rep — novo pedido v2, endereço de entrega e cancelamento de orçamento
- NewOrderPage: etapas 1-5 — merge de cards, busca de produto melhorada (autocomplete com estoque + modal catálogo), responsividade mobile (tabela → cards, footer compacto), histórico de compras do cliente e campo de endereço de entrega (cadastrado vs livre) - endEntrega: campo TEXT adicionado em sar.pedidos; contrato CreatePedidoSchema e PedidoDetailSchema atualizados; aparece no detalhe (OrderDetailPage, OrderDetailModal) e no PDF (OrderPrintPage) - Cancelamento de orçamento: PATCH /orders/:id/cancel — rep cancela seu próprio orçamento (situa 0); histórico registrado; UI com Modal.confirm substituindo alert() em OrderActionsMenu - Fix: useClientDetail chamado para cliente selecionado manualmente, garantindo que clientEndStr seja preenchido mesmo sem clientIdParam na URL Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ model Pedido {
|
|||||||
comissao Decimal @default(0) @db.Decimal(15, 2)
|
comissao Decimal @default(0) @db.Decimal(15, 2)
|
||||||
pedFlex Decimal @default(0) @db.Decimal(15, 2) @map("ped_flex")
|
pedFlex Decimal @default(0) @db.Decimal(15, 2) @map("ped_flex")
|
||||||
obs String?
|
obs String?
|
||||||
|
endEntrega String? @map("end_entrega")
|
||||||
aprovadoPor Int? @map("aprovado_por")
|
aprovadoPor Int? @map("aprovado_por")
|
||||||
aprovadoEm DateTime? @map("aprovado_em")
|
aprovadoEm DateTime? @map("aprovado_em")
|
||||||
motivoRecusa String? @map("motivo_recusa")
|
motivoRecusa String? @map("motivo_recusa")
|
||||||
|
|||||||
@@ -439,6 +439,7 @@ export class ClientsService {
|
|||||||
|
|
||||||
interface Row {
|
interface Row {
|
||||||
id_produto: number;
|
id_produto: number;
|
||||||
|
codigo: string;
|
||||||
descricao: string;
|
descricao: string;
|
||||||
unidade: string | null;
|
unidade: string | null;
|
||||||
qtd_total: string;
|
qtd_total: string;
|
||||||
@@ -449,37 +450,39 @@ export class ClientsService {
|
|||||||
|
|
||||||
const rows = await prisma.$queryRawUnsafe<Row[]>(`
|
const rows = await prisma.$queryRawUnsafe<Row[]>(`
|
||||||
SELECT
|
SELECT
|
||||||
i.produ AS id_produto,
|
i.id_produto,
|
||||||
|
TRIM(p.codigo) AS codigo,
|
||||||
TRIM(p.descricao) AS descricao,
|
TRIM(p.descricao) AS descricao,
|
||||||
TRIM(p.unidade) AS unidade,
|
TRIM(p.unidade) AS unidade,
|
||||||
SUM(i.qtd)::numeric(15,3)::text AS qtd_total,
|
SUM(i.qtd)::numeric(15,3)::text AS qtd_total,
|
||||||
COUNT(DISTINCT ped.id_pedido)::text AS num_pedidos,
|
COUNT(DISTINCT ped.id_pedido)::text AS num_pedidos,
|
||||||
MAX(ped.data) AS ultima_compra,
|
MAX(ped.dt_pedido) AS ultima_compra,
|
||||||
(
|
(
|
||||||
SELECT i2.pruni::numeric(15,2)::text
|
SELECT i2.preco_unitario::numeric(15,2)::text
|
||||||
FROM sig.peditens i2
|
FROM sar.vw_peditens_erp i2
|
||||||
JOIN sig.pedidos p2 ON p2.id_pedido = i2.id_pedido
|
JOIN sar.vw_pedidos_erp p2 ON p2.id_pedido = i2.id_pedido
|
||||||
WHERE i2.produ = i.produ
|
WHERE i2.id_produto = i.id_produto
|
||||||
AND p2.clien = ped.clien
|
AND p2.id_cliente = ped.id_cliente
|
||||||
AND p2.id_empresa = ${idEmpresa}
|
AND p2.id_empresa = ${idEmpresa}
|
||||||
AND p2.situa NOT IN (5)
|
AND p2.situa NOT IN (5)
|
||||||
ORDER BY p2.data DESC, p2.id_pedido DESC
|
ORDER BY p2.dt_pedido DESC, p2.id_pedido DESC
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
) AS ultimo_preco
|
) AS ultimo_preco
|
||||||
FROM sig.pedidos ped
|
FROM sar.vw_pedidos_erp ped
|
||||||
JOIN sig.peditens i ON i.id_pedido = ped.id_pedido
|
JOIN sar.vw_peditens_erp i ON i.id_pedido = ped.id_pedido
|
||||||
JOIN gestao.produto p ON p.id_erp = i.produ
|
JOIN sar.vw_produtos p ON p.id_erp = i.id_produto
|
||||||
AND p.id_empresa = ${idEmpresaMatriz}
|
AND p.id_empresa = ${idEmpresaMatriz}
|
||||||
WHERE ped.clien = ${idCliente}
|
WHERE ped.id_cliente = ${idCliente}
|
||||||
AND ped.id_empresa = ${idEmpresa}
|
AND ped.id_empresa = ${idEmpresa}
|
||||||
AND ped.situa NOT IN (5)
|
AND ped.situa NOT IN (5)
|
||||||
GROUP BY i.produ, p.descricao, p.unidade, ped.clien
|
GROUP BY i.id_produto, p.descricao, p.unidade, ped.id_cliente
|
||||||
ORDER BY SUM(i.qtd) DESC
|
ORDER BY SUM(i.qtd) DESC
|
||||||
LIMIT ${limit}
|
LIMIT ${limit}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
return rows.map((r) => ({
|
return rows.map((r) => ({
|
||||||
idProduto: Number(r.id_produto),
|
idProduto: Number(r.id_produto),
|
||||||
|
codProduto: r.codigo ?? '',
|
||||||
descricao: r.descricao,
|
descricao: r.descricao,
|
||||||
unidade: r.unidade,
|
unidade: r.unidade,
|
||||||
qtdTotal: parseFloat(r.qtd_total ?? '0'),
|
qtdTotal: parseFloat(r.qtd_total ?? '0'),
|
||||||
@@ -646,12 +649,12 @@ export class ClientsService {
|
|||||||
nf.dt_emissao,
|
nf.dt_emissao,
|
||||||
nf.vl_nf::text AS vl_nf,
|
nf.vl_nf::text AS vl_nf,
|
||||||
TRIM(nf.chave_acesso_nfe) AS chave_acesso_nfe
|
TRIM(nf.chave_acesso_nfe) AS chave_acesso_nfe
|
||||||
FROM gestao.nf nf
|
FROM sar.vw_nf nf
|
||||||
JOIN sig.pedidos p
|
JOIN sar.vw_pedidos_erp p
|
||||||
ON p.numero = nf.num_entrega
|
ON p.numero = nf.num_entrega
|
||||||
AND p.tipo = 'E'
|
AND p.tipo = 'E'
|
||||||
AND p.id_empresa IN (1, 9001)
|
AND p.id_empresa IN (1, 9001)
|
||||||
WHERE p.clien = $1
|
WHERE p.id_cliente = $1
|
||||||
AND nf.id_empresa = 1
|
AND nf.id_empresa = 1
|
||||||
AND nf.status = 'E'
|
AND nf.status = 'E'
|
||||||
AND TRIM(COALESCE(nf.chave_acesso_nfe, '')) != ''
|
AND TRIM(COALESCE(nf.chave_acesso_nfe, '')) != ''
|
||||||
|
|||||||
@@ -84,6 +84,11 @@ export class OrdersController {
|
|||||||
return this.orders.reject(id, parsed);
|
return this.orders.reject(id, parsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Patch(':id/cancel')
|
||||||
|
cancel(@Param('id', ParseUUIDPipe) id: string): Promise<PedidoDetail> {
|
||||||
|
return this.orders.cancel(id);
|
||||||
|
}
|
||||||
|
|
||||||
@Get('erp/:idPedido')
|
@Get('erp/:idPedido')
|
||||||
findOneErp(@Param('idPedido', ParseIntPipe) idPedido: number): Promise<PedidoDetail> {
|
findOneErp(@Param('idPedido', ParseIntPipe) idPedido: number): Promise<PedidoDetail> {
|
||||||
return this.orders.findOneErp(idPedido);
|
return this.orders.findOneErp(idPedido);
|
||||||
|
|||||||
@@ -128,10 +128,10 @@ export class OrdersService {
|
|||||||
const dataHistoricoStr = dataHistorico.toISOString().split('T')[0];
|
const dataHistoricoStr = dataHistorico.toISOString().split('T')[0];
|
||||||
|
|
||||||
const vendedorFilter = role === 'rep' ? `AND a.cod_vendedor = ${codVendedor}` : '';
|
const vendedorFilter = role === 'rep' ? `AND a.cod_vendedor = ${codVendedor}` : '';
|
||||||
const fromFilterE = from ? `AND COALESCE(a.data, b.data) >= '${from}'` : '';
|
const fromFilterE = from ? `AND COALESCE(a.dt_pedido, b.dt_pedido) >= '${from}'` : '';
|
||||||
const toFilterE = to ? `AND COALESCE(a.data, b.data) <= '${to}'` : '';
|
const toFilterE = to ? `AND COALESCE(a.dt_pedido, b.dt_pedido) <= '${to}'` : '';
|
||||||
const fromFilterP = from ? `AND a.data >= '${from}'` : '';
|
const fromFilterP = from ? `AND a.dt_pedido >= '${from}'` : '';
|
||||||
const toFilterP = to ? `AND a.data <= '${to}'` : '';
|
const toFilterP = to ? `AND a.dt_pedido <= '${to}'` : '';
|
||||||
const safe = (s: string) => s.replace(/'/g, "''");
|
const safe = (s: string) => s.replace(/'/g, "''");
|
||||||
const searchFilter = search
|
const searchFilter = search
|
||||||
? `AND (r.numero_pedido::text ILIKE '%${safe(search)}%' OR LOWER(cl.nome) LIKE LOWER('%${safe(search)}%') OR LOWER(cl.razao) LIKE LOWER('%${safe(search)}%'))`
|
? `AND (r.numero_pedido::text ILIKE '%${safe(search)}%' OR LOWER(cl.nome) LIKE LOWER('%${safe(search)}%') OR LOWER(cl.razao) LIKE LOWER('%${safe(search)}%'))`
|
||||||
@@ -164,14 +164,14 @@ export class OrdersService {
|
|||||||
WITH all_rows AS (
|
WITH all_rows AS (
|
||||||
SELECT
|
SELECT
|
||||||
b.numero AS numero_pedido,
|
b.numero AS numero_pedido,
|
||||||
COALESCE(b.data, a.data) AS data,
|
COALESCE(b.dt_pedido, a.dt_pedido) AS data,
|
||||||
a.dtprevent AS dt_prevent,
|
a.dt_prevent,
|
||||||
a.data_emissao,
|
a.dt_emissao AS data_emissao,
|
||||||
a.situa,
|
a.situa,
|
||||||
NULL::text AS status_descr,
|
NULL::text AS status_descr,
|
||||||
a.clien::integer AS id_cliente,
|
a.id_cliente,
|
||||||
a.cod_vendedor,
|
a.cod_vendedor,
|
||||||
TRIM(c.descr) AS forma_pagamento,
|
a.forma_pagamento,
|
||||||
a.total::text,
|
a.total::text,
|
||||||
a.obs,
|
a.obs,
|
||||||
'E'::varchar(1) AS tipo,
|
'E'::varchar(1) AS tipo,
|
||||||
@@ -181,36 +181,33 @@ export class OrdersService {
|
|||||||
d.chave_acesso_nfe,
|
d.chave_acesso_nfe,
|
||||||
b.situa AS sit_ped,
|
b.situa AS sit_ped,
|
||||||
a.id_pedido
|
a.id_pedido
|
||||||
FROM sig.pedidos a
|
FROM sar.vw_pedidos_erp a
|
||||||
LEFT JOIN sig.pedidos b
|
LEFT JOIN sar.vw_pedidos_erp b
|
||||||
ON a.numer_ped_vinc = b.numero
|
ON a.numer_ped_vinc = b.numero
|
||||||
AND a.id_empresa = b.id_empresa
|
AND a.id_empresa = b.id_empresa
|
||||||
AND b.tipo = 'P'
|
AND b.tipo = 'P'
|
||||||
LEFT JOIN gestao.formapag c
|
LEFT JOIN sar.vw_nf d
|
||||||
ON c.id_empresa = ${idMatriz}
|
|
||||||
AND a.cod_formapag = c.codigo
|
|
||||||
LEFT JOIN gestao.nf d
|
|
||||||
ON a.numero = d.num_entrega
|
ON a.numero = d.num_entrega
|
||||||
AND d.id_empresa = ${idMatriz}
|
AND d.id_empresa = ${idMatriz}
|
||||||
WHERE a.id_empresa = ${idEmpresa}
|
WHERE a.id_empresa = ${idEmpresa}
|
||||||
AND a.tipo = 'E'
|
AND a.tipo = 'E'
|
||||||
${vendedorFilter}
|
${vendedorFilter}
|
||||||
AND b.id_pedido IS NOT NULL
|
AND b.id_pedido IS NOT NULL
|
||||||
AND COALESCE(a.data, b.data) >= '${dataHistoricoStr}'
|
AND COALESCE(a.dt_pedido, b.dt_pedido) >= '${dataHistoricoStr}'
|
||||||
${fromFilterE} ${toFilterE}
|
${fromFilterE} ${toFilterE}
|
||||||
|
|
||||||
UNION ALL
|
UNION ALL
|
||||||
|
|
||||||
SELECT
|
SELECT
|
||||||
a.numero AS numero_pedido,
|
a.numero AS numero_pedido,
|
||||||
a.data,
|
a.dt_pedido AS data,
|
||||||
a.dtprevent AS dt_prevent,
|
a.dt_prevent,
|
||||||
a.data_emissao,
|
a.dt_emissao AS data_emissao,
|
||||||
a.situa,
|
a.situa,
|
||||||
c.descricao AS status_descr,
|
c.descricao AS status_descr,
|
||||||
a.clien::integer AS id_cliente,
|
a.id_cliente,
|
||||||
a.cod_vendedor,
|
a.cod_vendedor,
|
||||||
TRIM(b.descr) AS forma_pagamento,
|
a.forma_pagamento,
|
||||||
a.total::text,
|
a.total::text,
|
||||||
a.obs,
|
a.obs,
|
||||||
'P'::varchar(1) AS tipo,
|
'P'::varchar(1) AS tipo,
|
||||||
@@ -220,10 +217,7 @@ export class OrdersService {
|
|||||||
NULL::varchar AS chave_acesso_nfe,
|
NULL::varchar AS chave_acesso_nfe,
|
||||||
NULL::integer AS sit_ped,
|
NULL::integer AS sit_ped,
|
||||||
a.id_pedido
|
a.id_pedido
|
||||||
FROM sig.pedidos a
|
FROM sar.vw_pedidos_erp a
|
||||||
LEFT JOIN gestao.formapag b
|
|
||||||
ON b.id_empresa = 1
|
|
||||||
AND a.cod_formapag = b.codigo
|
|
||||||
LEFT JOIN vw_sitpedido c
|
LEFT JOIN vw_sitpedido c
|
||||||
ON c.id_sitpedido = a.situa
|
ON c.id_sitpedido = a.situa
|
||||||
WHERE a.id_empresa = ${idEmpresa}
|
WHERE a.id_empresa = ${idEmpresa}
|
||||||
@@ -369,6 +363,7 @@ export class OrdersService {
|
|||||||
descontoPerc: dto.descontoPerc,
|
descontoPerc: dto.descontoPerc,
|
||||||
descontoValor: descontoValorGlobal,
|
descontoValor: descontoValorGlobal,
|
||||||
obs: dto.obs ?? null,
|
obs: dto.obs ?? null,
|
||||||
|
endEntrega: dto.endEntrega ?? null,
|
||||||
idempotencyKey: dto.idempotencyKey ?? null,
|
idempotencyKey: dto.idempotencyKey ?? null,
|
||||||
itens: { create: itemsData },
|
itens: { create: itemsData },
|
||||||
historico: {
|
historico: {
|
||||||
@@ -552,6 +547,44 @@ export class OrdersService {
|
|||||||
return this.mapDetail(final);
|
return this.mapDetail(final);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async cancel(id: string): Promise<PedidoDetail> {
|
||||||
|
const prisma = this.cls.get('prisma');
|
||||||
|
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||||
|
const idEmpresa = this.cls.get('idEmpresa');
|
||||||
|
const userId = this.cls.get('userId') ?? '0';
|
||||||
|
const codVendedor = parseInt(userId, 10);
|
||||||
|
|
||||||
|
// Rep só pode cancelar seu próprio orçamento (situa 0).
|
||||||
|
const pedido = await prisma.pedido.findFirst({ where: { id, idEmpresa, codVendedor } });
|
||||||
|
if (!pedido) throw new NotFoundException(`Pedido ${id} não encontrado`);
|
||||||
|
if (pedido.situa !== SITUA_ORCAMENTO)
|
||||||
|
throw new BadRequestException('Apenas orçamentos podem ser cancelados pelo representante');
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
|
||||||
|
await prisma.pedido.update({ where: { id }, data: { situa: SITUA_CANCELADO } });
|
||||||
|
await prisma.historicoPedido.create({
|
||||||
|
data: {
|
||||||
|
idPedido: id,
|
||||||
|
situaAnterior: SITUA_ORCAMENTO,
|
||||||
|
situaNova: SITUA_CANCELADO,
|
||||||
|
changedBy: codVendedor,
|
||||||
|
changedAt: now,
|
||||||
|
nota: 'Cancelado pelo representante',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const final = await prisma.pedido.findUniqueOrThrow({
|
||||||
|
where: { id },
|
||||||
|
include: {
|
||||||
|
itens: { orderBy: { ordem: 'asc' } },
|
||||||
|
historico: { orderBy: { changedAt: 'asc' } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.mapDetail(final);
|
||||||
|
}
|
||||||
|
|
||||||
// Resolve nome do cliente (nome + razão) e nome do representante a partir dos
|
// Resolve nome do cliente (nome + razão) e nome do representante a partir dos
|
||||||
// códigos, lendo das views do ERP. Usado no detalhe de pedidos SAR-nativos.
|
// códigos, lendo das views do ERP. Usado no detalhe de pedidos SAR-nativos.
|
||||||
private async lookupNames(
|
private async lookupNames(
|
||||||
@@ -602,6 +635,7 @@ export class OrdersService {
|
|||||||
aprovadoEm: Date | null;
|
aprovadoEm: Date | null;
|
||||||
motivoRecusa: string | null;
|
motivoRecusa: string | null;
|
||||||
obs: string | null;
|
obs: string | null;
|
||||||
|
endEntrega: string | null;
|
||||||
idempotencyKey: string | null;
|
idempotencyKey: string | null;
|
||||||
createdAt: Date;
|
createdAt: Date;
|
||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
@@ -639,6 +673,7 @@ export class OrdersService {
|
|||||||
total: decimalToString(o.total),
|
total: decimalToString(o.total),
|
||||||
descontoPerc: decimalToString(o.descontoPerc),
|
descontoPerc: decimalToString(o.descontoPerc),
|
||||||
obs: o.obs,
|
obs: o.obs,
|
||||||
|
endEntrega: o.endEntrega,
|
||||||
createdAt: o.createdAt.toISOString(),
|
createdAt: o.createdAt.toISOString(),
|
||||||
totalProdutos: decimalToString(o.totalProdutos),
|
totalProdutos: decimalToString(o.totalProdutos),
|
||||||
totalIpi: decimalToString(o.totalIpi),
|
totalIpi: decimalToString(o.totalIpi),
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -425,18 +425,23 @@ export function OrderDetailPage() {
|
|||||||
)}
|
)}
|
||||||
<Descriptions.Item label="Total produtos">{fmt(order.totalProdutos)}</Descriptions.Item>
|
<Descriptions.Item label="Total produtos">{fmt(order.totalProdutos)}</Descriptions.Item>
|
||||||
<Descriptions.Item label="Desc. Global">{order.descontoPerc}%</Descriptions.Item>
|
<Descriptions.Item label="Desc. Global">{order.descontoPerc}%</Descriptions.Item>
|
||||||
<Descriptions.Item label="Total">
|
<Descriptions.Item label="Total" span={isOrcamento ? 1 : 2}>
|
||||||
<Text strong style={{ fontSize: 16 }}>
|
<Text strong style={{ fontSize: 16 }}>
|
||||||
{fmt(order.total)}
|
{fmt(order.total)}
|
||||||
</Text>
|
</Text>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
|
{order.endEntrega && (
|
||||||
|
<Descriptions.Item label="End. Entrega" span={isOrcamento ? 3 : 2}>
|
||||||
|
{order.endEntrega}
|
||||||
|
</Descriptions.Item>
|
||||||
|
)}
|
||||||
{order.obs && (
|
{order.obs && (
|
||||||
<Descriptions.Item label="Observações" span={2}>
|
<Descriptions.Item label="Observações" span={isOrcamento ? 3 : 2}>
|
||||||
{order.obs}
|
{order.obs}
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
{order.motivoRecusa && (
|
{order.motivoRecusa && (
|
||||||
<Descriptions.Item label="Motivo Recusa" span={2}>
|
<Descriptions.Item label="Motivo Recusa" span={isOrcamento ? 3 : 2}>
|
||||||
<Text type="danger">{order.motivoRecusa}</Text>
|
<Text type="danger">{order.motivoRecusa}</Text>
|
||||||
</Descriptions.Item>
|
</Descriptions.Item>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -399,11 +399,23 @@ export function OrderPrintPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Observações + rodapé ────────────────────────────────────────── */}
|
{/* ── Endereço de entrega + Observações ───────────────────────────── */}
|
||||||
{order.obs && (
|
{(order.endEntrega || order.obs) && (
|
||||||
<div style={{ padding: '10px 28px 0' }}>
|
<div style={{ padding: '10px 28px 0', display: 'flex', gap: 24, flexWrap: 'wrap' }}>
|
||||||
<span style={label}>Observações</span>
|
{order.endEntrega && (
|
||||||
<div style={{ fontSize: 11, color: '#475569', lineHeight: 1.5 }}>{order.obs}</div>
|
<div style={{ flex: 1, minWidth: 220 }}>
|
||||||
|
<span style={label}>Endereço de entrega</span>
|
||||||
|
<div style={{ fontSize: 11, color: '#475569', lineHeight: 1.5 }}>
|
||||||
|
{order.endEntrega}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{order.obs && (
|
||||||
|
<div style={{ flex: 1, minWidth: 220 }}>
|
||||||
|
<span style={label}>Observações</span>
|
||||||
|
<div style={{ fontSize: 11, color: '#475569', lineHeight: 1.5 }}>{order.obs}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useState, useMemo } from 'react';
|
import { useState, useMemo } from 'react';
|
||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
import {
|
import {
|
||||||
App,
|
App,
|
||||||
Button,
|
Button,
|
||||||
@@ -43,6 +44,7 @@ import { SITUA_LABEL } from '@sar/api-interface';
|
|||||||
import { useOrderList, useOrderDetail } from '../../lib/queries/orders';
|
import { useOrderList, useOrderDetail } from '../../lib/queries/orders';
|
||||||
import { usePendingOrders } from '../../lib/hooks/usePendingOrders';
|
import { usePendingOrders } from '../../lib/hooks/usePendingOrders';
|
||||||
import { removePendingOrder, retryPendingOrder } from '../../lib/offline/order-queue';
|
import { removePendingOrder, retryPendingOrder } from '../../lib/offline/order-queue';
|
||||||
|
import { apiFetch } from '../../lib/api-client';
|
||||||
|
|
||||||
const { Title, Text } = Typography;
|
const { Title, Text } = Typography;
|
||||||
const { useBreakpoint } = Grid;
|
const { useBreakpoint } = Grid;
|
||||||
@@ -250,6 +252,19 @@ function OrderActionsMenu({
|
|||||||
onView: (id: string) => void;
|
onView: (id: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const qc = useQueryClient();
|
||||||
|
const { modal, message: msg } = App.useApp();
|
||||||
|
|
||||||
|
const cancelMutation = useMutation({
|
||||||
|
mutationFn: () => apiFetch(`/orders/${order.id}/cancel`, { method: 'PATCH' }),
|
||||||
|
onSuccess: () => {
|
||||||
|
void msg.success('Orçamento cancelado.');
|
||||||
|
void qc.invalidateQueries({ queryKey: ['orders'] });
|
||||||
|
},
|
||||||
|
onError: (e: unknown) => void msg.error(e instanceof Error ? e.message : 'Erro ao cancelar'),
|
||||||
|
});
|
||||||
|
|
||||||
|
const canCancel = order.situa === 0 && order.fonte !== 'erp';
|
||||||
|
|
||||||
const items: MenuProps['items'] = [
|
const items: MenuProps['items'] = [
|
||||||
{
|
{
|
||||||
@@ -287,8 +302,18 @@ function OrderActionsMenu({
|
|||||||
icon: <CloseCircleOutlined />,
|
icon: <CloseCircleOutlined />,
|
||||||
label: 'Cancelar pedido',
|
label: 'Cancelar pedido',
|
||||||
danger: true,
|
danger: true,
|
||||||
disabled: order.situa === 3,
|
disabled: !canCancel,
|
||||||
onClick: () => alert('Cancelamento em breve'),
|
onClick: () => {
|
||||||
|
if (!canCancel) return;
|
||||||
|
void modal.confirm({
|
||||||
|
title: 'Cancelar orçamento?',
|
||||||
|
content: `O pedido ${order.numPedSar} será marcado como cancelado. Esta ação não pode ser desfeita.`,
|
||||||
|
okText: 'Cancelar pedido',
|
||||||
|
okButtonProps: { danger: true },
|
||||||
|
cancelText: 'Voltar',
|
||||||
|
onOk: () => cancelMutation.mutate(),
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -387,7 +412,7 @@ function OrderDetailModal({ id, onClose }: { id: string | null; onClose: () => v
|
|||||||
{isLoading && <Spin style={{ display: 'block', margin: '48px auto' }} />}
|
{isLoading && <Spin style={{ display: 'block', margin: '48px auto' }} />}
|
||||||
|
|
||||||
{data && (
|
{data && (
|
||||||
<Space direction="vertical" size={20} style={{ width: '100%' }}>
|
<Space orientation="vertical" size={20} style={{ width: '100%' }}>
|
||||||
{/* Status */}
|
{/* Status */}
|
||||||
<div>
|
<div>
|
||||||
<OrderStatusBadge situa={data.situa} descr={data.statusDescr} />
|
<OrderStatusBadge situa={data.situa} descr={data.statusDescr} />
|
||||||
@@ -434,6 +459,12 @@ function OrderDetailModal({ id, onClose }: { id: string | null; onClose: () => v
|
|||||||
<span style={label}>Representante</span>
|
<span style={label}>Representante</span>
|
||||||
<Text>{data.nomeVendedor ?? `Cód. ${data.codVendedor}`}</Text>
|
<Text>{data.nomeVendedor ?? `Cód. ${data.codVendedor}`}</Text>
|
||||||
</Col>
|
</Col>
|
||||||
|
{data.endEntrega && (
|
||||||
|
<Col span={24}>
|
||||||
|
<span style={label}>End. Entrega</span>
|
||||||
|
<Text type="secondary">{data.endEntrega}</Text>
|
||||||
|
</Col>
|
||||||
|
)}
|
||||||
{data.obs && (
|
{data.obs && (
|
||||||
<Col span={24}>
|
<Col span={24}>
|
||||||
<span style={label}>Observações</span>
|
<span style={label}>Observações</span>
|
||||||
@@ -460,7 +491,7 @@ function OrderDetailModal({ id, onClose }: { id: string | null; onClose: () => v
|
|||||||
title: 'Produto',
|
title: 'Produto',
|
||||||
key: 'produto',
|
key: 'produto',
|
||||||
render: (_: unknown, item) => (
|
render: (_: unknown, item) => (
|
||||||
<Space direction="vertical" size={0}>
|
<Space orientation="vertical" size={0}>
|
||||||
<Text style={{ fontSize: 11, color: '#94A3B8' }}>{item.codProduto}</Text>
|
<Text style={{ fontSize: 11, color: '#94A3B8' }}>{item.codProduto}</Text>
|
||||||
<Text style={{ fontWeight: 500 }}>{item.descProduto}</Text>
|
<Text style={{ fontWeight: 500 }}>{item.descProduto}</Text>
|
||||||
</Space>
|
</Space>
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export type ContatoResult = z.infer<typeof ContatoResultSchema>;
|
|||||||
|
|
||||||
export const TopProdutoSchema = z.object({
|
export const TopProdutoSchema = z.object({
|
||||||
idProduto: z.number().int(),
|
idProduto: z.number().int(),
|
||||||
|
codProduto: z.string(),
|
||||||
descricao: z.string(),
|
descricao: z.string(),
|
||||||
unidade: z.string().nullable(),
|
unidade: z.string().nullable(),
|
||||||
qtdTotal: z.number(),
|
qtdTotal: z.number(),
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ export const PedidoDetailSchema = PedidoSummarySchema.extend({
|
|||||||
comissao: z.string(),
|
comissao: z.string(),
|
||||||
pedFlex: z.string(),
|
pedFlex: z.string(),
|
||||||
formaPagamento: z.string().nullable().optional(),
|
formaPagamento: z.string().nullable().optional(),
|
||||||
|
endEntrega: z.string().nullable().optional(),
|
||||||
aprovadoPor: z.number().int().nullable(),
|
aprovadoPor: z.number().int().nullable(),
|
||||||
aprovadoEm: z.iso.datetime().nullable(),
|
aprovadoEm: z.iso.datetime().nullable(),
|
||||||
motivoRecusa: z.string().nullable(),
|
motivoRecusa: z.string().nullable(),
|
||||||
@@ -138,6 +139,7 @@ export const CreatePedidoSchema = z.object({
|
|||||||
idPauta: z.number().int().optional(),
|
idPauta: z.number().int().optional(),
|
||||||
codFormapag: z.number().int().optional(),
|
codFormapag: z.number().int().optional(),
|
||||||
obs: z.string().optional(),
|
obs: z.string().optional(),
|
||||||
|
endEntrega: z.string().optional(),
|
||||||
idempotencyKey: z.string().optional(),
|
idempotencyKey: z.string().optional(),
|
||||||
itens: z.array(CreatePedidoItemSchema).min(1),
|
itens: z.array(CreatePedidoItemSchema).min(1),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user