| 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> |
| ); |
| } |
|
|