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. 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. 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.", "noSharedWorkstationAssets": "No shared resources.",
"editWorkstation": "Edit workstation", "editWorkstation": "Edit workstation",
"workstationPerson": "Workstation assignee", "workstationPerson": "Workstation assignee",
"workstationSuggestedName": "{{person}}'s workstation at {{site}}",
"workstationHistory": "Workstation history", "workstationHistory": "Workstation history",
"workstationAssets": "Workstation assets", "workstationAssets": "Workstation assets",
"noWorkstations": "This company has no workstations yet.", "noWorkstations": "This company has no workstations yet.",

View File

@ -144,6 +144,7 @@
"noSharedWorkstationAssets": "Sin recursos compartidos.", "noSharedWorkstationAssets": "Sin recursos compartidos.",
"editWorkstation": "Editar puesto", "editWorkstation": "Editar puesto",
"workstationPerson": "Persona del puesto", "workstationPerson": "Persona del puesto",
"workstationSuggestedName": "Puesto de {{person}} en {{site}}",
"workstationHistory": "Historial del puesto", "workstationHistory": "Historial del puesto",
"workstationAssets": "Activos del puesto", "workstationAssets": "Activos del puesto",
"noWorkstations": "Esta compañía todavía no tiene puestos de trabajo.", "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 { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { WorkstationInput } from "../../../../common"; import type { WorkstationInput } from "../../../../common";
import { WorkstationInputSchema } from "../../../../common"; import { WorkstationInputSchema } from "../../../../common";
@ -10,12 +11,15 @@ import { catalogApi, catalogKeys } from "../../../catalog/shared/api";
import { inventoryKeys } from "../../../shared/queries"; import { inventoryKeys } from "../../../shared/queries";
import { saveWorkstation } from "../api"; import { saveWorkstation } from "../api";
import type { WorkstationFormProps } from "../types"; import type { WorkstationFormProps } from "../types";
import { suggestedWorkstationName } from "../workstation-name";
export function useWorkstationFormController({ export function useWorkstationFormController({
companyId, companyId,
workstation, workstation,
onSaved, onSaved,
lookups,
}: WorkstationFormProps) { }: WorkstationFormProps) {
const { t } = useTranslation("assets");
const client = useQueryClient(); const client = useQueryClient();
const mounted = useRef(true); 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({ const mutation = useMutation({
mutationFn: (input: WorkstationInput) => saveWorkstation(companyId, input, workstation?.id), mutationFn: (input: WorkstationInput) => saveWorkstation(companyId, input, workstation?.id),
onSuccess: async (response) => { onSuccess: async (response) => {
@ -87,13 +111,13 @@ export function useWorkstationFormController({
const siteCreated = (id: string) => { const siteCreated = (id: string) => {
if (mounted.current) { if (mounted.current) {
form.setValue("siteId", id); selectAssociation("siteId", id);
} }
}; };
const personCreated = (id: string) => { const personCreated = (id: string) => {
if (mounted.current) { if (mounted.current) {
form.setValue("personId", id); selectAssociation("personId", id);
} }
}; };
@ -109,6 +133,7 @@ export function useWorkstationFormController({
assets, assets,
unavailable, unavailable,
add, add,
selectAssociation,
mutation, mutation,
submit, submit,
siteCreated, siteCreated,

View File

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