diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index fda626e..5e987df 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -440,9 +440,9 @@ se añade una primitiva genérica de wizard sin otros consumidores. Véase ### Presentación compacta del alta por pasos -El alta de recursos usa `FormDialog size="medium"` para limitar el ancho a 768 px; -la edición completa conserva el tamaño grande. La cabecera aporta un icono decorativo de -recurso. El indicador numera los pasos, distingue el actual y marca los completados con +El alta de recursos y el alta de activos desde el inventario de compañía usan +`FormDialog size="medium"` para limitar el ancho a 768 px; la edición completa conserva +el tamaño grande. La cabecera aporta un icono decorativo de la entidad. El indicador numera los pasos, distingue el actual y marca los completados con un símbolo y texto accesible; las etiquetas permanecen visibles también en móvil. Cada paso incluye una instrucción breve y, tras elegir la plantilla, muestra su nombre. @@ -455,9 +455,23 @@ comparten fila cuando hay espacio. Se conservan los márgenes del anillo de foco `FormBody`, la ayuda de campo, el desplazamiento del cuerpo y las acciones fijas. Se emplean los tokens semánticos del tema, sin estilos ni cambios en vendor. +El alta de activos sigue tres pasos: plantilla, características e identificación/notas. +Conserva marca y secciones propias de la plantilla; sede, estado técnico y fecha de alta +se completan al final. Propone el primer campo de texto distinto de marca, o el nombre +de la plantilla si no hay ninguno; un nombre editado manualmente se conserva al volver. +Cambiar de plantilla reinicia características y marca; volver sin cambiarla conserva los valores. +La edición y el alta anidada desde un puesto mantienen el formulario completo y sus pestañas. +`AssetForm.creationLayout` activa los pasos únicamente al crear. + +`FormStepActions` de `@repo/rdx-ui/components/form-step-actions` comparte el pie entre +recursos y activos: recibe paso/total, estado pendiente, disponibilidad de plantilla, +callback de retroceso y etiquetas traducidas. Solo muestra Guardar al final (o en modo +completo) y bloquea las acciones durante el envío; Cancelar lo aporta `FormDialog`. +No conoce entidades, formularios, consultas ni endpoints. + ## Progreso de formularios y alta de puestos -`FormProgress` de `@repo/rdx-ui/components/form-progress` presenta pasos numerados, marca el actual con `aria-current="step"`, identifica los completados y lleva el foco al anuncio traducido de progreso. Recibe etiquetas, descripción y textos accesibles del consumidor; no conoce entidades ni consultas. Lo usan los asistentes de recursos y puestos. +`FormProgress` de `@repo/rdx-ui/components/form-progress` presenta pasos numerados, marca el actual con `aria-current="step"`, identifica los completados y lleva el foco al anuncio traducido de progreso. Recibe etiquetas, descripción y textos accesibles del consumidor; no conoce entidades ni consultas. Lo usan los asistentes de recursos, activos y puestos. El nuevo puesto se organiza en sede/responsable, activos, ordenador principal y resumen. Los controles de asociaciones, composición, ordenador y resumen tienen responsabilidades propias y se reutilizan en la edición cuando corresponde. Las acciones Anterior/Siguiente conservan valores; Guardar aparece en el resumen. La sede única y el activo único tienen selección automática. Véase [Puestos de trabajo](puestos-de-trabajo.md#alta-en-cuatro-pasos). diff --git a/docs/patron-de-dialogos.md b/docs/patron-de-dialogos.md index ec52102..0a2dd2d 100644 --- a/docs/patron-de-dialogos.md +++ b/docs/patron-de-dialogos.md @@ -115,6 +115,11 @@ el [inventario de modales](inventario-de-modales.md) recoge las implementaciones - Elegir la anchura del diálogo según su contenido y respetar los márgenes de la ventana. Mantener cabecera y pie visibles y el cuerpo desplazable cuando el contenido lo requiera. +- Las altas guiadas de recursos y activos desde el inventario de compañía siguen tres + pasos: plantilla, características y datos finales. Reutilizar `FormProgress` y + `FormStepActions`, validar antes de avanzar y conservar el borrador al retroceder. + Guardar se muestra al final; ayuda, cancelación y errores permanecen dentro del diálogo. + El alta usa tamaño medio y la edición completa conserva tamaño grande. - Los formularios conservan altura según contenido; la altura estable por resultados es la regla de los `PickerList`. No introducir pestañas ni pasos para resolver solo espacio. - Situar ayuda contextual general al final del cuerpo, inmediatamente antes de `FormActions`. diff --git a/modules/assets/src/common/locales/en.json b/modules/assets/src/common/locales/en.json index c37d1a2..4d0e1a9 100644 --- a/modules/assets/src/common/locales/en.json +++ b/modules/assets/src/common/locales/en.json @@ -432,5 +432,19 @@ "confirm": "Select", "clear": "Clear selection", "required": "Required" + }, + "creationSteps": { + "label": "New asset steps", + "progress": "Step {{step}} of {{total}}", + "template": "Choose template", + "fields": "Complete fields", + "name": "Identification and notes", + "next": "Next", + "previous": "Back", + "nameHelp": "Name suggested from the asset fields. You can edit it.", + "completed": " (completed)", + "templateDescription": "Select the template that defines the asset characteristics.", + "fieldsDescription": "Complete the template details. Fields marked with * are required.", + "nameDescription": "Review the suggested name, technical status, site and registration date. Add notes if you need to provide more context." } } diff --git a/modules/assets/src/common/locales/es.json b/modules/assets/src/common/locales/es.json index 22342d9..d1f3dd8 100644 --- a/modules/assets/src/common/locales/es.json +++ b/modules/assets/src/common/locales/es.json @@ -432,5 +432,19 @@ "confirm": "Seleccionar", "clear": "Quitar selección", "required": "Obligatorio" + }, + "creationSteps": { + "label": "Pasos del nuevo activo", + "progress": "Paso {{step}} de {{total}}", + "template": "Elegir plantilla", + "fields": "Completar campos", + "name": "Identificación y notas", + "next": "Siguiente", + "previous": "Anterior", + "nameHelp": "Nombre propuesto a partir de los campos del activo. Puedes modificarlo.", + "completed": " (completado)", + "templateDescription": "Selecciona la plantilla que define las características del activo.", + "fieldsDescription": "Completa los datos de la plantilla. Los campos marcados con * son obligatorios.", + "nameDescription": "Revisa el nombre propuesto, el estado técnico, la sede y la fecha de alta. Añade notas si necesitas aportar más contexto." } } diff --git a/modules/assets/src/web/assets/create/ui/asset-create-progress.tsx b/modules/assets/src/web/assets/create/ui/asset-create-progress.tsx new file mode 100644 index 0000000..a660b7b --- /dev/null +++ b/modules/assets/src/web/assets/create/ui/asset-create-progress.tsx @@ -0,0 +1,31 @@ +import { FormProgress } from "@repo/rdx-ui/components/form-progress"; +import { useTranslation } from "react-i18next"; + +export function AssetCreateProgress({ + step, + templateName, +}: { + step: 1 | 2 | 3; + templateName?: string | undefined; +}) { + const { t } = useTranslation("assets"); + const keys = ["template", "fields", "name"] as const; + + return ( +
+ t(`creationSteps.${key}`))} + progress={t("creationSteps.progress", { step, total: 3 })} + step={step} + /> + {templateName && step > 1 && ( +

+ {t("template")}: {templateName} +

+ )} +
+ ); +} diff --git a/modules/assets/src/web/assets/shared/controllers/use-asset-form-controller.ts b/modules/assets/src/web/assets/shared/controllers/use-asset-form-controller.ts index 3b5ab6c..c6a27e7 100644 --- a/modules/assets/src/web/assets/shared/controllers/use-asset-form-controller.ts +++ b/modules/assets/src/web/assets/shared/controllers/use-asset-form-controller.ts @@ -1,15 +1,27 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQueryClient } from "@tanstack/react-query"; +import type { FormEvent } from "react"; import { useEffect, useRef, useState } from "react"; import { useForm } from "react-hook-form"; +import { useTranslation } from "react-i18next"; import type { AssetInput } from "../../../../common/inventory"; import { AssetInputSchema } from "../../../../common/inventory"; import { inventoryKeys } from "../../../shared/queries"; import { saveAsset } from "../api"; +import { hasMissingAssetFields, suggestedAssetName } from "../helpers/asset-creation-values"; import type { AssetFormProps } from "../types"; -export function useAssetFormController({ companyId, asset, items, onSaved }: AssetFormProps) { +export function useAssetFormController({ + companyId, + asset, + items, + onSaved, + creationLayout = "single-page", +}: AssetFormProps) { + const { t } = useTranslation("assets"); + const isWizard = !asset && creationLayout === "steps"; + const [step, setStep] = useState<1 | 2 | 3>(1); const client = useQueryClient(); const [creatingTemplate, setCreatingTemplate] = useState(false); const mounted = useRef(true); @@ -87,11 +99,57 @@ export function useAssetFormController({ companyId, asset, items, onSaved }: Ass characteristics[fieldId] = value; } form.setValue("characteristics", characteristics, { shouldDirty: true }); + form.clearErrors("characteristics"); }; - const submit = form.handleSubmit((input) => mutation.mutate(input)); + const save = form.handleSubmit((input) => mutation.mutate(input)); + + const submit = async (event: FormEvent) => { + event.preventDefault(); + if (mutation.isPending) { + return; + } + if (!isWizard || step === 3) { + await save(event); + + return; + } + if (step === 1) { + if (template && (await form.trigger("templateId"))) { + setStep(2); + } else { + form.setError("templateId", { message: t("inventoryErrors.TEMPLATE_UNAVAILABLE") }); + } + + return; + } + if (hasMissingAssetFields(fields, form.getValues("characteristics"))) { + form.setError("characteristics", { message: t("inventoryErrors.REQUIRED_CHARACTERISTICS") }); + + return; + } + if (!(await form.trigger(["characteristics", "brandId"]))) { + return; + } + if (!form.formState.dirtyFields.name) { + form.setValue( + "name", + suggestedAssetName(template, fields, form.getValues("characteristics"), brandFieldId) + ); + } + setStep(3); + }; + + const previousStep = () => { + form.clearErrors(); + setStep((current) => (current === 3 ? 2 : 1)); + }; const selectTemplate = (templateId: string) => { + if (templateId === form.getValues("templateId")) { + return; + } + form.clearErrors(); form.setValue("templateId", templateId); form.setValue("characteristics", {}); form.setValue("brandId", null); @@ -106,9 +164,7 @@ export function useAssetFormController({ companyId, asset, items, onSaved }: Ass }; const templateCreated = (id: string) => { - form.setValue("templateId", id); - form.setValue("characteristics", {}); - form.setValue("brandId", null); + selectTemplate(id); setCreatingTemplate(false); }; @@ -119,6 +175,10 @@ export function useAssetFormController({ companyId, asset, items, onSaved }: Ass }; return { + isWizard, + step, + previousStep, + template, creatingTemplate, setCreatingTemplate, form, diff --git a/modules/assets/src/web/assets/shared/helpers/asset-creation-values.ts b/modules/assets/src/web/assets/shared/helpers/asset-creation-values.ts new file mode 100644 index 0000000..c6f1a22 --- /dev/null +++ b/modules/assets/src/web/assets/shared/helpers/asset-creation-values.ts @@ -0,0 +1,36 @@ +import type { CatalogItemDTO } from "../../../../common"; +import type { AssetCharacteristic, AssetInput } from "../../../../common/inventory"; + +export function hasMissingAssetFields( + fields: readonly AssetCharacteristic[], + characteristics: AssetInput["characteristics"] +) { + return fields.some((field) => { + const value = characteristics[field.fieldId]; + + return field.required && (value === undefined || (typeof value === "string" && !value.trim())); + }); +} + +/** Usa el primer texto propio de la plantilla y evita proponer solo el nombre de una marca. */ +export function suggestedAssetName( + template: CatalogItemDTO | undefined, + fields: readonly AssetCharacteristic[], + characteristics: AssetInput["characteristics"], + brandFieldId: string | undefined +) { + for (const field of fields) { + const value = characteristics[field.fieldId]; + + if ( + field.type === "text" && + field.fieldId !== brandFieldId && + typeof value === "string" && + value.trim() + ) { + return value.trim().slice(0, 100); + } + } + + return template?.values.name.slice(0, 100) ?? ""; +} diff --git a/modules/assets/src/web/assets/shared/inventory.test.tsx b/modules/assets/src/web/assets/shared/inventory.test.tsx index 6509c44..79cb9fc 100644 --- a/modules/assets/src/web/assets/shared/inventory.test.tsx +++ b/modules/assets/src/web/assets/shared/inventory.test.tsx @@ -2,6 +2,7 @@ import assert from "node:assert/strict"; import { test } from "node:test"; import { UniqueID } from "@repo/rdx-ddd"; +import { FormStepActions } from "@repo/rdx-ui/components/form-step-actions"; import { Maybe } from "@repo/rdx-utils"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { createInstance } from "i18next"; @@ -27,10 +28,12 @@ import { CatalogTemplateForm } from "../../catalog-templates"; import { assetsEnglish, assetsSpanish } from "../../shared"; import { inventoryKeys } from "../../shared/queries"; import { WorkstationForm, WorkstationsPage } from "../../workstations"; +import { AssetCreateProgress } from "../create/ui/asset-create-progress"; import { AssetSelectionPanel } from "../detail/ui/asset-selection-panel"; import { AssetInventoryPage } from "../list"; import { assetCharacteristicsSummary } from "./helpers/asset-characteristics-summary"; +import { hasMissingAssetFields, suggestedAssetName } from "./helpers/asset-creation-values"; import { AssetForm } from "./ui"; import { AssetCharacteristicControl } from "./ui/asset-characteristic-control"; @@ -427,6 +430,7 @@ test("la ficha lateral agrupa información y ofrece acciones superiores y enlace undefined} @@ -435,6 +439,9 @@ test("la ficha lateral agrupa información y ofrece acciones superiores y enlace assert.ok(form.includes(language === "es" ? "Fecha de alta" : "Registration date")); assert.ok(form.includes('name="registeredOn"')); + assert.ok(form.includes('name="name"')); + assert.ok(form.includes('name="notes"')); + assert.equal(form.includes('aria-current="step"'), false); } const html = await render( "es", @@ -638,3 +645,144 @@ test("el resumen usa los campos propios de la plantilla, omite vacíos y texto l "" ); }); + +test("alta de activo por pasos: empieza por plantilla, traduce la navegación y bloquea el avance sin selección", async () => { + const catalog = buildCatalogResponse(seedAssetCatalog(AssetCatalog.create()), Maybe.none()); + + for (const language of ["es", "en"]) { + const html = await render( + language, + undefined} + /> + ); + + assert.ok(html.includes(language === "es" ? "Paso 1 de 3" : "Step 1 of 3")); + assert.ok(html.includes(language === "es" ? "Siguiente" : "Next")); + assert.ok(html.includes('aria-current="step"')); + assert.ok( + html.includes(`aria-label="${language === "es" ? "Nueva plantilla" : "New template"}"`) + ); + assert.equal(html.includes('name="name"'), false); + assert.equal(html.includes('name="notes"'), false); + assert.equal(html.includes('name="registeredOn"'), false); + assert.match(html, /]*type="submit")(?=[^>]*disabled="")[^>]*>/); + assert.ok(html.indexOf(" { + for (const { language, completed, fieldsDescription, nameDescription } of [ + { + language: "es", + completed: "(completado)", + fieldsDescription: "Los campos marcados con * son obligatorios.", + nameDescription: "fecha de alta", + }, + { + language: "en", + completed: "(completed)", + fieldsDescription: "Fields marked with * are required.", + nameDescription: "registration date", + }, + ]) { + for (const step of [2, 3] as const) { + const html = await render( + language, + + ); + + assert.ok(html.includes("Portátil")); + assert.ok(html.includes(completed)); + assert.ok(html.includes(step === 2 ? fieldsDescription : nameDescription)); + assert.equal((html.match(/aria-current="step"/g) ?? []).length, 1); + assert.equal(html.includes("creationSteps."), false); + } + } +}); + +test("las acciones por pasos solo ofrecen guardar al final y bloquean acciones durante el envío", async () => { + for (const step of [1, 2, 3] as const) { + for (const pending of [false, true]) { + const html = await render( + "es", + undefined} + pending={pending} + previousLabel="Anterior" + saveLabel="Guardar" + step={step} + total={3} + /> + ); + + assert.equal(html.includes("Anterior"), step > 1); + assert.equal(html.includes("Guardar"), step === 3); + assert.equal(html.includes("Siguiente"), step < 3); + assert.equal( + (html.match(/]*\sdisabled=""/g) ?? []).length, + pending ? (step > 1 ? 2 : 1) : 0 + ); + } + } +}); + +test("características obligatorias: rechaza ausentes y espacios y conserva cero y falso", () => { + const field = { + fieldId: companyId, + name: "Campo", + type: "text" as const, + unit: "", + choices: [], + required: true, + }; + + for (const characteristics of [{}, { [companyId]: "" }, { [companyId]: " " }]) { + assert.equal(hasMissingAssetFields([field], characteristics), true); + } + for (const value of [0, false, "Serie"]) { + assert.equal(hasMissingAssetFields([field], { [companyId]: value }), false); + } + assert.equal(hasMissingAssetFields([{ ...field, required: false }], {}), false); +}); + +test("nombre propuesto: usa texto de la plantilla, omite marca y vacíos y respeta el límite", () => { + const catalog = buildCatalogResponse(seedAssetCatalog(AssetCatalog.create()), Maybe.none()); + const template = catalog.items.find((item) => item.values.kind === "template"); + + assert.ok(template); + const fields = ["brand", "empty", "model"].map((fieldId) => ({ + fieldId, + name: fieldId, + type: "text" as const, + unit: "", + choices: [], + required: false, + })); + + assert.equal( + suggestedAssetName( + template, + fields, + { brand: "Marca", empty: " ", model: " Modelo " }, + "brand" + ), + "Modelo" + ); + assert.equal( + suggestedAssetName(template, fields, { brand: "Marca" }, "brand"), + template.values.name + ); + assert.equal( + suggestedAssetName(template, fields, { model: "a".repeat(101) }, "brand").length, + 100 + ); + assert.equal(suggestedAssetName(undefined, fields, {}, undefined), ""); +}); diff --git a/modules/assets/src/web/assets/shared/types/asset-form-props.ts b/modules/assets/src/web/assets/shared/types/asset-form-props.ts index 5c265e5..d09b1c9 100644 --- a/modules/assets/src/web/assets/shared/types/asset-form-props.ts +++ b/modules/assets/src/web/assets/shared/types/asset-form-props.ts @@ -22,4 +22,5 @@ export type AssetFormProps = { lookups: InventoryLookups; onSaved: (id: string) => void; allowQuickCreate?: boolean; + creationLayout?: "single-page" | "steps"; }; diff --git a/modules/assets/src/web/assets/shared/ui/asset-editor-dialog.tsx b/modules/assets/src/web/assets/shared/ui/asset-editor-dialog.tsx index 68bedd8..4ee4329 100644 --- a/modules/assets/src/web/assets/shared/ui/asset-editor-dialog.tsx +++ b/modules/assets/src/web/assets/shared/ui/asset-editor-dialog.tsx @@ -1,4 +1,5 @@ import { FormDialog } from "@repo/rdx-ui/components/form-dialog"; +import { BoxIcon } from "@repo/rdx-ui/lib/icons"; import type { ComponentProps } from "react"; import { useTranslation } from "react-i18next"; @@ -31,12 +32,18 @@ export function AssetEditorDialog({ } }} open - title={t(asset ? "editAsset" : "newAsset")} - wide + size={asset ? "large" : "medium"} + title={ + + + } > ; + prefix: string; + disabled: boolean; + allowQuickCreate: boolean; + brandFieldId: string | undefined; + onCharacteristicChange: ( + id: string, + value: AssetInput["characteristics"][string] | undefined + ) => void; + isWizard: boolean; + hasErrors: boolean; +}) { + const { t } = useTranslation("assets"); + const fieldHelp = fieldHelpById(items); + const Sections = isWizard ? FieldSections : TabbedFieldSections; + + return ( + + + field.fieldId === brandFieldId ? ( + { + form.setValue("brandId", brand?.id ?? null, { shouldDirty: true }); + onCharacteristicChange(field.fieldId, brand?.name); + }} + required={field.required} + /> + ) : ( + onCharacteristicChange(field.fieldId, value)} + prefix={prefix} + value={values.characteristics[field.fieldId]} + /> + ) + } + sections={sections} + ungroupedLabel={t("sections.ungrouped")} + /> + + ); +} diff --git a/modules/assets/src/web/assets/shared/ui/asset-form.tsx b/modules/assets/src/web/assets/shared/ui/asset-form.tsx index 9ece1e3..e841914 100644 --- a/modules/assets/src/web/assets/shared/ui/asset-form.tsx +++ b/modules/assets/src/web/assets/shared/ui/asset-form.tsx @@ -1,29 +1,28 @@ import { ContextHelp } from "@repo/rdx-ui/components/context-help"; -import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; +import { FormBody } from "@repo/rdx-ui/components/form-dialog"; import { FormLabel } from "@repo/rdx-ui/components/form-label"; -import { FormSection } from "@repo/rdx-ui/components/form-section"; -import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; -import { TabbedFieldSections } from "@repo/rdx-ui/components/tabbed-field-sections"; -import { fieldHelpById } from "@repo/rdx-ui/lib/field-help"; -import { Button } from "@repo/shadcn-ui/components/button"; -import { Input } from "@repo/shadcn-ui/components/input"; +import { FormStepActions } from "@repo/rdx-ui/components/form-step-actions"; import { Textarea } from "@repo/shadcn-ui/components/textarea"; import type { FormEvent } from "react"; import { useId } from "react"; import { useTranslation } from "react-i18next"; -import { AssetStatusSchema } from "../../../../common/inventory"; -import { BrandSelect } from "../../../brands"; import { CatalogTemplateCreateDialog } from "../../../catalog-templates"; import { InventoryFeedback } from "../../../shared"; +import { AssetCreateProgress } from "../../create/ui/asset-create-progress"; import { useAssetFormController } from "../controllers"; import type { AssetFormProps } from "../types"; -import { AssetCharacteristicControl } from "./asset-characteristic-control"; +import { AssetFields } from "./asset-fields"; +import { AssetIdentityFields } from "./asset-identity-fields"; export function AssetForm(props: AssetFormProps) { const { companyId, asset, items, lookups, allowQuickCreate = true } = props; const { + isWizard, + step, + previousStep, + template, creatingTemplate, setCreatingTemplate, form, @@ -42,7 +41,6 @@ export function AssetForm(props: AssetFormProps) { } = useAssetFormController(props); const { t } = useTranslation("assets"); const prefix = useId(); - const selectProps = { disabled: mutation.isPending, placeholder: t("noAssignment") }; const submitWithTabs = async (event: FormEvent) => { event.preventDefault(); @@ -56,6 +54,9 @@ export function AssetForm(props: AssetFormProps) { return; } await submit(event); + if (isWizard && step !== 3) { + return; + } const invalidGeneralField = ( [ "name", @@ -72,7 +73,12 @@ export function AssetForm(props: AssetFormProps) { } }; - const fieldHelp = fieldHelpById(items); + const errorMessage = [ + form.formState.errors.templateId?.message, + form.formState.errors.characteristics?.message, + ].find((message) => typeof message === "string"); + + const layout = isWizard ? CREATION_STEP_LAYOUTS[step] : FULL_FORM_LAYOUT; return ( <> @@ -82,170 +88,56 @@ export function AssetForm(props: AssetFormProps) { onSubmit={submitWithTabs} > + {isWizard && }
- -
-
- - {t("name")} - - -
-
- - {t("template")} - - setCreatingTemplate(true) } - } - disabled={!!asset || mutation.isPending} - id={`${prefix}-template`} - label={t("template")} - onValueChange={selectTemplate} - options={ - asset - ? [ - { - value: asset.values.templateId, - label: asset.specification.templateName, - }, - ] - : templates.map((item) => ({ value: item.id, label: item.values.name })) - } - placeholder={t("chooseValue")} - required - value={values.templateId} - /> -
-
- - {t("technicalStatus")} - - ({ - value, - label: t(value), - }))} - placeholder={t("chooseValue")} - required - value={values.status} - /> -

- {t("technicalStatusHelp")} -

-
-
- {t("site")} - lookups.onCreateSite(siteCreated), - } - : undefined - } - id={`${prefix}-site`} - label={t("site")} - onValueChange={(id) => form.setValue("siteId", id || null)} - options={lookups.sites.map((site) => ({ - value: site.id, - label: site.name, - }))} - value={values.siteId ?? ""} - /> -
-
- {t("registeredOn")} - value || null })} - /> -

- {t("registeredOnHelp")} -

- {form.formState.errors.registeredOn && ( -

- {t("inventoryErrors.INVALID_REQUEST")} -

- )} -
-
-
- {fields.length > 0 && ( - - - field.fieldId === brandFieldId ? ( - { - form.setValue("brandId", brand?.id ?? null, { shouldDirty: true }); - setCharacteristic(field.fieldId, brand?.name); - }} - required={field.required} - /> - ) : ( - setCharacteristic(field.fieldId, value)} - prefix={prefix} - value={values.characteristics[field.fieldId]} - /> - ) - } - sections={sections} - ungroupedLabel={t("sections.ungrouped")} - /> - - )} -
- {t("notes")} -