sipracd / frontend /src /components /WorkItemForm.tsx
Guilherme Silberfarb Costa
Deploy current SIPRAC app snapshot
7fabf33
import { FormEvent, useEffect, useState } from "react";
import { dateValueToMonthInput, monthInputToDateValue } from "../lib/format";
import type { ActivityType, EvaluatorSummary, WorkItemCreatePayload, WorkItemFormOptions } from "../lib/types";
import { WORK_TYPE_OPTIONS } from "../lib/work-types";
import {
collectRegistrationGroups,
DEFAULT_REGISTRATION_GROUP,
fieldUsesIndividualScope,
hasIndividualScope,
normalizeRegistrationGroup,
propertyHasContent,
REGISTRATION_OPERATIONAL_SCOPE_FIELDS,
ScopeField,
seedPropertyRegistrationDetailsFromWorkItem,
setPayloadFieldScope,
} from "../lib/work-item-form";
import { WORK_UNIT_OPTIONS, normalizeWorkUnit } from "../lib/work-units";
const OTHER_OPTION = "__other__";
const operationalScopeOptions: Array<{ field: ScopeField; label: string }> = [
{ field: "schedule_forecast", label: "Previsão de cronograma" },
{ field: "priority", label: "Prioridade" },
{ field: "process_due_date", label: "Prazo final" },
{ field: "sei_due_date", label: "Prazo SEI" },
{ field: "sei_sent_date", label: "Envio ao SEI" },
{ field: "work_type", label: "Tipo do trabalho" },
{ field: "work_product_number", label: "Numeração" },
{ field: "support_reference", label: "Referência de apoio" },
];
const assignmentScopeOptions: Array<{ field: ScopeField; label: string }> = [
{ field: "evaluator", label: "Avaliador" },
{ field: "evaluator_due_date", label: "Prazo do avaliador" },
];
const modelScopeOptions: Array<{ field: ScopeField; label: string }> = [
{ field: "model", label: "Modelo" },
];
type WorkItemFormProps = {
mode: "create" | "edit";
initialValue: WorkItemCreatePayload;
activityTypes: ActivityType[];
evaluators: EvaluatorSummary[];
formOptions: WorkItemFormOptions;
saving: boolean;
error: string;
onSubmit: (payload: WorkItemCreatePayload) => Promise<void>;
};
function normalizeNullableText(value: string | null | undefined) {
const trimmed = (value ?? "").trim();
return trimmed || null;
}
function CatalogField({
label,
value,
options,
onChange,
}: {
label: string;
value: string | null;
options: string[];
onChange: (value: string | null) => void;
}) {
const [isCustom, setIsCustom] = useState(() => Boolean(value && !options.includes(value)));
useEffect(() => {
setIsCustom(Boolean(value && !options.includes(value)));
}, [options, value]);
const selectValue = isCustom ? OTHER_OPTION : value ? value : "";
return (
<div className="catalog-field">
<label>
{label}
<select
value={selectValue}
onChange={(event) => {
const nextValue = event.target.value;
if (!nextValue) {
setIsCustom(false);
onChange(null);
return;
}
if (nextValue === OTHER_OPTION) {
setIsCustom(true);
onChange(options.includes(value ?? "") ? "" : (value ?? ""));
return;
}
setIsCustom(false);
onChange(nextValue);
}}
>
<option value="">Sem informação</option>
{options.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
<option value={OTHER_OPTION}>Outro</option>
</select>
</label>
{isCustom ? (
<label>
{`${label} - outro`}
<input value={value ?? ""} onChange={(event) => onChange(event.target.value)} />
</label>
) : null}
</div>
);
}
function ScopeStatusChip({
label,
individual,
onClick,
}: {
label: string;
individual: boolean;
onClick: () => void;
}) {
return (
<button
type="button"
className={`scope-status-chip${individual ? " scope-status-chip-active" : ""}`}
onClick={onClick}
>
<span>
<strong>{label}</strong>
<small>{individual ? "Por inscrição" : "No processo"}</small>
</span>
</button>
);
}
function joinLabels(labels: string[]) {
if (labels.length === 0) return "";
if (labels.length === 1) return labels[0];
if (labels.length === 2) return `${labels[0]} e ${labels[1]}`;
return `${labels.slice(0, -1).join(", ")} e ${labels[labels.length - 1]}`;
}
export function WorkItemForm({
mode,
initialValue,
activityTypes,
evaluators,
formOptions,
saving,
error,
onSubmit,
}: WorkItemFormProps) {
const [form, setForm] = useState<WorkItemCreatePayload>(initialValue);
useEffect(() => {
setForm(initialValue);
}, [initialValue]);
const registrationGroupListId = `registration-groups-${mode}`;
const registrationGroups = collectRegistrationGroups(form.properties);
const usesIndividualOperationalDetails = hasIndividualScope(form.field_scopes, REGISTRATION_OPERATIONAL_SCOPE_FIELDS);
const usesIndividualModelDetails = usesIndividualField("model");
const individualOperationalLabels = operationalScopeOptions.filter((option) => usesIndividualField(option.field)).map((option) => option.label);
const individualAssignmentLabels = assignmentScopeOptions.filter((option) => usesIndividualField(option.field)).map((option) => option.label);
function usesIndividualField(fieldName: ScopeField) {
return fieldUsesIndividualScope(form.field_scopes, fieldName);
}
function updateField<K extends keyof WorkItemCreatePayload>(key: K, value: WorkItemCreatePayload[K]) {
setForm((current) => ({ ...current, [key]: value }));
}
function updateProperty(
index: number,
updater: (property: WorkItemCreatePayload["properties"][number]) => WorkItemCreatePayload["properties"][number],
) {
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) => (itemIndex === index ? updater(item) : item)),
}));
}
function updateScope(fieldName: ScopeField, checked: boolean) {
setForm((current) => {
const nextForm = { ...current, field_scopes: { ...current.field_scopes } };
setPayloadFieldScope(nextForm, fieldName, checked ? "individual" : "shared");
return nextForm;
});
}
async function handleSubmit(event: FormEvent) {
event.preventDefault();
const normalizedModelStatus = normalizeNullableText(form.model_status) ?? "analisar";
const payload: WorkItemCreatePayload = {
...form,
origin: normalizeNullableText(form.origin),
request_type: normalizeNullableText(form.request_type),
purpose: normalizeNullableText(form.purpose),
priority: normalizeNullableText(form.priority),
model_status: normalizedModelStatus,
entry_unit: normalizeWorkUnit(form.entry_unit),
model_name:
normalizedModelStatus === "sim" || normalizedModelStatus === "talvez" ? normalizeNullableText(form.model_name) : null,
model_solution: normalizedModelStatus === "nao" ? normalizeNullableText(form.model_solution) : null,
model_notes: normalizeNullableText(form.model_notes),
notes_summary: normalizeNullableText(form.notes_summary),
properties: form.properties
.filter((item) => propertyHasContent(item))
.map((item) => {
const propertyModelStatus = normalizeNullableText(item.model_status) ?? "analisar";
return {
...item,
model_status: propertyModelStatus,
model_name:
propertyModelStatus === "sim" || propertyModelStatus === "talvez" ? normalizeNullableText(item.model_name) : null,
model_solution: propertyModelStatus === "nao" ? normalizeNullableText(item.model_solution) : null,
model_notes: normalizeNullableText(item.model_notes),
};
}),
planned_activities: form.planned_activities.filter((item) => item.activity_type_id > 0),
};
await onSubmit(payload);
}
return (
<form className="form-grid" onSubmit={handleSubmit}>
<div className="form-section wide">
<h4>Dados do processo</h4>
<div className="form-columns three">
<label>
Processo
<input value={form.process_number} onChange={(event) => updateField("process_number", event.target.value)} required />
</label>
<CatalogField label="Origem" value={form.origin} options={formOptions.origins} onChange={(value) => updateField("origin", value)} />
<CatalogField
label="Tipo de solicitação"
value={form.request_type}
options={formOptions.request_types}
onChange={(value) => updateField("request_type", value)}
/>
</div>
<div className="form-columns two">
<label>
Título
<input value={form.title} onChange={(event) => updateField("title", event.target.value)} required />
</label>
<CatalogField
label="Finalidade"
value={form.purpose}
options={formOptions.purposes}
onChange={(value) => updateField("purpose", value)}
/>
</div>
<div className="form-columns one">
<label>
Palavras-chave
<input
value={form.search_keywords ?? ""}
onChange={(event) => updateField("search_keywords", event.target.value || null)}
placeholder="Ex.: apelido do imóvel, nome conhecido, referência interna"
/>
</label>
</div>
<div className="form-columns three">
<label>
Status
<select value={form.status} onChange={(event) => updateField("status", event.target.value)}>
<option value="sem_atribuicao">Sem atribuição</option>
<option value="com_avaliador">Com avaliador</option>
<option value="pendente_revisao">Pendente de revisão</option>
<option value="em_ajuste">Em ajuste</option>
<option value="revisado">Revisado</option>
<option value="enviado">Enviado</option>
</select>
</label>
<label>
Data de entrada
<input type="date" value={form.entry_date ?? ""} onChange={(event) => updateField("entry_date", event.target.value || null)} />
</label>
<label>
Unidade de entrada na DAI
<select value={form.entry_unit} onChange={(event) => updateField("entry_unit", normalizeWorkUnit(event.target.value))}>
{WORK_UNIT_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
</div>
</div>
<div className="form-section wide">
<h4>Compartilhamento entre inscrições</h4>
<p className="plain-note form-inline-note">
Por padrão, tudo começa no processo. Clique em um campo apenas quando ele precisar ser preenchido por inscrição.
</p>
<p className="plain-note section-inline-note">A data de entrada sempre pertence ao processo.</p>
<div className="scope-status-chip-list">
{[...operationalScopeOptions, ...assignmentScopeOptions, ...modelScopeOptions].map((option) => (
<ScopeStatusChip
key={option.field}
label={option.label}
individual={usesIndividualField(option.field)}
onClick={() => updateScope(option.field, !usesIndividualField(option.field))}
/>
))}
</div>
</div>
<div className="form-section wide">
<h4>Observações gerais</h4>
<label className="wide">
Observações gerais do processo
<textarea
value={form.notes_summary ?? ""}
onChange={(event) => updateField("notes_summary", event.target.value || null)}
rows={4}
/>
</label>
</div>
<div className="form-section wide">
<h4>Prazos, prioridade, produto e apoio</h4>
{usesIndividualOperationalDetails ? (
<p className="plain-note section-inline-note">
Por inscrição: {joinLabels(individualOperationalLabels)}.
</p>
) : null}
<div className="form-columns three">
{!usesIndividualField("schedule_forecast") ? (
<label>
Previsão de cronograma
<input
type="month"
value={dateValueToMonthInput(form.schedule_forecast)}
onChange={(event) => updateField("schedule_forecast", monthInputToDateValue(event.target.value))}
/>
</label>
) : null}
{!usesIndividualField("priority") ? (
<CatalogField
label="Prioridade"
value={form.priority}
options={formOptions.priorities}
onChange={(value) => updateField("priority", value)}
/>
) : null}
{!usesIndividualField("process_due_date") ? (
<label>
Prazo final do processo
<input
type="date"
value={form.process_due_date ?? ""}
onChange={(event) => updateField("process_due_date", event.target.value || null)}
/>
</label>
) : null}
</div>
<div className="form-columns three">
{!usesIndividualField("sei_due_date") ? (
<label>
Prazo SEI
<input type="date" value={form.sei_due_date ?? ""} onChange={(event) => updateField("sei_due_date", event.target.value || null)} />
</label>
) : null}
{!usesIndividualField("sei_sent_date") ? (
<label>
Data de envio ao SEI
<input
type="date"
value={form.sei_sent_date ?? ""}
onChange={(event) => updateField("sei_sent_date", event.target.value || null)}
/>
</label>
) : null}
{!usesIndividualField("work_type") ? (
<label>
Tipo do trabalho
<select value={form.work_type ?? ""} onChange={(event) => updateField("work_type", event.target.value || null)}>
<option value="">Sem definição</option>
{WORK_TYPE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
) : null}
</div>
<div className="form-columns two">
{!usesIndividualField("work_product_number") ? (
<label>
Numeração
<input
value={form.work_product_number ?? ""}
onChange={(event) => updateField("work_product_number", event.target.value || null)}
placeholder="Ex.: LA_043_2026"
/>
</label>
) : null}
{!usesIndividualField("support_reference") ? (
<label>
Referência de apoio
<input
value={form.support_reference ?? ""}
onChange={(event) => updateField("support_reference", event.target.value || null)}
/>
</label>
) : null}
</div>
{!usesIndividualOperationalDetails ? null : operationalScopeOptions.every((option) => usesIndividualField(option.field)) ? (
<p className="plain-note section-inline-note">Todos os dados desta seção serão preenchidos nas inscrições.</p>
) : null}
</div>
<div className="form-section wide">
<h4>Atribuição</h4>
{individualAssignmentLabels.length > 0 ? (
<p className="plain-note section-inline-note">
Por inscrição: {joinLabels(individualAssignmentLabels)}.
</p>
) : null}
<div className="form-columns two">
{!usesIndividualField("evaluator") ? (
<label>
Avaliador do processo
<select
value={form.evaluator_id ?? ""}
onChange={(event) => updateField("evaluator_id", event.target.value ? Number(event.target.value) : null)}
>
<option value="">Sem atribuição</option>
{evaluators.map((evaluator) => (
<option key={evaluator.id} value={evaluator.id}>
{evaluator.name}
</option>
))}
</select>
</label>
) : null}
{!usesIndividualField("evaluator_due_date") ? (
<label>
Prazo do avaliador para o processo inteiro
<input
type="date"
value={form.evaluator_due_date ?? ""}
onChange={(event) => updateField("evaluator_due_date", event.target.value || null)}
/>
</label>
) : null}
</div>
{assignmentScopeOptions.every((option) => usesIndividualField(option.field)) ? (
<p className="plain-note section-inline-note">A atribuição será feita diretamente nas inscrições.</p>
) : null}
</div>
<div className="form-section wide">
<h4>Modelo</h4>
{usesIndividualModelDetails ? (
<p className="plain-note section-inline-note">Os dados de modelo serão preenchidos diretamente nas inscrições.</p>
) : (
<div className="form-columns four">
<label>
Há modelo atualizado?
<select
value={form.model_status ?? "analisar"}
onChange={(event) => {
const nextStatus = event.target.value || null;
updateField("model_status", nextStatus);
if (nextStatus !== "nao") updateField("model_solution", null);
if (nextStatus !== "sim" && nextStatus !== "talvez") updateField("model_name", null);
}}
>
<option value="sim">Sim</option>
<option value="nao">Não</option>
<option value="talvez">Talvez</option>
<option value="analisar">Analisar</option>
</select>
</label>
{(form.model_status ?? "analisar") === "sim" || (form.model_status ?? "analisar") === "talvez" ? (
<label>
Nome do modelo
<input value={form.model_name ?? ""} onChange={(event) => updateField("model_name", event.target.value || null)} />
</label>
) : null}
{(form.model_status ?? "analisar") === "nao" ? (
<label>
Solução
<select value={form.model_solution ?? ""} onChange={(event) => updateField("model_solution", event.target.value || null)}>
<option value="">Sem definição</option>
<option value="atualizar">Atualizar</option>
<option value="elaborar">Elaborar</option>
</select>
</label>
) : null}
<label className="wide-label">
Observações do modelo
<input value={form.model_notes ?? ""} onChange={(event) => updateField("model_notes", event.target.value || null)} />
</label>
</div>
)}
</div>
<div className="form-section wide">
<div className="section-header">
<h4>Imóveis e inscrições</h4>
<button
type="button"
className="secondary-button"
onClick={() =>
setForm((current) => {
const nextProperty = {
street: "",
number: "",
registration: "",
registration_group: current.properties[0]?.registration_group ?? DEFAULT_REGISTRATION_GROUP,
schedule_forecast: null,
priority: null,
model_status: "analisar",
model_name: "",
model_solution: "",
model_notes: "",
assigned_evaluator_id: current.evaluator_id ?? null,
evaluator_due_date: null,
process_due_date: null,
sei_due_date: null,
sei_sent_date: null,
work_type: null,
work_product_number: null,
support_reference: null,
};
return {
...current,
properties: [
...current.properties,
hasIndividualScope(current.field_scopes)
? seedPropertyRegistrationDetailsFromWorkItem(nextProperty, current)
: nextProperty,
],
};
})
}
>
Adicionar imóvel
</button>
</div>
<p className="plain-note">
Todas as inscrições começam no mesmo grupo. Ajuste esse campo quando a linha técnica precisar avançar separadamente.
</p>
<datalist id={registrationGroupListId}>
{registrationGroups.map((group) => (
<option key={group} value={group} />
))}
</datalist>
{form.properties.map((property, index) => (
<div key={index} className="subform-block">
<div className="subform-head">
<strong>Imóvel {index + 1}</strong>
<button
type="button"
className="secondary-button danger-button"
onClick={() =>
setForm((current) => ({
...current,
properties: current.properties.filter((_, itemIndex) => itemIndex !== index),
}))
}
>
Remover imóvel
</button>
</div>
<div className="form-columns four">
<label>
Logradouro
<input
value={property.street ?? ""}
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index ? { ...item, street: event.target.value } : item,
),
}))
}
/>
</label>
<label>
Número
<input
value={property.number ?? ""}
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index ? { ...item, number: event.target.value } : item,
),
}))
}
/>
</label>
<label>
Inscrição
<input
value={property.registration ?? ""}
placeholder="Pode ficar em branco"
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index ? { ...item, registration: event.target.value } : item,
),
}))
}
/>
</label>
<label>
Complemento
<input
value={property.complement ?? ""}
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index ? { ...item, complement: event.target.value } : item,
),
}))
}
/>
</label>
</div>
<div className="form-columns four">
<label>
Grupo de inscrições
<input
list={registrationGroupListId}
value={property.registration_group ?? DEFAULT_REGISTRATION_GROUP}
placeholder="Ex.: Grupo 1"
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index
? { ...item, registration_group: normalizeRegistrationGroup(event.target.value) }
: item,
),
}))
}
/>
</label>
{usesIndividualField("evaluator") ? (
<label>
Avaliador desta inscrição
<select
value={property.assigned_evaluator_id ?? ""}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
assigned_evaluator_id: event.target.value ? Number(event.target.value) : null,
}))
}
>
<option value="">Sem atribuição</option>
{evaluators.map((evaluator) => (
<option key={evaluator.id} value={evaluator.id}>
{evaluator.name}
</option>
))}
</select>
</label>
) : null}
{usesIndividualField("evaluator_due_date") ? (
<label>
Prazo desta inscrição
<input
type="date"
value={property.evaluator_due_date ?? ""}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
evaluator_due_date: event.target.value || null,
}))
}
/>
</label>
) : null}
<label>
Observação da distribuição
<input
value={property.assignment_notes ?? ""}
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index ? { ...item, assignment_notes: event.target.value || null } : item,
),
}))
}
/>
</label>
</div>
{usesIndividualOperationalDetails ? (
<>
<div className="form-columns three">
{usesIndividualField("schedule_forecast") ? (
<label>
Previsão desta inscrição
<input
type="month"
value={dateValueToMonthInput(property.schedule_forecast)}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
schedule_forecast: monthInputToDateValue(event.target.value),
}))
}
/>
</label>
) : null}
{usesIndividualField("priority") ? (
<CatalogField
label="Prioridade desta inscrição"
value={property.priority ?? null}
options={formOptions.priorities}
onChange={(value) => updateProperty(index, (item) => ({ ...item, priority: value }))}
/>
) : null}
{usesIndividualField("process_due_date") ? (
<label>
Prazo final desta inscrição
<input
type="date"
value={property.process_due_date ?? ""}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
process_due_date: event.target.value || null,
}))
}
/>
</label>
) : null}
</div>
<div className="form-columns three">
{usesIndividualField("sei_due_date") ? (
<label>
Prazo SEI desta inscrição
<input
type="date"
value={property.sei_due_date ?? ""}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
sei_due_date: event.target.value || null,
}))
}
/>
</label>
) : null}
{usesIndividualField("sei_sent_date") ? (
<label>
Data de envio ao SEI
<input
type="date"
value={property.sei_sent_date ?? ""}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
sei_sent_date: event.target.value || null,
}))
}
/>
</label>
) : null}
{usesIndividualField("work_type") ? (
<label>
Tipo do trabalho
<select
value={property.work_type ?? ""}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
work_type: event.target.value || null,
}))
}
>
<option value="">Sem definição</option>
{WORK_TYPE_OPTIONS.map((option) => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</label>
) : null}
</div>
<div className="form-columns two">
{usesIndividualField("work_product_number") ? (
<label>
Numeração
<input
value={property.work_product_number ?? ""}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
work_product_number: event.target.value || null,
}))
}
/>
</label>
) : null}
{usesIndividualField("support_reference") ? (
<label>
Referência de apoio
<input
value={property.support_reference ?? ""}
onChange={(event) =>
updateProperty(index, (item) => ({
...item,
support_reference: event.target.value || null,
}))
}
/>
</label>
) : null}
</div>
</>
) : null}
{usesIndividualModelDetails ? (
<div className="form-columns four">
<label>
Há modelo atualizado?
<select
value={property.model_status ?? "analisar"}
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index
? {
...item,
model_status: event.target.value,
model_solution: event.target.value === "nao" ? item.model_solution : null,
}
: item,
),
}))
}
>
<option value="sim">Sim</option>
<option value="nao">Não</option>
<option value="talvez">Talvez</option>
<option value="analisar">Analisar</option>
</select>
</label>
{(property.model_status ?? "analisar") === "sim" || (property.model_status ?? "analisar") === "talvez" ? (
<label>
Nome do modelo
<input
value={property.model_name ?? ""}
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index ? { ...item, model_name: event.target.value || null } : item,
),
}))
}
/>
</label>
) : null}
{(property.model_status ?? "analisar") === "nao" ? (
<label>
Solução
<select
value={property.model_solution ?? ""}
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index ? { ...item, model_solution: event.target.value || null } : item,
),
}))
}
>
<option value="">Sem definição</option>
<option value="atualizar">Atualizar</option>
<option value="elaborar">Elaborar</option>
</select>
</label>
) : null}
<label className="wide-label">
Observações do modelo
<input
value={property.model_notes ?? ""}
onChange={(event) =>
setForm((current) => ({
...current,
properties: current.properties.map((item, itemIndex) =>
itemIndex === index ? { ...item, model_notes: event.target.value || null } : item,
),
}))
}
/>
</label>
</div>
) : null}
</div>
))}
</div>
<div className="form-section wide">
<div className="section-header">
<h4>Atividades planejadas</h4>
<button
type="button"
className="secondary-button"
onClick={() =>
setForm((current) => ({
...current,
planned_activities: [
...current.planned_activities,
{
activity_type_id: activityTypes[0]?.id ?? 0,
quantity: 1,
estimated_effort_days: activityTypes[0]?.standard_effort_days ?? 0,
status: "planejada",
planned_start: current.entry_date,
planned_end: current.process_due_date,
},
],
}))
}
>
Adicionar atividade
</button>
</div>
{form.planned_activities.map((activity, index) => (
<div key={index} className="subform-block">
<div className="subform-head">
<strong>Atividade {index + 1}</strong>
<button
type="button"
className="secondary-button danger-button"
onClick={() =>
setForm((current) => ({
...current,
planned_activities: current.planned_activities.filter((_, itemIndex) => itemIndex !== index),
}))
}
>
Remover atividade
</button>
</div>
<div className="form-columns four">
<label>
Atividade
<select
value={activity.activity_type_id}
onChange={(event) => {
const activityTypeId = Number(event.target.value);
const selected = activityTypes.find((item) => item.id === activityTypeId);
setForm((current) => ({
...current,
planned_activities: current.planned_activities.map((item, itemIndex) =>
itemIndex === index
? {
...item,
activity_type_id: activityTypeId,
estimated_effort_days: selected?.standard_effort_days ?? item.estimated_effort_days,
}
: item,
),
}));
}}
>
{activityTypes.map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</select>
</label>
<label>
Quantidade
<input
type="number"
min="1"
step="1"
value={activity.quantity}
onChange={(event) =>
setForm((current) => ({
...current,
planned_activities: current.planned_activities.map((item, itemIndex) =>
itemIndex === index ? { ...item, quantity: Number(event.target.value) } : item,
),
}))
}
/>
</label>
<label>
Esforço estimado
<input
type="number"
min="0"
step="0.25"
value={activity.estimated_effort_days}
onChange={(event) =>
setForm((current) => ({
...current,
planned_activities: current.planned_activities.map((item, itemIndex) =>
itemIndex === index ? { ...item, estimated_effort_days: Number(event.target.value) } : item,
),
}))
}
/>
</label>
<label>
Status
<select
value={activity.status}
onChange={(event) =>
setForm((current) => ({
...current,
planned_activities: current.planned_activities.map((item, itemIndex) =>
itemIndex === index ? { ...item, status: event.target.value } : item,
),
}))
}
>
<option value="planejada">Planejada</option>
<option value="em_andamento">Em andamento</option>
<option value="concluida">Concluída</option>
</select>
</label>
</div>
</div>
))}
</div>
{error ? <p className="feedback error">{error}</p> : null}
<div className="form-actions wide">
<button type="submit" className="primary-button" disabled={saving}>
{saving ? "Salvando..." : mode === "create" ? "Criar processo" : "Salvar alterações"}
</button>
</div>
</form>
);
}