feat(reports): configure date format per section when type is date
This commit is contained in:
parent
8054ca9a6d
commit
7624cff094
13 changed files with 256 additions and 76 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ public record ReportSectionOutput(
|
|||
String title,
|
||||
String sectionType,
|
||||
String defaultValue,
|
||||
String dateFormat,
|
||||
Long filterCategoryId,
|
||||
String filterCategoryName,
|
||||
String filterStatus,
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ public record CreateReportTemplateInput(
|
|||
String title,
|
||||
String sectionType,
|
||||
String defaultValue,
|
||||
String dateFormat,
|
||||
Long filterCategoryId,
|
||||
String filterStatus,
|
||||
Boolean includeTime,
|
||||
|
|
|
|||
|
|
@ -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++);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE report_sections ADD COLUMN date_format VARCHAR(100);
|
||||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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" />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
Loading…
Reference in a new issue