feat(reports): configure date format per section when type is date

This commit is contained in:
Gabriel Menezes 2026-10-31 12:10:33 -04:00
parent 8054ca9a6d
commit 7624cff094
13 changed files with 256 additions and 76 deletions

View file

@ -8,8 +8,9 @@ public class ReportSection {
private Long reportTemplateId;
private String sectionKey;
private String title;
private String sectionType; // 'FREE_TEXT', 'FIXED_TEXT', 'TASK_LIST'
private String sectionType; // 'FREE_TEXT', 'FIXED_TEXT', 'DATE', 'TASK_LIST'
private String defaultValue;
private String dateFormat;
private Category filterCategory;
private String filterStatus; // 'ALL', 'COMPLETED', 'IN_PROGRESS'
private Boolean includeTime;
@ -96,6 +97,14 @@ public class ReportSection {
this.orderIndex = orderIndex;
}
public String getDateFormat() {
return dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}

View file

@ -30,11 +30,14 @@ public class ReportSectionEntity {
private String title;
@Column(name = "section_type", nullable = false, length = 50)
private String sectionType; // 'FREE_TEXT', 'FIXED_TEXT', 'TASK_LIST'
private String sectionType; // 'FREE_TEXT', 'FIXED_TEXT', 'DATE', 'TASK_LIST'
@Column(name = "default_value", columnDefinition = "TEXT")
private String defaultValue;
@Column(name = "date_format", length = 100)
private String dateFormat;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "filter_category_id")
private CategoryEntity filterCategory;
@ -131,6 +134,14 @@ public class ReportSectionEntity {
this.orderIndex = orderIndex;
}
public String getDateFormat() {
return dateFormat;
}
public void setDateFormat(String dateFormat) {
this.dateFormat = dateFormat;
}
public LocalDateTime getCreatedAt() {
return createdAt;
}

View file

@ -53,6 +53,7 @@ public class ReportEntityMapper {
entity.setTitle(domain.getTitle());
entity.setSectionType(domain.getSectionType());
entity.setDefaultValue(domain.getDefaultValue());
entity.setDateFormat(domain.getDateFormat());
entity.setFilterStatus(domain.getFilterStatus());
entity.setIncludeTime(domain.getIncludeTime());
entity.setOrderIndex(domain.getOrderIndex());
@ -106,6 +107,7 @@ public class ReportEntityMapper {
domain.setTitle(entity.getTitle());
domain.setSectionType(entity.getSectionType());
domain.setDefaultValue(entity.getDefaultValue());
domain.setDateFormat(entity.getDateFormat());
domain.setFilterStatus(entity.getFilterStatus());
domain.setIncludeTime(entity.getIncludeTime());
domain.setOrderIndex(entity.getOrderIndex());

View file

@ -47,6 +47,7 @@ public class ReportMapper {
section.getTitle(),
section.getSectionType(),
section.getDefaultValue(),
section.getDateFormat(),
section.getFilterCategory() != null ? section.getFilterCategory().getId() : null,
section.getFilterCategory() != null ? section.getFilterCategory().getName() : null,
section.getFilterStatus(),

View file

@ -8,6 +8,7 @@ public record ReportSectionOutput(
String title,
String sectionType,
String defaultValue,
String dateFormat,
Long filterCategoryId,
String filterCategoryName,
String filterStatus,

View file

@ -23,6 +23,7 @@ public record CreateReportTemplateInput(
String title,
String sectionType,
String defaultValue,
String dateFormat,
Long filterCategoryId,
String filterStatus,
Boolean includeTime,

View file

@ -108,6 +108,7 @@ public class CreateReportTemplateUseCase {
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.setDateFormat(sInput.dateFormat() != null ? sInput.dateFormat().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++);

View file

@ -185,6 +185,39 @@ public class GenerateReportUseCase {
if (("FREE_TEXT".equalsIgnoreCase(sec.getSectionType()) || "FIXED_TEXT".equalsIgnoreCase(sec.getSectionType()))
&& sec.getDefaultValue() != null) {
variables.put(sec.getSectionKey(), sec.getDefaultValue());
} else if ("DATE".equalsIgnoreCase(sec.getSectionType())) {
String datePattern = sec.getDateFormat() != null && !sec.getDateFormat().isBlank()
? sec.getDateFormat().trim()
: "DD/MM/AAAA";
String formattedVal;
String customVal = input.customFields() != null ? input.customFields().get(sec.getSectionKey()) : null;
if (customVal != null && !customVal.isBlank()) {
try {
LocalDate d = LocalDate.parse(customVal.trim());
formattedVal = formatSingleDate(d, datePattern);
} catch (Exception e) {
formattedVal = customVal.trim();
}
} else {
if (isPeriodPattern(datePattern) || sec.getSectionKey().contains("periodo") || sec.getSectionKey().contains("vigencia")) {
formattedVal = formatPeriodWithPattern(input.startDate(), input.endDate(), datePattern);
} else if (sec.getSectionKey().contains("fim") || sec.getSectionKey().contains("termino")) {
formattedVal = formatSingleDate(input.endDate(), datePattern);
} else if (sec.getSectionKey().contains("inicio")) {
formattedVal = formatSingleDate(input.startDate(), datePattern);
} else if (sec.getSectionKey().contains("emissao") || sec.getSectionKey().contains("hoje")) {
formattedVal = formatSingleDate(LocalDate.now(), datePattern);
} else {
formattedVal = isPeriodPattern(datePattern)
? formatPeriodWithPattern(input.startDate(), input.endDate(), datePattern)
: formatSingleDate(input.endDate(), datePattern);
}
}
variables.put(sec.getSectionKey(), formattedVal);
if ("periodo".equalsIgnoreCase(sec.getSectionKey()) || "periodo_vigencia".equalsIgnoreCase(sec.getSectionKey())) {
variables.put("periodo", formattedVal);
variables.put("periodo_vigencia", formattedVal);
}
}
}
}
@ -387,17 +420,52 @@ public class GenerateReportUseCase {
return text.substring(0, 1).toUpperCase(PT_BR) + text.substring(1);
}
private boolean isPeriodPattern(String pattern) {
if (pattern == null) return false;
return pattern.contains("até") || pattern.contains(" a ") || pattern.contains(" - ");
}
private String formatSingleDate(LocalDate date, String pattern) {
if (date == null) return "";
if (pattern == null || pattern.isBlank()) pattern = "DD/MM/AAAA";
return switch (pattern) {
case "DD/MM/AAAA" -> date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
case "DD-MM-AAAA" -> date.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
case "AAAA-MM-DD" -> date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
case "DD de Mês de AAAA" -> {
String m = capitalize(date.getMonth().getDisplayName(TextStyle.FULL, PT_BR));
yield String.format("%02d de %s de %d", date.getDayOfMonth(), m, date.getYear());
}
case "MM/AAAA" -> date.format(DateTimeFormatter.ofPattern("MM/yyyy"));
case "Mês de AAAA" -> {
String m = capitalize(date.getMonth().getDisplayName(TextStyle.FULL, PT_BR));
yield m + " de " + date.getYear();
}
default -> {
if (pattern.contains("-")) {
yield date.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
}
yield date.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
}
};
}
private String formatPeriodWithPattern(LocalDate start, LocalDate end, String pattern) {
if (pattern == null || pattern.isBlank()) {
pattern = "DD/MM/AAAA até DD/MM/AAAA";
}
String strStart = start.format(DATE_FORMATTER);
String strEnd = end.format(DATE_FORMATTER);
String strSlashStart = start.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
String strSlashEnd = end.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
String strDashStart = start.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
String strDashEnd = end.format(DateTimeFormatter.ofPattern("dd-MM-yyyy"));
return switch (pattern) {
case "DD/MM/AAAA até DD/MM/AAAA" -> strStart + " até " + strEnd;
case "DD/MM/AAAA a DD/MM/AAAA" -> strStart + " a " + strEnd;
case "DD/MM/AAAA até DD/MM/AAAA" -> strSlashStart + " até " + strSlashEnd;
case "DD-MM-AAAA até DD-MM-AAAA" -> strDashStart + " até " + strDashEnd;
case "DD/MM/AAAA a DD/MM/AAAA" -> strSlashStart + " a " + strSlashEnd;
case "DD-MM-AAAA a DD-MM-AAAA" -> strDashStart + " a " + strDashEnd;
case "DD de Mês de AAAA até DD de Mês de AAAA" -> {
String mStart = capitalize(start.getMonth().getDisplayName(TextStyle.FULL, PT_BR));
String mEnd = capitalize(end.getMonth().getDisplayName(TextStyle.FULL, PT_BR));
@ -413,7 +481,10 @@ public class GenerateReportUseCase {
case "DD/MM - DD/MM/AAAA" -> String.format("%02d/%02d - %02d/%02d/%d",
start.getDayOfMonth(), start.getMonthValue(),
end.getDayOfMonth(), end.getMonthValue(), end.getYear());
default -> strStart + " até " + strEnd;
case "DD-MM-AAAA" -> strDashStart;
case "DD/MM/AAAA" -> strSlashStart;
case "AAAA-MM-DD" -> start.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
default -> strSlashStart + " até " + strSlashEnd;
};
}
}

View file

@ -0,0 +1 @@
ALTER TABLE report_sections ADD COLUMN date_format VARCHAR(100);

View file

@ -28,8 +28,9 @@ interface CreateTemplateModalProps {
interface SectionFormItem {
sectionKey: string
title: string
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST'
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
defaultValue?: string
dateFormat?: string
filterCategoryId?: number
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'
includeTime: boolean
@ -47,7 +48,6 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
const [description, setDescription] = useState('')
const [scope, setScope] = useState<'ORGANIZATION' | 'PROJECT'>('ORGANIZATION')
const [projectId, setProjectId] = useState<number | undefined>(undefined)
const [dateFormatPattern, setDateFormatPattern] = useState('DD/MM/AAAA até DD/MM/AAAA')
const [file, setFile] = useState<File | null>(null)
const [sections, setSections] = useState<SectionFormItem[]>([])
@ -65,6 +65,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
title: `Campo ${nextIndex}`,
sectionType: 'FREE_TEXT',
defaultValue: '',
dateFormat: 'DD/MM/AAAA',
includeTime: false,
},
])
@ -115,17 +116,17 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
formData.append('name', name.trim())
formData.append('description', description.trim())
formData.append('scope', scope)
formData.append('dateFormatPattern', dateFormatPattern)
if (scope === 'PROJECT' && projectId) {
formData.append('projectId', String(projectId))
}
// Sanitizar seções com chave segura e valor padrão
// Sanitizar seções com chave segura, valor padrão e formato de data
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() : '',
dateFormat: s.sectionType === 'DATE' ? (s.dateFormat || 'DD/MM/AAAA') : undefined,
filterCategoryId: s.filterCategoryId,
filterStatus: s.filterStatus || 'ALL',
includeTime: s.includeTime ?? true,
@ -162,7 +163,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
Cadastrar Modelo de Relatório (.DOCX)
</h2>
<p className="text-xs text-muted">
Envie seu arquivo Word e configure os campos dinâmicos e valores pré-preenchidos.
Envie seu arquivo Word e configure os campos dinâmicos, tipos e formatos.
</p>
</div>
</div>
@ -235,8 +236,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
</div>
)}
{/* Descrição e Formato do Período */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
{/* Descrição */}
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground">
Descrição <span className="text-muted font-normal">(Opcional)</span>
@ -245,46 +245,23 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Ex.: Modelo oficial para prestação de contas"
placeholder="Ex.: Modelo 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"
/>
</div>
<div className="space-y-1.5">
<label className="text-xs font-bold text-foreground flex items-center justify-between">
<span className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5 text-muted" />
Formato da tag {'{{periodo}}'}
</span>
</label>
<CustomSelect
size="sm"
value={dateFormatPattern}
onChange={(val) => 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)' },
]}
/>
</div>
</div>
{/* Dica de Tags Nativas */}
<div className="p-3.5 rounded-2xl bg-brand/5 border border-brand/15 text-xs text-foreground/80 flex items-start gap-2.5">
<Info className="w-4 h-4 text-brand shrink-0 mt-0.5" />
<div className="space-y-1 text-[11px] leading-relaxed">
<p className="font-bold text-brand">Tags Padrão Automáticas Disponíveis no Word:</p>
<p className="font-bold text-brand">Tags Nativas Automáticas Disponíveis no Word:</p>
<p className="text-muted">
Você pode utilizar diretamente as tags{' '}
<code className="text-brand font-bold bg-brand/10 px-1 py-0.5 rounded">{'{{nome}}'}</code> (usuário logado),{' '}
<code className="text-brand font-bold bg-brand/10 px-1 py-0.5 rounded">{'{{periodo}}'}</code> (período formatado),{' '}
<code className="text-brand font-bold bg-brand/10 px-1 py-0.5 rounded">{'{{periodo}}'}</code> (período de vigência),{' '}
<code className="text-brand font-bold bg-brand/10 px-1 py-0.5 rounded">{'{{data_inicio}}'}</code>,{' '}
<code className="text-brand font-bold bg-brand/10 px-1 py-0.5 rounded">{'{{data_fim}}'}</code> e{' '}
<code className="text-brand font-bold bg-brand/10 px-1 py-0.5 rounded">{'{{total_horas}}'}</code> sem precisar cadastrar seções para elas.
<code className="text-brand font-bold bg-brand/10 px-1 py-0.5 rounded">{'{{total_horas}}'}</code> sem precisar cadastrar campos para elas.
</p>
</div>
</div>
@ -324,7 +301,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
<div>
<h4 className="text-xs font-bold text-foreground">Campos e Seções do Relatório</h4>
<p className="text-[11px] text-muted">
Crie tags que serão substituídas pelo Word. Escolha se o campo é texto livre, texto fixo pré-preenchido ou listagem de tarefas.
Crie tags que serão substituídas pelo Word. Escolha se o campo é texto livre, texto fixo, data (com formato personalizado) ou listagem de tarefas.
</p>
</div>
<button
@ -342,7 +319,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
<Sparkles className="w-6 h-6 text-muted mx-auto" />
<p className="text-xs font-semibold text-foreground">Nenhum campo personalizado adicionado</p>
<p className="text-[11px] text-muted max-w-md mx-auto">
Caso seu documento precise de variáveis como Gestor, Nome do Projeto, Resumo ou Lista de Tarefas, clique em <strong>Adicionar Campo / Seção</strong>.
Caso seu documento precise de variáveis como Gestor, Nome do Projeto, Data formatada, Resumo ou Lista de Tarefas, clique em <strong>Adicionar Campo / Seção</strong>.
</p>
</div>
) : (
@ -381,7 +358,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
sectionKey: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'),
})
}
placeholder="ex: gestor ou resumo"
placeholder="ex: gestor ou periodo"
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"
/>
</div>
@ -392,7 +369,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
type="text"
value={section.title}
onChange={(e) => handleUpdateSection(idx, { title: e.target.value })}
placeholder="ex: Nome do Gestor"
placeholder="ex: Período de Vigência"
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"
/>
</div>
@ -404,18 +381,51 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
value={section.sectionType}
onChange={(val) =>
handleUpdateSection(idx, {
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST',
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST',
})
}
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: 'TASK_LIST', label: 'Lista de Tarefas do Período' },
]}
/>
</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">
<label className="text-[10px] font-semibold text-foreground flex items-center justify-between">
<span className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5 text-brand" />
Formato de Exibição da Data / Período
</span>
<span className="text-[9px] text-muted">Escolha a formatação para o Word</span>
</label>
<CustomSelect
size="sm"
value={section.dateFormat || 'DD/MM/AAAA'}
onChange={(val) => handleUpdateSection(idx, { dateFormat: String(val) })}
options={[
{ value: 'DD/MM/AAAA', label: 'DD/MM/AAAA (ex: 31/10/2026)' },
{ value: 'DD-MM-AAAA', label: 'DD-MM-AAAA (ex: 31-10-2026)' },
{ value: 'AAAA-MM-DD', label: 'AAAA-MM-DD (ex: 2026-10-31)' },
{ 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 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-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', label: 'DD de Mês de AAAA (ex: 31 de Outubro de 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)' },
]}
/>
</div>
)}
{/* Campo de Valor Pré-preenchido (Fixo ou Sugerido) */}
{(section.sectionType === 'FIXED_TEXT' || section.sectionType === 'FREE_TEXT') && (
<div className="pt-1 border-t border-border-soft/60 animate-in fade-in duration-150">

View file

@ -94,6 +94,12 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
)
}, [selectedTemplate])
// Seções de data do template selecionado
const dateSections = useMemo(() => {
if (!selectedTemplate) return []
return selectedTemplate.sections.filter((s) => s.sectionType === 'DATE')
}, [selectedTemplate])
// Lista de seções de tarefas do template selecionado
const taskListSections = useMemo(() => {
if (!selectedTemplate) return []
@ -358,6 +364,70 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
</div>
</div>
{/* Campos de Data Customizados do Modelo */}
{dateSections.length > 0 && (
<div className="space-y-3 p-4 rounded-2xl border border-border-soft bg-surface-raised/40">
<div className="border-b border-border-soft pb-1">
<h4 className="text-xs font-bold text-foreground flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5 text-brand" />
Datas Configuradas no Modelo
</h4>
<p className="text-[11px] text-muted">
Campos formatados de acordo com as regras definidas no modelo.
</p>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{dateSections.map((sec) => {
const isPeriod =
sec.dateFormat?.includes('até') ||
sec.dateFormat?.includes(' a ') ||
sec.dateFormat?.includes(' - ') ||
sec.sectionKey.includes('periodo') ||
sec.sectionKey.includes('vigencia')
return (
<div key={sec.sectionKey} className="space-y-1">
<label className="text-[11px] font-semibold text-foreground flex items-center justify-between">
<span>{sec.title}</span>
<code className="text-[10px] text-brand font-mono font-bold">{`{{${sec.sectionKey}}}`}</code>
</label>
{isPeriod ? (
<div className="px-3 py-2 rounded-xl border border-border-soft bg-surface/70 text-xs text-foreground/80 flex items-center justify-between">
<span className="truncate">Período de Vigência</span>
<span className="text-[10px] font-mono text-brand font-bold bg-brand/10 px-1.5 py-0.5 rounded">
{sec.dateFormat || 'DD/MM/AAAA'}
</span>
</div>
) : (
<div>
<input
type="date"
value={
customFields[sec.sectionKey] ||
(sec.sectionKey.includes('inicio')
? startDate
: sec.sectionKey.includes('fim')
? endDate
: endDate)
}
onChange={(e) => handleCustomFieldChange(sec.sectionKey, e.target.value)}
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"
/>
<span className="text-[10px] text-muted block mt-0.5">
Formato no Word:{' '}
<strong className="font-mono text-brand">{sec.dateFormat || 'DD/MM/AAAA'}</strong>
</span>
</div>
)}
</div>
)
})}
</div>
</div>
)}
{/* Seções de Texto (Livre ou Pré-preenchido) Dinâmicas */}
{textSections.length > 0 && (
<div className="space-y-3.5">

View file

@ -272,7 +272,8 @@ export const ReportsPage: React.FC = () => {
)}
</div>
{/* Dados da Seção Padrão */}
{/* Informações adicionais se houver */}
{(tpl.managerName || tpl.customProjectName || tpl.projectName) && (
<div className="p-2.5 rounded-xl bg-surface-raised/60 border border-border-soft text-[11px] space-y-1">
{tpl.managerName && (
<div className="flex items-center justify-between gap-1 text-muted">
@ -286,11 +287,8 @@ export const ReportsPage: React.FC = () => {
<span className="font-semibold text-foreground truncate">{tpl.customProjectName || tpl.projectName}</span>
</div>
)}
<div className="flex items-center justify-between gap-1 text-muted">
<span>Padrão de Data:</span>
<span className="font-mono text-[10px] text-brand truncate">{tpl.dateFormatPattern || 'DD/MM/AAAA até DD/MM/AAAA'}</span>
</div>
</div>
)}
{/* Tags configuradas */}
<div className="space-y-1.5 pt-1">
@ -301,10 +299,13 @@ export const ReportsPage: React.FC = () => {
{tpl.sections.map((s) => (
<span
key={s.sectionKey}
title={`${s.title} (${s.sectionType}${s.defaultValue ? ` — Valor fixo: ${s.defaultValue}` : ''})`}
title={`${s.title} (${s.sectionType}${s.dateFormat ? ` — Formato: ${s.dateFormat}` : ''}${s.defaultValue ? ` — Valor fixo: ${s.defaultValue}` : ''})`}
className="px-2 py-0.5 rounded-md text-[10px] font-mono font-semibold bg-surface-raised border border-border-soft text-foreground/80 inline-flex items-center gap-1"
>
<span>{`{{${s.sectionKey}}}`}</span>
{s.sectionType === 'DATE' && (
<span className="text-[9px] text-brand font-sans font-bold">[{s.dateFormat || 'data'}]</span>
)}
{s.defaultValue && (
<span className="w-1.5 h-1.5 rounded-full bg-brand" title="Valor pré-preenchido" />
)}

View file

@ -4,8 +4,9 @@ export interface ReportSectionOutput {
id: number
sectionKey: string
title: string
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST'
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
defaultValue?: string
dateFormat?: string
filterCategoryId?: number
filterCategoryName?: string
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'