feat(reports): add SIGNATURE section type inserting user default signature into docx
This commit is contained in:
parent
cc317e274b
commit
c00a5ff4b7
8 changed files with 209 additions and 27 deletions
|
|
@ -85,6 +85,7 @@ public class ReportUseCaseConfig {
|
|||
ProjectRepository projectRepository,
|
||||
UserRepository userRepository,
|
||||
StorageRepository storageRepository,
|
||||
net.beetechgroup.beetask.usecase.repository.UserSignatureRepository userSignatureRepository,
|
||||
AuthorizeOrganizationMemberUseCase authorizer
|
||||
) {
|
||||
return new GenerateReportUseCase(
|
||||
|
|
@ -95,6 +96,7 @@ public class ReportUseCaseConfig {
|
|||
projectRepository,
|
||||
userRepository,
|
||||
storageRepository,
|
||||
userSignatureRepository,
|
||||
authorizer
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
package net.beetechgroup.beetask.usecase.report.docengine;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.apache.poi.xwpf.usermodel.IBodyElement;
|
||||
import org.apache.poi.util.Units;
|
||||
import org.apache.poi.xwpf.usermodel.Document;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFHeader;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFFooter;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFHeader;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
||||
|
|
@ -43,30 +46,39 @@ public class DocxReportEngine {
|
|||
InputStream templateStream,
|
||||
Map<String, String> variables,
|
||||
Map<String, List<TaskItem>> listSections
|
||||
) throws Exception {
|
||||
return generate(templateStream, variables, listSections, Collections.emptyMap());
|
||||
}
|
||||
|
||||
public static byte[] generate(
|
||||
InputStream templateStream,
|
||||
Map<String, String> variables,
|
||||
Map<String, List<TaskItem>> listSections,
|
||||
Map<String, byte[]> images
|
||||
) throws Exception {
|
||||
try (XWPFDocument doc = new XWPFDocument(templateStream)) {
|
||||
// 1. Processar Tabelas Dinâmicas primeiro (linhas com tags de lista)
|
||||
processTables(doc, variables, listSections);
|
||||
// 1. Processar Tabelas Dinâmicas primeiro (linhas com tags de lista ou campos)
|
||||
processTables(doc, variables, listSections, images);
|
||||
|
||||
// 2. Processar Parágrafos do corpo do documento
|
||||
processParagraphs(doc, variables, listSections);
|
||||
processParagraphs(doc, variables, listSections, images);
|
||||
|
||||
// 3. Processar Cabeçalhos e Rodapés
|
||||
for (XWPFHeader header : doc.getHeaderList()) {
|
||||
for (XWPFParagraph p : header.getParagraphs()) {
|
||||
replaceTextInParagraph(p, variables, null);
|
||||
replaceTextInParagraph(p, variables, null, images);
|
||||
}
|
||||
for (XWPFTable table : header.getTables()) {
|
||||
replaceTextInTableCells(table, variables);
|
||||
replaceTextInTableCells(table, variables, images);
|
||||
}
|
||||
}
|
||||
|
||||
for (XWPFFooter footer : doc.getFooterList()) {
|
||||
for (XWPFParagraph p : footer.getParagraphs()) {
|
||||
replaceTextInParagraph(p, variables, null);
|
||||
replaceTextInParagraph(p, variables, null, images);
|
||||
}
|
||||
for (XWPFTable table : footer.getTables()) {
|
||||
replaceTextInTableCells(table, variables);
|
||||
replaceTextInTableCells(table, variables, images);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -79,18 +91,20 @@ public class DocxReportEngine {
|
|||
private static void processParagraphs(
|
||||
XWPFDocument doc,
|
||||
Map<String, String> variables,
|
||||
Map<String, List<TaskItem>> listSections
|
||||
Map<String, List<TaskItem>> listSections,
|
||||
Map<String, byte[]> images
|
||||
) {
|
||||
List<XWPFParagraph> paragraphs = new ArrayList<>(doc.getParagraphs());
|
||||
for (XWPFParagraph p : paragraphs) {
|
||||
replaceTextInParagraph(p, variables, listSections);
|
||||
replaceTextInParagraph(p, variables, listSections, images);
|
||||
}
|
||||
}
|
||||
|
||||
private static void processTables(
|
||||
XWPFDocument doc,
|
||||
Map<String, String> variables,
|
||||
Map<String, List<TaskItem>> listSections
|
||||
Map<String, List<TaskItem>> listSections,
|
||||
Map<String, byte[]> images
|
||||
) {
|
||||
for (XWPFTable table : doc.getTables()) {
|
||||
List<XWPFTableRow> rows = new ArrayList<>(table.getRows());
|
||||
|
|
@ -132,10 +146,10 @@ public class DocxReportEngine {
|
|||
table.removeRow(r);
|
||||
r += items.size() - 1; // Ajustar índice
|
||||
} else {
|
||||
// Linha normal: substituir variáveis simples
|
||||
// Linha normal: substituir variáveis simples e imagens/assinaturas
|
||||
for (XWPFTableCell cell : row.getTableCells()) {
|
||||
for (XWPFParagraph p : cell.getParagraphs()) {
|
||||
replaceTextInParagraph(p, variables, null);
|
||||
replaceTextInParagraph(p, variables, null, images);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -143,11 +157,11 @@ public class DocxReportEngine {
|
|||
}
|
||||
}
|
||||
|
||||
private static void replaceTextInTableCells(XWPFTable table, Map<String, String> variables) {
|
||||
private static void replaceTextInTableCells(XWPFTable table, Map<String, String> variables, Map<String, byte[]> images) {
|
||||
for (XWPFTableRow row : table.getRows()) {
|
||||
for (XWPFTableCell cell : row.getTableCells()) {
|
||||
for (XWPFParagraph p : cell.getParagraphs()) {
|
||||
replaceTextInParagraph(p, variables, null);
|
||||
replaceTextInParagraph(p, variables, null, images);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -156,7 +170,8 @@ public class DocxReportEngine {
|
|||
private static void replaceTextInParagraph(
|
||||
XWPFParagraph p,
|
||||
Map<String, String> variables,
|
||||
Map<String, List<TaskItem>> listSections
|
||||
Map<String, List<TaskItem>> listSections,
|
||||
Map<String, byte[]> images
|
||||
) {
|
||||
String text = p.getText();
|
||||
if (text == null || !text.contains("{{")) {
|
||||
|
|
@ -196,7 +211,27 @@ public class DocxReportEngine {
|
|||
}
|
||||
}
|
||||
|
||||
// 2. Substituir variáveis simples ou campos de texto livre
|
||||
// 2. Processar imagens / assinaturas (ex: {{assinatura}}, {{assinatura_responsavel}})
|
||||
if (images != null && !images.isEmpty()) {
|
||||
for (Map.Entry<String, byte[]> imgEntry : images.entrySet()) {
|
||||
String tag = "{{" + imgEntry.getKey() + "}}";
|
||||
if (text.contains(tag)) {
|
||||
byte[] imgBytes = imgEntry.getValue();
|
||||
if (imgBytes != null && imgBytes.length > 0) {
|
||||
insertImageIntoParagraph(p, tag, imgBytes);
|
||||
text = p.getText();
|
||||
if (text == null || !text.contains("{{")) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// "se nao tem, nao assina": remove a tag para não ficar visível no documento
|
||||
text = text.replace(tag, "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Substituir variáveis simples ou campos de texto livre
|
||||
String replaced = text;
|
||||
if (variables != null) {
|
||||
for (Map.Entry<String, String> var : variables.entrySet()) {
|
||||
|
|
@ -208,7 +243,7 @@ public class DocxReportEngine {
|
|||
}
|
||||
|
||||
// Se o texto mudou, reaplicamos no parágrafo preservando o estilo original do run 0
|
||||
if (!replaced.equals(text)) {
|
||||
if (!replaced.equals(p.getText())) {
|
||||
String font = null;
|
||||
int fontSize = -1;
|
||||
String color = null;
|
||||
|
|
@ -244,6 +279,40 @@ public class DocxReportEngine {
|
|||
}
|
||||
}
|
||||
|
||||
private static void insertImageIntoParagraph(XWPFParagraph p, String tag, byte[] imgBytes) {
|
||||
String fullText = p.getText();
|
||||
if (fullText == null || !fullText.contains(tag)) return;
|
||||
|
||||
int tagIndex = fullText.indexOf(tag);
|
||||
String before = fullText.substring(0, tagIndex);
|
||||
String after = fullText.substring(tagIndex + tag.length());
|
||||
|
||||
clearRuns(p);
|
||||
|
||||
if (!before.isEmpty()) {
|
||||
XWPFRun rBefore = p.createRun();
|
||||
rBefore.setText(before);
|
||||
}
|
||||
|
||||
XWPFRun imgRun = p.createRun();
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(imgBytes)) {
|
||||
int format = Document.PICTURE_TYPE_PNG;
|
||||
if (imgBytes.length > 2 && (imgBytes[0] & 0xFF) == 0xFF && (imgBytes[1] & 0xFF) == 0xD8) {
|
||||
format = Document.PICTURE_TYPE_JPEG;
|
||||
}
|
||||
int width = Units.toEMU(160);
|
||||
int height = Units.toEMU(60);
|
||||
imgRun.addPicture(bais, format, "signature.png", width, height);
|
||||
} catch (Exception e) {
|
||||
LOGGER.errorf("Erro ao inserir imagem no docx: %s", e.getMessage());
|
||||
}
|
||||
|
||||
if (!after.isEmpty()) {
|
||||
XWPFRun rAfter = p.createRun();
|
||||
rAfter.setText(after);
|
||||
}
|
||||
}
|
||||
|
||||
private static String replaceItemTags(String cellText, TaskItem item) {
|
||||
if (cellText == null) return "";
|
||||
return cellText
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import java.util.Objects;
|
|||
import java.util.UUID;
|
||||
import net.beetechgroup.beetask.entities.Project;
|
||||
import net.beetechgroup.beetask.entities.User;
|
||||
import net.beetechgroup.beetask.entities.UserSignature;
|
||||
import net.beetechgroup.beetask.entities.organization.Organization;
|
||||
import net.beetechgroup.beetask.entities.report.GeneratedReport;
|
||||
import net.beetechgroup.beetask.entities.report.ReportSection;
|
||||
|
|
@ -33,6 +34,7 @@ import net.beetechgroup.beetask.usecase.repository.ReportTemplateRepository;
|
|||
import net.beetechgroup.beetask.usecase.repository.StorageRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||
import net.beetechgroup.beetask.usecase.repository.UserSignatureRepository;
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
public class GenerateReportUseCase {
|
||||
|
|
@ -47,6 +49,7 @@ public class GenerateReportUseCase {
|
|||
private final ProjectRepository projectRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final StorageRepository storageRepository;
|
||||
private final UserSignatureRepository userSignatureRepository;
|
||||
private final AuthorizeOrganizationMemberUseCase authorizer;
|
||||
|
||||
public GenerateReportUseCase(
|
||||
|
|
@ -57,6 +60,7 @@ public class GenerateReportUseCase {
|
|||
ProjectRepository projectRepository,
|
||||
UserRepository userRepository,
|
||||
StorageRepository storageRepository,
|
||||
UserSignatureRepository userSignatureRepository,
|
||||
AuthorizeOrganizationMemberUseCase authorizer
|
||||
) {
|
||||
this.reportTemplateRepository = reportTemplateRepository;
|
||||
|
|
@ -66,6 +70,7 @@ public class GenerateReportUseCase {
|
|||
this.projectRepository = projectRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.storageRepository = storageRepository;
|
||||
this.userSignatureRepository = userSignatureRepository;
|
||||
this.authorizer = authorizer;
|
||||
}
|
||||
|
||||
|
|
@ -251,16 +256,66 @@ public class GenerateReportUseCase {
|
|||
}
|
||||
}
|
||||
|
||||
// 5. Baixar o template do MinIO e gerar o documento preenchido
|
||||
// 5. Carregar assinatura padrão do usuário (se houver)
|
||||
byte[] defaultSignatureBytes = null;
|
||||
if (userSignatureRepository != null) {
|
||||
List<UserSignature> userSignatures = userSignatureRepository.findByUserId(user.getId());
|
||||
UserSignature defaultSig = userSignatures.stream()
|
||||
.filter(UserSignature::isDefault)
|
||||
.findFirst()
|
||||
.or(() -> userSignatures.stream().findFirst())
|
||||
.orElse(null);
|
||||
|
||||
if (defaultSig != null && defaultSig.getStorageKey() != null) {
|
||||
try (InputStream sigStream = storageRepository.download(defaultSig.getStorageKey())) {
|
||||
if (sigStream != null) {
|
||||
defaultSignatureBytes = sigStream.readAllBytes();
|
||||
LOGGER.infof("Assinatura padrão do usuário %s carregada com sucesso (%d bytes)", user.getEmail(), defaultSignatureBytes.length);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
LOGGER.warnf("Não foi possível carregar imagem da assinatura padrão: %s", e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mapear imagens e assinaturas para inserção no Word
|
||||
Map<String, byte[]> images = new HashMap<>();
|
||||
|
||||
if (defaultSignatureBytes != null) {
|
||||
images.put("assinatura", defaultSignatureBytes);
|
||||
images.put("assinatura_usuario", defaultSignatureBytes);
|
||||
images.put("assinatura_colaborador", defaultSignatureBytes);
|
||||
} else {
|
||||
// "se nao tem, nao assina": remove as tags do documento
|
||||
variables.put("assinatura", "");
|
||||
variables.put("assinatura_usuario", "");
|
||||
variables.put("assinatura_colaborador", "");
|
||||
}
|
||||
|
||||
// Processar seções configuradas no modelo do tipo SIGNATURE ou IMAGE
|
||||
if (template.getSections() != null) {
|
||||
for (ReportSection section : template.getSections()) {
|
||||
if ("SIGNATURE".equalsIgnoreCase(section.getSectionType()) || "IMAGE".equalsIgnoreCase(section.getSectionType())) {
|
||||
if (defaultSignatureBytes != null) {
|
||||
images.put(section.getSectionKey(), defaultSignatureBytes);
|
||||
} else {
|
||||
// "se nao tem, nao assina": substitui por vazio
|
||||
variables.put(section.getSectionKey(), "");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Baixar o template do MinIO e gerar o documento preenchido
|
||||
byte[] docxBytes;
|
||||
try (InputStream templateStream = storageRepository.download(template.getStorageKey())) {
|
||||
docxBytes = DocxReportEngine.generate(templateStream, variables, listSections);
|
||||
docxBytes = DocxReportEngine.generate(templateStream, variables, listSections, images);
|
||||
} catch (Exception e) {
|
||||
LOGGER.errorf("Erro ao processar template .docx %d: %s", template.getId(), e.getMessage());
|
||||
throw new RuntimeException("Falha ao gerar o documento Word: " + e.getMessage(), e);
|
||||
}
|
||||
|
||||
// 6. Fazer upload do relatório gerado para o MinIO
|
||||
// 7. Fazer upload do relatório gerado para o MinIO
|
||||
String cleanTitle = (template.getName() + "_" + strStart.replace("/", "-") + "_" + strEnd.replace("/", "-"))
|
||||
.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||
String generatedFileName = cleanTitle + ".docx";
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
Calendar,
|
||||
Sparkles,
|
||||
Info,
|
||||
PenTool,
|
||||
} from 'lucide-react'
|
||||
import { CustomSelect } from '../ui/Select'
|
||||
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
||||
|
|
@ -28,7 +29,7 @@ interface CreateTemplateModalProps {
|
|||
interface SectionFormItem {
|
||||
sectionKey: string
|
||||
title: string
|
||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE'
|
||||
defaultValue?: string
|
||||
dateFormat?: string
|
||||
filterCategoryId?: number
|
||||
|
|
@ -376,19 +377,33 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
|||
value={section.sectionType}
|
||||
onChange={(val) =>
|
||||
handleUpdateSection(idx, {
|
||||
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST',
|
||||
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE',
|
||||
})
|
||||
}
|
||||
options={[
|
||||
{ value: 'FREE_TEXT', label: 'Texto Livre (Preenchido na emissão)' },
|
||||
{ value: 'FIXED_TEXT', label: 'Texto Fixo (Pré-preenchido no cadastro)' },
|
||||
{ value: 'DATE', label: 'Data (Com formato personalizado)' },
|
||||
{ value: 'SIGNATURE', label: '✍️ Assinatura Digital (Assinatura do usuário)' },
|
||||
{ value: 'TASK_LIST', label: 'Lista de Tarefas do Período' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuração de Campo de Tipo ASSINATURA */}
|
||||
{section.sectionType === 'SIGNATURE' && (
|
||||
<div className="p-3 rounded-xl bg-brand/5 border border-brand/15 text-[11px] text-foreground/80 flex items-start gap-2.5 animate-in fade-in duration-150">
|
||||
<PenTool className="w-4 h-4 text-brand shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-bold text-brand">Preenchimento Automático com Assinatura Digital</p>
|
||||
<p className="text-muted mt-0.5">
|
||||
O BeeTask buscará automaticamente a <strong>assinatura padrão</strong> cadastrada no perfil do usuário que gerar o relatório e inserirá a imagem no Word na tag <code className="text-brand font-mono font-bold">{`{{${section.sectionKey || 'assinatura'}}}`}</code>. Se o usuário não tiver assinatura cadastrada, o campo ficará em branco (não assina).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Configuração de Campo de Tipo DATA */}
|
||||
{section.sectionType === 'DATE' && (
|
||||
<div className="pt-2 border-t border-border-soft/60 space-y-2 animate-in fade-in duration-150">
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
Sparkles,
|
||||
Info,
|
||||
Check,
|
||||
PenTool,
|
||||
} from 'lucide-react'
|
||||
import { CustomSelect } from '../ui/Select'
|
||||
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
||||
|
|
@ -30,7 +31,7 @@ interface EditTemplateModalProps {
|
|||
interface SectionFormItem {
|
||||
sectionKey: string
|
||||
title: string
|
||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE'
|
||||
defaultValue?: string
|
||||
dateFormat?: string
|
||||
filterCategoryId?: number
|
||||
|
|
@ -398,19 +399,33 @@ export const EditTemplateModal: React.FC<EditTemplateModalProps> = ({
|
|||
value={section.sectionType}
|
||||
onChange={(val) =>
|
||||
handleUpdateSection(idx, {
|
||||
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST',
|
||||
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE',
|
||||
})
|
||||
}
|
||||
options={[
|
||||
{ value: 'FREE_TEXT', label: 'Texto Livre (Preenchido na emissão)' },
|
||||
{ value: 'FIXED_TEXT', label: 'Texto Fixo (Pré-preenchido no cadastro)' },
|
||||
{ value: 'DATE', label: 'Data (Com formato personalizado)' },
|
||||
{ value: 'SIGNATURE', label: '✍️ Assinatura Digital (Assinatura do usuário)' },
|
||||
{ value: 'TASK_LIST', label: 'Lista de Tarefas do Período' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Configuração de Campo de Tipo ASSINATURA */}
|
||||
{section.sectionType === 'SIGNATURE' && (
|
||||
<div className="p-3 rounded-xl bg-brand/5 border border-brand/15 text-[11px] text-foreground/80 flex items-start gap-2.5 animate-in fade-in duration-150">
|
||||
<PenTool className="w-4 h-4 text-brand shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<p className="font-bold text-brand">Preenchimento Automático com Assinatura Digital</p>
|
||||
<p className="text-muted mt-0.5">
|
||||
O BeeTask buscará automaticamente a <strong>assinatura padrão</strong> cadastrada no perfil do usuário que gerar o relatório e inserirá a imagem no Word na tag <code className="text-brand font-mono font-bold">{`{{${section.sectionKey || 'assinatura'}}}`}</code>. Se o usuário não tiver assinatura cadastrada, o campo ficará em branco (não assina).
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Configuração de Campo de Tipo DATA */}
|
||||
{section.sectionType === 'DATE' && (
|
||||
<div className="pt-2 border-t border-border-soft/60 space-y-2 animate-in fade-in duration-150">
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Loader2,
|
||||
Lock,
|
||||
Tag,
|
||||
PenTool,
|
||||
} from 'lucide-react'
|
||||
import { CustomSelect } from '../ui/Select'
|
||||
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
||||
|
|
@ -107,6 +108,12 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
|||
return selectedTemplate.sections.filter((s) => s.sectionType === 'TASK_LIST')
|
||||
}, [selectedTemplate])
|
||||
|
||||
// Lista de seções de assinatura do template selecionado
|
||||
const signatureSections = useMemo(() => {
|
||||
if (!selectedTemplate) return []
|
||||
return selectedTemplate.sections.filter((s) => s.sectionType === 'SIGNATURE')
|
||||
}, [selectedTemplate])
|
||||
|
||||
// Preset Handlers
|
||||
const handlePresetChange = (preset: 'current_month' | 'last_month' | 'custom') => {
|
||||
setPeriodPreset(preset)
|
||||
|
|
@ -515,6 +522,24 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Seções de Assinatura Digital do Modelo */}
|
||||
{signatureSections.length > 0 && (
|
||||
<div className="p-4 rounded-2xl bg-surface-raised/40 border border-border-soft space-y-2">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-foreground flex items-center gap-1.5">
|
||||
<PenTool className="w-4 h-4 text-brand" />
|
||||
Assinatura Digital no Relatório
|
||||
</span>
|
||||
<span className="text-[10px] text-muted font-mono font-semibold">
|
||||
{signatureSections.map((s) => `{{${s.sectionKey}}}`).join(', ')}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted leading-relaxed">
|
||||
Este modelo contém campo de assinatura ({signatureSections.map((s) => s.title).join(', ')}). O BeeTask incluirá automaticamente a assinatura padrão cadastrada no seu perfil. Caso não possua assinatura cadastrada, o campo ficará em branco.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</form>
|
||||
|
||||
{/* Footer */}
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ export const ReportsPage: React.FC = () => {
|
|||
|
||||
const standardTags = [
|
||||
{ tag: '{{nome}}', desc: 'Nome do colaborador (pega automaticamente do usuário logado)' },
|
||||
{ tag: '{{assinatura}}', desc: 'Assinatura digital padrão do usuário logado (se não tiver cadastrada, fica em branco)' },
|
||||
{ tag: '{{data_inicio}}', desc: 'Data inicial do período escolhido (ex: 01/10/2026)' },
|
||||
{ tag: '{{data_fim}}', desc: 'Data final do período escolhido (ex: 31/10/2026)' },
|
||||
{ tag: '{{mes_ano}}', desc: 'Mês e ano por extenso (ex: Outubro/2026)' },
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ export interface ReportSectionOutput {
|
|||
id: number
|
||||
sectionKey: string
|
||||
title: string
|
||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE'
|
||||
defaultValue?: string
|
||||
dateFormat?: string
|
||||
filterCategoryId?: number
|
||||
|
|
|
|||
Loading…
Reference in a new issue