feat(api,web): logo da empresa na impressao e cabecalho A4 compacto
- Tabela sar.config_empresa (logo em data URL por empresa matriz) com migration idempotente, espelho no schema SQL e GRANT no provision - PUT /catalog/company/logo (gerente/admin) e logoBase64 no GET /catalog/company - vale para a impressao de todos os representantes - Nova tela /ger/config "Configuracoes" com upload/preview/remocao do logo (PNG/JPG/WebP ate 500KB) - Cabecalho da impressao redesenhado: logo a esquerda, dados da empresa condensados em 3 linhas, numero/status/data compactos a direita - ocupa ~metade da altura anterior no A4 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
-- CreateTable: sar.config_empresa (configuracoes por empresa - logo da impressao)
|
||||
-- logo_base64: data URL (data:image/png;base64,...) usado no cabecalho do pedido.
|
||||
-- Definicao espelha scripts/sar-erp-schema.sql: provision-client.ts roda o schema SQL
|
||||
-- antes de `prisma migrate deploy`, entao esta migration precisa ser idempotente.
|
||||
-- ATENCAO: bancos ERP usam LATIN1 - nao usar em-dash nem box-drawing aqui.
|
||||
CREATE TABLE IF NOT EXISTS sar.config_empresa (
|
||||
id_empresa INTEGER PRIMARY KEY,
|
||||
logo_base64 TEXT,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
|
||||
);
|
||||
@@ -195,6 +195,19 @@ model RegraDesconto {
|
||||
@@map("regras_desconto")
|
||||
}
|
||||
|
||||
// ─── ConfigEmpresa ───────────────────────────────────────────────────────────
|
||||
//
|
||||
// Configurações por empresa. logo_base64: data URL exibido no cabeçalho da
|
||||
// impressão do pedido de todos os representantes.
|
||||
|
||||
model ConfigEmpresa {
|
||||
idEmpresa Int @id @map("id_empresa")
|
||||
logoBase64 String? @map("logo_base64")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
@@map("config_empresa")
|
||||
}
|
||||
|
||||
// ─── Oportunidade (Funil de Vendas) ──────────────────────────────────────────
|
||||
//
|
||||
// Oportunidade de venda do representante. Pode referenciar cliente ERP (id_cliente)
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import { Controller, Get, NotFoundException, Param, ParseIntPipe, Query } from '@nestjs/common';
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
NotFoundException,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Put,
|
||||
Query,
|
||||
} from '@nestjs/common';
|
||||
import { createZodDto } from 'nestjs-zod';
|
||||
import {
|
||||
ProdutoListQuerySchema,
|
||||
UpdateLogoBodySchema,
|
||||
type EmpresaInfo,
|
||||
type UpdateLogoBody,
|
||||
type FormaPagamento,
|
||||
type Municipio,
|
||||
type Pauta,
|
||||
@@ -13,6 +25,7 @@ import {
|
||||
import { CatalogService } from './catalog.service';
|
||||
|
||||
class ProdutoListQueryDto extends createZodDto(ProdutoListQuerySchema) {}
|
||||
class UpdateLogoDto extends createZodDto(UpdateLogoBodySchema) {}
|
||||
|
||||
// ADR 0006 revogado: UUID → Int para ID de produto. Sync removido (ERP direto via view).
|
||||
|
||||
@@ -40,6 +53,12 @@ export class CatalogController {
|
||||
return this.catalog.company();
|
||||
}
|
||||
|
||||
@Put('company/logo')
|
||||
@HttpCode(204)
|
||||
updateLogo(@Body() body: UpdateLogoDto): Promise<void> {
|
||||
return this.catalog.updateLogo(body as unknown as UpdateLogoBody);
|
||||
}
|
||||
|
||||
@Get()
|
||||
list(@Query() query: ProdutoListQueryDto): Promise<ProdutoListResponse> {
|
||||
const parsed = ProdutoListQuerySchema.parse(query) as ProdutoListQuery;
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { ClsService } from 'nestjs-cls';
|
||||
import type {
|
||||
EmpresaInfo,
|
||||
UpdateLogoBody,
|
||||
FormaPagamento,
|
||||
Municipio,
|
||||
Pauta,
|
||||
@@ -101,6 +102,8 @@ export class CatalogService {
|
||||
const r = rows[0];
|
||||
if (!r) throw new Error(`Empresa matriz ${idEmpresa} não encontrada`);
|
||||
|
||||
const config = await prisma.configEmpresa.findUnique({ where: { idEmpresa } });
|
||||
|
||||
const clean = (v: string | null) => {
|
||||
const t = (v ?? '').trim();
|
||||
return t === '' ? null : t;
|
||||
@@ -120,9 +123,28 @@ export class CatalogService {
|
||||
cep: clean(r.cep),
|
||||
telefone: clean(r.telefone),
|
||||
email: clean(r.email),
|
||||
logoBase64: config?.logoBase64 ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
// Logo do cabeçalho da impressão — só gerente/admin altera; vale para todos
|
||||
// os representantes da empresa (gravado na matriz, mesma fonte do company()).
|
||||
async updateLogo(body: UpdateLogoBody): Promise<void> {
|
||||
const role = this.cls.get('role') ?? 'rep';
|
||||
if (role !== 'manager' && role !== 'admin') {
|
||||
throw new ForbiddenException('Apenas gerentes podem alterar o logo da empresa');
|
||||
}
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
const idEmpresa = matrizEmpresa(this.cls.get('idEmpresa'));
|
||||
|
||||
await prisma.configEmpresa.upsert({
|
||||
where: { idEmpresa },
|
||||
create: { idEmpresa, logoBase64: body.logoBase64 },
|
||||
update: { logoBase64: body.logoBase64 },
|
||||
});
|
||||
}
|
||||
|
||||
async municipios(q?: string): Promise<Municipio[]> {
|
||||
const prisma = this.cls.get('prisma');
|
||||
if (!prisma) throw new Error('prisma não disponível no CLS');
|
||||
|
||||
Reference in New Issue
Block a user