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,
|
ProjectRepository projectRepository,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
StorageRepository storageRepository,
|
StorageRepository storageRepository,
|
||||||
|
net.beetechgroup.beetask.usecase.repository.UserSignatureRepository userSignatureRepository,
|
||||||
AuthorizeOrganizationMemberUseCase authorizer
|
AuthorizeOrganizationMemberUseCase authorizer
|
||||||
) {
|
) {
|
||||||
return new GenerateReportUseCase(
|
return new GenerateReportUseCase(
|
||||||
|
|
@ -95,6 +96,7 @@ public class ReportUseCaseConfig {
|
||||||
projectRepository,
|
projectRepository,
|
||||||
userRepository,
|
userRepository,
|
||||||
storageRepository,
|
storageRepository,
|
||||||
|
userSignatureRepository,
|
||||||
authorizer
|
authorizer
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,17 @@
|
||||||
package net.beetechgroup.beetask.usecase.report.docengine;
|
package net.beetechgroup.beetask.usecase.report.docengine;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
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.XWPFDocument;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFHeader;
|
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFFooter;
|
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.XWPFParagraph;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||||
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
||||||
|
|
@ -43,30 +46,39 @@ public class DocxReportEngine {
|
||||||
InputStream templateStream,
|
InputStream templateStream,
|
||||||
Map<String, String> variables,
|
Map<String, String> variables,
|
||||||
Map<String, List<TaskItem>> listSections
|
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 {
|
) throws Exception {
|
||||||
try (XWPFDocument doc = new XWPFDocument(templateStream)) {
|
try (XWPFDocument doc = new XWPFDocument(templateStream)) {
|
||||||
// 1. Processar Tabelas Dinâmicas primeiro (linhas com tags de lista)
|
// 1. Processar Tabelas Dinâmicas primeiro (linhas com tags de lista ou campos)
|
||||||
processTables(doc, variables, listSections);
|
processTables(doc, variables, listSections, images);
|
||||||
|
|
||||||
// 2. Processar Parágrafos do corpo do documento
|
// 2. Processar Parágrafos do corpo do documento
|
||||||
processParagraphs(doc, variables, listSections);
|
processParagraphs(doc, variables, listSections, images);
|
||||||
|
|
||||||
// 3. Processar Cabeçalhos e Rodapés
|
// 3. Processar Cabeçalhos e Rodapés
|
||||||
for (XWPFHeader header : doc.getHeaderList()) {
|
for (XWPFHeader header : doc.getHeaderList()) {
|
||||||
for (XWPFParagraph p : header.getParagraphs()) {
|
for (XWPFParagraph p : header.getParagraphs()) {
|
||||||
replaceTextInParagraph(p, variables, null);
|
replaceTextInParagraph(p, variables, null, images);
|
||||||
}
|
}
|
||||||
for (XWPFTable table : header.getTables()) {
|
for (XWPFTable table : header.getTables()) {
|
||||||
replaceTextInTableCells(table, variables);
|
replaceTextInTableCells(table, variables, images);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for (XWPFFooter footer : doc.getFooterList()) {
|
for (XWPFFooter footer : doc.getFooterList()) {
|
||||||
for (XWPFParagraph p : footer.getParagraphs()) {
|
for (XWPFParagraph p : footer.getParagraphs()) {
|
||||||
replaceTextInParagraph(p, variables, null);
|
replaceTextInParagraph(p, variables, null, images);
|
||||||
}
|
}
|
||||||
for (XWPFTable table : footer.getTables()) {
|
for (XWPFTable table : footer.getTables()) {
|
||||||
replaceTextInTableCells(table, variables);
|
replaceTextInTableCells(table, variables, images);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -79,18 +91,20 @@ public class DocxReportEngine {
|
||||||
private static void processParagraphs(
|
private static void processParagraphs(
|
||||||
XWPFDocument doc,
|
XWPFDocument doc,
|
||||||
Map<String, String> variables,
|
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());
|
List<XWPFParagraph> paragraphs = new ArrayList<>(doc.getParagraphs());
|
||||||
for (XWPFParagraph p : paragraphs) {
|
for (XWPFParagraph p : paragraphs) {
|
||||||
replaceTextInParagraph(p, variables, listSections);
|
replaceTextInParagraph(p, variables, listSections, images);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void processTables(
|
private static void processTables(
|
||||||
XWPFDocument doc,
|
XWPFDocument doc,
|
||||||
Map<String, String> variables,
|
Map<String, String> variables,
|
||||||
Map<String, List<TaskItem>> listSections
|
Map<String, List<TaskItem>> listSections,
|
||||||
|
Map<String, byte[]> images
|
||||||
) {
|
) {
|
||||||
for (XWPFTable table : doc.getTables()) {
|
for (XWPFTable table : doc.getTables()) {
|
||||||
List<XWPFTableRow> rows = new ArrayList<>(table.getRows());
|
List<XWPFTableRow> rows = new ArrayList<>(table.getRows());
|
||||||
|
|
@ -132,10 +146,10 @@ public class DocxReportEngine {
|
||||||
table.removeRow(r);
|
table.removeRow(r);
|
||||||
r += items.size() - 1; // Ajustar índice
|
r += items.size() - 1; // Ajustar índice
|
||||||
} else {
|
} else {
|
||||||
// Linha normal: substituir variáveis simples
|
// Linha normal: substituir variáveis simples e imagens/assinaturas
|
||||||
for (XWPFTableCell cell : row.getTableCells()) {
|
for (XWPFTableCell cell : row.getTableCells()) {
|
||||||
for (XWPFParagraph p : cell.getParagraphs()) {
|
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 (XWPFTableRow row : table.getRows()) {
|
||||||
for (XWPFTableCell cell : row.getTableCells()) {
|
for (XWPFTableCell cell : row.getTableCells()) {
|
||||||
for (XWPFParagraph p : cell.getParagraphs()) {
|
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(
|
private static void replaceTextInParagraph(
|
||||||
XWPFParagraph p,
|
XWPFParagraph p,
|
||||||
Map<String, String> variables,
|
Map<String, String> variables,
|
||||||
Map<String, List<TaskItem>> listSections
|
Map<String, List<TaskItem>> listSections,
|
||||||
|
Map<String, byte[]> images
|
||||||
) {
|
) {
|
||||||
String text = p.getText();
|
String text = p.getText();
|
||||||
if (text == null || !text.contains("{{")) {
|
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;
|
String replaced = text;
|
||||||
if (variables != null) {
|
if (variables != null) {
|
||||||
for (Map.Entry<String, String> var : variables.entrySet()) {
|
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
|
// 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;
|
String font = null;
|
||||||
int fontSize = -1;
|
int fontSize = -1;
|
||||||
String color = null;
|
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) {
|
private static String replaceItemTags(String cellText, TaskItem item) {
|
||||||
if (cellText == null) return "";
|
if (cellText == null) return "";
|
||||||
return cellText
|
return cellText
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import java.util.Objects;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import net.beetechgroup.beetask.entities.Project;
|
import net.beetechgroup.beetask.entities.Project;
|
||||||
import net.beetechgroup.beetask.entities.User;
|
import net.beetechgroup.beetask.entities.User;
|
||||||
|
import net.beetechgroup.beetask.entities.UserSignature;
|
||||||
import net.beetechgroup.beetask.entities.organization.Organization;
|
import net.beetechgroup.beetask.entities.organization.Organization;
|
||||||
import net.beetechgroup.beetask.entities.report.GeneratedReport;
|
import net.beetechgroup.beetask.entities.report.GeneratedReport;
|
||||||
import net.beetechgroup.beetask.entities.report.ReportSection;
|
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.StorageRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||||
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
import net.beetechgroup.beetask.usecase.repository.UserRepository;
|
||||||
|
import net.beetechgroup.beetask.usecase.repository.UserSignatureRepository;
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
public class GenerateReportUseCase {
|
public class GenerateReportUseCase {
|
||||||
|
|
@ -47,6 +49,7 @@ public class GenerateReportUseCase {
|
||||||
private final ProjectRepository projectRepository;
|
private final ProjectRepository projectRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final StorageRepository storageRepository;
|
private final StorageRepository storageRepository;
|
||||||
|
private final UserSignatureRepository userSignatureRepository;
|
||||||
private final AuthorizeOrganizationMemberUseCase authorizer;
|
private final AuthorizeOrganizationMemberUseCase authorizer;
|
||||||
|
|
||||||
public GenerateReportUseCase(
|
public GenerateReportUseCase(
|
||||||
|
|
@ -57,6 +60,7 @@ public class GenerateReportUseCase {
|
||||||
ProjectRepository projectRepository,
|
ProjectRepository projectRepository,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
StorageRepository storageRepository,
|
StorageRepository storageRepository,
|
||||||
|
UserSignatureRepository userSignatureRepository,
|
||||||
AuthorizeOrganizationMemberUseCase authorizer
|
AuthorizeOrganizationMemberUseCase authorizer
|
||||||
) {
|
) {
|
||||||
this.reportTemplateRepository = reportTemplateRepository;
|
this.reportTemplateRepository = reportTemplateRepository;
|
||||||
|
|
@ -66,6 +70,7 @@ public class GenerateReportUseCase {
|
||||||
this.projectRepository = projectRepository;
|
this.projectRepository = projectRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.storageRepository = storageRepository;
|
this.storageRepository = storageRepository;
|
||||||
|
this.userSignatureRepository = userSignatureRepository;
|
||||||
this.authorizer = authorizer;
|
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;
|
byte[] docxBytes;
|
||||||
try (InputStream templateStream = storageRepository.download(template.getStorageKey())) {
|
try (InputStream templateStream = storageRepository.download(template.getStorageKey())) {
|
||||||
docxBytes = DocxReportEngine.generate(templateStream, variables, listSections);
|
docxBytes = DocxReportEngine.generate(templateStream, variables, listSections, images);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.errorf("Erro ao processar template .docx %d: %s", template.getId(), e.getMessage());
|
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);
|
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("/", "-"))
|
String cleanTitle = (template.getName() + "_" + strStart.replace("/", "-") + "_" + strEnd.replace("/", "-"))
|
||||||
.replaceAll("[^a-zA-Z0-9._-]", "_");
|
.replaceAll("[^a-zA-Z0-9._-]", "_");
|
||||||
String generatedFileName = cleanTitle + ".docx";
|
String generatedFileName = cleanTitle + ".docx";
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Calendar,
|
Calendar,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Info,
|
Info,
|
||||||
|
PenTool,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { CustomSelect } from '../ui/Select'
|
import { CustomSelect } from '../ui/Select'
|
||||||
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
||||||
|
|
@ -28,7 +29,7 @@ interface CreateTemplateModalProps {
|
||||||
interface SectionFormItem {
|
interface SectionFormItem {
|
||||||
sectionKey: string
|
sectionKey: string
|
||||||
title: string
|
title: string
|
||||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE'
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
dateFormat?: string
|
dateFormat?: string
|
||||||
filterCategoryId?: number
|
filterCategoryId?: number
|
||||||
|
|
@ -376,19 +377,33 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
value={section.sectionType}
|
value={section.sectionType}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
handleUpdateSection(idx, {
|
handleUpdateSection(idx, {
|
||||||
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST',
|
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'FREE_TEXT', label: 'Texto Livre (Preenchido na emissão)' },
|
{ value: 'FREE_TEXT', label: 'Texto Livre (Preenchido na emissão)' },
|
||||||
{ value: 'FIXED_TEXT', label: 'Texto Fixo (Pré-preenchido no cadastro)' },
|
{ value: 'FIXED_TEXT', label: 'Texto Fixo (Pré-preenchido no cadastro)' },
|
||||||
{ value: 'DATE', label: 'Data (Com formato personalizado)' },
|
{ 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' },
|
{ value: 'TASK_LIST', label: 'Lista de Tarefas do Período' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Configuração de Campo de Tipo DATA */}
|
||||||
{section.sectionType === 'DATE' && (
|
{section.sectionType === 'DATE' && (
|
||||||
<div className="pt-2 border-t border-border-soft/60 space-y-2 animate-in fade-in duration-150">
|
<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,
|
Sparkles,
|
||||||
Info,
|
Info,
|
||||||
Check,
|
Check,
|
||||||
|
PenTool,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { CustomSelect } from '../ui/Select'
|
import { CustomSelect } from '../ui/Select'
|
||||||
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
||||||
|
|
@ -30,7 +31,7 @@ interface EditTemplateModalProps {
|
||||||
interface SectionFormItem {
|
interface SectionFormItem {
|
||||||
sectionKey: string
|
sectionKey: string
|
||||||
title: string
|
title: string
|
||||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE'
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
dateFormat?: string
|
dateFormat?: string
|
||||||
filterCategoryId?: number
|
filterCategoryId?: number
|
||||||
|
|
@ -398,19 +399,33 @@ export const EditTemplateModal: React.FC<EditTemplateModalProps> = ({
|
||||||
value={section.sectionType}
|
value={section.sectionType}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
handleUpdateSection(idx, {
|
handleUpdateSection(idx, {
|
||||||
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST',
|
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'FREE_TEXT', label: 'Texto Livre (Preenchido na emissão)' },
|
{ value: 'FREE_TEXT', label: 'Texto Livre (Preenchido na emissão)' },
|
||||||
{ value: 'FIXED_TEXT', label: 'Texto Fixo (Pré-preenchido no cadastro)' },
|
{ value: 'FIXED_TEXT', label: 'Texto Fixo (Pré-preenchido no cadastro)' },
|
||||||
{ value: 'DATE', label: 'Data (Com formato personalizado)' },
|
{ 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' },
|
{ value: 'TASK_LIST', label: 'Lista de Tarefas do Período' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Configuração de Campo de Tipo DATA */}
|
||||||
{section.sectionType === 'DATE' && (
|
{section.sectionType === 'DATE' && (
|
||||||
<div className="pt-2 border-t border-border-soft/60 space-y-2 animate-in fade-in duration-150">
|
<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,
|
Loader2,
|
||||||
Lock,
|
Lock,
|
||||||
Tag,
|
Tag,
|
||||||
|
PenTool,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { CustomSelect } from '../ui/Select'
|
import { CustomSelect } from '../ui/Select'
|
||||||
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
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')
|
return selectedTemplate.sections.filter((s) => s.sectionType === 'TASK_LIST')
|
||||||
}, [selectedTemplate])
|
}, [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
|
// Preset Handlers
|
||||||
const handlePresetChange = (preset: 'current_month' | 'last_month' | 'custom') => {
|
const handlePresetChange = (preset: 'current_month' | 'last_month' | 'custom') => {
|
||||||
setPeriodPreset(preset)
|
setPeriodPreset(preset)
|
||||||
|
|
@ -515,6 +522,24 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</form>
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
|
|
|
||||||
|
|
@ -98,6 +98,7 @@ export const ReportsPage: React.FC = () => {
|
||||||
|
|
||||||
const standardTags = [
|
const standardTags = [
|
||||||
{ tag: '{{nome}}', desc: 'Nome do colaborador (pega automaticamente do usuário logado)' },
|
{ 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_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: '{{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)' },
|
{ tag: '{{mes_ano}}', desc: 'Mês e ano por extenso (ex: Outubro/2026)' },
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ export interface ReportSectionOutput {
|
||||||
id: number
|
id: number
|
||||||
sectionKey: string
|
sectionKey: string
|
||||||
title: string
|
title: string
|
||||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST' | 'SIGNATURE'
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
dateFormat?: string
|
dateFormat?: string
|
||||||
filterCategoryId?: number
|
filterCategoryId?: number
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue