diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index 630aa7a..6f2a04f 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -473,6 +473,17 @@ No conoce entidades, formularios, consultas ni endpoints. `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 diálogo de alta de puesto usa tamaño medio y cabecera con icono decorativo; la edición +mantiene tamaño grande. Asociaciones, activos, identificación y resumen utilizan +`FormSection variant="plain"`, títulos de grupo uniformes y separación de 20 px. Las +asociaciones y el resumen pasan a dos columnas según el ancho del contenedor; el nombre +del alta ocupa todo el ancho. Los activos seleccionados usan filas compactas con separadores, +nombres que pueden partirse y botones de retirada con etiqueta accesible. El resumen +muestra `—` para valores ausentes. La ayuda del ordenador se vincula al selector mediante +`aria-describedby`; el nombre identifica su error junto al campo. La ayuda general queda +al final, antes del pie fijo, con navegación por flechas y cancelación deshabilitada durante +el guardado. Se conservan el cuerpo desplazable y la altura según contenido. + 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). ## Selección de activos en diálogo diff --git a/docs/puestos-de-trabajo.md b/docs/puestos-de-trabajo.md index 331f50b..3f2715e 100644 --- a/docs/puestos-de-trabajo.md +++ b/docs/puestos-de-trabajo.md @@ -72,6 +72,13 @@ El estado mostrado del activo se calcula desde su estado técnico y los puestos 3. Ordenador principal. Se elige en un diálogo de selección única entre los activos seleccionados. Si solo queda uno se asigna automáticamente; al retirar el ordenador se limpia la selección o se asigna el único restante. 4. Resumen. Muestra sede, responsable, composición y ordenador principal. El nombre propuesto por sede/responsable se puede editar antes de Guardar. +El diálogo de alta utiliza tamaño medio y cabecera con icono decorativo. Los grupos +reutilizan `FormSection variant="plain"`; asociaciones y resumen responden al ancho +del contenedor, con dos columnas cuando caben y una en espacios estrechos. Los activos +seleccionados se muestran como filas compactas, sin tarjetas anidadas. El nombre del +resumen ocupa el ancho completo y los valores ausentes muestran `—`. La edición conserva +el diálogo grande y los mismos grupos, conforme al [patrón de diálogos](patron-de-dialogos.md). + Anterior conserva valores y Siguiente valida los requisitos del paso. Guardar solo aparece en el resumen y no se envía el alta desde pasos intermedios. La edición mantiene un formulario único y permite conservar/completar puestos anteriores o desactivados. Cancelar permanece en el pie del modal y la ayuda inmediatamente encima. Se elimina `sharedAssetIds` de contratos, dominio, reservas y vistas. La migración explícita `removeSharedWorkstationAssets`, ejecutada durante la preparación del esquema, elimina las asociaciones de tipo compartido tanto actuales como históricas y limpia sus referencias técnicas cuando no existe ninguna asociación exclusiva histórica. No convierte activos compartidos en exclusivos, no borra activos ni revisiones y no genera reservas conflictivas. Es transaccional e idempotente. Las composiciones exclusivas e identidades se conservan. diff --git a/modules/assets/src/common/locales/en.json b/modules/assets/src/common/locales/en.json index 4d0e1a9..246a685 100644 --- a/modules/assets/src/common/locales/en.json +++ b/modules/assets/src/common/locales/en.json @@ -326,7 +326,10 @@ }, "formLayout": { "identity": "Identification and assignment", - "template": "Template details" + "template": "Template details", + "workstationAssociations": "Location and owner", + "workstationIdentity": "Workstation identification", + "workstationSummary": "Selected details" }, "detailSections": { "identification": "Identification", diff --git a/modules/assets/src/common/locales/es.json b/modules/assets/src/common/locales/es.json index d1f3dd8..3c80e02 100644 --- a/modules/assets/src/common/locales/es.json +++ b/modules/assets/src/common/locales/es.json @@ -326,7 +326,10 @@ }, "formLayout": { "identity": "Identificación y asignación", - "template": "Datos de la plantilla" + "template": "Datos de la plantilla", + "workstationAssociations": "Ubicación y responsable", + "workstationIdentity": "Identificación del puesto", + "workstationSummary": "Datos seleccionados" }, "detailSections": { "identification": "Identificación", diff --git a/modules/assets/src/web/assets/shared/inventory.test.tsx b/modules/assets/src/web/assets/shared/inventory.test.tsx index 416e50c..afe8f18 100644 --- a/modules/assets/src/web/assets/shared/inventory.test.tsx +++ b/modules/assets/src/web/assets/shared/inventory.test.tsx @@ -20,7 +20,7 @@ import type { WorkstationDTO, } from "../../../common"; import { AssetSchema } from "../../../common/inventory"; -import { WorkstationSchema } from "../../../common/workstations"; +import { WorkstationInputSchema, WorkstationSchema } from "../../../common/workstations"; import { AssetRelationships } from "../../asset-relationships"; import { catalogKeys } from "../../catalog/shared/api"; import { CatalogRelationForm } from "../../catalog-relations"; @@ -28,6 +28,10 @@ import { CatalogTemplateForm } from "../../catalog-templates"; import { assetsEnglish, assetsSpanish } from "../../shared"; import { inventoryKeys } from "../../shared/queries"; import { WorkstationForm, WorkstationsPage } from "../../workstations"; +import { WorkstationAssetFields } from "../../workstations/shared/ui/workstation-asset-fields"; +import { WorkstationComputerField } from "../../workstations/shared/ui/workstation-computer-field"; +import { WorkstationFormActions } from "../../workstations/shared/ui/workstation-form-actions"; +import { WorkstationSummary } from "../../workstations/shared/ui/workstation-summary"; import { AssetCreateProgress } from "../create/ui/asset-create-progress"; import { AssetSelectionPanel } from "../detail/ui/asset-selection-panel"; import { AssetInventoryPage } from "../list"; @@ -786,3 +790,91 @@ test("nombre propuesto: usa texto de la plantilla, omite marca y vacíos y respe ); assert.equal(suggestedAssetName(undefined, fields, {}, undefined), ""); }); + +test("diseño del puesto: grupos compactos, columnas por contenedor y resumen sin textos de ausencia", async () => { + const values = WorkstationInputSchema.parse({ + name: "Puesto", + active: true, + siteId: null, + personId: null, + assetIds: [], + computerAssetId: null, + }); + + for (const language of ["es", "en"]) { + const initial = await render( + language, + undefined} /> + ); + + assert.ok( + initial.includes(language === "es" ? "Ubicación y responsable" : "Location and owner") + ); + assert.ok(initial.includes("uppercase tracking-wide")); + assert.ok(initial.includes("@min-[560px]:grid-cols-2")); + assert.ok(initial.indexOf(" + ); + + assert.ok(summary.includes(language === "es" ? "Datos seleccionados" : "Selected details")); + assert.ok(summary.includes("@min-[560px]:grid-cols-2")); + assert.equal((summary.match(/—/g) ?? []).length, 4); + assert.equal(summary.includes(language === "es" ? "Sin asignación" : "No assignment"), false); + const computer = await render( + language, + undefined} + prefix="design" + required + singleComputer={false} + sites={[]} + values={values} + /> + ); + + assert.ok( + computer.includes('aria-describedby="design-computer-help design-computer-required"') + ); + assert.ok(computer.includes('id="design-computer-help"')); + const selection = await render( + language, + undefined} + onCreate={() => undefined} + onRemove={() => undefined} + prefix="design" + selectedIds={[companyId]} + sites={[]} + unavailableReason={() => "ASSET_NOT_FOUND"} + /> + ); + + assert.ok(selection.includes("divide-y divide-border/60")); + assert.ok(selection.includes("—")); + assert.match(selection, /]*aria-label="[^"]*—[^"]*")(?=[^>]*disabled="")[^>]*>/); + } +}); + +test("diseño del puesto: navegación traducida y acciones bloqueadas durante el guardado", async () => { + for (const language of ["es", "en"]) { + for (const step of [2, 4] as const) { + const html = await render( + language, + undefined} pending step={step} /> + ); + + assert.ok(html.includes(language === "es" ? "Anterior" : "Previous")); + assert.ok(html.includes(language === "es" ? "Guardando" : "Saving")); + assert.equal((html.match(/]*\sdisabled=""/g) ?? []).length, 2); + assert.ok(html.includes('aria-hidden="true"')); + } + } +}); diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-asset-fields.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-asset-fields.tsx index 753642f..784c527 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-asset-fields.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-asset-fields.tsx @@ -1,4 +1,6 @@ import { FormLabel } from "@repo/rdx-ui/components/form-label"; +import { FormSection } from "@repo/rdx-ui/components/form-section"; +import { XIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import { useTranslation } from "react-i18next"; @@ -35,68 +37,71 @@ export function WorkstationAssetFields({ const { t } = useTranslation("assets"); return ( -
- {t("addWorkstationAsset")} - = 200} - id={`${prefix}-assets`} - label={t("addWorkstationAsset")} - maxSelection={200 - selectedIds.length} - mode="multiple" - onValueChange={onAdd} - options={assets - .filter((asset) => !selectedIds.includes(asset.id)) - .map((asset) => { - const reason = unavailableReason(asset.id); + +
+ {t("addWorkstationAsset")} + = 200} + id={`${prefix}-assets`} + label={t("addWorkstationAsset")} + maxSelection={200 - selectedIds.length} + mode="multiple" + onValueChange={onAdd} + options={assets + .filter((asset) => !selectedIds.includes(asset.id)) + .map((asset) => { + const reason = unavailableReason(asset.id); - return { - value: asset.id, - label: - reason === "ASSET_NOT_OPERATIONAL" - ? `${asset.values.name} · ${t(asset.values.status)}` - : reason - ? t("workstationOccupied", { name: asset.values.name }) - : asset.values.name, - disabled: !!reason, - }; - })} - placeholder={t("assetPicker.multiple")} - sites={sites} - value={[]} - /> - {selectedIds.length ? ( -
    - {selectedIds.map((id) => { - const name = assets.find((asset) => asset.id === id)?.values.name ?? id; - const reason = unavailableReason(id); + return { + value: asset.id, + label: + reason === "ASSET_NOT_OPERATIONAL" + ? `${asset.values.name} · ${t(asset.values.status)}` + : reason + ? t("workstationOccupied", { name: asset.values.name }) + : asset.values.name, + disabled: !!reason, + }; + })} + placeholder={t("assetPicker.multiple")} + sites={sites} + value={[]} + /> + {selectedIds.length ? ( +
      + {selectedIds.map((id) => { + const name = assets.find((asset) => asset.id === id)?.values.name ?? "—"; + const reason = unavailableReason(id); - return ( -
    • -
      -

      {name}

      - {reason &&

      {t(`inventoryErrors.${reason}`)}

      } -
      - -
    • - ); - })} -
    - ) : ( -

    {t("noWorkstationAssets")}

    - )} -
+
+

{name}

+ {reason &&

{t(`inventoryErrors.${reason}`)}

} +
+ + + ); + })} + + ) : ( +

{t("noWorkstationAssets")}

+ )} +
+ ); } diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-association-fields.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-association-fields.tsx index 25cbbd5..a79400f 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-association-fields.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-association-fields.tsx @@ -1,4 +1,5 @@ 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 { useTranslation } from "react-i18next"; @@ -27,44 +28,46 @@ export function WorkstationAssociationFields({ const { t } = useTranslation("assets"); return ( -
-
- - {t("site")} - - lookups.onCreateSite(onSiteCreated), - }} - id={`${prefix}-site`} - label={t("site")} - onValueChange={(id) => onSelect("siteId", id)} - options={lookups.sites - .filter((item) => item.active || item.id === workstation?.values.siteId) - .map((item) => ({ value: item.id, label: item.name }))} - placeholder={t("noAssignment")} - required={required} - value={values.siteId ?? ""} - /> + +
+
+ + {t("site")} + + lookups.onCreateSite(onSiteCreated), + }} + id={`${prefix}-site`} + label={t("site")} + onValueChange={(id) => onSelect("siteId", id)} + options={lookups.sites + .filter((item) => item.active || item.id === workstation?.values.siteId) + .map((item) => ({ value: item.id, label: item.name }))} + placeholder={t("noAssignment")} + required={required} + value={values.siteId ?? ""} + /> +
+
+ {t("workstationPersonOptional")} + lookups.onCreatePerson(onPersonCreated), + }} + id={`${prefix}-person`} + label={t("workstationPersonOptional")} + onValueChange={(id) => onSelect("personId", id)} + options={lookups.people + .filter((item) => item.active || item.id === workstation?.values.personId) + .map((item) => ({ value: item.id, label: item.name }))} + placeholder={t("noAssignment")} + value={values.personId ?? ""} + /> +
-
- {t("workstationPersonOptional")} - lookups.onCreatePerson(onPersonCreated), - }} - id={`${prefix}-person`} - label={t("workstationPersonOptional")} - onValueChange={(id) => onSelect("personId", id)} - options={lookups.people - .filter((item) => item.active || item.id === workstation?.values.personId) - .map((item) => ({ value: item.id, label: item.name }))} - placeholder={t("noAssignment")} - value={values.personId ?? ""} - /> -
-
+ ); } diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-computer-field.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-computer-field.tsx index 83a98d1..8bf252d 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-computer-field.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-computer-field.tsx @@ -33,6 +33,7 @@ export function WorkstationComputerField({ -

+

{t(singleComputer ? "workstationSteps.singleComputer" : "workstationComputerHelp")}

diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-editor-dialog.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-editor-dialog.tsx index 602edc9..01e6ed1 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-editor-dialog.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-editor-dialog.tsx @@ -1,4 +1,5 @@ import { FormDialog } from "@repo/rdx-ui/components/form-dialog"; +import { MonitorIcon } from "@repo/rdx-ui/lib/icons"; import type { ComponentProps } from "react"; import { useTranslation } from "react-i18next"; @@ -27,8 +28,13 @@ export function WorkstationEditorDialog({ } }} open - title={t(workstation ? "editWorkstation" : "newWorkstation")} - wide + size={workstation ? "large" : "medium"} + title={ + + + } > + {step && step > 1 && ( )} ); diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-form.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-form.tsx index 15bfaef..4d03e63 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-form.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-form.tsx @@ -1,9 +1,6 @@ import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { FormBody } from "@repo/rdx-ui/components/form-dialog"; -import { FormLabel } from "@repo/rdx-ui/components/form-label"; import { FormProgress } from "@repo/rdx-ui/components/form-progress"; -import { SwitchField } from "@repo/rdx-ui/components/switch-field"; -import { Input } from "@repo/shadcn-ui/components/input"; import { useId } from "react"; import { useTranslation } from "react-i18next"; @@ -16,6 +13,7 @@ import { WorkstationAssetFields } from "./workstation-asset-fields"; import { WorkstationAssociationFields } from "./workstation-association-fields"; import { WorkstationComputerField } from "./workstation-computer-field"; import { WorkstationFormActions } from "./workstation-form-actions"; +import { WorkstationIdentityFields } from "./workstation-identity-fields"; import { WorkstationSummary } from "./workstation-summary"; export function WorkstationForm(props: WorkstationFormProps) { @@ -63,7 +61,7 @@ export function WorkstationForm(props: WorkstationFormProps) { step={step} /> )} -
+
{layout.location && ( )} - {layout.summary && ( - <> -
- - {t("name")} - - -
- {isWizard ? ( - - ) : ( - form.setValue("active", active)} - /> - )} - + {layout.identity && ( + + )} + {layout.review && ( + )}
{Object.keys(form.formState.errors).length > 0 && ( @@ -129,7 +113,12 @@ export function WorkstationForm(props: WorkstationFormProps) { {t("inventoryErrors.INVALID_REQUEST")}

)} - + {(inventory.isPending || catalog.isPending) && ( +

+ {t("loading")} +

+ )} + ; + prefix: string; + showStatus: boolean; +}) { + const { t } = useTranslation("assets"); + const invalidName = !!form.formState.errors.name; + + return ( + +
+
+ + {t("name")} + + + {invalidName && ( + + )} +
+ {showStatus && ( + form.setValue("active", active)} + /> + )} +
+
+ ); +} diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-summary.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-summary.tsx index 8743bc1..833902c 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-summary.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-summary.tsx @@ -1,3 +1,4 @@ +import { FormSection } from "@repo/rdx-ui/components/form-section"; import { useTranslation } from "react-i18next"; import type { WorkstationInput } from "../../../../common"; @@ -16,37 +17,35 @@ export function WorkstationSummary({ const { t } = useTranslation("assets"); return ( -
-
-
{t("site")}
-
- {lookups.sites.find((site) => site.id === values.siteId)?.name ?? t("noAssignment")} -
-
-
-
{t("workstationPerson")}
-
- {lookups.people.find((person) => person.id === values.personId)?.name ?? - t("noAssignment")} -
-
-
-
{t("workstationAssets")}
-
-
    - {values.assetIds.map((id) => ( -
  • {assets.find((asset) => asset.id === id)?.values.name ?? id}
  • - ))} -
-
-
-
-
{t("workstationComputer")}
-
- {assets.find((asset) => asset.id === values.computerAssetId)?.values.name ?? - t("noAssignment")} -
-
-
+ +
+
+
{t("site")}
+
{lookups.sites.find((site) => site.id === values.siteId)?.name ?? "—"}
+
+
+
{t("workstationPerson")}
+
{lookups.people.find((person) => person.id === values.personId)?.name ?? "—"}
+
+
+
{t("workstationAssets")}
+
+ {values.assetIds.length > 0 ? ( +
    + {values.assetIds.map((id) => ( +
  • {assets.find((asset) => asset.id === id)?.values.name ?? "—"}
  • + ))} +
+ ) : ( + "—" + )} +
+
+
+
{t("workstationComputer")}
+
{assets.find((asset) => asset.id === values.computerAssetId)?.values.name ?? "—"}
+
+
+
); } diff --git a/packages/rdx-ui/src/components/context-help.tsx b/packages/rdx-ui/src/components/context-help.tsx index d04e2d0..0c69571 100644 --- a/packages/rdx-ui/src/components/context-help.tsx +++ b/packages/rdx-ui/src/components/context-help.tsx @@ -25,7 +25,7 @@ export function ContextHelp({ aria-label={title} className={ variant === "plain" - ? "space-y-1 border-t border-border/60 pt-3 text-sm" + ? "space-y-1 border-t border-border/60 pt-3 text-sm mt-12" : "space-y-4 rounded-xl border bg-muted/30 p-4 text-sm sm:p-5" } > diff --git a/packages/rdx-ui/src/components/form-progress.tsx b/packages/rdx-ui/src/components/form-progress.tsx index 6da5dae..b12571f 100644 --- a/packages/rdx-ui/src/components/form-progress.tsx +++ b/packages/rdx-ui/src/components/form-progress.tsx @@ -44,7 +44,7 @@ export function FormProgress({ })}
-
+

{labels[step - 1]}