559 lines
26 KiB
TypeScript
559 lines
26 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|
import {
|
|
X,
|
|
UploadCloud,
|
|
Plus,
|
|
Trash2,
|
|
FileText,
|
|
Tag,
|
|
AlertCircle,
|
|
Folder,
|
|
Calendar,
|
|
Sparkles,
|
|
Info,
|
|
Check,
|
|
} from 'lucide-react'
|
|
import { CustomSelect } from '../ui/Select'
|
|
import { reportService, type ReportTemplateOutput } from '../../services/reportService'
|
|
import type { Project, Category } from '../../types/task'
|
|
|
|
interface EditTemplateModalProps {
|
|
isOpen: boolean
|
|
onClose: () => void
|
|
onSuccess: (updatedTemplate: ReportTemplateOutput) => void
|
|
template: ReportTemplateOutput
|
|
orgId: number
|
|
projects: Project[]
|
|
categories: Category[]
|
|
}
|
|
|
|
interface SectionFormItem {
|
|
sectionKey: string
|
|
title: string
|
|
sectionType: 'FREE_TEXT' | 'FIXED_TEXT' | 'DATE' | 'TASK_LIST'
|
|
defaultValue?: string
|
|
dateFormat?: string
|
|
filterCategoryId?: number
|
|
filterStatus?: 'ALL' | 'COMPLETED' | 'IN_PROGRESS'
|
|
includeTime: boolean
|
|
}
|
|
|
|
export const EditTemplateModal: React.FC<EditTemplateModalProps> = ({
|
|
isOpen,
|
|
onClose,
|
|
onSuccess,
|
|
template,
|
|
orgId,
|
|
projects,
|
|
categories,
|
|
}) => {
|
|
const [name, setName] = useState(template.name)
|
|
const [description, setDescription] = useState(template.description || '')
|
|
const [scope, setScope] = useState<'ORGANIZATION' | 'PROJECT'>(template.scope)
|
|
const [projectId, setProjectId] = useState<number | undefined>(template.projectId)
|
|
const [file, setFile] = useState<File | null>(null)
|
|
const [sections, setSections] = useState<SectionFormItem[]>([])
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
setName(template.name)
|
|
setDescription(template.description || '')
|
|
setScope(template.scope)
|
|
setProjectId(template.projectId)
|
|
setFile(null)
|
|
setSections(
|
|
template.sections?.map((s) => ({
|
|
sectionKey: s.sectionKey,
|
|
title: s.title,
|
|
sectionType: s.sectionType,
|
|
defaultValue: s.defaultValue || '',
|
|
dateFormat: s.dateFormat || 'DD/MM/AAAA',
|
|
filterCategoryId: s.filterCategoryId,
|
|
filterStatus: s.filterStatus || 'ALL',
|
|
includeTime: s.includeTime ?? true,
|
|
})) || []
|
|
)
|
|
}, [template])
|
|
|
|
if (!isOpen) return null
|
|
|
|
const handleAddSection = () => {
|
|
const nextIndex = sections.length + 1
|
|
setSections((prev) => [
|
|
...prev,
|
|
{
|
|
sectionKey: `campo_${nextIndex}`,
|
|
title: `Campo ${nextIndex}`,
|
|
sectionType: 'FREE_TEXT',
|
|
defaultValue: '',
|
|
dateFormat: 'DD/MM/AAAA',
|
|
includeTime: false,
|
|
},
|
|
])
|
|
}
|
|
|
|
const handleRemoveSection = (index: number) => {
|
|
setSections((prev) => prev.filter((_, i) => i !== index))
|
|
}
|
|
|
|
const handleUpdateSection = (index: number, patch: Partial<SectionFormItem>) => {
|
|
setSections((prev) =>
|
|
prev.map((s, i) => (i === index ? { ...s, ...patch } : s))
|
|
)
|
|
}
|
|
|
|
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
if (e.target.files && e.target.files[0]) {
|
|
const selected = e.target.files[0]
|
|
if (!selected.name.toLowerCase().endsWith('.docx')) {
|
|
setError('O modelo deve ser um arquivo com extensão .docx (Word)')
|
|
return
|
|
}
|
|
setFile(selected)
|
|
setError(null)
|
|
}
|
|
}
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
if (!name.trim()) {
|
|
setError('Informe o nome do modelo de relatório')
|
|
return
|
|
}
|
|
|
|
try {
|
|
setLoading(true)
|
|
setError(null)
|
|
|
|
const formData = new FormData()
|
|
if (file) {
|
|
formData.append('file', file)
|
|
}
|
|
formData.append('name', name.trim())
|
|
formData.append('description', description.trim())
|
|
formData.append('scope', scope)
|
|
if (scope === 'PROJECT' && projectId) {
|
|
formData.append('projectId', String(projectId))
|
|
}
|
|
|
|
// 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,
|
|
orderIndex: idx,
|
|
}))
|
|
|
|
formData.append('sectionsJson', JSON.stringify(sanitizedSections))
|
|
|
|
const updated = await reportService.updateTemplate(orgId, template.id, formData)
|
|
onSuccess(updated)
|
|
onClose()
|
|
} catch (err: unknown) {
|
|
console.error(err)
|
|
setError(err instanceof Error ? err.message : 'Falha ao atualizar modelo de relatório')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-black/60 backdrop-blur-xs animate-in fade-in duration-200">
|
|
<div
|
|
className="w-full max-w-2xl bg-surface border border-border-soft rounded-3xl shadow-2xl overflow-hidden flex flex-col max-h-[90vh] animate-in zoom-in-95 duration-200"
|
|
role="dialog"
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between p-6 border-b border-border-soft">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-2xl bg-brand/10 text-brand border border-brand/20 flex items-center justify-center shadow-xs">
|
|
<FileText className="w-5 h-5 text-brand" />
|
|
</div>
|
|
<div>
|
|
<h2 className="text-base font-extrabold text-foreground">
|
|
Editar Modelo de Relatório (.DOCX)
|
|
</h2>
|
|
<p className="text-xs text-muted">
|
|
Altere configurações, tags, campos ou substitua o arquivo Word do modelo.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
className="p-2 rounded-xl text-muted hover:text-foreground hover:bg-surface-raised transition-colors cursor-pointer"
|
|
>
|
|
<X className="w-5 h-5" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Form Body */}
|
|
<form onSubmit={handleSubmit} className="p-6 overflow-y-auto space-y-5 flex-1">
|
|
{error && (
|
|
<div className="p-3.5 rounded-2xl bg-red-500/10 border border-red-500/20 text-red-700 dark:text-red-400 text-xs flex items-center gap-2">
|
|
<AlertCircle className="w-4 h-4 shrink-0" />
|
|
<span>{error}</span>
|
|
</div>
|
|
)}
|
|
|
|
{/* Nome e Escopo */}
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
|
|
<div className="sm:col-span-2 space-y-1.5">
|
|
<label className="text-xs font-bold text-foreground">
|
|
Nome do Modelo <span className="text-rose-500">*</span>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(e) => setName(e.target.value)}
|
|
placeholder="Ex.: Relatório Mensal de Atividades P&D"
|
|
className="w-full px-3.5 py-2.5 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"
|
|
required
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-1.5">
|
|
<label className="text-xs font-bold text-foreground">Nível / Escopo</label>
|
|
<CustomSelect
|
|
value={scope}
|
|
onChange={(val) => setScope(val as 'ORGANIZATION' | 'PROJECT')}
|
|
options={[
|
|
{ value: 'ORGANIZATION', label: 'Organização' },
|
|
{ value: 'PROJECT', label: 'Projeto Específico' },
|
|
]}
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Se for escopo de Projeto, selecionar qual */}
|
|
{scope === 'PROJECT' && (
|
|
<div className="space-y-1.5 animate-in fade-in duration-150">
|
|
<label className="text-xs font-bold text-foreground">Projeto Vinculado</label>
|
|
<CustomSelect
|
|
value={projectId ?? 0}
|
|
onChange={(val) => setProjectId(Number(val) === 0 ? undefined : Number(val))}
|
|
placeholder="Selecione o Projeto..."
|
|
leftIcon={<Folder className="w-3.5 h-3.5 text-brand" />}
|
|
options={[
|
|
{ value: 0, label: 'Selecione o Projeto...' },
|
|
...projects.map((p) => ({
|
|
value: p.id,
|
|
label: p.name,
|
|
color: p.color || '#0f766e',
|
|
})),
|
|
]}
|
|
/>
|
|
</div>
|
|
)}
|
|
|
|
{/* 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>
|
|
</label>
|
|
<input
|
|
type="text"
|
|
value={description}
|
|
onChange={(e) => setDescription(e.target.value)}
|
|
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>
|
|
|
|
{/* 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 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 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 campos para elas.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Upload de Arquivo .DOCX (Substituição Opcional) */}
|
|
<div className="space-y-2">
|
|
<label className="text-xs font-bold text-foreground flex items-center justify-between">
|
|
<span>Arquivo Template .DOCX</span>
|
|
<span className="text-[11px] text-muted font-normal">Opcional (apenas se desejar substituir)</span>
|
|
</label>
|
|
|
|
<label className="relative border-2 border-dashed border-border-soft hover:border-brand/40 bg-surface-raised/60 hover:bg-surface-raised rounded-2xl p-5 flex flex-col items-center justify-center cursor-pointer transition-all duration-200">
|
|
<input
|
|
type="file"
|
|
accept=".docx,application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
|
onChange={handleFileChange}
|
|
className="sr-only"
|
|
/>
|
|
<UploadCloud className="w-8 h-8 text-brand/70 mb-2" />
|
|
{file ? (
|
|
<div className="text-center">
|
|
<p className="text-xs font-bold text-brand flex items-center justify-center gap-1">
|
|
<Check className="w-3.5 h-3.5" /> Novo arquivo: {file.name}
|
|
</p>
|
|
<p className="text-[10px] text-muted">{(file.size / 1024).toFixed(1)} KB — Clique para escolher outro</p>
|
|
</div>
|
|
) : (
|
|
<div className="text-center space-y-0.5">
|
|
<p className="text-xs font-semibold text-foreground">
|
|
Arquivo atual: <span className="text-brand font-mono font-bold">{template.fileName}</span>
|
|
</p>
|
|
<p className="text-[10px] text-muted">Clique aqui para enviar um novo arquivo .docx se desejar alterá-lo</p>
|
|
</div>
|
|
)}
|
|
</label>
|
|
</div>
|
|
|
|
{/* Construtor de Campos e Seções */}
|
|
<div className="space-y-3 pt-2">
|
|
<div className="flex items-center justify-between">
|
|
<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, data (com formato personalizado) ou listagem de tarefas.
|
|
</p>
|
|
</div>
|
|
{sections.length > 0 && (
|
|
<span className="text-[11px] font-semibold text-muted bg-surface-raised px-2.5 py-1 rounded-xl border border-border-soft">
|
|
{sections.length} {sections.length === 1 ? 'campo' : 'campos'}
|
|
</span>
|
|
)}
|
|
</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 cadastrado</p>
|
|
<p className="text-[11px] text-muted max-w-md mx-auto">
|
|
Caso seu documento precise de variáveis como Gestor, Nome do Projeto, Data formatada, Resumo ou Lista de Tarefas, adicione campos abaixo.
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{sections.map((section, idx) => (
|
|
<div
|
|
key={idx}
|
|
className="p-3.5 rounded-2xl border border-border-soft bg-surface-raised/40 space-y-3"
|
|
>
|
|
<div className="flex items-center justify-between gap-2">
|
|
<span className="text-[11px] font-bold text-brand flex items-center gap-1.5">
|
|
<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">
|
|
{`{{${section.sectionKey || 'tag'}}}`}
|
|
</code>
|
|
</span>
|
|
<button
|
|
type="button"
|
|
onClick={() => handleRemoveSection(idx)}
|
|
className="p-1 rounded-lg text-muted hover:text-rose-500 hover:bg-rose-500/10 transition-colors cursor-pointer"
|
|
title="Remover campo"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" />
|
|
</button>
|
|
</div>
|
|
|
|
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2.5">
|
|
<div>
|
|
<label className="text-[10px] font-semibold text-muted block mb-1">Identificador da Tag</label>
|
|
<input
|
|
type="text"
|
|
value={section.sectionKey}
|
|
onChange={(e) =>
|
|
handleUpdateSection(idx, {
|
|
sectionKey: e.target.value.toLowerCase().replace(/[^a-z0-9_]/g, '_'),
|
|
})
|
|
}
|
|
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>
|
|
|
|
<div>
|
|
<label className="text-[10px] font-semibold text-muted block mb-1">Nome / Rótulo</label>
|
|
<input
|
|
type="text"
|
|
value={section.title}
|
|
onChange={(e) => handleUpdateSection(idx, { title: e.target.value })}
|
|
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>
|
|
|
|
<div>
|
|
<label className="text-[10px] font-semibold text-muted block mb-1">Tipo de Campo</label>
|
|
<CustomSelect
|
|
size="sm"
|
|
value={section.sectionType}
|
|
onChange={(val) =>
|
|
handleUpdateSection(idx, {
|
|
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">
|
|
<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' && (
|
|
<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>
|
|
<label className="text-[10px] font-semibold text-muted block mb-1">
|
|
Filtrar por Categoria (Opcional)
|
|
</label>
|
|
<CustomSelect
|
|
size="sm"
|
|
value={section.filterCategoryId ?? 0}
|
|
onChange={(val) =>
|
|
handleUpdateSection(idx, {
|
|
filterCategoryId: Number(val) === 0 ? undefined : Number(val),
|
|
})
|
|
}
|
|
options={[
|
|
{ value: 0, label: 'Todas as Categorias' },
|
|
...categories.map((c) => ({
|
|
value: c.id,
|
|
label: c.name,
|
|
color: c.color,
|
|
})),
|
|
]}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between pt-4">
|
|
<label className="flex items-center gap-2 cursor-pointer text-xs text-foreground">
|
|
<input
|
|
type="checkbox"
|
|
checked={section.includeTime}
|
|
onChange={(e) =>
|
|
handleUpdateSection(idx, { includeTime: e.target.checked })
|
|
}
|
|
className="rounded border-border text-brand focus:ring-brand"
|
|
/>
|
|
<span>Incluir duração/horas da tarefa</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* Botão de Adicionar Campo / Seção embaixo */}
|
|
<div className="pt-2">
|
|
<button
|
|
type="button"
|
|
onClick={handleAddSection}
|
|
className="w-full py-2.5 px-4 rounded-2xl border-2 border-dashed border-brand/35 hover:border-brand bg-brand/5 hover:bg-brand/10 text-brand text-xs font-bold flex items-center justify-center gap-2 transition-all cursor-pointer shadow-xs"
|
|
>
|
|
<Plus className="w-4 h-4" />
|
|
<span>Adicionar Campo / Seção</span>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
|
|
{/* Footer */}
|
|
<div className="flex items-center justify-end gap-3 p-5 border-t border-border-soft bg-surface-raised/40">
|
|
<button
|
|
type="button"
|
|
onClick={onClose}
|
|
disabled={loading}
|
|
className="px-4 py-2 rounded-xl text-xs font-semibold text-muted hover:text-foreground hover:bg-surface-raised transition-colors cursor-pointer"
|
|
>
|
|
Cancelar
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleSubmit}
|
|
disabled={loading}
|
|
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-gradient-to-r from-brand to-teal-700 hover:from-brand-hover hover:to-teal-800 text-white text-xs font-bold shadow-md shadow-brand/20 transition-all cursor-pointer disabled:opacity-50"
|
|
>
|
|
{loading ? 'Salvando Alterações...' : 'Salvar Alterações'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|