diff --git a/backend/src/main/java/net/beetechgroup/beetask/entities/report/ReportSection.java b/backend/src/main/java/net/beetechgroup/beetask/entities/report/ReportSection.java index bcedcc9..6d41d4b 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/entities/report/ReportSection.java +++ b/backend/src/main/java/net/beetechgroup/beetask/entities/report/ReportSection.java @@ -8,7 +8,8 @@ public class ReportSection { private Long reportTemplateId; private String sectionKey; private String title; - private String sectionType; // 'FREE_TEXT' ou 'TASK_LIST' + private String sectionType; // 'FREE_TEXT', 'FIXED_TEXT', 'TASK_LIST' + private String defaultValue; private Category filterCategory; private String filterStatus; // 'ALL', 'COMPLETED', 'IN_PROGRESS' private Boolean includeTime; @@ -55,6 +56,14 @@ public class ReportSection { this.sectionType = sectionType; } + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + public Category getFilterCategory() { return filterCategory; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/ReportSectionEntity.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/ReportSectionEntity.java index d8be910..bf3bea5 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/ReportSectionEntity.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/entities/ReportSectionEntity.java @@ -30,7 +30,10 @@ public class ReportSectionEntity { private String title; @Column(name = "section_type", nullable = false, length = 50) - private String sectionType; // 'FREE_TEXT' ou 'TASK_LIST' + private String sectionType; // 'FREE_TEXT', 'FIXED_TEXT', 'TASK_LIST' + + @Column(name = "default_value", columnDefinition = "TEXT") + private String defaultValue; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "filter_category_id") @@ -88,6 +91,14 @@ public class ReportSectionEntity { this.sectionType = sectionType; } + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + public CategoryEntity getFilterCategory() { return filterCategory; } diff --git a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/ReportEntityMapper.java b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/ReportEntityMapper.java index 7a2786e..0e88cf7 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/ReportEntityMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/frameworks/persistence/mapper/ReportEntityMapper.java @@ -52,6 +52,7 @@ public class ReportEntityMapper { entity.setSectionKey(domain.getSectionKey()); entity.setTitle(domain.getTitle()); entity.setSectionType(domain.getSectionType()); + entity.setDefaultValue(domain.getDefaultValue()); entity.setFilterStatus(domain.getFilterStatus()); entity.setIncludeTime(domain.getIncludeTime()); entity.setOrderIndex(domain.getOrderIndex()); @@ -104,6 +105,7 @@ public class ReportEntityMapper { domain.setSectionKey(entity.getSectionKey()); domain.setTitle(entity.getTitle()); domain.setSectionType(entity.getSectionType()); + domain.setDefaultValue(entity.getDefaultValue()); domain.setFilterStatus(entity.getFilterStatus()); domain.setIncludeTime(entity.getIncludeTime()); domain.setOrderIndex(entity.getOrderIndex()); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/ReportMapper.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/ReportMapper.java index 00a5b7b..e0b8666 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/ReportMapper.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/ReportMapper.java @@ -46,6 +46,7 @@ public class ReportMapper { section.getSectionKey(), section.getTitle(), section.getSectionType(), + section.getDefaultValue(), section.getFilterCategory() != null ? section.getFilterCategory().getId() : null, section.getFilterCategory() != null ? section.getFilterCategory().getName() : null, section.getFilterStatus(), diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/ReportSectionOutput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/ReportSectionOutput.java index af58086..e591619 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/ReportSectionOutput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/ReportSectionOutput.java @@ -7,6 +7,7 @@ public record ReportSectionOutput( String sectionKey, String title, String sectionType, + String defaultValue, Long filterCategoryId, String filterCategoryName, String filterStatus, diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/create/CreateReportTemplateInput.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/create/CreateReportTemplateInput.java index 93c924e..8aad81b 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/create/CreateReportTemplateInput.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/create/CreateReportTemplateInput.java @@ -22,6 +22,7 @@ public record CreateReportTemplateInput( String sectionKey, String title, String sectionType, + String defaultValue, Long filterCategoryId, String filterStatus, Boolean includeTime, diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/create/CreateReportTemplateUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/create/CreateReportTemplateUseCase.java index 356cc32..7f76559 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/create/CreateReportTemplateUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/create/CreateReportTemplateUseCase.java @@ -107,6 +107,7 @@ public class CreateReportTemplateUseCase { section.setSectionKey(sInput.sectionKey().trim().toLowerCase().replaceAll("[^a-z0-9_]", "_")); section.setTitle(sInput.title() != null ? sInput.title().trim() : section.getSectionKey()); section.setSectionType(sInput.sectionType() != null ? sInput.sectionType() : "FREE_TEXT"); + section.setDefaultValue(sInput.defaultValue() != null ? sInput.defaultValue().trim() : null); section.setFilterStatus(sInput.filterStatus() != null ? sInput.filterStatus() : "ALL"); section.setIncludeTime(sInput.includeTime() != null ? sInput.includeTime() : true); section.setOrderIndex(sInput.orderIndex() != null ? sInput.orderIndex() : order++); diff --git a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/generate/GenerateReportUseCase.java b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/generate/GenerateReportUseCase.java index ab8a9db..f18748d 100644 --- a/backend/src/main/java/net/beetechgroup/beetask/usecase/report/generate/GenerateReportUseCase.java +++ b/backend/src/main/java/net/beetechgroup/beetask/usecase/report/generate/GenerateReportUseCase.java @@ -179,10 +179,22 @@ public class GenerateReportUseCase { variables.put("total_horas", formattedTotalTime); variables.put("data_emissao", LocalDate.now().format(DATE_FORMATTER)); + // Valores padrão das seções configuradas no modelo + if (template.getSections() != null) { + for (ReportSection sec : template.getSections()) { + if (("FREE_TEXT".equalsIgnoreCase(sec.getSectionType()) || "FIXED_TEXT".equalsIgnoreCase(sec.getSectionType())) + && sec.getDefaultValue() != null) { + variables.put(sec.getSectionKey(), sec.getDefaultValue()); + } + } + } + // Incorporar campos de texto customizados enviados pelo usuário if (input.customFields() != null) { for (Map.Entry entry : input.customFields().entrySet()) { - variables.put(entry.getKey(), entry.getValue() != null ? entry.getValue() : ""); + if (entry.getValue() != null && !entry.getValue().isBlank()) { + variables.put(entry.getKey(), entry.getValue()); + } } } diff --git a/backend/src/main/resources/db/migration/V12__add_report_section_default_value.sql b/backend/src/main/resources/db/migration/V12__add_report_section_default_value.sql new file mode 100644 index 0000000..97481e8 --- /dev/null +++ b/backend/src/main/resources/db/migration/V12__add_report_section_default_value.sql @@ -0,0 +1 @@ +ALTER TABLE report_sections ADD COLUMN default_value TEXT; diff --git a/frontend/src/components/reports/CreateTemplateModal.tsx b/frontend/src/components/reports/CreateTemplateModal.tsx index 6fdd5b5..4fe80b7 100644 --- a/frontend/src/components/reports/CreateTemplateModal.tsx +++ b/frontend/src/components/reports/CreateTemplateModal.tsx @@ -8,9 +8,9 @@ import { Tag, AlertCircle, Folder, - UserCheck, Calendar, - User, + Sparkles, + Info, } from 'lucide-react' import { CustomSelect } from '../ui/Select' import { reportService, type ReportTemplateOutput } from '../../services/reportService' @@ -28,7 +28,8 @@ interface CreateTemplateModalProps { interface SectionFormItem { sectionKey: string title: string - sectionType: 'FREE_TEXT' | 'TASK_LIST' + sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST' + defaultValue?: string filterCategoryId?: number filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS' includeTime: boolean @@ -46,32 +47,9 @@ export const CreateTemplateModal: React.FC = ({ const [description, setDescription] = useState('') const [scope, setScope] = useState<'ORGANIZATION' | 'PROJECT'>('ORGANIZATION') const [projectId, setProjectId] = useState(undefined) - const [managerName, setManagerName] = useState('') - const [customProjectName, setCustomProjectName] = useState('') const [dateFormatPattern, setDateFormatPattern] = useState('DD/MM/AAAA até DD/MM/AAAA') const [file, setFile] = useState(null) - const [sections, setSections] = useState([ - { - sectionKey: 'resumo_andamento', - title: 'Resumo do Andamento do Projeto', - sectionType: 'FREE_TEXT', - includeTime: false, - }, - { - sectionKey: 'atividades', - title: 'Atividades e Entregas Desenvolvidas', - sectionType: 'TASK_LIST', - filterStatus: 'ALL', - includeTime: true, - }, - { - sectionKey: 'reunioes', - title: 'Reuniões e Alinhamentos Técnicos', - sectionType: 'TASK_LIST', - filterStatus: 'ALL', - includeTime: true, - }, - ]) + const [sections, setSections] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) @@ -79,12 +57,14 @@ export const CreateTemplateModal: React.FC = ({ if (!isOpen) return null const handleAddSection = () => { + const nextIndex = sections.length + 1 setSections((prev) => [ ...prev, { - sectionKey: `secao_${prev.length + 1}`, - title: `Nova Seção ${prev.length + 1}`, + sectionKey: `campo_${nextIndex}`, + title: `Campo ${nextIndex}`, sectionType: 'FREE_TEXT', + defaultValue: '', includeTime: false, }, ]) @@ -135,18 +115,17 @@ export const CreateTemplateModal: React.FC = ({ formData.append('name', name.trim()) formData.append('description', description.trim()) formData.append('scope', scope) - formData.append('managerName', managerName.trim()) - formData.append('customProjectName', customProjectName.trim()) formData.append('dateFormatPattern', dateFormatPattern) if (scope === 'PROJECT' && projectId) { formData.append('projectId', String(projectId)) } - // Limpar chaves das seções para serem seguras para {{tag}} + // Sanitizar seções com chave segura e valor padrão const sanitizedSections = sections.map((s, idx) => ({ sectionKey: s.sectionKey.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_'), title: s.title.trim() || s.sectionKey, sectionType: s.sectionType, + defaultValue: s.defaultValue ? s.defaultValue.trim() : '', filterCategoryId: s.filterCategoryId, filterStatus: s.filterStatus || 'ALL', includeTime: s.includeTime ?? true, @@ -183,7 +162,7 @@ export const CreateTemplateModal: React.FC = ({ Cadastrar Modelo de Relatório (.DOCX)

- Envie seu arquivo Word e configure as tags que serão preenchidas automaticamente. + Envie seu arquivo Word e configure os campos dinâmicos e valores pré-preenchidos.

@@ -256,104 +235,57 @@ export const CreateTemplateModal: React.FC = ({ )} - {/* Descrição */} -
- - setDescription(e.target.value)} - placeholder="Ex.: Template padrão exigido para prestação de contas mensal." - className="w-full px-3.5 py-2 rounded-xl border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand shadow-xs" - /> -
- - {/* Seção Padrão (Identificação, Gestão e Período) */} -
-
- -
-

- Seção Padrão: Identificação e Gestão -

-

- Valores configurados no modelo que alimentam o cabeçalho e identificação no Word. -

-
+ {/* Descrição e Formato do Período */} +
+
+ + setDescription(e.target.value)} + placeholder="Ex.: Modelo oficial para prestação de contas" + className="w-full px-3.5 py-2 rounded-xl border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand shadow-xs" + />
-
- {/* Colaborador / Usuário (Automático) */} -
- -
- Preenchido automaticamente com o usuário logado -
-
+
+ + setDateFormatPattern(String(val))} + options={[ + { value: 'DD/MM/AAAA até DD/MM/AAAA', label: 'DD/MM/AAAA até DD/MM/AAAA (ex: 01/10/2026 até 31/10/2026)' }, + { value: 'DD/MM/AAAA a DD/MM/AAAA', label: 'DD/MM/AAAA a DD/MM/AAAA (ex: 01/10/2026 a 31/10/2026)' }, + { value: 'DD de Mês de AAAA até DD de Mês de AAAA', label: '01 de Outubro de 2026 até 31 de Outubro de 2026' }, + { value: 'MM/AAAA', label: 'MM/AAAA (ex: 10/2026)' }, + { value: 'Mês de AAAA', label: 'Mês de AAAA (ex: Outubro de 2026)' }, + { value: 'DD/MM - DD/MM/AAAA', label: 'DD/MM - DD/MM/AAAA (ex: 01/10 - 31/10/2026)' }, + ]} + /> +
+
- {/* Nome do Gestor */} -
- - setManagerName(e.target.value)} - placeholder="Ex.: Dr. Roberto Silva" - className="w-full px-3 py-2 rounded-xl border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand shadow-xs" - /> -
- - {/* Nome do Projeto */} -
- - setCustomProjectName(e.target.value)} - placeholder="Ex.: Projeto BeeTask Inovação Tecnológica" - className="w-full px-3 py-2 rounded-xl border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand shadow-xs" - /> -
- - {/* Formato da Data do Período */} -
- - setDateFormatPattern(String(val))} - options={[ - { value: 'DD/MM/AAAA até DD/MM/AAAA', label: 'DD/MM/AAAA até DD/MM/AAAA (ex: 01/10/2026 até 31/10/2026)' }, - { value: 'DD/MM/AAAA a DD/MM/AAAA', label: 'DD/MM/AAAA a DD/MM/AAAA (ex: 01/10/2026 a 31/10/2026)' }, - { value: 'DD de Mês de AAAA até DD de Mês de AAAA', label: '01 de Outubro de 2026 até 31 de Outubro de 2026' }, - { value: 'MM/AAAA', label: 'MM/AAAA (ex: 10/2026)' }, - { value: 'Mês de AAAA', label: 'Mês de AAAA (ex: Outubro de 2026)' }, - { value: 'DD/MM - DD/MM/AAAA', label: 'DD/MM - DD/MM/AAAA (ex: 01/10 - 31/10/2026)' }, - ]} - /> -
+ {/* Dica de Tags Nativas */} +
+ +
+

Tags Padrão Automáticas Disponíveis no Word:

+

+ Você pode utilizar diretamente as tags{' '} + {'{{nome}}'} (usuário logado),{' '} + {'{{periodo}}'} (período formatado),{' '} + {'{{data_inicio}}'},{' '} + {'{{data_fim}}'} e{' '} + {'{{total_horas}}'} sem precisar cadastrar seções para elas. +

@@ -386,13 +318,13 @@ export const CreateTemplateModal: React.FC = ({
- {/* Construtor de Seções Dinâmicas */} + {/* Construtor de Campos e Seções */}
-

Seções e Tags Dinâmicas do Modelo

+

Campos e Seções do Relatório

- Insira essas tags entre {'{{nome_da_tag}}'} no seu arquivo Word. + Crie tags que serão substituídas pelo Word. Escolha se o campo é texto livre, texto fixo pré-preenchido ou listagem de tarefas.

-
- {sections.map((section, idx) => ( -
-
- - - Tag no Word: - {`{{${section.sectionKey || 'tag'}}}`} - - - -
- -
-
- - - handleUpdateSection(idx, { - sectionKey: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'), - }) - } - placeholder="ex: resumo_projeto" - className="w-full px-2.5 py-1.5 rounded-lg border border-border-soft bg-surface text-xs font-mono text-foreground focus:outline-hidden focus:border-brand" - /> + {sections.length === 0 ? ( +
+ +

Nenhum campo personalizado adicionado

+

+ Caso seu documento precise de variáveis como Gestor, Nome do Projeto, Resumo ou Lista de Tarefas, clique em Adicionar Campo / Seção. +

+
+ ) : ( +
+ {sections.map((section, idx) => ( +
+
+ + + Tag no Word:{' '} + + {`{{${section.sectionKey || 'tag'}}}`} + + +
-
- - handleUpdateSection(idx, { title: e.target.value })} - placeholder="ex: Resumo do Andamento" - className="w-full px-2.5 py-1.5 rounded-lg border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand" - /> -
- -
- - - handleUpdateSection(idx, { sectionType: val as 'FREE_TEXT' | 'TASK_LIST' }) - } - options={[ - { value: 'FREE_TEXT', label: 'Texto Livre (Digitado)' }, - { value: 'TASK_LIST', label: 'Lista de Tarefas do Período' }, - ]} - /> -
-
- - {section.sectionType === 'TASK_LIST' && ( -
+
- - + + handleUpdateSection(idx, { - filterCategoryId: Number(val) === 0 ? undefined : Number(val), + sectionKey: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'), }) } - options={[ - { value: 0, label: 'Todas as Categorias' }, - ...categories.map((c) => ({ - value: c.id, - label: c.name, - color: c.color, - })), - ]} + placeholder="ex: gestor ou resumo" + className="w-full px-2.5 py-1.5 rounded-lg border border-border-soft bg-surface text-xs font-mono text-foreground focus:outline-hidden focus:border-brand" />
-
- +
+ + handleUpdateSection(idx, { title: e.target.value })} + placeholder="ex: Nome do Gestor" + className="w-full px-2.5 py-1.5 rounded-lg border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand" + /> +
+ +
+ + + handleUpdateSection(idx, { + sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST', + }) + } + options={[ + { value: 'FREE_TEXT', label: 'Texto Livre (Preenchido na emissão)' }, + { value: 'FIXED_TEXT', label: 'Texto Fixo (Pré-preenchido no cadastro)' }, + { value: 'TASK_LIST', label: 'Lista de Tarefas do Período' }, + ]} + />
- )} -
- ))} -
+ + {/* Campo de Valor Pré-preenchido (Fixo ou Sugerido) */} + {(section.sectionType === 'FIXED_TEXT' || section.sectionType === 'FREE_TEXT') && ( +
+ + handleUpdateSection(idx, { defaultValue: e.target.value })} + placeholder={ + section.sectionType === 'FIXED_TEXT' + ? 'Ex.: Dr. Roberto Silva ou Nome do Projeto' + : 'Ex.: Deixar uma observação inicial...' + } + className="w-full px-2.5 py-1.5 rounded-lg border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand" + /> +
+ )} + + {/* Configurações de Lista de Tarefas */} + {section.sectionType === 'TASK_LIST' && ( +
+
+ + + handleUpdateSection(idx, { + filterCategoryId: Number(val) === 0 ? undefined : Number(val), + }) + } + options={[ + { value: 0, label: 'Todas as Categorias' }, + ...categories.map((c) => ({ + value: c.id, + label: c.name, + color: c.color, + })), + ]} + /> +
+ +
+ +
+
+ )} +
+ ))} +
+ )}
diff --git a/frontend/src/components/reports/GenerateReportModal.tsx b/frontend/src/components/reports/GenerateReportModal.tsx index 328e776..2d26f8f 100644 --- a/frontend/src/components/reports/GenerateReportModal.tsx +++ b/frontend/src/components/reports/GenerateReportModal.tsx @@ -8,6 +8,7 @@ import { CheckCircle2, FileText, Loader2, + Lock, } from 'lucide-react' import { CustomSelect } from '../ui/Select' import { reportService, type ReportTemplateOutput } from '../../services/reportService' @@ -67,17 +68,30 @@ export const GenerateReportModal: React.FC = ({ return templates.find((t) => t.id === selectedTemplateId) }, [templates, selectedTemplateId]) - // Ajustar projeto selecionado de acordo com o template + // Ajustar projeto selecionado e preencher valores padrão das seções useEffect(() => { - if (selectedTemplate?.projectId) { - setSelectedProjectId(selectedTemplate.projectId) + if (selectedTemplate) { + if (selectedTemplate.projectId) { + setSelectedProjectId(selectedTemplate.projectId) + } + + // Preencher campos com os valores padrão configurados no modelo + const initialFields: Record = {} + selectedTemplate.sections?.forEach((sec) => { + if (sec.defaultValue) { + initialFields[sec.sectionKey] = sec.defaultValue + } + }) + setCustomFields(initialFields) } }, [selectedTemplate]) - // Seções de texto livre do template selecionado - const freeTextSections = useMemo(() => { + // Seções de texto (livre ou fixo) do template selecionado + const textSections = useMemo(() => { if (!selectedTemplate) return [] - return selectedTemplate.sections.filter((s) => s.sectionType === 'FREE_TEXT') + return selectedTemplate.sections.filter( + (s) => s.sectionType === 'FREE_TEXT' || s.sectionType === 'FIXED_TEXT' + ) }, [selectedTemplate]) // Lista de seções de tarefas do template selecionado @@ -288,7 +302,7 @@ export const GenerateReportModal: React.FC = ({
- {/* Seções de Texto Livre Dinâmicas */} - {freeTextSections.length > 0 && ( + {/* Seções de Texto (Livre ou Pré-preenchido) Dinâmicas */} + {textSections.length > 0 && (

Informações Descritivas do Relatório

- Preencha os textos que serão substituídos no documento Word. + Campos que serão inseridos no documento Word. Valores fixos foram pré-preenchidos automaticamente pelo modelo.

- {freeTextSections.map((sec) => ( + {textSections.map((sec) => (
-