feat(reports): allow fully customizable sections with pre-filled fixed values
This commit is contained in:
parent
5b4135b125
commit
8054ca9a6d
13 changed files with 312 additions and 263 deletions
|
|
@ -8,7 +8,8 @@ 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' ou 'TASK_LIST'
|
private String sectionType; // 'FREE_TEXT', 'FIXED_TEXT', 'TASK_LIST'
|
||||||
|
private String defaultValue;
|
||||||
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;
|
||||||
|
|
@ -55,6 +56,14 @@ public class ReportSection {
|
||||||
this.sectionType = sectionType;
|
this.sectionType = sectionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDefaultValue() {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultValue(String defaultValue) {
|
||||||
|
this.defaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
public Category getFilterCategory() {
|
public Category getFilterCategory() {
|
||||||
return filterCategory;
|
return filterCategory;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,10 @@ 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' 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)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
@JoinColumn(name = "filter_category_id")
|
@JoinColumn(name = "filter_category_id")
|
||||||
|
|
@ -88,6 +91,14 @@ public class ReportSectionEntity {
|
||||||
this.sectionType = sectionType;
|
this.sectionType = sectionType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getDefaultValue() {
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDefaultValue(String defaultValue) {
|
||||||
|
this.defaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
public CategoryEntity getFilterCategory() {
|
public CategoryEntity getFilterCategory() {
|
||||||
return filterCategory;
|
return filterCategory;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ public class ReportEntityMapper {
|
||||||
entity.setSectionKey(domain.getSectionKey());
|
entity.setSectionKey(domain.getSectionKey());
|
||||||
entity.setTitle(domain.getTitle());
|
entity.setTitle(domain.getTitle());
|
||||||
entity.setSectionType(domain.getSectionType());
|
entity.setSectionType(domain.getSectionType());
|
||||||
|
entity.setDefaultValue(domain.getDefaultValue());
|
||||||
entity.setFilterStatus(domain.getFilterStatus());
|
entity.setFilterStatus(domain.getFilterStatus());
|
||||||
entity.setIncludeTime(domain.getIncludeTime());
|
entity.setIncludeTime(domain.getIncludeTime());
|
||||||
entity.setOrderIndex(domain.getOrderIndex());
|
entity.setOrderIndex(domain.getOrderIndex());
|
||||||
|
|
@ -104,6 +105,7 @@ public class ReportEntityMapper {
|
||||||
domain.setSectionKey(entity.getSectionKey());
|
domain.setSectionKey(entity.getSectionKey());
|
||||||
domain.setTitle(entity.getTitle());
|
domain.setTitle(entity.getTitle());
|
||||||
domain.setSectionType(entity.getSectionType());
|
domain.setSectionType(entity.getSectionType());
|
||||||
|
domain.setDefaultValue(entity.getDefaultValue());
|
||||||
domain.setFilterStatus(entity.getFilterStatus());
|
domain.setFilterStatus(entity.getFilterStatus());
|
||||||
domain.setIncludeTime(entity.getIncludeTime());
|
domain.setIncludeTime(entity.getIncludeTime());
|
||||||
domain.setOrderIndex(entity.getOrderIndex());
|
domain.setOrderIndex(entity.getOrderIndex());
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ public class ReportMapper {
|
||||||
section.getSectionKey(),
|
section.getSectionKey(),
|
||||||
section.getTitle(),
|
section.getTitle(),
|
||||||
section.getSectionType(),
|
section.getSectionType(),
|
||||||
|
section.getDefaultValue(),
|
||||||
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(),
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ public record ReportSectionOutput(
|
||||||
String sectionKey,
|
String sectionKey,
|
||||||
String title,
|
String title,
|
||||||
String sectionType,
|
String sectionType,
|
||||||
|
String defaultValue,
|
||||||
Long filterCategoryId,
|
Long filterCategoryId,
|
||||||
String filterCategoryName,
|
String filterCategoryName,
|
||||||
String filterStatus,
|
String filterStatus,
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ public record CreateReportTemplateInput(
|
||||||
String sectionKey,
|
String sectionKey,
|
||||||
String title,
|
String title,
|
||||||
String sectionType,
|
String sectionType,
|
||||||
|
String defaultValue,
|
||||||
Long filterCategoryId,
|
Long filterCategoryId,
|
||||||
String filterStatus,
|
String filterStatus,
|
||||||
Boolean includeTime,
|
Boolean includeTime,
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,7 @@ public class CreateReportTemplateUseCase {
|
||||||
section.setSectionKey(sInput.sectionKey().trim().toLowerCase().replaceAll("[^a-z0-9_]", "_"));
|
section.setSectionKey(sInput.sectionKey().trim().toLowerCase().replaceAll("[^a-z0-9_]", "_"));
|
||||||
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.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++);
|
||||||
|
|
|
||||||
|
|
@ -179,10 +179,22 @@ public class GenerateReportUseCase {
|
||||||
variables.put("total_horas", formattedTotalTime);
|
variables.put("total_horas", formattedTotalTime);
|
||||||
variables.put("data_emissao", LocalDate.now().format(DATE_FORMATTER));
|
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
|
// Incorporar campos de texto customizados enviados pelo usuário
|
||||||
if (input.customFields() != null) {
|
if (input.customFields() != null) {
|
||||||
for (Map.Entry<String, String> entry : input.customFields().entrySet()) {
|
for (Map.Entry<String, String> 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE report_sections ADD COLUMN default_value TEXT;
|
||||||
|
|
@ -8,9 +8,9 @@ import {
|
||||||
Tag,
|
Tag,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
Folder,
|
Folder,
|
||||||
UserCheck,
|
|
||||||
Calendar,
|
Calendar,
|
||||||
User,
|
Sparkles,
|
||||||
|
Info,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { CustomSelect } from '../ui/Select'
|
import { CustomSelect } from '../ui/Select'
|
||||||
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
||||||
|
|
@ -28,7 +28,8 @@ interface CreateTemplateModalProps {
|
||||||
interface SectionFormItem {
|
interface SectionFormItem {
|
||||||
sectionKey: string
|
sectionKey: string
|
||||||
title: string
|
title: string
|
||||||
sectionType: 'FREE_TEXT' | 'TASK_LIST'
|
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST'
|
||||||
|
defaultValue?: string
|
||||||
filterCategoryId?: number
|
filterCategoryId?: number
|
||||||
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'
|
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'
|
||||||
includeTime: boolean
|
includeTime: boolean
|
||||||
|
|
@ -46,32 +47,9 @@ 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 [managerName, setManagerName] = useState('')
|
|
||||||
const [customProjectName, setCustomProjectName] = useState('')
|
|
||||||
const [dateFormatPattern, setDateFormatPattern] = useState('DD/MM/AAAA até DD/MM/AAAA')
|
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[]>([])
|
||||||
{
|
|
||||||
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 [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
|
@ -79,12 +57,14 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
if (!isOpen) return null
|
if (!isOpen) return null
|
||||||
|
|
||||||
const handleAddSection = () => {
|
const handleAddSection = () => {
|
||||||
|
const nextIndex = sections.length + 1
|
||||||
setSections((prev) => [
|
setSections((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
sectionKey: `secao_${prev.length + 1}`,
|
sectionKey: `campo_${nextIndex}`,
|
||||||
title: `Nova Seção ${prev.length + 1}`,
|
title: `Campo ${nextIndex}`,
|
||||||
sectionType: 'FREE_TEXT',
|
sectionType: 'FREE_TEXT',
|
||||||
|
defaultValue: '',
|
||||||
includeTime: false,
|
includeTime: false,
|
||||||
},
|
},
|
||||||
])
|
])
|
||||||
|
|
@ -135,18 +115,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('managerName', managerName.trim())
|
|
||||||
formData.append('customProjectName', customProjectName.trim())
|
|
||||||
formData.append('dateFormatPattern', dateFormatPattern)
|
formData.append('dateFormatPattern', dateFormatPattern)
|
||||||
if (scope === 'PROJECT' && projectId) {
|
if (scope === 'PROJECT' && projectId) {
|
||||||
formData.append('projectId', String(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) => ({
|
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() : '',
|
||||||
filterCategoryId: s.filterCategoryId,
|
filterCategoryId: s.filterCategoryId,
|
||||||
filterStatus: s.filterStatus || 'ALL',
|
filterStatus: s.filterStatus || 'ALL',
|
||||||
includeTime: s.includeTime ?? true,
|
includeTime: s.includeTime ?? true,
|
||||||
|
|
@ -183,7 +162,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 as tags que serão preenchidas automaticamente.
|
Envie seu arquivo Word e configure os campos dinâmicos e valores pré-preenchidos.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -256,89 +235,27 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Descrição */}
|
{/* Descrição e Formato do Período */}
|
||||||
|
<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 ou Orientações <span className="text-muted font-normal">(Opcional)</span>
|
Descrição <span className="text-muted font-normal">(Opcional)</span>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="Ex.: Template padrão exigido para prestação de contas mensal."
|
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"
|
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>
|
||||||
|
|
||||||
{/* Seção Padrão (Identificação, Gestão e Período) */}
|
<div className="space-y-1.5">
|
||||||
<div className="p-4 rounded-2xl border border-brand/25 bg-brand-soft/20 dark:bg-brand/10 space-y-3.5">
|
<label className="text-xs font-bold text-foreground flex items-center justify-between">
|
||||||
<div className="flex items-center gap-2">
|
<span className="flex items-center gap-1.5">
|
||||||
<UserCheck className="w-4 h-4 text-brand" />
|
<Calendar className="w-3.5 h-3.5 text-muted" />
|
||||||
<div>
|
Formato da tag {'{{periodo}}'}
|
||||||
<h4 className="text-xs font-extrabold text-brand uppercase tracking-wider">
|
|
||||||
Seção Padrão: Identificação e Gestão
|
|
||||||
</h4>
|
|
||||||
<p className="text-[11px] text-muted">
|
|
||||||
Valores configurados no modelo que alimentam o cabeçalho e identificação no Word.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
|
|
||||||
{/* Colaborador / Usuário (Automático) */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[11px] font-bold text-foreground flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<User className="w-3 h-3 text-muted" /> Nome do Colaborador
|
|
||||||
</span>
|
</span>
|
||||||
<code className="text-[10px] text-brand font-mono font-bold">{'{{nome}}'}</code>
|
|
||||||
</label>
|
|
||||||
<div className="px-3 py-2 rounded-xl border border-border-soft bg-surface/60 text-xs text-muted font-medium">
|
|
||||||
Preenchido automaticamente com o usuário logado
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nome do Gestor */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[11px] font-bold text-foreground flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<UserCheck className="w-3 h-3 text-muted" /> Nome do Gestor Responsável
|
|
||||||
</span>
|
|
||||||
<code className="text-[10px] text-brand font-mono font-bold">{'{{nome_gestor}}'}</code>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={managerName}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nome do Projeto */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[11px] font-bold text-foreground flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Folder className="w-3 h-3 text-muted" /> Nome do Projeto no Relatório
|
|
||||||
</span>
|
|
||||||
<code className="text-[10px] text-brand font-mono font-bold">{'{{nome_projeto}}'}</code>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={customProjectName}
|
|
||||||
onChange={(e) => 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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Formato da Data do Período */}
|
|
||||||
<div className="space-y-1">
|
|
||||||
<label className="text-[11px] font-bold text-foreground flex items-center justify-between">
|
|
||||||
<span className="flex items-center gap-1">
|
|
||||||
<Calendar className="w-3 h-3 text-muted" /> Formato do Período
|
|
||||||
</span>
|
|
||||||
<code className="text-[10px] text-brand font-mono font-bold">{'{{periodo}}'}</code>
|
|
||||||
</label>
|
</label>
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
@ -355,6 +272,21 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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="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">{'{{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.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Upload de Arquivo .DOCX */}
|
{/* Upload de Arquivo .DOCX */}
|
||||||
|
|
@ -386,13 +318,13 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Construtor de Seções Dinâmicas */}
|
{/* Construtor de Campos e Seções */}
|
||||||
<div className="space-y-3 pt-2">
|
<div className="space-y-3 pt-2">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<h4 className="text-xs font-bold text-foreground">Seções e Tags Dinâmicas do Modelo</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">
|
||||||
Insira essas tags entre <code className="text-brand font-bold">{'{{nome_da_tag}}'}</code> 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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
|
|
@ -401,10 +333,19 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-brand/10 hover:bg-brand/20 text-brand text-xs font-bold transition-colors cursor-pointer"
|
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-brand/10 hover:bg-brand/20 text-brand text-xs font-bold transition-colors cursor-pointer"
|
||||||
>
|
>
|
||||||
<Plus className="w-3.5 h-3.5" />
|
<Plus className="w-3.5 h-3.5" />
|
||||||
<span>Adicionar Seção</span>
|
<span>Adicionar Campo / Seção</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{sections.length === 0 ? (
|
||||||
|
<div className="p-6 rounded-2xl border border-dashed border-border-soft text-center space-y-2 bg-surface/40">
|
||||||
|
<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>.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{sections.map((section, idx) => (
|
{sections.map((section, idx) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -414,7 +355,8 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="text-[11px] font-bold text-brand flex items-center gap-1.5">
|
<span className="text-[11px] font-bold text-brand flex items-center gap-1.5">
|
||||||
<Tag className="w-3 h-3" />
|
<Tag className="w-3 h-3" />
|
||||||
Tag no Word: <code className="bg-surface px-1.5 py-0.5 rounded-md border border-border-soft text-foreground font-mono font-bold">
|
Tag no Word:{' '}
|
||||||
|
<code className="bg-surface px-1.5 py-0.5 rounded-md border border-border-soft text-foreground font-mono font-bold">
|
||||||
{`{{${section.sectionKey || 'tag'}}}`}
|
{`{{${section.sectionKey || 'tag'}}}`}
|
||||||
</code>
|
</code>
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -422,7 +364,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRemoveSection(idx)}
|
onClick={() => handleRemoveSection(idx)}
|
||||||
className="p-1 rounded-lg text-muted hover:text-rose-500 hover:bg-rose-500/10 transition-colors cursor-pointer"
|
className="p-1 rounded-lg text-muted hover:text-rose-500 hover:bg-rose-500/10 transition-colors cursor-pointer"
|
||||||
title="Remover seção"
|
title="Remover campo"
|
||||||
>
|
>
|
||||||
<Trash2 className="w-3.5 h-3.5" />
|
<Trash2 className="w-3.5 h-3.5" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -439,38 +381,71 @@ 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: resumo_projeto"
|
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"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] font-semibold text-muted block mb-1">Título Amigável</label>
|
<label className="text-[10px] font-semibold text-muted block mb-1">Nome / Rótulo</label>
|
||||||
<input
|
<input
|
||||||
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: Resumo do Andamento"
|
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"
|
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>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label className="text-[10px] font-semibold text-muted block mb-1">Tipo de Seção</label>
|
<label className="text-[10px] font-semibold text-muted block mb-1">Tipo de Campo</label>
|
||||||
<CustomSelect
|
<CustomSelect
|
||||||
size="sm"
|
size="sm"
|
||||||
value={section.sectionType}
|
value={section.sectionType}
|
||||||
onChange={(val) =>
|
onChange={(val) =>
|
||||||
handleUpdateSection(idx, { sectionType: val as 'FREE_TEXT' | 'TASK_LIST' })
|
handleUpdateSection(idx, {
|
||||||
|
sectionType: val as 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'FREE_TEXT', label: 'Texto Livre (Digitado)' },
|
{ 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' },
|
{ value: 'TASK_LIST', label: 'Lista de Tarefas do Período' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
||||||
|
<label className="text-[10px] font-semibold text-muted block mb-1 flex items-center justify-between">
|
||||||
|
<span>
|
||||||
|
{section.sectionType === 'FIXED_TEXT'
|
||||||
|
? 'Valor Fixo Pré-preenchido'
|
||||||
|
: 'Valor Padrão / Sugestão Inicial (Opcional)'}
|
||||||
|
</span>
|
||||||
|
<span className="text-[9px] text-brand font-medium">
|
||||||
|
{section.sectionType === 'FIXED_TEXT'
|
||||||
|
? 'Sempre virá preenchido no relatório'
|
||||||
|
: 'Sugestão inicial editável'}
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={section.defaultValue || ''}
|
||||||
|
onChange={(e) => 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"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Configurações de Lista de Tarefas */}
|
||||||
{section.sectionType === 'TASK_LIST' && (
|
{section.sectionType === 'TASK_LIST' && (
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 pt-1 border-t border-border-soft/60 animate-in fade-in duration-150">
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2.5 pt-1 border-t border-border-soft/60 animate-in fade-in duration-150">
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -514,6 +489,7 @@ export const CreateTemplateModal: React.FC<CreateTemplateModalProps> = ({
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import {
|
||||||
CheckCircle2,
|
CheckCircle2,
|
||||||
FileText,
|
FileText,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Lock,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { CustomSelect } from '../ui/Select'
|
import { CustomSelect } from '../ui/Select'
|
||||||
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
||||||
|
|
@ -67,17 +68,30 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
||||||
return templates.find((t) => t.id === selectedTemplateId)
|
return templates.find((t) => t.id === selectedTemplateId)
|
||||||
}, [templates, selectedTemplateId])
|
}, [templates, selectedTemplateId])
|
||||||
|
|
||||||
// Ajustar projeto selecionado de acordo com o template
|
// Ajustar projeto selecionado e preencher valores padrão das seções
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (selectedTemplate?.projectId) {
|
if (selectedTemplate) {
|
||||||
|
if (selectedTemplate.projectId) {
|
||||||
setSelectedProjectId(selectedTemplate.projectId)
|
setSelectedProjectId(selectedTemplate.projectId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preencher campos com os valores padrão configurados no modelo
|
||||||
|
const initialFields: Record<string, string> = {}
|
||||||
|
selectedTemplate.sections?.forEach((sec) => {
|
||||||
|
if (sec.defaultValue) {
|
||||||
|
initialFields[sec.sectionKey] = sec.defaultValue
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setCustomFields(initialFields)
|
||||||
|
}
|
||||||
}, [selectedTemplate])
|
}, [selectedTemplate])
|
||||||
|
|
||||||
// Seções de texto livre do template selecionado
|
// Seções de texto (livre ou fixo) do template selecionado
|
||||||
const freeTextSections = useMemo(() => {
|
const textSections = useMemo(() => {
|
||||||
if (!selectedTemplate) return []
|
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])
|
}, [selectedTemplate])
|
||||||
|
|
||||||
// Lista de seções de tarefas do template selecionado
|
// Lista de seções de tarefas do template selecionado
|
||||||
|
|
@ -288,7 +302,7 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setPeriodPreset('custom')}
|
onClick={() => handlePresetChange('custom')}
|
||||||
className={`px-2.5 py-1 rounded-lg font-medium transition-all cursor-pointer ${
|
className={`px-2.5 py-1 rounded-lg font-medium transition-all cursor-pointer ${
|
||||||
periodPreset === 'custom'
|
periodPreset === 'custom'
|
||||||
? 'bg-brand text-white shadow-xs'
|
? 'bg-brand text-white shadow-xs'
|
||||||
|
|
@ -337,31 +351,48 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
||||||
<div className="text-[11px] text-muted flex items-center gap-1.5 pt-1">
|
<div className="text-[11px] text-muted flex items-center gap-1.5 pt-1">
|
||||||
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
<span className="w-1.5 h-1.5 rounded-full bg-accent animate-pulse" />
|
||||||
<span>
|
<span>
|
||||||
Refletirá nas tags: <code className="text-brand font-semibold">{'{{data_inicio}}'}</code>,{' '}
|
Refletirá nas tags: <code className="text-brand font-semibold">{'{{periodo}}'}</code>,{' '}
|
||||||
<code className="text-brand font-semibold">{'{{data_fim}}'}</code> e{' '}
|
<code className="text-brand font-semibold">{'{{data_inicio}}'}</code> e{' '}
|
||||||
<code className="text-brand font-semibold">{'{{periodo_vigencia}}'}</code>.
|
<code className="text-brand font-semibold">{'{{data_fim}}'}</code>.
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Seções de Texto Livre Dinâmicas */}
|
{/* Seções de Texto (Livre ou Pré-preenchido) Dinâmicas */}
|
||||||
{freeTextSections.length > 0 && (
|
{textSections.length > 0 && (
|
||||||
<div className="space-y-3.5">
|
<div className="space-y-3.5">
|
||||||
<div className="border-b border-border-soft pb-1">
|
<div className="border-b border-border-soft pb-1">
|
||||||
<h4 className="text-xs font-bold text-foreground">
|
<h4 className="text-xs font-bold text-foreground">
|
||||||
Informações Descritivas do Relatório
|
Informações Descritivas do Relatório
|
||||||
</h4>
|
</h4>
|
||||||
<p className="text-[11px] text-muted">
|
<p className="text-[11px] text-muted">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{freeTextSections.map((sec) => (
|
{textSections.map((sec) => (
|
||||||
<div key={sec.sectionKey} className="space-y-1.5">
|
<div key={sec.sectionKey} className="space-y-1.5">
|
||||||
<label className="text-xs font-bold text-foreground flex items-center justify-between">
|
<label className="text-xs font-bold text-foreground flex items-center justify-between">
|
||||||
|
<span className="flex items-center gap-1.5">
|
||||||
|
{sec.sectionType === 'FIXED_TEXT' && (
|
||||||
|
<span className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold bg-brand/10 text-brand">
|
||||||
|
<Lock className="w-2.5 h-2.5" /> Pré-preenchido
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
<span>{sec.title}</span>
|
<span>{sec.title}</span>
|
||||||
|
</span>
|
||||||
<span className="text-[10px] text-muted font-mono">{`{{${sec.sectionKey}}}`}</span>
|
<span className="text-[10px] text-muted font-mono">{`{{${sec.sectionKey}}}`}</span>
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
{sec.sectionType === 'FIXED_TEXT' ? (
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={customFields[sec.sectionKey] || ''}
|
||||||
|
onChange={(e) => handleCustomFieldChange(sec.sectionKey, e.target.value)}
|
||||||
|
placeholder={`Valor para "${sec.title}"...`}
|
||||||
|
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"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<textarea
|
<textarea
|
||||||
rows={3}
|
rows={3}
|
||||||
value={customFields[sec.sectionKey] || ''}
|
value={customFields[sec.sectionKey] || ''}
|
||||||
|
|
@ -369,6 +400,7 @@ export const GenerateReportModal: React.FC<GenerateReportModalProps> = ({
|
||||||
placeholder={`Escreva aqui o conteúdo para "${sec.title}"...`}
|
placeholder={`Escreva aqui o conteúdo para "${sec.title}"...`}
|
||||||
className="w-full p-3 rounded-xl border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand focus:ring-1 focus:ring-brand shadow-xs resize-y"
|
className="w-full p-3 rounded-xl border border-border-soft bg-surface text-xs text-foreground focus:outline-hidden focus:border-brand focus:ring-1 focus:ring-brand shadow-xs resize-y"
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -95,9 +95,7 @@ export const ReportsPage: React.FC = () => {
|
||||||
|
|
||||||
const standardTags = [
|
const standardTags = [
|
||||||
{ tag: '{{nome}}', desc: 'Nome do colaborador (pega automaticamente do usuário logado)' },
|
{ tag: '{{nome}}', desc: 'Nome do colaborador (pega automaticamente do usuário logado)' },
|
||||||
{ tag: '{{nome_gestor}}', desc: 'Nome do gestor responsável (pré-definido no modelo)' },
|
{ tag: '{{periodo}}', desc: 'Período formatado no padrão escolhido no modelo (ex.: 01/10/2026 até 31/10/2026)' },
|
||||||
{ tag: '{{nome_projeto}}', desc: 'Nome do projeto (pré-definido no cadastro do modelo)' },
|
|
||||||
{ tag: '{{periodo}}', desc: 'Período formatado no padrão escolhido no modelo (ex.: DD/MM/AAAA até DD/MM/AAAA)' },
|
|
||||||
{ tag: '{{data_inicio}}', desc: 'Data inicial do período escolhido (ex: 01/10/2026)' },
|
{ tag: '{{data_inicio}}', desc: 'Data inicial do período escolhido (ex: 01/10/2026)' },
|
||||||
{ tag: '{{data_fim}}', desc: 'Data final do período escolhido (ex: 31/10/2026)' },
|
{ tag: '{{data_fim}}', desc: 'Data final do período escolhido (ex: 31/10/2026)' },
|
||||||
{ tag: '{{periodo_vigencia}}', desc: 'Intervalo formatado no modelo (ex: 01/10/2026 até 31/10/2026)' },
|
{ tag: '{{periodo_vigencia}}', desc: 'Intervalo formatado no modelo (ex: 01/10/2026 até 31/10/2026)' },
|
||||||
|
|
@ -297,16 +295,19 @@ export const ReportsPage: React.FC = () => {
|
||||||
{/* Tags configuradas */}
|
{/* Tags configuradas */}
|
||||||
<div className="space-y-1.5 pt-1">
|
<div className="space-y-1.5 pt-1">
|
||||||
<span className="text-[10px] font-bold text-muted uppercase tracking-wider block">
|
<span className="text-[10px] font-bold text-muted uppercase tracking-wider block">
|
||||||
Seções Dinâmicas ({tpl.sections.length})
|
Campos e Seções ({tpl.sections.length})
|
||||||
</span>
|
</span>
|
||||||
<div className="flex flex-wrap gap-1.5 max-h-20 overflow-y-auto">
|
<div className="flex flex-wrap gap-1.5 max-h-20 overflow-y-auto">
|
||||||
{tpl.sections.map((s) => (
|
{tpl.sections.map((s) => (
|
||||||
<span
|
<span
|
||||||
key={s.sectionKey}
|
key={s.sectionKey}
|
||||||
title={`${s.title} (${s.sectionType})`}
|
title={`${s.title} (${s.sectionType}${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"
|
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"
|
||||||
>
|
>
|
||||||
{`{{${s.sectionKey}}}`}
|
<span>{`{{${s.sectionKey}}}`}</span>
|
||||||
|
{s.defaultValue && (
|
||||||
|
<span className="w-1.5 h-1.5 rounded-full bg-brand" title="Valor pré-preenchido" />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@ export interface ReportSectionOutput {
|
||||||
id: number
|
id: number
|
||||||
sectionKey: string
|
sectionKey: string
|
||||||
title: string
|
title: string
|
||||||
sectionType: 'FREE_TEXT' | 'TASK_LIST'
|
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'TASK_LIST'
|
||||||
|
defaultValue?: 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