feat: implementar sugerencia de nombre para puestos de trabajo y mejorar formularios

This commit is contained in:
David Arranz 2026-10-10 22:30:46 +02:00
parent 6d6f807486
commit 45b559ab92
7 changed files with 59 additions and 5 deletions

View File

@ -4,7 +4,7 @@
La sección Puestos de trabajo de cada compañía agrupa los activos utilizados habitualmente como un conjunto. Un puesto tiene UUID v7, nombre, estado activo/inactivo, sede opcional, persona opcional y hasta 200 activos exclusivos y 200 recursos compartidos. No se admite duplicar un activo ni incluirlo en ambas listas. Se puede crear vacío y completar posteriormente. Una persona puede tener varios puestos. Los puestos no contienen otros puestos.
Nuevo puesto abre un modal desde la cabecera derecha; Editar abre otro modal con los datos existentes. El formulario ofrece selectores con «+» para dar de alta sedes, personas y activos sin perder sus datos. Guardar un elemento relacionado lo selecciona o incorpora al puesto; cancelarlo mantiene el formulario principal. Los elementos guardados sobreviven si posteriormente se cancela el puesto. El alta de activo desde un puesto usa plantillas y sedes existentes para evitar un tercer modal.
Nuevo puesto abre un modal desde la cabecera derecha; Editar abre otro modal con los datos existentes. Al crear, elegir persona y sede propone el nombre «Puesto de {persona} en {sede}»; el técnico puede modificarlo o borrarlo antes de guardar y esa edición manual no se sobrescribe. El formulario ofrece selectores con «+» para dar de alta sedes, personas y activos sin perder sus datos. Guardar un elemento relacionado lo selecciona o incorpora al puesto; cancelarlo mantiene el formulario principal. Los elementos guardados sobreviven si posteriormente se cancela el puesto. El alta de activo desde un puesto usa plantillas y sedes existentes para evitar un tercer modal.
La ayuda aparece inmediatamente antes de Guardar y Cancelar. El listado permite buscar por nombre, persona o sede. Historial del puesto muestra las versiones completas, con estado, asignación y composición, fecha y responsable. La ficha de cada activo muestra los puestos vigentes en los que se utiliza.

View File

@ -144,6 +144,7 @@
"noSharedWorkstationAssets": "No shared resources.",
"editWorkstation": "Edit workstation",
"workstationPerson": "Workstation assignee",
"workstationSuggestedName": "{{person}}'s workstation at {{site}}",
"workstationHistory": "Workstation history",
"workstationAssets": "Workstation assets",
"noWorkstations": "This company has no workstations yet.",

View File

@ -144,6 +144,7 @@
"noSharedWorkstationAssets": "Sin recursos compartidos.",
"editWorkstation": "Editar puesto",
"workstationPerson": "Persona del puesto",
"workstationSuggestedName": "Puesto de {{person}} en {{site}}",
"workstationHistory": "Historial del puesto",
"workstationAssets": "Activos del puesto",
"noWorkstations": "Esta compañía todavía no tiene puestos de trabajo.",

View File

@ -2,6 +2,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { WorkstationInput } from "../../../../common";
import { WorkstationInputSchema } from "../../../../common";
@ -10,12 +11,15 @@ import { catalogApi, catalogKeys } from "../../../catalog/shared/api";
import { inventoryKeys } from "../../../shared/queries";
import { saveWorkstation } from "../api";
import type { WorkstationFormProps } from "../types";
import { suggestedWorkstationName } from "../workstation-name";
export function useWorkstationFormController({
companyId,
workstation,
onSaved,
lookups,
}: WorkstationFormProps) {
const { t } = useTranslation("assets");
const client = useQueryClient();
const mounted = useRef(true);
@ -74,6 +78,26 @@ export function useWorkstationFormController({
}
};
const selectAssociation = (field: "siteId" | "personId", id: string) => {
const selectedId = id || null;
form.setValue(field, selectedId);
if (workstation || form.formState.dirtyFields.name) {
return;
}
const siteId = field === "siteId" ? selectedId : form.getValues("siteId");
const personId = field === "personId" ? selectedId : form.getValues("personId");
const name = suggestedWorkstationName(
lookups.people.find((item) => item.id === personId)?.name,
lookups.sites.find((item) => item.id === siteId)?.name,
(person, site) => t("workstationSuggestedName", { person, site })
);
if (name) {
form.setValue("name", name, { shouldDirty: false });
}
};
const mutation = useMutation({
mutationFn: (input: WorkstationInput) => saveWorkstation(companyId, input, workstation?.id),
onSuccess: async (response) => {
@ -87,13 +111,13 @@ export function useWorkstationFormController({
const siteCreated = (id: string) => {
if (mounted.current) {
form.setValue("siteId", id);
selectAssociation("siteId", id);
}
};
const personCreated = (id: string) => {
if (mounted.current) {
form.setValue("personId", id);
selectAssociation("personId", id);
}
};
@ -109,6 +133,7 @@ export function useWorkstationFormController({
assets,
unavailable,
add,
selectAssociation,
mutation,
submit,
siteCreated,

View File

@ -27,6 +27,7 @@ export function WorkstationForm(props: WorkstationFormProps) {
assets,
unavailable,
add,
selectAssociation,
mutation,
submit,
siteCreated,
@ -60,7 +61,7 @@ export function WorkstationForm(props: WorkstationFormProps) {
}}
id={`${prefix}-site`}
label={t("site")}
onValueChange={(id) => form.setValue("siteId", id || null)}
onValueChange={(id) => selectAssociation("siteId", id)}
options={lookups.sites.map((item) => ({ value: item.id, label: item.name }))}
placeholder={t("noAssignment")}
value={values.siteId ?? ""}
@ -75,7 +76,7 @@ export function WorkstationForm(props: WorkstationFormProps) {
}}
id={`${prefix}-person`}
label={t("workstationPerson")}
onValueChange={(id) => form.setValue("personId", id || null)}
onValueChange={(id) => selectAssociation("personId", id)}
options={lookups.people
.filter((item) => item.active || item.id === workstation?.values.personId)
.map((item) => ({ value: item.id, label: item.name }))}

View File

@ -0,0 +1,15 @@
import assert from "node:assert/strict";
import { test } from "node:test";
import { suggestedWorkstationName } from "./workstation-name";
test("propone un nombre de puesto solo al conocer persona y sede", () => {
const format = (person: string, site: string) => `${person} @ ${site}`;
assert.equal(suggestedWorkstationName(undefined, "Fábrica", format), undefined);
assert.equal(suggestedWorkstationName("Ana", undefined, format), undefined);
assert.equal(
suggestedWorkstationName("Ana García", "Uecko Fábrica", format),
"Ana García @ Uecko Fábrica"
);
});

View File

@ -0,0 +1,11 @@
export function suggestedWorkstationName(
personName: string | undefined,
siteName: string | undefined,
format: (person: string, site: string) => string
) {
if (!(personName && siteName)) {
return undefined;
}
return format(personName, siteName);
}