feat: enhance work profile and workstation management
- Added `prepareProfileHosts` function to facilitate the setup of independent workstations for profile scenarios. - Updated `verifyMail` and `verifyWorkProfiles` functions to utilize the new `prepareProfileHosts` for better host management. - Refactored `verifyProcesses` to improve workstation update logic and ensure correct status checks. - Introduced `CurrentWorkstations` component to display workstations under a person's responsibility and usage. - Implemented `personWorkstations` helper to differentiate between responsible and used workstations for a person. - Enhanced localization files to include new error messages and descriptions related to workstation management. - Updated tests to cover new functionality and ensure proper behavior of work profile and workstation interactions.
This commit is contained in:
parent
e061d60eb7
commit
b183627df0
@ -23,6 +23,8 @@ import { mailSnapshot } from "../../../modules/companies/src/api/infrastructure/
|
|||||||
import { snapshotValues } from "../../../modules/companies/src/api/infrastructure/persistence/scalar-snapshot-reader";
|
import { snapshotValues } from "../../../modules/companies/src/api/infrastructure/persistence/scalar-snapshot-reader";
|
||||||
import { writeSnapshotRow } from "../../../modules/companies/src/api/infrastructure/persistence/scalar-snapshot-writer";
|
import { writeSnapshotRow } from "../../../modules/companies/src/api/infrastructure/persistence/scalar-snapshot-writer";
|
||||||
|
|
||||||
|
import { prepareProfileHosts } from "./work-profile-hosts.fixture";
|
||||||
|
|
||||||
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
||||||
|
|
||||||
export async function verifyMail(
|
export async function verifyMail(
|
||||||
@ -57,6 +59,7 @@ export async function verifyMail(
|
|||||||
assert.equal(response.status, 200, await response.clone().text());
|
assert.equal(response.status, 200, await response.clone().text());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
await prepareProfileHosts(request, companyId, [first.id, second.id]);
|
||||||
const personFields = {
|
const personFields = {
|
||||||
name: "Persona correo",
|
name: "Persona correo",
|
||||||
position: null,
|
position: null,
|
||||||
|
|||||||
@ -77,18 +77,19 @@ export async function verifyProcesses(
|
|||||||
|
|
||||||
assert.equal(profileResponse.status, 201);
|
assert.equal(profileResponse.status, 201);
|
||||||
const profile = WorkProfileResponseSchema.parse(await profileResponse.json()).profile;
|
const profile = WorkProfileResponseSchema.parse(await profileResponse.json()).profile;
|
||||||
const workstationResponse = await request(`/assets/${companyId}/workstations`, {
|
const station = AssetsSchema.parse(
|
||||||
values: {
|
await (await request(`/assets/${companyId}`)).json()
|
||||||
name: "Puesto proceso",
|
).workstations.find((item) => item.values.active && item.values.computerAssetId === host.id);
|
||||||
active: true,
|
|
||||||
siteId: null,
|
|
||||||
personId: person.id,
|
|
||||||
assetIds: [host.id],
|
|
||||||
sharedAssetIds: [],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
assert.equal(workstationResponse.status, 201);
|
assert.ok(station);
|
||||||
|
const workstationResponse = await request(
|
||||||
|
`/assets/${companyId}/workstations/${station.id}/update`,
|
||||||
|
{
|
||||||
|
values: { ...station.values, name: "Puesto proceso", personId: person.id },
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.equal(workstationResponse.status, 200);
|
||||||
const resource = ResourcesSchema.parse(
|
const resource = ResourcesSchema.parse(
|
||||||
await (await request(`/resources/${companyId}`)).json()
|
await (await request(`/resources/${companyId}`)).json()
|
||||||
).resources.find((r) => r.values.active && r.specification.templateName === "Aplicación");
|
).resources.find((r) => r.values.active && r.specification.templateName === "Aplicación");
|
||||||
|
|||||||
51
apps/server/src/work-profile-hosts.fixture.ts
Normal file
51
apps/server/src/work-profile-hosts.fixture.ts
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
|
||||||
|
import { InventoryResponseSchema, WorkstationResponseSchema } from "@metis/assets/common";
|
||||||
|
|
||||||
|
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
||||||
|
|
||||||
|
/** Prepara puestos independientes para los ordenadores de los escenarios HTTP. */
|
||||||
|
export async function prepareProfileHosts(
|
||||||
|
request: Request,
|
||||||
|
companyId: string,
|
||||||
|
assetIds: readonly string[]
|
||||||
|
) {
|
||||||
|
const current = InventoryResponseSchema.parse(
|
||||||
|
await (await request(`/assets/${companyId}`)).json()
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const station of current.workstations) {
|
||||||
|
if (
|
||||||
|
station.values.active &&
|
||||||
|
assetIds.some((id) =>
|
||||||
|
[...station.values.assetIds, ...station.values.sharedAssetIds].includes(id)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
const closed = await request(`/assets/${companyId}/workstations/${station.id}/update`, {
|
||||||
|
values: { ...station.values, active: false },
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(closed.status, 200, await closed.clone().text());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const hosts = [];
|
||||||
|
|
||||||
|
for (const assetId of assetIds) {
|
||||||
|
const response = await request(`/assets/${companyId}/workstations`, {
|
||||||
|
values: {
|
||||||
|
name: `Puesto perfiles ${assetId}`,
|
||||||
|
active: true,
|
||||||
|
siteId: null,
|
||||||
|
personId: null,
|
||||||
|
computerAssetId: assetId,
|
||||||
|
assetIds: [assetId],
|
||||||
|
sharedAssetIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(response.status, 201, await response.clone().text());
|
||||||
|
hosts.push(WorkstationResponseSchema.parse(await response.json()).workstation);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hosts;
|
||||||
|
}
|
||||||
@ -11,6 +11,8 @@ import {
|
|||||||
import type { Sequelize } from "sequelize";
|
import type { Sequelize } from "sequelize";
|
||||||
import { z } from "zod/v4";
|
import { z } from "zod/v4";
|
||||||
|
|
||||||
|
import { prepareProfileHosts } from "./work-profile-hosts.fixture";
|
||||||
|
|
||||||
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;
|
||||||
|
|
||||||
export async function verifyWorkProfiles(
|
export async function verifyWorkProfiles(
|
||||||
@ -39,6 +41,7 @@ export async function verifyWorkProfiles(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
const hosts = await prepareProfileHosts(request, companyId, [first.id, second.id]);
|
||||||
const fields = {
|
const fields = {
|
||||||
name: "Persona perfiles",
|
name: "Persona perfiles",
|
||||||
position: null,
|
position: null,
|
||||||
@ -180,6 +183,33 @@ export async function verifyWorkProfiles(
|
|||||||
.profiles.length,
|
.profiles.length,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
const host = hosts[0];
|
||||||
|
|
||||||
|
assert.ok(host);
|
||||||
|
assert.equal(
|
||||||
|
(
|
||||||
|
await request(`/assets/${companyId}/workstations/${host.id}/update`, {
|
||||||
|
values: { ...host.values, computerAssetId: null },
|
||||||
|
})
|
||||||
|
).status,
|
||||||
|
200
|
||||||
|
);
|
||||||
|
const detached = WorkProfilesResponseSchema.parse(
|
||||||
|
await (await request(path)).json()
|
||||||
|
).profiles.find((item) => item.id === replacement.id);
|
||||||
|
|
||||||
|
assert.equal(detached?.needsReview, true);
|
||||||
|
assert.equal(detached?.values.assetId, first.id);
|
||||||
|
await error(
|
||||||
|
{ ...input, personId: otherPerson.id, name: "Otro perfil" },
|
||||||
|
"PROFILE_REFERENCE_UNAVAILABLE"
|
||||||
|
);
|
||||||
|
await save({ ...replacement.values, notes: "Revisar equipo" }, replacement.id);
|
||||||
|
assert.equal(
|
||||||
|
(await request(`/assets/${companyId}/workstations/${host.id}/update`, { values: host.values }))
|
||||||
|
.status,
|
||||||
|
200
|
||||||
|
);
|
||||||
await database.query("UPDATE work_profiles SET currentKey=NULL WHERE id=:id", {
|
await database.query("UPDATE work_profiles SET currentKey=NULL WHERE id=:id", {
|
||||||
replacements: { id: replacement.id },
|
replacements: { id: replacement.id },
|
||||||
});
|
});
|
||||||
|
|||||||
@ -12,17 +12,17 @@ Persona y dispositivo son la identidad funcional del perfil y no cambian despué
|
|||||||
|
|
||||||
## Persona, puesto y activo
|
## Persona, puesto y activo
|
||||||
|
|
||||||
Un puesto agrupa activos; el perfil pertenece a una persona y a un dispositivo. No se almacena otra asignación de puesto en el perfil: la ficha consulta los puestos vigentes que incluyen el dispositivo. Esta relación no presupone que todas las personas que usan un ordenador sean titulares del puesto o responsables del activo.
|
Un puesto agrupa activos; el perfil pertenece a una persona y a un dispositivo. No se almacena otra asignación de puesto en el perfil: la ficha consulta el puesto vigente cuyo ordenador principal es el dispositivo. Esta relación no presupone que todas las personas que usan un ordenador sean titulares del puesto o responsables del activo.
|
||||||
|
|
||||||
La persona del puesto y las personas con perfiles tienen significados distintos. No se propagan cambios automáticamente entre ellas. Ninguna de estas referencias demuestra una autorización ni que una configuración esté lista.
|
El responsable del puesto y las personas con perfiles tienen significados distintos. No se propagan cambios automáticamente entre ellas. Ninguna de estas referencias demuestra una autorización ni que una configuración esté lista.
|
||||||
|
|
||||||
Los puestos distinguen activos exclusivos y recursos compartidos. Consultar [Puestos de trabajo](puestos-de-trabajo.md).
|
Los puestos distinguen activos exclusivos y recursos compartidos. Consultar [Puestos de trabajo](puestos-de-trabajo.md).
|
||||||
|
|
||||||
## Revisión y altas rápidas
|
## Revisión y altas rápidas
|
||||||
|
|
||||||
Un perfil vigente muestra un aviso si la persona se desactiva, el equipo se retira o la cuenta pierde disponibilidad o autorización. Se conservan sus valores y revisiones. Se permite editar nombre/notas y archivarlo aunque necesite revisión. Crear, reactivar o cambiar la cuenta de un perfil vigente exige referencias disponibles. Las referencias siempre deben pertenecer a la compañía, incluso al archivar.
|
Un perfil vigente muestra un aviso si la persona se desactiva, el equipo se retira, deja de ser el ordenador principal de un puesto activo o la cuenta pierde disponibilidad o autorización. Se conservan sus valores y revisiones. Se permite editar nombre/notas y archivarlo aunque necesite revisión. Crear, reactivar o cambiar la cuenta de un perfil vigente exige referencias disponibles. Las referencias siempre deben pertenecer a la compañía, incluso al archivar.
|
||||||
|
|
||||||
Persona, dispositivo y cuenta reutilizan SelectWithAction para altas en un modal adicional. Se conserva el formulario padre y el elemento guardado queda seleccionado. La autorización de cuenta también puede registrarse mediante una acción explícita. El editor adicional es compartido con los correos; no abre un tercer nivel de alta. Cancelar mantiene los valores del padre. Una creación ya guardada permanece aunque se cancele el perfil.
|
Persona y cuenta reutilizan SelectWithAction para altas en un modal adicional. Se conserva el formulario padre y el elemento guardado queda seleccionado. La autorización de cuenta también puede registrarse mediante una acción explícita. El editor adicional es compartido con los correos; no abre un tercer nivel de alta. Cancelar mantiene los valores del padre. Una creación ya guardada permanece aunque se cancele el perfil.
|
||||||
|
|
||||||
La ayuda aparece inmediatamente antes del pie con Guardar y Cancelar. Se mantienen traducciones es/en y estados de carga, error y vacío.
|
La ayuda aparece inmediatamente antes del pie con Guardar y Cancelar. Se mantienen traducciones es/en y estados de carga, error y vacío.
|
||||||
|
|
||||||
@ -50,4 +50,7 @@ La comprobación en navegador verifica la nueva sección, los desplegables de pe
|
|||||||
|
|
||||||
El formulario de alta pide puesto y persona. La persona no se toma automáticamente del responsable del puesto; puede ser cualquier persona activa de la compañía. El ordenador se obtiene de `computerAssetId` del puesto activo y no se pide como campo editable. Desde la ficha del puesto, «Nuevo perfil» abre el mismo modal con ese puesto fijado. Si no tiene un ordenador principal disponible, se indica que hay que completar el puesto primero.
|
El formulario de alta pide puesto y persona. La persona no se toma automáticamente del responsable del puesto; puede ser cualquier persona activa de la compañía. El ordenador se obtiene de `computerAssetId` del puesto activo y no se pide como campo editable. Desde la ficha del puesto, «Nuevo perfil» abre el mismo modal con ese puesto fijado. Si no tiene un ordenador principal disponible, se indica que hay que completar el puesto primero.
|
||||||
|
|
||||||
Se conserva la identidad persona/dispositivo y sus validaciones, permisos e historial. La relación con el puesto sigue consultándose por su composición; no se duplica una asignación independiente en el perfil. Cambiar responsable no modifica perfiles.
|
Se conserva la identidad persona/dispositivo y sus validaciones, permisos e historial. La relación con el puesto sigue consultándose por su ordenador principal; no se duplica una asignación independiente en el perfil. Cambiar responsable no modifica perfiles.
|
||||||
|
|
||||||
|
|
||||||
|
El backend comprueba también esta condición al crear o reactivar un perfil vigente y al cambiar su cuenta. Los periféricos y activos compartidos no alojan perfiles del puesto. Cambiar el ordenador principal o desactivar el puesto conserva los perfiles anteriores y su historial, pero señala los vigentes para revisión; no los transfiere al equipo nuevo. Se permite completar sus notas y archivarlos. Si el mismo equipo se incorpora como ordenador principal de otro puesto activo, la consulta refleja su ubicación actual.
|
||||||
|
|||||||
@ -61,3 +61,8 @@ Cuentas informáticas, grupos, recursos, autorizaciones y necesidades se gestion
|
|||||||
Los perfiles, sus configuraciones y el inventario de activos están implementados. La [autorización de teletrabajo](teletrabajo.md) también está disponible. Quedan pendientes las conexiones remotas y los procesos y plantillas de alta/cambio/salida.
|
Los perfiles, sus configuraciones y el inventario de activos están implementados. La [autorización de teletrabajo](teletrabajo.md) también está disponible. Quedan pendientes las conexiones remotas y los procesos y plantillas de alta/cambio/salida.
|
||||||
|
|
||||||
Los [procesos de personas](procesos-de-personas.md) organizan el alta, cambio y salida, conservando listas de comprobación, resultados e historial por compañía. Admiten personas inactivas para revisar accesos conservados; completar un proceso no desactiva ni elimina a la persona.
|
Los [procesos de personas](procesos-de-personas.md) organizan el alta, cambio y salida, conservando listas de comprobación, resultados e historial por compañía. Admiten personas inactivas para revisar accesos conservados; completar un proceso no desactiva ni elimina a la persona.
|
||||||
|
|
||||||
|
|
||||||
|
## Responsabilidad y uso de puestos
|
||||||
|
|
||||||
|
La ficha de persona separa los puestos activos bajo su responsabilidad de los que utiliza mediante un perfil vigente en su ordenador principal. Ambas relaciones son independientes: puede aparecer en los dos grupos, utilizar un puesto de otro responsable o ser responsable sin tener perfil. Los perfiles archivados y puestos inactivos no acreditan uso actual. Cada puesto enlaza a su ficha y conserva enlaces a sus activos. Si falla la consulta de perfiles se indica que no se puede determinar el uso, sin presentarlo como ausencia de puestos.
|
||||||
|
|||||||
@ -41,3 +41,6 @@ Un puesto agrupa equipos y recursos; el [perfil de usuario](perfiles-de-trabajo.
|
|||||||
La relación opcional `personId` identifica al responsable del puesto, no a todos sus usuarios. No se propaga a los perfiles. Estos identifican a cada persona que utiliza el ordenador, de modo que un puesto compartido puede tener varios perfiles.
|
La relación opcional `personId` identifica al responsable del puesto, no a todos sus usuarios. No se propaga a los perfiles. Estos identifican a cada persona que utiliza el ordenador, de modo que un puesto compartido puede tener varios perfiles.
|
||||||
|
|
||||||
`computerAssetId` identifica el único ordenador principal entre los activos exclusivos del puesto. No se deduce por nombre de plantilla ni por orden de selección. El resto de activos se registra como periféricos. Dos ordenadores se organizan en dos puestos. El ordenador principal se persiste como columna con clave foránea en el puesto y sus revisiones; un cambio conserva historial. Los puestos existentes mantienen su composición y necesitan designar el ordenador antes de crear nuevos perfiles. Retirarlo de la composición limpia la selección del ordenador principal.
|
`computerAssetId` identifica el único ordenador principal entre los activos exclusivos del puesto. No se deduce por nombre de plantilla ni por orden de selección. El resto de activos se registra como periféricos. Dos ordenadores se organizan en dos puestos. El ordenador principal se persiste como columna con clave foránea en el puesto y sus revisiones; un cambio conserva historial. Los puestos existentes mantienen su composición y necesitan designar el ordenador antes de crear nuevos perfiles. Retirarlo de la composición limpia la selección del ordenador principal.
|
||||||
|
|
||||||
|
|
||||||
|
La ficha reúne únicamente perfiles del ordenador principal, incluidos los archivados cuando corresponda. Los periféricos y activos compartidos no aportan perfiles. El entorno actual y la consulta del perfil utilizan el mismo criterio. Cambiar el ordenador principal no reasigna perfiles automáticamente: los perfiles vigentes que dejan de tener un puesto activo requieren revisión.
|
||||||
|
|||||||
@ -132,11 +132,16 @@ function inventoryAssignments(context: InventoryContext) {
|
|||||||
name: string;
|
name: string;
|
||||||
personId: string | null;
|
personId: string | null;
|
||||||
available: boolean;
|
available: boolean;
|
||||||
|
isProfileHost: boolean;
|
||||||
}[] = [];
|
}[] = [];
|
||||||
|
|
||||||
for (const asset of context.assets) {
|
for (const asset of context.assets) {
|
||||||
const values = asset.values.toPrimitive();
|
const values = asset.values.toPrimitive();
|
||||||
const people = peopleByAsset.get(asset.id.toPrimitive()) ?? new Set<string>();
|
const people = peopleByAsset.get(asset.id.toPrimitive()) ?? new Set<string>();
|
||||||
|
const isProfileHost = context.workstations.some(
|
||||||
|
(station) =>
|
||||||
|
station.values.active && station.values.computerAssetId.getOrUndefined()?.equals(asset.id)
|
||||||
|
);
|
||||||
|
|
||||||
if (people.size) {
|
if (people.size) {
|
||||||
for (const personId of people) {
|
for (const personId of people) {
|
||||||
@ -145,6 +150,7 @@ function inventoryAssignments(context: InventoryContext) {
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
personId,
|
personId,
|
||||||
available: values.status !== "retired",
|
available: values.status !== "retired",
|
||||||
|
isProfileHost,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -153,6 +159,7 @@ function inventoryAssignments(context: InventoryContext) {
|
|||||||
name: values.name,
|
name: values.name,
|
||||||
personId: null,
|
personId: null,
|
||||||
available: values.status !== "retired",
|
available: values.status !== "retired",
|
||||||
|
isProfileHost,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,10 @@ export function profileReferencesAvailable(values: WorkProfileValues, context: W
|
|||||||
if (
|
if (
|
||||||
!(
|
!(
|
||||||
context.people.some((person) => person.id.equals(values.personId) && person.active) &&
|
context.people.some((person) => person.id.equals(values.personId) && person.active) &&
|
||||||
context.assets.some((asset) => asset.id.equals(values.assetId) && asset.available)
|
context.assets.some(
|
||||||
|
(asset) =>
|
||||||
|
asset.id.equals(values.assetId) && asset.available && asset.isProfileHost === true
|
||||||
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@ -6,13 +6,15 @@ import type { WorkProfile } from "../domain/work-profile.aggregate";
|
|||||||
import type { WorkProfileError } from "../domain/work-profile-values";
|
import type { WorkProfileError } from "../domain/work-profile-values";
|
||||||
|
|
||||||
export interface IWorkProfileInventoryService {
|
export interface IWorkProfileInventoryService {
|
||||||
list(companyId: UniqueID): Promise<readonly { id: UniqueID; available: boolean }[]>;
|
list(
|
||||||
|
companyId: UniqueID
|
||||||
|
): Promise<readonly { id: UniqueID; available: boolean; isProfileHost?: boolean }[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkProfileContext = Readonly<{
|
export type WorkProfileContext = Readonly<{
|
||||||
profiles: readonly WorkProfile[];
|
profiles: readonly WorkProfile[];
|
||||||
people: readonly { id: UniqueID; active: boolean }[];
|
people: readonly { id: UniqueID; active: boolean }[];
|
||||||
assets: readonly { id: UniqueID; available: boolean }[];
|
assets: readonly { id: UniqueID; available: boolean; isProfileHost?: boolean }[];
|
||||||
items: readonly { id: UniqueID; values: AccessDefinition }[];
|
items: readonly { id: UniqueID; values: AccessDefinition }[];
|
||||||
actors: ReadonlyMap<string, string>;
|
actors: ReadonlyMap<string, string>;
|
||||||
today: string;
|
today: string;
|
||||||
|
|||||||
@ -53,7 +53,7 @@ function context(): WorkProfileContext {
|
|||||||
return {
|
return {
|
||||||
profiles: [],
|
profiles: [],
|
||||||
people: [{ id: person, active: true }],
|
people: [{ id: person, active: true }],
|
||||||
assets: [{ id: asset, available: true }],
|
assets: [{ id: asset, available: true, isProfileHost: true }],
|
||||||
items: [],
|
items: [],
|
||||||
actors: new Map([[actor.toPrimitive(), "Técnico"]]),
|
actors: new Map([[actor.toPrimitive(), "Técnico"]]),
|
||||||
today: "2026-10-04",
|
today: "2026-10-04",
|
||||||
@ -113,7 +113,7 @@ test("Perfil: misma persona/dispositivo único vigente, múltiples dispositivos/
|
|||||||
assert.ok(
|
assert.ok(
|
||||||
mapWorkProfileInput(
|
mapWorkProfileInput(
|
||||||
{ ...input(), assetId: second.toPrimitive() },
|
{ ...input(), assetId: second.toPrimitive() },
|
||||||
{ ...ctx, assets: [...ctx.assets, { id: second, available: true }] }
|
{ ...ctx, assets: [...ctx.assets, { id: second, available: true, isProfileHost: true }] }
|
||||||
).isSuccess
|
).isSuccess
|
||||||
);
|
);
|
||||||
assert.ok(
|
assert.ok(
|
||||||
@ -197,3 +197,24 @@ test("Perfil: persistencia conserva revisiones y rechaza raíz o cuenta históri
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("Perfil: solo un ordenador principal activo admite altas y reactivaciones; el historial se conserva", () => {
|
||||||
|
const previous = profile();
|
||||||
|
const unavailable = {
|
||||||
|
...context(),
|
||||||
|
assets: [{ id: asset, available: true, isProfileHost: false }],
|
||||||
|
};
|
||||||
|
const result = mapWorkProfileInput(input(), unavailable);
|
||||||
|
|
||||||
|
assert.ok(!result.isSuccess);
|
||||||
|
assert.equal(result.error.code, "PROFILE_REFERENCE_UNAVAILABLE");
|
||||||
|
assert.equal(profileReferencesAvailable(previous.values, unavailable), false);
|
||||||
|
assert.ok(
|
||||||
|
mapWorkProfileInput({ ...input(), name: "Revisar ordenador" }, unavailable, previous).isSuccess
|
||||||
|
);
|
||||||
|
const archived = profile({ ...input(), active: false });
|
||||||
|
|
||||||
|
assert.ok(mapWorkProfileInput({ ...input(), active: false }, unavailable, previous).isSuccess);
|
||||||
|
assert.ok(!mapWorkProfileInput(input(), unavailable, archived).isSuccess);
|
||||||
|
assert.ok(previous.values.assetId.equals(asset));
|
||||||
|
});
|
||||||
|
|||||||
@ -732,7 +732,7 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"INVALID_REQUEST": "Check the form fields.",
|
"INVALID_REQUEST": "Check the form fields.",
|
||||||
"PROFILE_REFERENCE_NOT_FOUND": "The person, device or account does not belong to this company.",
|
"PROFILE_REFERENCE_NOT_FOUND": "The person, device or account does not belong to this company.",
|
||||||
"PROFILE_REFERENCE_UNAVAILABLE": "The person or device is unavailable, or the account has no current authorization for this person.",
|
"PROFILE_REFERENCE_UNAVAILABLE": "The person or workstation main computer is unavailable, or the account has no current authorization for this person.",
|
||||||
"PROFILE_ALREADY_ACTIVE": "A current profile already exists for this person on this device.",
|
"PROFILE_ALREADY_ACTIVE": "A current profile already exists for this person on this device.",
|
||||||
"PROFILE_TARGET_IMMUTABLE": "A profile's person and device cannot change.",
|
"PROFILE_TARGET_IMMUTABLE": "A profile's person and device cannot change.",
|
||||||
"PROFILE_NOT_FOUND": "This profile is unavailable.",
|
"PROFILE_NOT_FOUND": "This profile is unavailable.",
|
||||||
@ -933,7 +933,12 @@
|
|||||||
"description": "This record and its history will be deleted. Related assets, resources, accounts and people will be preserved. This action cannot be undone.",
|
"description": "This record and its history will be deleted. Related assets, resources, accounts and people will be preserved. This action cannot be undone.",
|
||||||
"pending": "Deleting…",
|
"pending": "Deleting…",
|
||||||
"error": "The record could not be deleted. Please try again."
|
"error": "The record could not be deleted. Please try again."
|
||||||
}
|
},
|
||||||
|
"responsibleWorkstations": "Workstations under their responsibility",
|
||||||
|
"usedWorkstations": "Workstations they use",
|
||||||
|
"noResponsibleWorkstations": "This person is not responsible for any active workstation.",
|
||||||
|
"noUsedWorkstations": "This person has no active profiles on the main computers of active workstations.",
|
||||||
|
"workstationProfilesLoadError": "Profiles could not be loaded. Workstation usage cannot be determined."
|
||||||
},
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"helpTitle": "What is it and what is it for?",
|
"helpTitle": "What is it and what is it for?",
|
||||||
|
|||||||
@ -732,7 +732,7 @@
|
|||||||
"errors": {
|
"errors": {
|
||||||
"INVALID_REQUEST": "Revisa los campos del formulario.",
|
"INVALID_REQUEST": "Revisa los campos del formulario.",
|
||||||
"PROFILE_REFERENCE_NOT_FOUND": "La persona, el dispositivo o la cuenta no pertenecen a esta compañía.",
|
"PROFILE_REFERENCE_NOT_FOUND": "La persona, el dispositivo o la cuenta no pertenecen a esta compañía.",
|
||||||
"PROFILE_REFERENCE_UNAVAILABLE": "La persona o el dispositivo no están disponibles, o la cuenta no tiene una autorización vigente para esta persona.",
|
"PROFILE_REFERENCE_UNAVAILABLE": "La persona o el ordenador principal del puesto no están disponibles, o la cuenta no tiene una autorización vigente para esta persona.",
|
||||||
"PROFILE_ALREADY_ACTIVE": "Ya existe un perfil vigente de esta persona en este dispositivo.",
|
"PROFILE_ALREADY_ACTIVE": "Ya existe un perfil vigente de esta persona en este dispositivo.",
|
||||||
"PROFILE_TARGET_IMMUTABLE": "La persona y el dispositivo de un perfil no pueden cambiar.",
|
"PROFILE_TARGET_IMMUTABLE": "La persona y el dispositivo de un perfil no pueden cambiar.",
|
||||||
"PROFILE_NOT_FOUND": "El perfil ya no está disponible.",
|
"PROFILE_NOT_FOUND": "El perfil ya no está disponible.",
|
||||||
@ -933,7 +933,12 @@
|
|||||||
"description": "Se eliminarán este registro y su historial. Los activos, recursos, cuentas y personas relacionados se conservarán. Esta acción no se puede deshacer.",
|
"description": "Se eliminarán este registro y su historial. Los activos, recursos, cuentas y personas relacionados se conservarán. Esta acción no se puede deshacer.",
|
||||||
"pending": "Eliminando…",
|
"pending": "Eliminando…",
|
||||||
"error": "No se ha podido eliminar el registro. Inténtalo de nuevo."
|
"error": "No se ha podido eliminar el registro. Inténtalo de nuevo."
|
||||||
}
|
},
|
||||||
|
"responsibleWorkstations": "Puestos bajo su responsabilidad",
|
||||||
|
"usedWorkstations": "Puestos que utiliza",
|
||||||
|
"noResponsibleWorkstations": "No es responsable de ningún puesto activo.",
|
||||||
|
"noUsedWorkstations": "No tiene perfiles vigentes en los ordenadores principales de puestos activos.",
|
||||||
|
"workstationProfilesLoadError": "No se pudieron consultar los perfiles. No se puede determinar qué puestos utiliza."
|
||||||
},
|
},
|
||||||
"access": {
|
"access": {
|
||||||
"helpTitle": "¿Qué es y para qué sirve?",
|
"helpTitle": "¿Qué es y para qué sirve?",
|
||||||
|
|||||||
@ -0,0 +1,103 @@
|
|||||||
|
import { inventoryApi, inventoryKeys } from "@metis/assets/client";
|
||||||
|
import { BrowserLink, useListQuery } from "@metis/core/client";
|
||||||
|
import { Card, CardContent } from "@repo/shadcn-ui/components/card";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
|
import type { CompanyDTO } from "../../../../common";
|
||||||
|
import { profileApi, profileKeys } from "../../../work-profiles/shared/api";
|
||||||
|
import { personWorkstations } from "../../../workstations/detail/helpers/person-workstations";
|
||||||
|
|
||||||
|
export function CurrentWorkstations({
|
||||||
|
company,
|
||||||
|
personId,
|
||||||
|
}: {
|
||||||
|
company: CompanyDTO;
|
||||||
|
personId: string;
|
||||||
|
}) {
|
||||||
|
const { t } = useTranslation("companies");
|
||||||
|
const inventory = useListQuery({
|
||||||
|
queryKey: inventoryKeys.company(company.id),
|
||||||
|
queryFn: () => inventoryApi.get(company.id),
|
||||||
|
});
|
||||||
|
const profiles = useListQuery({
|
||||||
|
queryKey: profileKeys.company(company.id),
|
||||||
|
queryFn: () => profileApi.get(company.id),
|
||||||
|
});
|
||||||
|
const groups = personWorkstations(
|
||||||
|
company.id,
|
||||||
|
personId,
|
||||||
|
inventory.data?.workstations ?? [],
|
||||||
|
profiles.data?.profiles ?? []
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardContent className="space-y-5 pt-6">
|
||||||
|
<h2 className="text-xl font-semibold">{t("currentWorkstations")}</h2>
|
||||||
|
{inventory.isPending && <p role="status">{t("loading")}</p>}
|
||||||
|
{inventory.isError && <p role="alert">{t("workstationsLoadError")}</p>}
|
||||||
|
{inventory.data &&
|
||||||
|
(["responsible", "used"] as const).map((group) => {
|
||||||
|
const loading = group === "used" && profiles.isPending;
|
||||||
|
const failed = group === "used" && profiles.isError;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="space-y-2" key={group}>
|
||||||
|
<h3 className="text-sm font-semibold text-muted-foreground">
|
||||||
|
{t(group === "responsible" ? "responsibleWorkstations" : "usedWorkstations")}
|
||||||
|
</h3>
|
||||||
|
{loading && (
|
||||||
|
<p className="text-sm" role="status">
|
||||||
|
{t("loading")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{failed && (
|
||||||
|
<p className="text-sm" role="alert">
|
||||||
|
{t("workstationProfilesLoadError")}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{!(loading || failed) &&
|
||||||
|
(groups[group].length ? (
|
||||||
|
<div className="divide-y">
|
||||||
|
{groups[group].map((station) => (
|
||||||
|
<div className="space-y-1 py-3" key={station.id}>
|
||||||
|
<BrowserLink
|
||||||
|
className="font-medium"
|
||||||
|
href={`/companies/${company.id}/workstations/${station.id}`}
|
||||||
|
>
|
||||||
|
{station.values.name}
|
||||||
|
</BrowserLink>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{company.sites.find((site) => site.id === station.values.siteId)
|
||||||
|
?.name ?? t("noAssignment")}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
|
||||||
|
{[...station.values.assetIds, ...station.values.sharedAssetIds].map(
|
||||||
|
(id) => (
|
||||||
|
<BrowserLink
|
||||||
|
href={`/companies/${company.id}/assets/${id}`}
|
||||||
|
key={id}
|
||||||
|
>
|
||||||
|
{inventory.data?.assets.find((asset) => asset.id === id)?.values
|
||||||
|
.name ?? id}
|
||||||
|
</BrowserLink>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
{t(
|
||||||
|
group === "responsible" ? "noResponsibleWorkstations" : "noUsedWorkstations"
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import { inventoryApi, inventoryKeys } from "@metis/assets/client";
|
|
||||||
import { BrowserLink } from "@metis/core/client";
|
|
||||||
import {
|
import {
|
||||||
EntityAction,
|
EntityAction,
|
||||||
EntityListHeader,
|
EntityListHeader,
|
||||||
@ -11,7 +9,6 @@ import { BriefcaseIcon, ChevronLeftIcon, MailIcon, PhoneIcon } from "@repo/rdx-u
|
|||||||
import { AvatarBadge } from "@repo/shadcn-ui/components/avatar";
|
import { AvatarBadge } from "@repo/shadcn-ui/components/avatar";
|
||||||
import { Button } from "@repo/shadcn-ui/components/button";
|
import { Button } from "@repo/shadcn-ui/components/button";
|
||||||
import { Card, CardContent } from "@repo/shadcn-ui/components/card";
|
import { Card, CardContent } from "@repo/shadcn-ui/components/card";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@ -21,6 +18,8 @@ import { deleteAssignment } from "../../../assignments/shared/api";
|
|||||||
import { PersonAvatar } from "../../shared";
|
import { PersonAvatar } from "../../shared";
|
||||||
import { deletePerson } from "../../shared/api";
|
import { deletePerson } from "../../shared/api";
|
||||||
|
|
||||||
|
import { CurrentWorkstations } from "./current-workstations";
|
||||||
|
|
||||||
export function PersonDetails({
|
export function PersonDetails({
|
||||||
company,
|
company,
|
||||||
person,
|
person,
|
||||||
@ -263,74 +262,3 @@ export function PersonDetails({
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CurrentWorkstations({ company, personId }: { company: CompanyDTO; personId: string }) {
|
|
||||||
const { t, i18n } = useTranslation("companies");
|
|
||||||
const inventory = useQuery({
|
|
||||||
queryKey: inventoryKeys.company(company.id),
|
|
||||||
queryFn: () => inventoryApi.get(company.id),
|
|
||||||
});
|
|
||||||
const workstations = (inventory.data?.workstations ?? []).filter(
|
|
||||||
(item) => item.values.active && item.values.personId === personId
|
|
||||||
);
|
|
||||||
|
|
||||||
const assetName = (id: string) =>
|
|
||||||
inventory.data?.assets.find((item) => item.id === id)?.values.name ?? id;
|
|
||||||
|
|
||||||
const siteName = (id: string | null) =>
|
|
||||||
company.sites.find((item) => item.id === id)?.name ?? t("noAssignment");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<CardContent className="space-y-4 pt-6">
|
|
||||||
<EntityListHeader locale={i18n.language} title={t("currentWorkstations")} />
|
|
||||||
{inventory.isPending && <p className="text-sm text-muted-foreground">{t("loading")}</p>}
|
|
||||||
{inventory.isError && (
|
|
||||||
<p className="text-sm text-muted-foreground">{t("workstationsLoadError")}</p>
|
|
||||||
)}
|
|
||||||
{inventory.data &&
|
|
||||||
(workstations.length ? (
|
|
||||||
<div className="divide-y">
|
|
||||||
{workstations.map((workstation) => {
|
|
||||||
const assetIds = [
|
|
||||||
...workstation.values.assetIds,
|
|
||||||
...workstation.values.sharedAssetIds,
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="py-4 first:pt-0 last:pb-0" key={workstation.id}>
|
|
||||||
<BrowserLink
|
|
||||||
className="app-link font-medium"
|
|
||||||
href={`/companies/${company.id}/workstations/${workstation.id}`}
|
|
||||||
>
|
|
||||||
{workstation.values.name}
|
|
||||||
</BrowserLink>
|
|
||||||
<p className="mt-1 text-sm text-muted-foreground">
|
|
||||||
{siteName(workstation.values.siteId)}
|
|
||||||
</p>
|
|
||||||
<div className="mt-3 flex flex-wrap gap-x-4 gap-y-2 text-sm">
|
|
||||||
{assetIds.length ? (
|
|
||||||
assetIds.map((assetId) => (
|
|
||||||
<BrowserLink
|
|
||||||
className="app-link"
|
|
||||||
href={`/companies/${company.id}/assets/${assetId}`}
|
|
||||||
key={assetId}
|
|
||||||
>
|
|
||||||
{assetName(assetId)}
|
|
||||||
</BrowserLink>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<span className="text-muted-foreground">{t("noWorkstationAssets")}</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<p className="text-sm text-muted-foreground">{t("noCurrentWorkstations")}</p>
|
|
||||||
))}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -87,7 +87,7 @@ export function buildEnvironment(profile: WorkProfileDTO, sources: EnvironmentSo
|
|||||||
(item) =>
|
(item) =>
|
||||||
item.companyId === profile.companyId &&
|
item.companyId === profile.companyId &&
|
||||||
item.values.active &&
|
item.values.active &&
|
||||||
[...item.values.assetIds, ...item.values.sharedAssetIds].includes(profile.values.assetId)
|
item.values.computerAssetId === profile.values.assetId
|
||||||
);
|
);
|
||||||
const stationAssets = [
|
const stationAssets = [
|
||||||
...new Set(
|
...new Set(
|
||||||
|
|||||||
@ -183,6 +183,7 @@ const station = WorkstationSchema.parse(
|
|||||||
active: true,
|
active: true,
|
||||||
siteId: null,
|
siteId: null,
|
||||||
personId: null,
|
personId: null,
|
||||||
|
computerAssetId: host.id,
|
||||||
assetIds: [host.id],
|
assetIds: [host.id],
|
||||||
sharedAssetIds: [monitor.id],
|
sharedAssetIds: [monitor.id],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -48,10 +48,9 @@ export function WorkProfileHistoryDialog({
|
|||||||
{workstations
|
{workstations
|
||||||
.filter(
|
.filter(
|
||||||
(station) =>
|
(station) =>
|
||||||
|
station.companyId === profile.companyId &&
|
||||||
station.values.active &&
|
station.values.active &&
|
||||||
[...station.values.assetIds, ...station.values.sharedAssetIds].includes(
|
station.values.computerAssetId === profile.values.assetId
|
||||||
profile.values.assetId
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.map((station) => station.values.name)
|
.map((station) => station.values.name)
|
||||||
.join(" · ") || t("profiles.noStation")}
|
.join(" · ") || t("profiles.noStation")}
|
||||||
|
|||||||
@ -0,0 +1,31 @@
|
|||||||
|
import type { WorkstationDTO } from "@metis/assets/common";
|
||||||
|
|
||||||
|
import type { WorkProfileDTO } from "../../../../common/work-profiles";
|
||||||
|
|
||||||
|
export function personWorkstations(
|
||||||
|
companyId: string,
|
||||||
|
personId: string,
|
||||||
|
workstations: readonly WorkstationDTO[],
|
||||||
|
profiles: readonly WorkProfileDTO[]
|
||||||
|
) {
|
||||||
|
const active = workstations.filter(
|
||||||
|
(station) => station.companyId === companyId && station.values.active
|
||||||
|
);
|
||||||
|
const computers = new Set(
|
||||||
|
profiles
|
||||||
|
.filter(
|
||||||
|
(profile) =>
|
||||||
|
profile.companyId === companyId &&
|
||||||
|
profile.values.personId === personId &&
|
||||||
|
profile.values.active
|
||||||
|
)
|
||||||
|
.map((profile) => profile.values.assetId)
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
responsible: active.filter((station) => station.values.personId === personId),
|
||||||
|
used: active.filter(
|
||||||
|
(station) => station.values.computerAssetId && computers.has(station.values.computerAssetId)
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -5,6 +5,7 @@ import { WorkstationSchema } from "@metis/assets/common";
|
|||||||
|
|
||||||
import { WorkProfileSchema } from "../../../../common/work-profiles";
|
import { WorkProfileSchema } from "../../../../common/work-profiles";
|
||||||
|
|
||||||
|
import { personWorkstations } from "./person-workstations";
|
||||||
import { workstationRelations } from "./workstation-relations";
|
import { workstationRelations } from "./workstation-relations";
|
||||||
|
|
||||||
const id = (n: number) => `0199b200-0000-7000-8000-${String(n).padStart(12, "0")}`;
|
const id = (n: number) => `0199b200-0000-7000-8000-${String(n).padStart(12, "0")}`;
|
||||||
@ -14,6 +15,7 @@ const values = {
|
|||||||
active: true,
|
active: true,
|
||||||
siteId: null,
|
siteId: null,
|
||||||
personId: id(3),
|
personId: id(3),
|
||||||
|
computerAssetId: id(4),
|
||||||
assetIds: [id(4)],
|
assetIds: [id(4)],
|
||||||
sharedAssetIds: [id(5)],
|
sharedAssetIds: [id(5)],
|
||||||
};
|
};
|
||||||
@ -55,10 +57,11 @@ const profile = (
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
test("incluye perfiles de activos exclusivos y compartidos, distingue inactivos y deduplica personas", () => {
|
test("incluye solo perfiles del ordenador principal, distingue inactivos y deduplica personas", () => {
|
||||||
const profiles = [
|
const profiles = [
|
||||||
profile(11, id(4), id(3)),
|
profile(11, id(4), id(3)),
|
||||||
profile(12, id(5), id(6), false),
|
profile(12, id(4), id(6), false),
|
||||||
|
profile(16, id(5), id(8)),
|
||||||
profile(13, id(7), id(8)),
|
profile(13, id(7), id(8)),
|
||||||
profile(14, id(4), id(3)),
|
profile(14, id(4), id(3)),
|
||||||
profile(15, id(4), id(8), true, id(20)),
|
profile(15, id(4), id(8), true, id(20)),
|
||||||
@ -80,10 +83,44 @@ test("puesto inexistente o sin composición no hereda perfiles de otros activos"
|
|||||||
);
|
);
|
||||||
const empty = WorkstationSchema.parse({
|
const empty = WorkstationSchema.parse({
|
||||||
...workstation,
|
...workstation,
|
||||||
values: { ...values, assetIds: [], sharedAssetIds: [] },
|
values: { ...values, computerAssetId: null, assetIds: [], sharedAssetIds: [] },
|
||||||
});
|
});
|
||||||
const result = workstationRelations(empty, [profile(11, id(4), id(6))]);
|
const result = workstationRelations(empty, [profile(11, id(4), id(6))]);
|
||||||
|
|
||||||
assert.equal(result.relatedProfiles.length, 0);
|
assert.equal(result.relatedProfiles.length, 0);
|
||||||
assert.deepEqual([...result.personIds], [id(3)]);
|
assert.deepEqual([...result.personIds], [id(3)]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("persona: responsabilidad y uso son independientes y solo se consideran puestos y perfiles vigentes", () => {
|
||||||
|
const used = {
|
||||||
|
...workstation,
|
||||||
|
id: id(21),
|
||||||
|
values: { ...values, personId: id(8), computerAssetId: id(7) },
|
||||||
|
};
|
||||||
|
const inactive = { ...used, id: id(22), values: { ...used.values, active: false } };
|
||||||
|
const foreign = { ...used, id: id(23), companyId: id(20) };
|
||||||
|
const result = personWorkstations(
|
||||||
|
id(2),
|
||||||
|
id(3),
|
||||||
|
[workstation, used, inactive, foreign],
|
||||||
|
[
|
||||||
|
profile(11, id(7), id(3)),
|
||||||
|
profile(12, id(4), id(3), false),
|
||||||
|
profile(13, id(4), id(8)),
|
||||||
|
profile(14, id(4), id(3), true, id(20)),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepEqual(
|
||||||
|
result.responsible.map((item) => item.id),
|
||||||
|
[id(1)]
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
result.used.map((item) => item.id),
|
||||||
|
[id(21)]
|
||||||
|
);
|
||||||
|
const both = personWorkstations(id(2), id(3), [workstation], [profile(15, id(4), id(3))]);
|
||||||
|
|
||||||
|
assert.equal(both.responsible.length, 1);
|
||||||
|
assert.equal(both.used.length, 1);
|
||||||
|
});
|
||||||
|
|||||||
@ -2,18 +2,15 @@ import type { WorkstationDTO } from "@metis/assets/common";
|
|||||||
|
|
||||||
import type { WorkProfileDTO } from "../../../../common/work-profiles";
|
import type { WorkProfileDTO } from "../../../../common/work-profiles";
|
||||||
|
|
||||||
/** Un perfil pertenece a un activo; los compartidos pueden aparecer en varios puestos. */
|
/** Los perfiles se consultan únicamente por el ordenador principal del puesto. */
|
||||||
export function workstationRelations(
|
export function workstationRelations(
|
||||||
workstation: WorkstationDTO | undefined,
|
workstation: WorkstationDTO | undefined,
|
||||||
profiles: readonly WorkProfileDTO[]
|
profiles: readonly WorkProfileDTO[]
|
||||||
) {
|
) {
|
||||||
const assetIds = new Set([
|
|
||||||
...(workstation?.values.assetIds ?? []),
|
|
||||||
...(workstation?.values.sharedAssetIds ?? []),
|
|
||||||
]);
|
|
||||||
const relatedProfiles = profiles.filter(
|
const relatedProfiles = profiles.filter(
|
||||||
(profile) =>
|
(profile) =>
|
||||||
profile.companyId === workstation?.companyId && assetIds.has(profile.values.assetId)
|
profile.companyId === workstation?.companyId &&
|
||||||
|
profile.values.assetId === workstation?.values.computerAssetId
|
||||||
);
|
);
|
||||||
const personIds = new Set(relatedProfiles.map((profile) => profile.values.personId));
|
const personIds = new Set(relatedProfiles.map((profile) => profile.values.personId));
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,8 @@ import { createInstance } from "i18next";
|
|||||||
import { renderToStaticMarkup } from "react-dom/server";
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
import { I18nextProvider } from "react-i18next";
|
import { I18nextProvider } from "react-i18next";
|
||||||
|
|
||||||
|
import { WorkProfileSchema } from "../../../../common/work-profiles";
|
||||||
|
import { CurrentWorkstations } from "../../../people/detail/ui/current-workstations";
|
||||||
import { companiesEnglish, companiesSpanish } from "../../../shared";
|
import { companiesEnglish, companiesSpanish } from "../../../shared";
|
||||||
import { profileKeys } from "../../../work-profiles/shared/api";
|
import { profileKeys } from "../../../work-profiles/shared/api";
|
||||||
|
|
||||||
@ -111,3 +113,80 @@ test("un UUID de puesto ausente muestra no encontrado sin cabecera ni edición d
|
|||||||
assert.equal(html.includes("Administración"), false);
|
assert.equal(html.includes("Administración"), false);
|
||||||
assert.equal(html.includes("Editar puesto"), false);
|
assert.equal(html.includes("Editar puesto"), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ficha de persona es/en: distingue responsabilidad y uso, y enlaza los puestos", async () => {
|
||||||
|
const computerId = "0199b200-0000-7000-8000-000000000004";
|
||||||
|
const personId = "0199b200-0000-7000-8000-000000000005";
|
||||||
|
const usedId = "0199b200-0000-7000-8000-000000000006";
|
||||||
|
const own = {
|
||||||
|
...workstation,
|
||||||
|
values: { ...values, personId, name: "Responsabilidad sin perfil" },
|
||||||
|
};
|
||||||
|
const used = {
|
||||||
|
...workstation,
|
||||||
|
id: usedId,
|
||||||
|
values: {
|
||||||
|
...values,
|
||||||
|
computerAssetId: computerId,
|
||||||
|
assetIds: [computerId],
|
||||||
|
name: "Uso sin responsabilidad",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const profileValues = {
|
||||||
|
name: "Perfil",
|
||||||
|
assetId: computerId,
|
||||||
|
personId,
|
||||||
|
active: true,
|
||||||
|
accountId: null,
|
||||||
|
notes: null,
|
||||||
|
};
|
||||||
|
const profile = WorkProfileSchema.parse({
|
||||||
|
id: personId,
|
||||||
|
companyId,
|
||||||
|
values: profileValues,
|
||||||
|
needsReview: false,
|
||||||
|
history: [{ ...workstation.history[0], values: profileValues }],
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const language of ["es", "en"]) {
|
||||||
|
const i18n = createInstance();
|
||||||
|
|
||||||
|
await i18n.init({
|
||||||
|
lng: language,
|
||||||
|
resources: { es: { companies: companiesSpanish }, en: { companies: companiesEnglish } },
|
||||||
|
});
|
||||||
|
const client = new QueryClient({
|
||||||
|
defaultOptions: { queries: { retry: false, staleTime: Number.POSITIVE_INFINITY } },
|
||||||
|
});
|
||||||
|
|
||||||
|
client.setQueryData(inventoryKeys.company(companyId), {
|
||||||
|
assets: [],
|
||||||
|
relationships: [],
|
||||||
|
workstations: [own, used],
|
||||||
|
});
|
||||||
|
client.setQueryData(profileKeys.company(companyId), { profiles: [profile] });
|
||||||
|
try {
|
||||||
|
const html = renderToStaticMarkup(
|
||||||
|
<I18nextProvider i18n={i18n}>
|
||||||
|
<QueryClientProvider client={client}>
|
||||||
|
<CurrentWorkstations company={company} personId={personId} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</I18nextProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.ok(
|
||||||
|
html.includes(
|
||||||
|
language === "es"
|
||||||
|
? "Puestos bajo su responsabilidad"
|
||||||
|
: "Workstations under their responsibility"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert.ok(html.includes(language === "es" ? "Puestos que utiliza" : "Workstations they use"));
|
||||||
|
assert.ok(html.includes("Responsabilidad sin perfil"));
|
||||||
|
assert.ok(html.includes("Uso sin responsabilidad"));
|
||||||
|
assert.ok(html.includes(`href="/companies/${companyId}/workstations/${usedId}"`));
|
||||||
|
} finally {
|
||||||
|
client.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user