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 Long reportTemplateId;
|
||||||
private String sectionKey;
|
private String sectionKey;
|
||||||
private String title;
|
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 defaultValue;
|
||||||
|
private String dateFormat;
|
||||||
private Category filterCategory;
|
private Category filterCategory;
|
||||||
private String filterStatus; // 'ALL', 'COMPLETED', 'IN_PROGRESS'
|
private String filterStatus; // 'ALL', 'COMPLETED', 'IN_PROGRESS'
|
||||||
private Boolean includeTime;
|
private Boolean includeTime;
|
||||||
|
|
@ -96,6 +97,14 @@ public class ReportSection {
|
||||||
this.orderIndex = orderIndex;
|
this.orderIndex = orderIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDateFormat() {
|
||||||
|
return dateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDateFormat(String dateFormat) {
|
||||||
|
this.dateFormat = dateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,11 +30,14 @@ public class ReportSectionEntity {
|
||||||
private String title;
|
private String title;
|
||||||
|
|
||||||
@Column(name = "section_type", nullable = false, length = 50)
|
@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")
|
@Column(name = "default_value", columnDefinition = "TEXT")
|
||||||
private String defaultValue;
|
private String defaultValue;
|
||||||
|
|
||||||
|
@Column(name = "date_format", length = 100)
|
||||||
|
private String dateFormat;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "filter_category_id")
|
@JoinColumn(name = "filter_category_id")
|
||||||
private CategoryEntity filterCategory;
|
private CategoryEntity filterCategory;
|
||||||
|
|
@ -131,6 +134,14 @@ public class ReportSectionEntity {
|
||||||
this.orderIndex = orderIndex;
|
this.orderIndex = orderIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDateFormat() {
|
||||||
|
return dateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDateFormat(String dateFormat) {
|
||||||
|
this.dateFormat = dateFormat;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,7 @@ public class ReportEntityMapper {
|
||||||
entity.setTitle(domain.getTitle());
|
entity.setTitle(domain.getTitle());
|
||||||
entity.setSectionType(domain.getSectionType());
|
entity.setSectionType(domain.getSectionType());
|
||||||
entity.setDefaultValue(domain.getDefaultValue());
|
entity.setDefaultValue(domain.getDefaultValue());
|
||||||
|
entity.setDateFormat(domain.getDateFormat());
|
||||||
entity.setFilterStatus(domain.getFilterStatus());
|
entity.setFilterStatus(domain.getFilterStatus());
|
||||||
entity.setIncludeTime(domain.getIncludeTime());
|
entity.setIncludeTime(domain.getIncludeTime());
|
||||||
entity.setOrderIndex(domain.getOrderIndex());
|
entity.setOrderIndex(domain.getOrderIndex());
|
||||||
|
|
@ -106,6 +107,7 @@ public class ReportEntityMapper {
|
||||||
domain.setTitle(entity.getTitle());
|
domain.setTitle(entity.getTitle());
|
||||||
domain.setSectionType(entity.getSectionType());
|
domain.setSectionType(entity.getSectionType());
|
||||||
domain.setDefaultValue(entity.getDefaultValue());
|
domain.setDefaultValue(entity.getDefaultValue());
|
||||||
|
domain.setDateFormat(entity.getDateFormat());
|
||||||
domain.setFilterStatus(entity.getFilterStatus());
|
domain.setFilterStatus(entity.getFilterStatus());
|
||||||
domain.setIncludeTime(entity.getIncludeTime());
|
domain.setIncludeTime(entity.getIncludeTime());
|
||||||
domain.setOrderIndex(entity.getOrderIndex());
|
domain.setOrderIndex(entity.getOrderIndex());
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,7 @@ public class ReportMapper {
|
||||||
section.getTitle(),
|
section.getTitle(),
|
||||||
section.getSectionType(),
|
section.getSectionType(),
|
||||||
section.getDefaultValue(),
|
section.getDefaultValue(),
|
||||||
|
section.getDateFormat(),
|
||||||
section.getFilterCategory() != null ? section.getFilterCategory().getId() : null,
|
section.getFilterCategory() != null ? section.getFilterCategory().getId() : null,
|
||||||
section.getFilterCategory() != null ? section.getFilterCategory().getName() : null,
|
section.getFilterCategory() != null ? section.getFilterCategory().getName() : null,
|
||||||
section.getFilterStatus(),
|
section.getFilterStatus(),
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ public record ReportSectionOutput(
|
||||||
String title,
|
String title,
|
||||||
String sectionType,
|
String sectionType,
|
||||||
String defaultValue,
|
String defaultValue,
|
||||||
|
String dateFormat,
|
||||||
Long filterCategoryId,
|
Long filterCategoryId,
|
||||||
String filterCategoryName,
|
String filterCategoryName,
|
||||||
String filterStatus,
|
String filterStatus,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ public record CreateReportTemplateInput(
|
||||||
String title,
|
String title,
|
||||||
String sectionType,
|
String sectionType,
|
||||||
String defaultValue,
|
String defaultValue,
|
||||||
|
String dateFormat,
|
||||||
Long filterCategoryId,
|
Long filterCategoryId,
|
||||||
String filterStatus,
|
String filterStatus,
|
||||||
Boolean includeTime,
|
Boolean includeTime,
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ public class CreateReportTemplateUseCase {
|
||||||
section.setTitle(sInput.title() != null ? sInput.title().trim() : section.getSectionKey());
|
section.setTitle(sInput.title() != null ? sInput.title().trim() : section.getSectionKey());
|
||||||
section.setSectionType(sInput.sectionType() != null ? sInput.sectionType() : "FREE_TEXT");
|
section.setSectionType(sInput.sectionType() != null ? sInput.sectionType() : "FREE_TEXT");
|
||||||
section.setDefaultValue(sInput.defaultValue() != null ? sInput.defaultValue().trim() : null);
|
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.setFilterStatus(sInput.filterStatus() != null ? sInput.filterStatus() : "ALL");
|
||||||
section.setIncludeTime(sInput.includeTime() != null ? sInput.includeTime() : true);
|
section.setIncludeTime(sInput.includeTime() != null ? sInput.includeTime() : true);
|
||||||
section.setOrderIndex(sInput.orderIndex() != null ? sInput.orderIndex() : order++);
|
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()))
|
if (("FREE_TEXT".equalsIgnoreCase(sec.getSectionType()) || "FIXED_TEXT".equalsIgnoreCase(sec.getSectionType()))
|
||||||
&& sec.getDefaultValue() != null) {
|
&& sec.getDefaultValue() != null) {
|
||||||
variables.put(sec.getSectionKey(), sec.getDefaultValue());
|
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);
|
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) {
|
private String formatPeriodWithPattern(LocalDate start, LocalDate end, String pattern) {
|
||||||
if (pattern == null || pattern.isBlank()) {
|
if (pattern == null || pattern.isBlank()) {
|
||||||
pattern = "DD/MM/AAAA até DD/MM/AAAA";
|
pattern = "DD/MM/AAAA até DD/MM/AAAA";
|
||||||
}
|
}
|
||||||
|
|
||||||
String strStart = start.format(DATE_FORMATTER);
|
String strSlashStart = start.format(DateTimeFormatter.ofPattern("dd/MM/yyyy"));
|
||||||
String strEnd = end.format(DATE_FORMATTER);
|
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) {
|
return switch (pattern) {
|
||||||
case "DD/MM/AAAA até DD/MM/AAAA" -> strStart + " até " + strEnd;
|
case "DD/MM/AAAA até DD/MM/AAAA" -> strSlashStart + " até " + strSlashEnd;
|
||||||
case "DD/MM/AAAA a DD/MM/AAAA" -> strStart + " a " + strEnd;
|
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" -> {
|
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 mStart = capitalize(start.getMonth().getDisplayName(TextStyle.FULL, PT_BR));
|
||||||
String mEnd = capitalize(end.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",
|
case "DD/MM - DD/MM/AAAA" -> String.format("%02d/%02d - %02d/%02d/%d",
|
||||||
start.getDayOfMonth(), start.getMonthValue(),
|
start.getDayOfMonth(), start.getMonthValue(),
|
||||||
end.getDayOfMonth(), end.getMonthValue(), end.getYear());
|
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 {
|
interface SectionFormItem {
|
||||||
sectionKey: string
|
sectionKey: string
|
||||||
title: string
|
title: string
|
||||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST'
|
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
|
dateFormat?: string
|
||||||
filterCategoryId?: number
|
filterCategoryId?: number
|
||||||
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'
|
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'
|
||||||
includeTime: boolean
|
includeTime: boolean
|
||||||
|
|
@ -47,7 +48,6 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
const [description, setDescription] = useState('')
|
const [description, setDescription] = useState('')
|
||||||
const [scope, setScope] = useState<'ORGANIZATION' | 'PROJECT'>('ORGANIZATION')
|
const [scope, setScope] = useState<'ORGANIZATION' | 'PROJECT'>('ORGANIZATION')
|
||||||
const [projectId, setProjectId] = useState<number | undefined>(undefined)
|
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 [file, setFile] = useState<File | null>(null)
|
||||||
const [sections, setSections] = useState<SectionFormItem[]>([])
|
const [sections, setSections] = useState<SectionFormItem[]>([])
|
||||||
|
|
||||||
|
|
@ -65,6 +65,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
title: `Campo ${nextIndex}`,
|
title: `Campo ${nextIndex}`,
|
||||||
sectionType: 'FREE_TEXT',
|
sectionType: 'FREE_TEXT',
|
||||||
defaultValue: '',
|
defaultValue: '',
|
||||||
|
dateFormat: 'DD/MM/AAAA',
|
||||||
includeTime: false,
|
includeTime: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
@ -115,17 +116,17 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
formData.append('name', name.trim())
|
formData.append('name', name.trim())
|
||||||
formData.append('description', description.trim())
|
formData.append('description', description.trim())
|
||||||
formData.append('scope', scope)
|
formData.append('scope', scope)
|
||||||
formData.append('dateFormatPattern', dateFormatPattern)
|
|
||||||
if (scope === 'PROJECT' && projectId) {
|
if (scope === 'PROJECT' && projectId) {
|
||||||
formData.append('projectId', String(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) => ({
|
const sanitizedSections = sections.map((s, idx) => ({
|
||||||
sectionKey: s.sectionKey.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_'),
|
sectionKey: s.sectionKey.trim().toLowerCase().replace(/[^a-z0-9_]/g, '_'),
|
||||||
title: s.title.trim() || s.sectionKey,
|
title: s.title.trim() || s.sectionKey,
|
||||||
sectionType: s.sectionType,
|
sectionType: s.sectionType,
|
||||||
defaultValue: s.defaultValue ? s.defaultValue.trim() : '',
|
defaultValue: s.defaultValue ? s.defaultValue.trim() : '',
|
||||||
|
dateFormat: s.sectionType === 'DATE' ? (s.dateFormat || 'DD/MM/AAAA') : undefined,
|
||||||
filterCategoryId: s.filterCategoryId,
|
filterCategoryId: s.filterCategoryId,
|
||||||
filterStatus: s.filterStatus || 'ALL',
|
filterStatus: s.filterStatus || 'ALL',
|
||||||
includeTime: s.includeTime ?? true,
|
includeTime: s.includeTime ?? true,
|
||||||
|
|
@ -162,7 +163,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
Cadastrar Modelo de Relatório (.DOCX)
|
Cadastrar Modelo de Relatório (.DOCX)
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-xs text-muted">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -235,8 +236,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Descrição e Formato do Período */}
|
{/* Descrição */}
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
||||||
<div className="space-y-1.5">
|
<div className="space-y-1.5">
|
||||||
<label className="text-xs font-bold text-foreground">
|
<label className="text-xs font-bold text-foreground">
|
||||||
Descrição <span className="text-muted font-normal">(Opcional)</span>
|
Descrição <span className="text-muted font-normal">(Opcional)</span>
|
||||||
|
|
@ -245,46 +245,23 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
type="text"
|
type="text"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
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"
|
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>
|
||||||
|
|
||||||
<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 */}
|
{/* 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">
|
<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" />
|
<Info className="w-4 h-4 text-brand shrink-0 mt-0.5" />
|
||||||
<div className="space-y-1 text-[11px] leading-relaxed">
|
<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">
|
<p className="text-muted">
|
||||||
Você pode utilizar diretamente as tags{' '}
|
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">{'{{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_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">{'{{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -324,7 +301,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs font-bold text-foreground">Campos e Seções do Relatório</h4>
|
<h4 className="text-xs font-bold text-foreground">Campos e Seções do Relatório</h4>
|
||||||
<p className="text-[11px] text-muted">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -342,7 +319,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
<Sparkles className="w-6 h-6 text-muted mx-auto" />
|
<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-xs font-semibold text-foreground">Nenhum campo personalizado adicionado</p>
|
||||||
<p className="text-[11px] text-muted max-w-md mx-auto">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -381,7 +358,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
sectionKey: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'),
|
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"
|
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>
|
</div>
|
||||||
|
|
@ -392,7 +369,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
type="text"
|
type="text"
|
||||||
value={section.title}
|
value={section.title}
|
||||||
onChange={(e) => handleUpdateSection(idx, { title: e.target.value })}
|
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"
|
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>
|
</div>
|
||||||
|
|
@ -404,18 +381,51 @@ 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' | 'TASK_LIST',
|
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
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: '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 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) */}
|
{/* Campo de Valor Pré-preenchido (Fixo ou Sugerido) */}
|
||||||
{(section.sectionType === 'FIXED_TEXT' || section.sectionType === 'FREE_TEXT') && (
|
{(section.sectionType === 'FIXED_TEXT' || section.sectionType === 'FREE_TEXT') && (
|
||||||
<div className="pt-1 border-t border-border-soft/60 animate-in fade-in duration-150">
|
<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])
|
}, [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
|
// Lista de seções de tarefas do template selecionado
|
||||||
const taskListSections = useMemo(() => {
|
const taskListSections = useMemo(() => {
|
||||||
if (!selectedTemplate) return []
|
if (!selectedTemplate) return []
|
||||||
|
|
@ -358,6 +364,70 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
</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 */}
|
{/* Seções de Texto (Livre ou Pré-preenchido) Dinâmicas */}
|
||||||
{textSections.length > 0 && (
|
{textSections.length > 0 && (
|
||||||
<div className="space-y-3.5">
|
<div className="space-y-3.5">
|
||||||
|
|
|
||||||
|
|
@ -272,7 +272,8 @@ export const ReportsPage: React.FC = () => {
|
||||||
)}
|
)}
|
||||||
</div>
|
</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">
|
<div className="p-2.5 rounded-xl bg-surface-raised/60 border border-border-soft text-[11px] space-y-1">
|
||||||
{tpl.managerName && (
|
{tpl.managerName && (
|
||||||
<div className="flex items-center justify-between gap-1 text-muted">
|
<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>
|
<span className="font-semibold text-foreground truncate">{tpl.customProjectName || tpl.projectName}</span>
|
||||||
</div>
|
</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>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Tags configuradas */}
|
{/* Tags configuradas */}
|
||||||
<div className="space-y-1.5 pt-1">
|
<div className="space-y-1.5 pt-1">
|
||||||
|
|
@ -301,10 +299,13 @@ export const ReportsPage: React.FC = () => {
|
||||||
{tpl.sections.map((s) => (
|
{tpl.sections.map((s) => (
|
||||||
<span
|
<span
|
||||||
key={s.sectionKey}
|
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"
|
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>
|
<span>{`{{${s.sectionKey}}}`}</span>
|
||||||
|
{s.sectionType === 'DATE' && (
|
||||||
|
<span className="text-[9px] text-brand font-sans font-bold">[{s.dateFormat || 'data'}]</span>
|
||||||
|
)}
|
||||||
{s.defaultValue && (
|
{s.defaultValue && (
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-brand" title="Valor pré-preenchido" />
|
<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
|
id: number
|
||||||
sectionKey: string
|
sectionKey: string
|
||||||
title: string
|
title: string
|
||||||
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST'
|
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
||||||
defaultValue?: string
|
defaultValue?: string
|
||||||
|
dateFormat?: string
|
||||||
filterCategoryId?: number
|
filterCategoryId?: number
|
||||||
filterCategoryName?: string
|
filterCategoryName?: string
|
||||||
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'
|
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue