From b45925f3e24697ab09497a73c0464613eb89a053 Mon Sep 17 00:00:00 2001 From: David Date: Wed, 14 Oct 2026 19:42:59 +0200 Subject: [PATCH] feat(assets): implement asset picker dialog and selection components - Added `useAssetPickerController` for managing asset selection logic. - Created `AssetSelectProps` type for defining asset selection properties. - Implemented `AssetPickerDialog` for displaying the asset selection interface. - Developed `AssetPickerList` for rendering the list of selectable assets. - Introduced `AssetSelect` component for integrating the asset picker into forms. - Created `AssetList` component for shared asset listing functionality. - Added tests for workstation creation logic to ensure proper validation. - Implemented workstation asset fields and association fields for managing workstation assets. - Developed workstation lifecycle dialog for managing work profile states. - Added form progress component for visualizing multi-step forms. --- TODO.md | 16 +- apps/server/src/asset-states.integration.ts | 11 +- apps/server/src/printers.integration.ts | 1 - .../server/src/record-deletion.integration.ts | 34 +- .../src/unlink-asset.integration-helper.ts | 5 +- apps/server/src/work-profile-hosts.fixture.ts | 8 +- apps/server/src/work-profiles.integration.ts | 56 + .../src/workstation-lifecycle.integration.ts | 18 +- apps/server/src/workstations.integration.ts | 73 +- docs/campos-y-plantillas-de-activos.md | 5 + docs/componentes-de-interfaz.md | 22 +- docs/diagrama-entidad-relacion-completo.mmd | 1620 +++++++++++ docs/diagrama-entidad-relacion.md | 1487 +++++++++++ docs/diccionario-del-esquema-actual.md | 2374 +++++++++++++++++ docs/entorno-actual.md | 4 +- docs/evolucion-de-plantillas-de-activos.md | 102 + docs/inventario-de-activos.md | 8 +- docs/patron-de-listados-de-entidades.md | 6 +- docs/perfiles-de-trabajo.md | 34 +- docs/puestos-de-trabajo.md | 37 +- docs/rutas-del-cliente.md | 2 +- docs/traducciones.md | 4 + modules/assets/package.json | 2 +- .../src/api/application/asset-input.mapper.ts | 4 +- .../api/application/asset-response.builder.ts | 6 +- .../application/workstation-input.mapper.ts | 17 +- modules/assets/src/api/domain/asset-state.ts | 9 +- modules/assets/src/api/domain/asset.test.ts | 9 +- .../value-objects/workstation-values.vo.ts | 20 +- .../src/api/domain/workstation-values.ts | 1 - .../src/api/domain/workstation.aggregate.ts | 1 - .../assets/src/api/domain/workstation.test.ts | 36 - .../persistence/load-workstations.ts | 4 +- .../remove-shared-workstation-assets.ts | 20 + .../workstation-persistence.mapper.ts | 14 +- .../persistence/workstation-relational.ts | 19 +- .../persistence/workstation.model.ts | 2 + modules/assets/src/api/module.ts | 2 +- modules/assets/src/common/locales/en.json | 58 +- modules/assets/src/common/locales/es.json | 58 +- .../list/types/asset-relationships-props.ts | 1 + .../list/ui/asset-relationships.tsx | 1 + .../shared/types/relationship-form-props.ts | 1 + .../shared/ui/relationship-editor-dialog.tsx | 4 +- .../shared/ui/relationship-form.tsx | 7 +- .../use-asset-detail-controller.ts | 4 +- .../assets/detail/ui/asset-detail-dialog.tsx | 2 +- .../detail/ui/asset-selection-panel.tsx | 7 +- .../use-inventory-page-controller.ts | 17 - .../src/web/assets/list/ui/inventory-page.tsx | 176 +- .../assets/select/asset-selection.test.tsx | 159 ++ .../use-asset-picker-controller.ts | 89 + .../assets/select/types/asset-select-props.ts | 26 + .../assets/select/ui/asset-picker-dialog.tsx | 86 + .../assets/select/ui/asset-picker-list.tsx | 72 + .../src/web/assets/select/ui/asset-select.tsx | 70 + .../src/web/assets/shared/inventory.test.tsx | 42 +- .../src/web/assets/shared/ui/asset-list.tsx | 138 + modules/assets/src/web/index.ts | 1 + .../history/ui/workstation-history-dialog.tsx | 6 +- .../list/ui/workstations-page.tsx | 14 +- .../use-workstation-form-controller.ts | 137 +- .../helpers/workstation-creation.test.ts | 35 + .../shared/helpers/workstation-creation.ts | 29 + .../shared/ui/workstation-actions.tsx | 1 - .../shared/ui/workstation-asset-fields.tsx | 102 + .../ui/workstation-association-fields.tsx | 66 + .../shared/ui/workstation-computer-field.tsx | 51 + .../shared/ui/workstation-form-actions.tsx | 31 + .../shared/ui/workstation-form.tsx | 256 +- .../shared/ui/workstation-summary.tsx | 52 + .../application/work-profile.repository.ts | 1 + .../api/application/work-profile.use-cases.ts | 29 + .../src/api/domain/work-profile.aggregate.ts | 23 +- .../src/api/domain/work-profile.test.ts | 26 + .../http/work-profile.routes.ts | 58 + .../sequelize-work-profile.repository.ts | 47 + modules/companies/src/common/locales/en.json | 10 +- modules/companies/src/common/locales/es.json | 10 +- .../people/detail/ui/current-workstations.tsx | 17 +- .../shared/ui/printer-configuration-form.tsx | 5 +- .../shared/ui/remote-endpoint-fields.tsx | 57 +- .../shared/ui/remote-target-field.tsx | 95 + .../web/tickets/shared/ui/ticket-targets.tsx | 72 +- .../detail/ui/environment-content.tsx | 87 +- .../shared/helpers/work-environment.model.ts | 14 +- .../shared/work-environment.test.tsx | 19 +- .../use-work-profiles-page-controller.ts | 3 + .../list/ui/work-profiles-page.tsx | 24 + .../shared/api/work-profile-api.ts | 20 + .../web/work-profiles/shared/profile.test.tsx | 2 +- .../ui/work-profile-lifecycle-dialog.tsx | 59 + .../shared/helpers/workspace-routes.ts | 8 +- .../workspace/shared/workspace-routes.test.ts | 44 +- .../helpers/workstation-relations.test.ts | 3 +- .../detail/ui/workstation-assets.tsx | 8 +- .../detail/ui/workstation-detail.test.tsx | 3 +- modules/resources/src/common/locales/en.json | 2 + modules/resources/src/common/locales/es.json | 2 + .../create/ui/resource-create-progress.tsx | 77 +- .../use-resources-page-controller.ts | 37 +- .../web/resources/list/ui/resources-page.tsx | 147 +- .../web/resources/shared/resources.test.tsx | 112 +- .../rdx-ui/src/components/form-progress.tsx | 68 + 104 files changed, 8171 insertions(+), 910 deletions(-) create mode 100644 docs/diagrama-entidad-relacion-completo.mmd create mode 100644 docs/diagrama-entidad-relacion.md create mode 100644 docs/diccionario-del-esquema-actual.md create mode 100644 docs/evolucion-de-plantillas-de-activos.md create mode 100644 modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets.ts create mode 100644 modules/assets/src/web/assets/select/asset-selection.test.tsx create mode 100644 modules/assets/src/web/assets/select/controllers/use-asset-picker-controller.ts create mode 100644 modules/assets/src/web/assets/select/types/asset-select-props.ts create mode 100644 modules/assets/src/web/assets/select/ui/asset-picker-dialog.tsx create mode 100644 modules/assets/src/web/assets/select/ui/asset-picker-list.tsx create mode 100644 modules/assets/src/web/assets/select/ui/asset-select.tsx create mode 100644 modules/assets/src/web/assets/shared/ui/asset-list.tsx create mode 100644 modules/assets/src/web/workstations/shared/helpers/workstation-creation.test.ts create mode 100644 modules/assets/src/web/workstations/shared/helpers/workstation-creation.ts create mode 100644 modules/assets/src/web/workstations/shared/ui/workstation-asset-fields.tsx create mode 100644 modules/assets/src/web/workstations/shared/ui/workstation-association-fields.tsx create mode 100644 modules/assets/src/web/workstations/shared/ui/workstation-computer-field.tsx create mode 100644 modules/assets/src/web/workstations/shared/ui/workstation-form-actions.tsx create mode 100644 modules/assets/src/web/workstations/shared/ui/workstation-summary.tsx create mode 100644 modules/companies/src/web/remote-connections/shared/ui/remote-target-field.tsx create mode 100644 modules/companies/src/web/work-profiles/shared/ui/work-profile-lifecycle-dialog.tsx create mode 100644 packages/rdx-ui/src/components/form-progress.tsx diff --git a/TODO.md b/TODO.md index 530b5ea..bb28cb6 100644 --- a/TODO.md +++ b/TODO.md @@ -1,3 +1,15 @@ -- En el listado de puestos de trabajo de una compañía, cuando se hace click en nombre de un puesto, nos iríamos a una pantalla que hay que crear que es la ficha completa de ese puesto de trabajo. En esa ficha aparecerían de forma resumida la información sobre el activo, los perfiles de usuario que tiene, personas relacionadas, etc. Visualmente esquemático y bonito. +- En el listado de recursos, quiero poder filtrar por pestañas por cada plantilla de recurso existente que haya pero manteniendo una pestaña "Todo" para poder visualizar todos los recusos juntos. En el listado de activos ya está hecho como te estoy indicando. -- Analiza la posibildad de que los listados se autorefresquen para deterctar cambios hechos por otros usuarios, sabiendo que utilizamos @tanstack/react-query \ No newline at end of file +- En los cuadros de diálogo en los que hay un desplegable para seleccionar un activo, cámbialo para que aparezca otro cuadro de diálogo con el listado de activos para seleccionar uno o más activos si fuera el caso. Ese nuevo cuadro de diálogo para seleccionar activos debe tener la misma disposición que la pantalla de listado de activos. Necesito que tenga el cuadro de búsqueda rapida y las pestañas por cada plantilla de activo. Acuerdate de buscar siempre patrones visuales de componentes para poder reutilizarlos en futuras peticiones. + +- Permitir "eliminar" un perfil de usuario y "desactivar" un perfil de usaurio. Comprobar en qué condiciones se peude realizar cada acción. Primero habría que "desactivar" luego se puede proceder a "eliminar". + +- En la pantalla de un puesto de trabajo, permitir editar ciertos campos mediante editores inline. Por ejemplo, El nombre del puesto de trabajo se puede editr a traves de un icono con un lapiz. ¿Me explico? Auditar qué información de esa pantalla sería posible de editar de esa manera. + +- En la ficha de un perfil de trabajo, ordenar los grupos de los recursos de forma más lógica: primero las carpetas compartidas, luegos los correos electrónicos, luego las aplicaciones. El resto, como tú quieras. + +- Elimina la plantilla de recurso "impresoras del perfil" de forma global. + +- Cuando se modifica una plantilla de activos para añadir un campo nuevo, una sección nueva, cambiar orden de campos, etc, pensar cómo se debería reflejar este cambio en los activos que dependen de esta plantilla en todas las compañías. + +- En la ficha de perfil de usuario quiero cambiar la forma de añadir un recurso. \ No newline at end of file diff --git a/apps/server/src/asset-states.integration.ts b/apps/server/src/asset-states.integration.ts index 8298775..1441dd9 100644 --- a/apps/server/src/asset-states.integration.ts +++ b/apps/server/src/asset-states.integration.ts @@ -56,12 +56,11 @@ export async function verifyAssetStates(request: Request, companyId: string, dat }; const stationValues = { - name: "Uso compartido prueba", + name: "Asignación prueba", active: true, siteId: null, personId: null, - assetIds: [] as string[], - sharedAssetIds: [original.id], + assetIds: [original.id], }; const station = async (name: string) => { @@ -74,8 +73,7 @@ export async function verifyAssetStates(request: Request, companyId: string, dat assert.equal(original.displayStatus, "available"); assert.equal(original.assigned, false); - const first = await station("Compartido A"); - const second = await station("Compartido B"); + const first = await station("Puesto de prueba"); const assigned = await current(); assert.equal(assigned.values.status, "operational"); @@ -154,8 +152,7 @@ export async function verifyAssetStates(request: Request, companyId: string, dat assert.equal((await current()).values.status, "broken"); await state("operational"); assert.equal((await request(`${path}/workstations/${first.id}/decommission`, {})).status, 200); - assert.equal((await current()).displayStatus, "in_use"); - assert.equal((await request(`${path}/workstations/${second.id}/delete`, {})).status, 200); + assert.equal((await request(`${path}/workstations/${first.id}/delete`, {})).status, 200); assert.equal((await current()).displayStatus, "available"); assert.equal((await current()).assigned, false); const retired = await state("retired"); diff --git a/apps/server/src/printers.integration.ts b/apps/server/src/printers.integration.ts index a9c6d14..99cba09 100644 --- a/apps/server/src/printers.integration.ts +++ b/apps/server/src/printers.integration.ts @@ -51,7 +51,6 @@ export async function verifyPrinters( personId: null, computerAssetId: host.id, assetIds: [host.id], - sharedAssetIds: [], }; const mainResponse = await request( `/assets/${companyId}/workstations${station ? `/${station.id}/update` : ""}`, diff --git a/apps/server/src/record-deletion.integration.ts b/apps/server/src/record-deletion.integration.ts index 654b0c0..a501720 100644 --- a/apps/server/src/record-deletion.integration.ts +++ b/apps/server/src/record-deletion.integration.ts @@ -3,9 +3,31 @@ import { randomUUID } from "node:crypto"; import type { Sequelize } from "sequelize"; import { QueryTypes } from "sequelize"; +import { z } from "zod/v4"; type Request = (path: string, body?: unknown) => Promise; +async function verifyProfileDeletionBlocked( + request: Request, + row: { companyId: string; profileId?: string }, + table: string +) { + if (!row.profileId) { + return; + } + const path = `/work-profiles/${row.companyId}/${row.profileId}`; + + assert.equal((await request(`${path}/deactivate`, {})).status, 200); + const blocked = await request(`${path}/delete`, {}); + + assert.equal(blocked.status, 409); + assert.equal( + z.object({ code: z.string() }).parse(await blocked.json()).code, + "PROFILE_IN_USE", + `La referencia de ${table} debe impedir eliminar el perfil` + ); +} + export async function verifyRecordDeletion(request: Request, database: Sequelize) { const counts = async () => { const result: number[] = []; @@ -106,10 +128,12 @@ export async function verifyRecordDeletion(request: Request, database: Sequelize ] as const; for (const [table, history, foreignKey, endpoint] of specs) { - const rows = await database.query<{ id: string; companyId: string; assetId?: string }>( - `SELECT * FROM ${table}`, - { type: QueryTypes.SELECT } - ); + const rows = await database.query<{ + id: string; + companyId: string; + assetId?: string; + profileId?: string; + }>(`SELECT * FROM ${table}`, { type: QueryTypes.SELECT }); assert.ok(rows.length > 0, `No existen datos para probar ${table}`); // Todos los estados, incluidos los cerrados, admiten corregir un alta equivocada. @@ -122,6 +146,8 @@ export async function verifyRecordDeletion(request: Request, database: Sequelize : row.id; const path = `/${endpoint}/${row.companyId}/${suffix}/delete`; + await verifyProfileDeletionBlocked(request, row, table); + assert.equal( (await request(`/${endpoint}/${randomUUID()}/${suffix}/delete`, {})).status, 404 diff --git a/apps/server/src/unlink-asset.integration-helper.ts b/apps/server/src/unlink-asset.integration-helper.ts index dbe24d8..6a278bc 100644 --- a/apps/server/src/unlink-asset.integration-helper.ts +++ b/apps/server/src/unlink-asset.integration-helper.ts @@ -14,9 +14,7 @@ export async function unlinkAssetFromWorkstations( ); for (const station of inventory.workstations.filter( - (item) => - item.values.active && - [...item.values.assetIds, ...item.values.sharedAssetIds].includes(assetId) + (item) => item.values.active && item.values.assetIds.includes(assetId) )) { const response = await request(`/assets/${companyId}/workstations/${station.id}/update`, { values: { @@ -24,7 +22,6 @@ export async function unlinkAssetFromWorkstations( computerAssetId: station.values.computerAssetId === assetId ? null : station.values.computerAssetId, assetIds: station.values.assetIds.filter((id) => id !== assetId), - sharedAssetIds: station.values.sharedAssetIds.filter((id) => id !== assetId), }, }); diff --git a/apps/server/src/work-profile-hosts.fixture.ts b/apps/server/src/work-profile-hosts.fixture.ts index d23aca4..4df4709 100644 --- a/apps/server/src/work-profile-hosts.fixture.ts +++ b/apps/server/src/work-profile-hosts.fixture.ts @@ -15,12 +15,7 @@ export async function prepareProfileHosts( ); for (const station of current.workstations) { - if ( - station.values.active && - assetIds.some((id) => - [...station.values.assetIds, ...station.values.sharedAssetIds].includes(id) - ) - ) { + if (station.values.active && assetIds.some((id) => station.values.assetIds.includes(id))) { const closed = await request(`/assets/${companyId}/workstations/${station.id}/update`, { values: { ...station.values, active: false }, }); @@ -39,7 +34,6 @@ export async function prepareProfileHosts( personId: null, computerAssetId: assetId, assetIds: [assetId], - sharedAssetIds: [], }, }); diff --git a/apps/server/src/work-profiles.integration.ts b/apps/server/src/work-profiles.integration.ts index e5de9ac..a9407ab 100644 --- a/apps/server/src/work-profiles.integration.ts +++ b/apps/server/src/work-profiles.integration.ts @@ -241,4 +241,60 @@ export async function verifyWorkProfiles( )?.needsReview, true ); + const lifecyclePath = `${path}/${another.id}`; + const activeDeletion = await request(`${lifecyclePath}/delete`, {}); + + assert.equal(activeDeletion.status, 409); + assert.equal( + z.object({ code: z.string() }).parse(await activeDeletion.json()).code, + "PROFILE_ACTIVE" + ); + for (const action of ["deactivate", "delete"]) { + assert.equal((await request(`${lifecyclePath}/${action}`, { actorId: person.id })).status, 400); + assert.equal((await request(`${lifecyclePath}/${action}?unknown=1`, {})).status, 400); + assert.equal( + (await request(`${lifecyclePath}/${action}`, {}, "https://other.example")).status, + 403 + ); + assert.equal( + (await request(`${lifecyclePath}/${action}`, {}, undefined, "a".repeat(64))).status, + 403 + ); + assert.equal( + (await request(`/work-profiles/${otherId}/${another.id}/${action}`, {})).status, + 404 + ); + } + const deactivate = await request(`${lifecyclePath}/deactivate`, {}); + + assert.equal(deactivate.status, 200); + const inactive = WorkProfileResponseSchema.parse(await deactivate.json()).profile; + + assert.equal(inactive.values.active, false); + assert.equal(inactive.history.length, another.history.length + 1); + assert.deepEqual( + WorkProfileResponseSchema.parse(await (await request(`${lifecyclePath}/deactivate`, {})).json()) + .profile, + inactive + ); + const deleted = await request(`${lifecyclePath}/delete`, {}); + + assert.equal(deleted.status, 200); + assert.deepEqual(await deleted.json(), { deleted: true }); + assert.equal((await request(`${lifecyclePath}/delete`, {})).status, 404); + assert.equal((await request(`${lifecyclePath}/deactivate`, {})).status, 404); + assert.equal( + WorkProfilesResponseSchema.parse(await (await request(path)).json()).profiles.some( + (item) => item.id === another.id + ), + false + ); + const revisions = await database.query( + "SELECT id FROM work_profile_revisions WHERE profileId=:id", + { + replacements: { id: another.id }, + } + ); + + assert.equal(revisions[0].length, 0); } diff --git a/apps/server/src/workstation-lifecycle.integration.ts b/apps/server/src/workstation-lifecycle.integration.ts index 97b964e..d2649fd 100644 --- a/apps/server/src/workstation-lifecycle.integration.ts +++ b/apps/server/src/workstation-lifecycle.integration.ts @@ -32,7 +32,7 @@ export async function verifyWorkstationLifecycle( assert.ok(source); const assets = []; - for (const name of ["Equipo de baja", "Impresora compartida de baja"]) { + for (const name of ["Equipo de baja", "Impresora de baja"]) { const response = await request(`/assets/${companyId}`, { values: { ...source.values, name, status: "operational" }, }); @@ -62,8 +62,7 @@ export async function verifyWorkstationLifecycle( siteId: source.values.siteId, personId: person.id, computerAssetId: computer.id, - assetIds: [computer.id], - sharedAssetIds: [shared.id], + assetIds: [computer.id, shared.id], }; const create = async (data = values) => { @@ -105,19 +104,17 @@ export async function verifyWorkstationLifecycle( assert.equal(retired.values.personId, null); assert.equal(retired.values.computerAssetId ?? null, null); assert.deepEqual(retired.values.assetIds, []); - assert.deepEqual(retired.values.sharedAssetIds, []); assert.equal(retired.history.length, 2); - assert.deepEqual(retired.history[0]?.values.assetIds, [computer.id]); + assert.deepEqual(retired.history[0]?.values.assetIds, [computer.id, shared.id]); assert.deepEqual( WorkstationResponseSchema.parse(await (await request(retirePath, {})).json()).workstation, retired ); - // Ambas clases de composición pueden reservarse de nuevo tras la baja. + // Los activos de la composición pueden reservarse de nuevo tras la baja. const replacement = await create({ ...values, name: "Puesto reutiliza activos", assetIds: [computer.id, shared.id], - sharedAssetIds: [], }); const protectedResponse = await request(`/tickets/${companyId}`, { values: { @@ -180,18 +177,17 @@ export async function verifyWorkstationLifecycle( assert.equal(Number(remaining[0]?.total), 0); } // La eliminación directa de un puesto activo libera su reserva sin borrar activos. - const direct = await create({ ...values, name: "Eliminar puesto activo", sharedAssetIds: [] }); + const direct = await create({ ...values, name: "Eliminar puesto activo" }); assert.equal((await request(`${path}/${direct.id}/delete`, {})).status, 200); const reusable = await create({ ...values, name: "Reserva después de eliminación", - sharedAssetIds: [], }); assert.equal((await request(`${path}/${reusable.id}/delete`, {})).status, 200); - const concurrent = await create({ ...values, name: "Puesto concurrente", sharedAssetIds: [] }); - const candidate = { ...values, name: "Nueva reserva concurrente", sharedAssetIds: [] }; + const concurrent = await create({ ...values, name: "Puesto concurrente" }); + const candidate = { ...values, name: "Nueva reserva concurrente" }; const [retirement, reservation] = await Promise.all([ request(`${path}/${concurrent.id}/decommission`, {}), request(path, { values: candidate }), diff --git a/apps/server/src/workstations.integration.ts b/apps/server/src/workstations.integration.ts index 308cd18..c75dfb9 100644 --- a/apps/server/src/workstations.integration.ts +++ b/apps/server/src/workstations.integration.ts @@ -5,6 +5,7 @@ import { CompanyResponseSchema } from "@metis/companies/common"; import type { Sequelize } from "sequelize"; import { z } from "zod/v4"; +import { removeSharedWorkstationAssets } from "../../../modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets"; import { initializeWorkstationMembers } from "../../../modules/assets/src/api/infrastructure/persistence/workstation-relational"; type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise; @@ -70,7 +71,6 @@ export async function verifyWorkstations( siteId: own.sites[0]?.id ?? null, personId: person.id, assetIds: [first.id, second.id], - sharedAssetIds: [], }; const save = async (values: unknown, id?: string) => { @@ -188,29 +188,11 @@ export async function verifyWorkstations( InventoryResponseSchema.parse(await (await request(`/assets/${companyId}`)).json()), beforeInitialization ); - await error( - { ...input, personId: null, assetIds: [], sharedAssetIds: [first.id] }, - "ASSET_IN_WORKSTATION" - ); - await error( - { ...input, personId: null, assetIds: [], sharedAssetIds: [foreign.id] }, - "ASSET_NOT_FOUND", - undefined, - 404 - ); - await error({ ...input, sharedAssetIds: [first.id] }, "INVALID_MEMBERS"); + await error({ ...input, sharedAssetIds: [] }, "INVALID_REQUEST", undefined, 400); await save({ ...restored.values, active: false }, restored.id); - const sharedInput = { ...input, personId: null, assetIds: [], sharedAssetIds: [first.id] }; - const sharedOne = await save({ ...sharedInput, name: "Compartido uno" }); - const sharedTwo = await save({ ...sharedInput, name: "Compartido dos" }); + const exclusive = await save({ ...input, personId: null, assetIds: [first.id] }); - await error({ ...sharedInput, assetIds: [first.id], sharedAssetIds: [] }, "ASSET_IN_WORKSTATION"); - await save({ ...sharedOne.values, sharedAssetIds: [] }, sharedOne.id); - await error({ ...sharedInput, assetIds: [first.id], sharedAssetIds: [] }, "ASSET_IN_WORKSTATION"); - await save({ ...sharedTwo.values, active: false }, sharedTwo.id); - const exclusive = await save({ ...sharedInput, assetIds: [first.id], sharedAssetIds: [] }); - - await error({ ...sharedTwo.values, active: true }, "ASSET_IN_WORKSTATION", sharedTwo.id); + await error({ ...exclusive.values }, "ASSET_IN_WORKSTATION"); await save({ ...exclusive.values, active: false }, exclusive.id); const designated = await save( { ...exclusive.values, active: false, computerAssetId: first.id }, @@ -230,17 +212,58 @@ export async function verifyWorkstations( "INVALID_MEMBERS", designated.id ); + // Simula las asociaciones compartidas del esquema anterior, también en su última revisión. + const latest = designated.history.at(-1); + + assert.ok(latest); + await database.transaction(async (transaction) => { + const replacements = { + id: designated.id, + revision: latest.id, + asset: second.id, + company: companyId, + }; + + await database.query( + "INSERT INTO workstation_members (workstationId,assetId,kind,position) VALUES (:id,:asset,'shared',1)", + { replacements, transaction } + ); + await database.query( + "INSERT INTO workstation_revision_members (revisionId,assetId,kind,position) VALUES (:revision,:asset,'shared',1)", + { replacements, transaction } + ); + await database.query( + "INSERT INTO workstation_assets (workstationId,companyId,assetId,activeKey) VALUES (:id,:company,:asset,NULL)", + { replacements, transaction } + ); + }); + await removeSharedWorkstationAssets(database); + await removeSharedWorkstationAssets(database); + const migrated = InventoryResponseSchema.parse( + await (await request(`/assets/${companyId}`)).json() + ); + + assert.deepEqual( + migrated.workstations.find((item) => item.id === designated.id), + designated + ); + assert.ok(migrated.assets.some((asset) => asset.id === second.id)); const race = await Promise.all([ request(path, { values: { - ...sharedInput, + ...input, + personId: null, name: "Reserva concurrente", assetIds: [second.id], - sharedAssetIds: [], }, }), request(path, { - values: { ...sharedInput, name: "Uso compartido concurrente", sharedAssetIds: [second.id] }, + values: { + ...input, + personId: null, + name: "Segunda reserva concurrente", + assetIds: [second.id], + }, }), ]); diff --git a/docs/campos-y-plantillas-de-activos.md b/docs/campos-y-plantillas-de-activos.md index 73e3d8e..d95f7b2 100644 --- a/docs/campos-y-plantillas-de-activos.md +++ b/docs/campos-y-plantillas-de-activos.md @@ -49,6 +49,11 @@ El [inventario de activos](inventario-de-activos.md) utiliza estas plantillas. E Las plantillas permiten organizar campos en secciones ordenadas, globales o propias de compañía. Consultar [Secciones de campos en plantillas](secciones-de-campos-en-plantillas.md) para las reglas, presentación y conservación de datos existentes. +La [propuesta de evolución de plantillas de activos](evolucion-de-plantillas-de-activos.md) +define cómo propagar cambios a los activos existentes de todas las compañías, +conservando valores e historial. Está pendiente de implementación; no modifica el +contrato actual de instantáneas. + ## Iconos de plantilla Cada plantilla global o propia de compañía admite un icono opcional de la biblioteca Lucide existente. El formulario incluye un selector visual con nombres traducidos, selección marcada y opción «Sin icono». Las copias conservan el icono; las plantillas personalizadas permiten cambiarlo y retirarlo. diff --git a/docs/componentes-de-interfaz.md b/docs/componentes-de-interfaz.md index 12c2adf..d0ec170 100644 --- a/docs/componentes-de-interfaz.md +++ b/docs/componentes-de-interfaz.md @@ -153,7 +153,7 @@ EntityTable recibe datos y columnas tipadas; utiliza TanStack Table para búsque La paginación es local sobre los conjuntos que las API actuales ya devuelven. No simula paginación de servidor. La selección del activo para el panel sigue perteneciendo a Inventario, fuera de la tabla compartida. -El inventario de activos compone las primitivas Tabs de shadcn/ui con EntityTable para filtrar por plantilla. La barra admite desplazamiento horizontal y navegación por teclado; incluye «Todo» y las plantillas del catálogo de la compañía. Un único panel conserva la tabla, su ordenación y sus columnas al cambiar de pestaña. `filterKey` reinicia la página; `filtersApplied` y `onClearFilters` integran el filtro con los estados vacíos y la limpieza común del listado. +Los inventarios de activos y recursos componen las primitivas Tabs de shadcn/ui con EntityTable para filtrar por plantilla. La barra admite desplazamiento horizontal y navegación por teclado; incluye «Todo» y las plantillas del catálogo de la compañía. Un único panel conserva la tabla, su ordenación y sus columnas al cambiar de pestaña. `filterKey` reinicia la página; `filtersApplied` y `onClearFilters` integran el filtro con los estados vacíos y la limpieza común del listado. EntityTable separa internamente la configuración de columnas/ordenación, el contenido semántico y la paginación; useEntityTable concentra el estado de representación. entityLabel permite contar «plantillas», «campos» o «tickets» aunque el título del espacio sea más amplio. Los tamaños disponibles son 10, 25, 50 y 100; una opción vacía nunca cambia el tamaño a cero. @@ -383,9 +383,9 @@ El panel lateral de activos muestra plantilla, estado y puestos activos inmediat ## Ficha de puesto de trabajo -La ruta `/companies/:companyId/workstations/:id` muestra una ficha completa al pulsar el nombre en el listado. La cabecera concentra nombre, estado, sede, persona asignada y edición modal. El contenido adapta sus dos columnas a una en pantallas pequeñas: activos exclusivos/compartidos, perfiles de usuario y personas relacionadas con avatar. Los enlaces abren fichas reales de activos, personas y perfiles. El historial sigue disponible desde las acciones del listado. +La ruta `/companies/:companyId/workstations/:id` muestra una ficha completa al pulsar el nombre en el listado. La cabecera concentra nombre, estado, sede, persona asignada y edición modal. El contenido adapta sus dos columnas a una en pantallas pequeñas: activos del puesto, perfiles de usuario y personas relacionadas con avatar. Los enlaces abren fichas reales de activos, personas y perfiles. El historial sigue disponible desde las acciones del listado. -Los perfiles se deducen por los activos del puesto, incluidos los compartidos; no se introduce una relación de propiedad entre perfil y puesto. Los inactivos se identifican explícitamente. Las personas se reúnen sin duplicados a partir de la asignación al puesto y sus perfiles. Los fallos parciales al cargar perfiles no impiden consultar los activos; se ofrece reintento. No se confunde un error o carga pendiente con ausencia de perfiles. +Los perfiles se deducen por el ordenador principal del puesto; no se introduce una relación de propiedad entre perfil y puesto. Los inactivos se identifican explícitamente. Las personas se reúnen sin duplicados a partir de la asignación al puesto y sus perfiles. Los fallos parciales al cargar perfiles no impiden consultar los activos; se ofrece reintento. No se confunde un error o carga pendiente con ausencia de perfiles. ## Gestión de usuarios @@ -422,3 +422,19 @@ entre controles. Las columnas responden al ancho del contenedor; nombre y estado comparten fila cuando hay espacio. Se conservan los márgenes del anillo de foco de `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. + +## 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. + +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 + +`AssetSelect`, contrato público de `@metis/assets/client`, sustituye los desplegables de activos de formularios. Su disparador muestra el valor y conserva la acción de alta rápida. El modo único recibe un UUID y devuelve un UUID; el múltiple recibe una colección, devuelve una colección confirmada y declara `maxSelection`. Los consumidores aportan opciones permitidas, bloqueos, textos y sedes. Las reglas de negocio siguen en cada formulario. + +`AssetPickerDialog` carga inventario y catálogo mediante sus queries existentes. Conserva una selección provisional hasta confirmar; Cancelar, Escape y cerrar no modifican el formulario padre. No invoca endpoints de escritura. El diálogo muestra estados de carga/error y reintento, contador de seleccionados, controles accesibles y confirmación deshabilitada si hay selecciones inválidas o se supera el límite. Las selecciones sobreviven a búsqueda, pestañas y paginación. No navega ni cambia filtros URL de la pantalla subyacente. + +`AssetList`, en assets/shared/ui, concentra las pestañas por UUID de plantilla (incluidas vacías/inactivas y especificaciones históricas), búsqueda rápida por nombre/plantilla/estado/sede y las columnas Nombre, Plantilla, Estado y Sede. Tanto inventario como selector lo componen con `EntityTable`. El inventario aporta enlaces, acciones y resaltado; el selector aporta checkboxes o radios de shadcn/ui en la columna fija de acciones, accesible también al desplazar horizontalmente la tabla. Se reutilizan ordenación, columnas configurables, rangos y paginación existentes. Este componente conoce activos y permanece en su módulo; `EntityTable`, `FormDialog`, `FormBody`, `FormActions` y `ButtonGroup` siguen siendo los patrones genéricos independientes del dominio. + +El selector se aplica a activos del puesto (adición múltiple), ordenador principal (único entre miembros), extremo de relaciones, impresora por perfil, origen/destino de conexiones remotas y activos afectados de tickets (adición múltiple). Cada caso conserva exclusiones, referencias fijas y altas rápidas. Añadir varios activos a un puesto es una sola actualización de composición; solo se propone automáticamente el ordenador cuando la composición resultante contiene un único activo. diff --git a/docs/diagrama-entidad-relacion-completo.mmd b/docs/diagrama-entidad-relacion-completo.mmd new file mode 100644 index 0000000..5dd9df9 --- /dev/null +++ b/docs/diagrama-entidad-relacion-completo.mmd @@ -0,0 +1,1620 @@ +erDiagram + direction LR + access_account_person_revision_values { + char ownerId PK,FK + char accountId FK + char personId FK + varchar startDate + varchar endDate "nullable" + varchar configuration "nullable" + } + access_account_person_values { + char ownerId PK,FK + char accountId FK + char personId FK + varchar startDate + varchar endDate "nullable" + varchar configuration "nullable" + } + access_account_revision_values { + char ownerId PK,FK + char systemId FK + char environmentId FK "nullable" + varchar login + varchar usage + tinyint active + varchar expiresOn "nullable" + text notes "nullable" + } + access_account_values { + char ownerId PK,FK + char systemId FK + char environmentId FK "nullable" + varchar login + varchar usage + tinyint active + varchar expiresOn "nullable" + text notes "nullable" + } + access_coverage_revision_values { + char ownerId PK,FK + char needId FK + char grantId FK + char accountPersonId FK + char membershipId FK "nullable" + varchar startDate + varchar endDate "nullable" + } + access_coverage_values { + char ownerId PK,FK + char needId FK + char grantId FK + char accountPersonId FK + char membershipId FK "nullable" + varchar startDate + varchar endDate "nullable" + } + access_current_values { + char ownerId + varchar kind + varchar name "nullable" + tinyint active "nullable" + char resourceTypeId "nullable" + mediumtext description "nullable" + char systemId "nullable" + char environmentId "nullable" + varchar login "nullable" + varchar usage "nullable" + varchar expiresOn "nullable" + mediumtext notes "nullable" + char accountId "nullable" + char personId "nullable" + varchar startDate "nullable" + varchar endDate "nullable" + varchar configuration "nullable" + varchar identifier "nullable" + char templateId "nullable" + char groupId "nullable" + varchar reference "nullable" + char resourceId "nullable" + char permissionId "nullable" + char needId "nullable" + char grantId "nullable" + char accountPersonId "nullable" + char membershipId "nullable" + } + access_environment_revision_values { + char ownerId PK,FK + varchar name + tinyint active + char systemId FK + } + access_environment_values { + char ownerId PK,FK + varchar name + tinyint active + char systemId FK + } + access_grant_revision_values { + char ownerId PK,FK + char resourceId FK + char permissionId FK + char accountId FK "nullable" + char groupId FK "nullable" + varchar startDate + varchar endDate "nullable" + varchar configuration + } + access_grant_values { + char ownerId PK,FK + char resourceId FK + char permissionId FK + char accountId FK "nullable" + char groupId FK "nullable" + varchar startDate + varchar endDate "nullable" + varchar configuration + } + access_group_revision_values { + char ownerId PK,FK + char systemId FK + char environmentId FK "nullable" + varchar name + tinyint active + varchar identifier + text description "nullable" + char templateId FK "nullable" + } + access_group_values { + char ownerId PK,FK + char systemId FK + char environmentId FK "nullable" + varchar name + tinyint active + varchar identifier + text description "nullable" + char templateId FK "nullable" + } + access_historical_values { + char ownerId + varchar kind + varchar name "nullable" + tinyint active "nullable" + char resourceTypeId "nullable" + mediumtext description "nullable" + char systemId "nullable" + char environmentId "nullable" + varchar login "nullable" + varchar usage "nullable" + varchar expiresOn "nullable" + mediumtext notes "nullable" + char accountId "nullable" + char personId "nullable" + varchar startDate "nullable" + varchar endDate "nullable" + varchar configuration "nullable" + varchar identifier "nullable" + char templateId "nullable" + char groupId "nullable" + varchar reference "nullable" + char resourceId "nullable" + char permissionId "nullable" + char needId "nullable" + char grantId "nullable" + char accountPersonId "nullable" + char membershipId "nullable" + } + access_items { + char id PK + char workspaceId FK + varchar kind + } + access_membership_revision_values { + char ownerId PK,FK + char accountId FK + char groupId FK + varchar startDate + varchar endDate "nullable" + varchar configuration + } + access_membership_values { + char ownerId PK,FK + char accountId FK + char groupId FK + varchar startDate + varchar endDate "nullable" + varchar configuration + } + access_need_permissions { + char ownerId PK,FK + int position PK + char permissionId FK + } + access_need_revision_permissions { + char ownerId PK,FK + int position PK + char permissionId FK + } + access_need_revision_values { + char ownerId PK,FK + char personId FK + char resourceId FK + varchar startDate + varchar endDate "nullable" + text notes "nullable" + } + access_need_values { + char ownerId PK,FK + char personId FK + char resourceId FK + varchar startDate + varchar endDate "nullable" + text notes "nullable" + } + access_permission_revision_values { + char ownerId PK,FK + varchar name + tinyint active + char resourceTypeId FK + text description "nullable" + } + access_permission_values { + char ownerId PK,FK + varchar name + tinyint active + char resourceTypeId FK + text description "nullable" + } + access_person_references { + char itemId PK,FK + char personId PK,FK + } + access_references { + int id PK + char itemId FK + char targetId FK + } + access_resource_revision_values { + char ownerId PK,FK + varchar name + tinyint active + char resourceTypeId FK + char systemId FK "nullable" + char environmentId FK "nullable" + varchar reference "nullable" + text notes "nullable" + } + access_resource_type_revision_values { + char ownerId PK,FK + varchar name + tinyint active + } + access_resource_type_values { + char ownerId PK,FK + varchar name + tinyint active + } + access_resource_values { + char ownerId PK,FK + varchar name + tinyint active + char resourceTypeId FK + char systemId FK "nullable" + char environmentId FK "nullable" + varchar reference "nullable" + text notes "nullable" + } + access_revisions { + char id PK + char itemId FK + int sequence + char actorId FK + varchar recordedAt + } + access_system_revision_values { + char ownerId PK,FK + varchar name + tinyint active + } + access_system_values { + char ownerId PK,FK + varchar name + tinyint active + } + access_template_revision_values { + char ownerId PK,FK + varchar name + tinyint active + text description "nullable" + char systemId FK "nullable" + } + access_template_values { + char ownerId PK,FK + varchar name + tinyint active + text description "nullable" + char systemId FK "nullable" + } + access_workspaces { + char id PK + char companyId FK "nullable" + varchar scopeKey + } + application_configuration_revisions { + char id PK + char configurationId FK + int position + char actorId FK + varchar recordedAt + char profileId FK "nullable" + char resourceId FK "nullable" + varchar delivery + varchar version "nullable" + varchar state + text notes "nullable" + } + application_configurations { + char id PK + char companyId FK + char profileId FK + char resourceId FK + varchar currentKey "nullable" + varchar delivery + varchar version "nullable" + varchar state + text notes "nullable" + } + asset_availability { + char id PK + char catalogId FK + char companyId FK + char definitionId FK + tinyint active "nullable" + } + asset_brands { + char id PK + varchar name + tinyint active + tinyint builtin + text logoSvg "nullable" + } + asset_catalogs { + char id PK + varchar catalogKey + } + asset_characteristics { + char assetId PK,FK + char fieldId PK,FK + varchar valueType + text textValue "nullable" + double numberValue "nullable" + tinyint booleanValue "nullable" + date dateValue "nullable" + } + asset_definitions { + char id PK + char catalogId FK + char companyId FK "nullable" + varchar stockKey "nullable" + tinyint active + varchar kind + varchar name + text description + varchar fieldType "nullable" + varchar unit "nullable" + varchar inverseName "nullable" + tinyint symmetric "nullable" + tinyint sectionsDefined + varchar icon "nullable" + } + asset_field_choices { + char fieldId PK,FK + int position PK + varchar value + } + asset_relationships { + char id PK + char companyId FK + char sourceId FK + char targetId FK + char typeId FK + char createdBy FK + varchar createdAt + char endedBy FK "nullable" + varchar endedAt "nullable" + varchar activeKey "nullable" + varchar name + varchar inverseName + tinyint symmetric + text notes + } + asset_revision_characteristics { + char revisionId PK,FK + char assetId FK + char fieldId PK,FK + varchar valueType + text textValue "nullable" + double numberValue "nullable" + tinyint booleanValue "nullable" + date dateValue "nullable" + } + asset_revisions { + char id PK + char assetId FK + char actorId FK + varchar recordedAt + int position + char siteId FK "nullable" + char brandId FK "nullable" + varchar name + varchar status + text notes + char templateId FK + tinyint brandDefined + } + asset_specification_field_choices { + char assetId PK,FK + char fieldId PK,FK + int position PK + varchar value + } + asset_specification_fields { + char assetId PK,FK + char fieldId PK,FK + varchar name + varchar fieldType + varchar unit + tinyint required + char sectionId FK "nullable" + int position + } + asset_specification_sections { + char assetId PK,FK + char id PK + varchar name + text description + int position + } + asset_support { + char id PK + char companyId FK + char assetId FK + char preferredKey "nullable" + varchar destination + tinyint requiresVpn + tinyint preferred + varchar state + text notes + char resourceId FK + } + asset_support_revisions { + char id PK + char connectionId FK + char actorId FK + varchar recordedAt + int position + varchar destination + tinyint requiresVpn + tinyint preferred + varchar state + text notes + char resourceId FK + } + asset_template_fields { + char id PK + char templateId FK + char fieldId FK + tinyint required + int position + char sectionId FK "nullable" + } + asset_template_sections { + char id PK + char templateId FK + varchar name + text description + int position + } + assets { + char id PK + char companyId FK + char templateId FK + char siteId FK "nullable" + char brandId FK "nullable" + varchar name + varchar status + text notes + varchar templateName + tinyint sectionsDefined + tinyint brandDefined + } + brand_catalog_lock { + int id PK + tinyint legacyImported + tinyint logosImported + } + certificate_configuration_revisions { + char id PK + char configurationId FK + int position + char actorId FK + varchar recordedAt + char profileId FK "nullable" + char resourceId FK "nullable" + varchar location "nullable" + varchar state + text notes "nullable" + } + certificate_configurations { + char id PK + char companyId FK + char profileId FK + char resourceId FK + varchar currentKey "nullable" + varchar location "nullable" + varchar state + text notes "nullable" + } + companies { + char id PK + varchar name + tinyint active + varchar logoVersion "nullable" + } + company_contacts { + char id PK + char companyId FK + varchar name + varchar position "nullable" + varchar email "nullable" + varchar phone "nullable" + tinyint active + tinyint isContact + varchar photoVersion "nullable" + } + company_departments { + char id PK + char companyId FK + varchar name + varchar nameKey + tinyint active + } + company_logos { + char companyId PK,FK + mediumblob data + varchar version + } + company_sites { + char id PK + char companyId FK + varchar name + text address "nullable" + varchar phone "nullable" + } + department_assignment_revisions { + char id PK + char assignmentId FK + int sequence + varchar position "nullable" + date startDate + date endDate "nullable" + tinyint isPrimary + varchar recordedAt + } + folder_mapping_revisions { + char id PK + char mappingId FK + int position + char actorId FK + varchar recordedAt + char profileId FK "nullable" + char needId FK "nullable" + varchar path + varchar driveLetter "nullable" + varchar state + text notes "nullable" + } + folder_mappings { + char id PK + char companyId FK + char profileId FK + char needId FK + varchar currentKey "nullable" + varchar driveKey "nullable" + varchar path + varchar driveLetter "nullable" + varchar state + text notes "nullable" + } + http_sessions { + varchar sid PK + datetime expires "nullable" + datetime createdAt + datetime updatedAt + char userId FK "nullable" + int authVersion "nullable" + varchar csrfToken "nullable" + varchar cookieExpires "nullable" + bigint originalMaxAge "nullable" + varchar cookiePath "nullable" + varchar cookieDomain "nullable" + varchar cookieSecure "nullable" + tinyint cookieHttpOnly "nullable" + varchar cookieSameSite "nullable" + tinyint cookiePartitioned "nullable" + varchar cookiePriority "nullable" + } + identity_state { + int id PK + char initialAdminId FK "nullable" + } + intervention_counter { + int id PK + int nextNumber + } + intervention_procedure_steps { + char ownerId PK,FK + char stepId PK + int position + varchar title + text instructions + tinyint required + } + intervention_procedures { + char interventionId PK,FK + char templateId FK + char revisionId FK + varchar name + text description + tinyint active + } + intervention_revision_steps { + char ownerId PK,FK + char stepId PK + int position + varchar title + text instructions + tinyint required + varchar state + text notes + } + intervention_revision_targets { + char ownerId PK,FK + int position PK + char personId FK "nullable" + char workstationId FK "nullable" + char assetId FK "nullable" + char resourceId FK "nullable" + } + intervention_revisions { + char id PK + char interventionId FK + char actorId FK + char ticketId FK "nullable" + char assigneeId FK "nullable" + varchar recordedAt + int position + varchar subject + varchar state + varchar modality + date plannedDate "nullable" + varchar startedAt "nullable" + varchar endedAt "nullable" + int minutes "nullable" + text diagnosis + text workPerformed + text resolution + } + intervention_steps { + char ownerId PK,FK + char stepId PK + int position + varchar title + text instructions + tinyint required + varchar state + text notes + } + intervention_targets { + char ownerId PK,FK + int position PK + char personId FK "nullable" + char workstationId FK "nullable" + char assetId FK "nullable" + char resourceId FK "nullable" + } + intervention_template_lock { + int id PK + } + intervention_template_revision_steps { + char ownerId PK,FK + char stepId PK + int position + varchar title + text instructions + tinyint required + } + intervention_template_revisions { + char id PK + char templateId FK + char actorId FK "nullable" + varchar recordedAt + int position + varchar name + text description + tinyint active + } + intervention_template_steps { + char ownerId PK,FK + char stepId PK + int position + varchar title + text instructions + tinyint required + } + intervention_templates { + char id PK + char companyId FK "nullable" + char requestKey + varchar name + text description + tinyint active + } + interventions { + char id PK + char companyId FK + varchar reference + char requestKey + char ticketId FK "nullable" + char assigneeId FK "nullable" + varchar subject + varchar state + varchar modality + date plannedDate "nullable" + varchar startedAt "nullable" + varchar endedAt "nullable" + int minutes "nullable" + text diagnosis + text workPerformed + text resolution + } + mail_configuration_revisions { + char id PK + char configurationId FK + int position + char actorId FK + varchar recordedAt + varchar snapshotKind + char personId FK "nullable" + char accountId FK "nullable" + char assetId FK "nullable" + char profileId FK "nullable" + char mailboxId FK "nullable" + varchar address + varchar method + varchar state + text notes "nullable" + } + mail_configurations { + char id PK + char companyId FK + char personId FK "nullable" + char accountId FK "nullable" + char assetId FK "nullable" + varchar currentKey "nullable" + char profileId FK "nullable" + char mailboxId FK "nullable" + varchar snapshotKind + varchar address + varchar method + varchar state + text notes "nullable" + } + mailbox_revisions { + char id PK + char itemId FK + int position + char actorId FK + varchar recordedAt + varchar address + varchar kind + varchar context "nullable" + char accountId FK "nullable" + tinyint active + text notes "nullable" + } + mailbox_use_revisions { + char id PK + char itemId FK + int position + char actorId FK + varchar recordedAt + char mailboxId FK "nullable" + char personId FK "nullable" + varchar startDate + varchar endDate "nullable" + } + mailbox_uses { + char id PK + char companyId FK + char mailboxId FK + char personId FK + varchar startDate + varchar endDate "nullable" + } + mailboxes { + char id PK + char companyId FK + char accountId FK "nullable" + varchar addressKey + varchar address + varchar kind + varchar context "nullable" + tinyint active + text notes "nullable" + } + person_department_assignments { + char id PK + char companyId FK + char personId FK + char departmentId FK + varchar position "nullable" + date startDate + date endDate "nullable" + tinyint isPrimary + } + person_photos { + char personId PK,FK + char companyId FK + mediumblob data + varchar version + datetime createdAt + datetime updatedAt + } + person_process_revision_tasks { + char ownerId PK,FK + int position + varchar title + char taskId PK + varchar status + text notes "nullable" + varchar referenceKind "nullable" + char referenceId "nullable" + varchar referenceLabel "nullable" + varchar sourceKind "nullable" + } + person_process_revisions { + char id PK + int position + char actorId FK "nullable" + varchar recordedAt + char itemId FK + char personId FK "nullable" + char templateId FK "nullable" + varchar templateName + varchar kind + varchar name + varchar state + text notes "nullable" + } + person_process_tasks { + char ownerId PK,FK + int position + varchar title + char taskId PK + varchar status + text notes "nullable" + varchar referenceKind "nullable" + char referenceId "nullable" + varchar referenceLabel "nullable" + varchar sourceKind "nullable" + } + person_processes { + char id PK + char companyId FK + char personId FK + char templateId FK + varchar currentKey "nullable" + varchar templateName + varchar kind + varchar name + varchar state + text notes "nullable" + } + printer_configuration_revisions { + char id PK + char configurationId FK + int position + char actorId FK + varchar recordedAt + char profileId FK "nullable" + char printerId FK "nullable" + varchar connection + tinyint isDefault + varchar state + text notes "nullable" + } + printer_configurations { + char id PK + char companyId FK + char profileId FK + char printerId FK + varchar currentKey "nullable" + varchar defaultKey "nullable" + varchar connection + tinyint isDefault + varchar state + text notes "nullable" + } + process_catalog_lock { + int id PK + } + process_template_instructions { + char ownerId PK,FK + int position PK + varchar title + } + process_template_revision_instructions { + char ownerId PK,FK + int position PK + varchar title + } + process_template_revisions { + char id PK + int position + char actorId FK "nullable" + varchar recordedAt + char itemId FK + varchar name + varchar kind + tinyint active + text notes "nullable" + } + process_templates { + char id PK + varchar nameKey + varchar builtinKey "nullable" + varchar name + varchar kind + tinyint active + text notes "nullable" + } + remote_connection_revisions { + char id PK + char connectionId FK + int position + char actorId FK + varchar recordedAt + char personId FK "nullable" + varchar sourceKind + char sourceAssetId FK "nullable" + varchar targetKind + char targetAssetId FK "nullable" + char targetResourceId FK "nullable" + char siteId FK "nullable" + varchar method + tinyint requiresVpn + varchar state + text notes "nullable" + } + remote_connections { + char id PK + char companyId FK + char personId FK + char sourceAssetId FK "nullable" + char targetAssetId FK "nullable" + char targetResourceId FK "nullable" + char siteId FK "nullable" + varchar currentKey "nullable" + varchar sourceKind + varchar targetKind + varchar method + tinyint requiresVpn + varchar state + text notes "nullable" + } + resource_availability { + char id PK + char catalogId FK + char companyId FK + char definitionId FK + tinyint active "nullable" + } + resource_catalogs { + char id PK + varchar catalogKey + } + resource_characteristics { + char resourceId PK,FK + char fieldId PK,FK + varchar valueType + text textValue "nullable" + double numberValue "nullable" + tinyint booleanValue "nullable" + date dateValue "nullable" + } + resource_definitions { + char id PK + char catalogId FK + char companyId FK "nullable" + varchar stockKey "nullable" + tinyint active + varchar kind + varchar name + text description + varchar fieldType "nullable" + varchar unit "nullable" + varchar usage "nullable" + tinyint sectionsDefined + } + resource_field_choices { + char fieldId PK,FK + int position PK + varchar value + } + resource_revision_characteristics { + char revisionId PK,FK + char resourceId FK + char fieldId PK,FK + varchar valueType + text textValue "nullable" + double numberValue "nullable" + tinyint booleanValue "nullable" + date dateValue "nullable" + } + resource_revisions { + char id PK + char resourceId FK + char actorId FK + varchar recordedAt + int position + varchar name + tinyint active + text notes + varchar integrationKind "nullable" + char addressFieldId FK "nullable" + char contextFieldId FK "nullable" + varchar mailboxKind "nullable" + char accountId FK "nullable" + char referenceFieldId FK "nullable" + char resourceTypeId FK "nullable" + char systemId FK "nullable" + char environmentId FK "nullable" + char templateId FK "nullable" + } + resource_specification_field_choices { + char resourceId PK,FK + char fieldId PK,FK + int position PK + varchar value + } + resource_specification_fields { + char resourceId PK,FK + char fieldId PK,FK + varchar name + varchar fieldType + varchar unit + tinyint required + char sectionId "nullable" + int position + } + resource_specification_sections { + char resourceId PK,FK + char id PK + varchar name + text description + int position + } + resource_template_fields { + char id PK + char templateId FK + char fieldId FK + tinyint required + int position + char sectionId FK "nullable" + } + resource_template_sections { + char id PK + char templateId PK,FK + varchar name + text description + int position + } + resources { + char id PK + char companyId FK + char templateId FK + varchar name + tinyint active + text notes + varchar integrationKind "nullable" + char addressFieldId FK "nullable" + char contextFieldId FK "nullable" + varchar mailboxKind "nullable" + char accountId FK "nullable" + char referenceFieldId FK "nullable" + char resourceTypeId FK "nullable" + char systemId FK "nullable" + char environmentId FK "nullable" + varchar templateName + tinyint sectionsDefined + } + software_installation_revisions { + char id PK + char configurationId FK + int position + char assetId FK + char resourceId FK + varchar version "nullable" + varchar state + text notes "nullable" + char actorId FK + varchar recordedAt + char licenseResourceId FK "nullable" + } + software_installations { + char id PK + char companyId FK + char assetId FK + char resourceId FK + varchar version "nullable" + varchar state + text notes "nullable" + varchar currentKey "nullable" + char licenseResourceId FK "nullable" + } + telework_authorization_revisions { + char id PK + char authorizationId FK + int position + char actorId FK + varchar recordedAt + char personId FK "nullable" + tinyint active + varchar startsOn "nullable" + varchar expiresOn "nullable" + text notes "nullable" + } + telework_authorizations { + char id PK + char companyId FK + char personId FK + tinyint active + varchar startsOn "nullable" + varchar expiresOn "nullable" + text notes "nullable" + } + ticket_counter { + int id PK + int nextNumber + } + ticket_revision_targets { + char ownerId PK,FK + int position PK + char personId FK "nullable" + char workstationId FK "nullable" + char assetId FK "nullable" + char resourceId FK "nullable" + } + ticket_revisions { + char id PK + char ticketId FK + char actorId FK + char requesterId FK "nullable" + char assigneeId FK "nullable" + varchar recordedAt + int position + varchar subject + text description + varchar channel + varchar state + varchar priority + } + ticket_targets { + char ownerId PK,FK + int position PK + char personId FK "nullable" + char workstationId FK "nullable" + char assetId FK "nullable" + char resourceId FK "nullable" + } + tickets { + char id PK + char companyId FK + varchar reference + char requestKey + char requesterId FK "nullable" + char assigneeId FK "nullable" + varchar subject + text description + varchar channel + varchar state + varchar priority + } + users { + char id PK + varchar name + varchar email + varchar passwordHash + tinyint active + tinyint isInitialAdmin + int authVersion + } + work_profile_revisions { + char id PK + char profileId FK + char accountId FK "nullable" + int position + char actorId FK + varchar recordedAt + varchar name + char personId FK "nullable" + char assetId FK "nullable" + tinyint active + text notes "nullable" + } + work_profiles { + char id PK + char companyId FK + char personId FK + char accountId FK "nullable" + char assetId FK + varchar currentKey "nullable" + varchar name + tinyint active + text notes "nullable" + } + workstation_assets { + char workstationId PK,FK + char companyId FK + char assetId PK,FK + varchar activeKey "nullable" + } + workstation_members { + char workstationId PK,FK + char assetId PK,FK + varchar kind + int position + } + workstation_revision_members { + char revisionId PK,FK + char assetId PK,FK + varchar kind + int position + } + workstation_revisions { + char id PK + char workstationId FK + char actorId FK + char siteId FK "nullable" + char personId FK "nullable" + varchar recordedAt + int position + varchar name + tinyint active + char computerAssetId FK "nullable" + } + workstations { + char id PK + char companyId FK + char siteId FK "nullable" + char personId FK "nullable" + varchar name + tinyint active + char computerAssetId FK "nullable" + } + access_items ||..o{ access_account_person_revision_values : "accountId" + access_revisions ||..o| access_account_person_revision_values : "ownerId" + company_contacts ||..o{ access_account_person_revision_values : "personId" + access_items ||..o{ access_account_person_values : "accountId" + access_items ||..o| access_account_person_values : "ownerId" + company_contacts ||..o{ access_account_person_values : "personId" + access_items |o..o{ access_account_revision_values : "environmentId" + access_items ||..o{ access_account_revision_values : "systemId" + access_revisions ||..o| access_account_revision_values : "ownerId" + access_items |o..o{ access_account_values : "environmentId" + access_items ||..o| access_account_values : "ownerId" + access_items ||..o{ access_account_values : "systemId" + access_items ||..o{ access_coverage_revision_values : "accountPersonId" + access_items ||..o{ access_coverage_revision_values : "grantId" + access_items |o..o{ access_coverage_revision_values : "membershipId" + access_items ||..o{ access_coverage_revision_values : "needId" + access_revisions ||..o| access_coverage_revision_values : "ownerId" + access_items ||..o{ access_coverage_values : "accountPersonId" + access_items ||..o{ access_coverage_values : "grantId" + access_items |o..o{ access_coverage_values : "membershipId" + access_items ||..o{ access_coverage_values : "needId" + access_items ||..o| access_coverage_values : "ownerId" + access_items ||..o{ access_environment_revision_values : "systemId" + access_revisions ||..o| access_environment_revision_values : "ownerId" + access_items ||..o| access_environment_values : "ownerId" + access_items ||..o{ access_environment_values : "systemId" + access_items |o..o{ access_grant_revision_values : "accountId" + access_items |o..o{ access_grant_revision_values : "groupId" + access_items ||..o{ access_grant_revision_values : "permissionId" + access_items ||..o{ access_grant_revision_values : "resourceId" + access_revisions ||..o| access_grant_revision_values : "ownerId" + access_items |o..o{ access_grant_values : "accountId" + access_items |o..o{ access_grant_values : "groupId" + access_items ||..o| access_grant_values : "ownerId" + access_items ||..o{ access_grant_values : "permissionId" + access_items ||..o{ access_grant_values : "resourceId" + access_items |o..o{ access_group_revision_values : "environmentId" + access_items ||..o{ access_group_revision_values : "systemId" + access_items |o..o{ access_group_revision_values : "templateId" + access_revisions ||..o| access_group_revision_values : "ownerId" + access_items |o..o{ access_group_values : "environmentId" + access_items ||..o| access_group_values : "ownerId" + access_items ||..o{ access_group_values : "systemId" + access_items |o..o{ access_group_values : "templateId" + access_workspaces ||..o{ access_items : "workspaceId" + access_items ||..o{ access_membership_revision_values : "accountId" + access_items ||..o{ access_membership_revision_values : "groupId" + access_revisions ||..o| access_membership_revision_values : "ownerId" + access_items ||..o{ access_membership_values : "accountId" + access_items ||..o{ access_membership_values : "groupId" + access_items ||..o| access_membership_values : "ownerId" + access_items ||..o{ access_need_permissions : "permissionId" + access_need_values ||..o{ access_need_permissions : "ownerId" + access_items ||..o{ access_need_revision_permissions : "permissionId" + access_need_revision_values ||..o{ access_need_revision_permissions : "ownerId" + access_items ||..o{ access_need_revision_values : "resourceId" + access_revisions ||..o| access_need_revision_values : "ownerId" + company_contacts ||..o{ access_need_revision_values : "personId" + access_items ||..o| access_need_values : "ownerId" + access_items ||..o{ access_need_values : "resourceId" + company_contacts ||..o{ access_need_values : "personId" + access_items ||..o{ access_permission_revision_values : "resourceTypeId" + access_revisions ||..o| access_permission_revision_values : "ownerId" + access_items ||..o| access_permission_values : "ownerId" + access_items ||..o{ access_permission_values : "resourceTypeId" + access_items ||..o{ access_person_references : "itemId" + company_contacts ||..o{ access_person_references : "personId" + access_items ||..o{ access_references : "itemId" + access_items ||..o{ access_references : "targetId" + access_items |o..o{ access_resource_revision_values : "environmentId" + access_items ||..o{ access_resource_revision_values : "resourceTypeId" + access_items |o..o{ access_resource_revision_values : "systemId" + access_revisions ||..o| access_resource_revision_values : "ownerId" + access_revisions ||..o| access_resource_type_revision_values : "ownerId" + access_items ||..o| access_resource_type_values : "ownerId" + access_items |o..o{ access_resource_values : "environmentId" + access_items ||..o| access_resource_values : "ownerId" + access_items ||..o{ access_resource_values : "resourceTypeId" + access_items |o..o{ access_resource_values : "systemId" + access_items ||..o{ access_revisions : "itemId" + users ||..o{ access_revisions : "actorId" + access_revisions ||..o| access_system_revision_values : "ownerId" + access_items ||..o| access_system_values : "ownerId" + access_items |o..o{ access_template_revision_values : "systemId" + access_revisions ||..o| access_template_revision_values : "ownerId" + access_items ||..o| access_template_values : "ownerId" + access_items |o..o{ access_template_values : "systemId" + companies |o..o{ access_workspaces : "companyId" + application_configurations ||..o{ application_configuration_revisions : "configurationId" + resources |o..o{ application_configuration_revisions : "resourceId" + users ||..o{ application_configuration_revisions : "actorId" + work_profiles |o..o{ application_configuration_revisions : "profileId" + companies ||..o{ application_configurations : "companyId" + resources ||..o{ application_configurations : "resourceId" + work_profiles ||..o{ application_configurations : "profileId" + asset_catalogs ||..o{ asset_availability : "catalogId" + asset_definitions ||..o{ asset_availability : "definitionId" + companies ||..o{ asset_availability : "companyId" + asset_definitions ||..o{ asset_characteristics : "fieldId" + asset_specification_fields ||..o| asset_characteristics : "assetId, fieldId" + assets ||..o{ asset_characteristics : "assetId" + asset_catalogs ||..o{ asset_definitions : "catalogId" + companies |o..o{ asset_definitions : "companyId" + asset_definitions ||..o{ asset_field_choices : "fieldId" + asset_definitions ||..o{ asset_relationships : "typeId" + assets ||..o{ asset_relationships : "companyId, sourceId" + assets ||..o{ asset_relationships : "companyId, targetId" + assets ||..o{ asset_relationships : "sourceId" + assets ||..o{ asset_relationships : "targetId" + companies ||..o{ asset_relationships : "companyId" + users ||..o{ asset_relationships : "createdBy" + users |o..o{ asset_relationships : "endedBy" + asset_definitions ||..o{ asset_revision_characteristics : "fieldId" + asset_revisions ||..o{ asset_revision_characteristics : "revisionId" + asset_revisions ||..o{ asset_revision_characteristics : "revisionId, assetId" + asset_specification_fields ||..o{ asset_revision_characteristics : "assetId, fieldId" + assets ||..o{ asset_revision_characteristics : "assetId" + asset_brands |o..o{ asset_revisions : "brandId" + asset_definitions ||..o{ asset_revisions : "templateId" + assets ||..o{ asset_revisions : "assetId" + company_sites |o..o{ asset_revisions : "siteId" + users ||..o{ asset_revisions : "actorId" + asset_definitions ||..o{ asset_specification_field_choices : "fieldId" + asset_specification_fields ||..o{ asset_specification_field_choices : "assetId, fieldId" + assets ||..o{ asset_specification_field_choices : "assetId" + asset_definitions ||..o{ asset_specification_fields : "fieldId" + asset_specification_sections |o..o{ asset_specification_fields : "assetId, sectionId" + assets ||..o{ asset_specification_fields : "assetId" + assets ||..o{ asset_specification_sections : "assetId" + assets ||..o{ asset_support : "assetId" + assets ||..o{ asset_support : "companyId, assetId" + companies ||..o{ asset_support : "companyId" + resources ||..o{ asset_support : "companyId, resourceId" + resources ||..o{ asset_support : "resourceId" + asset_support ||..o{ asset_support_revisions : "connectionId" + resources ||..o{ asset_support_revisions : "resourceId" + users ||..o{ asset_support_revisions : "actorId" + asset_definitions ||..o{ asset_template_fields : "fieldId" + asset_definitions ||..o{ asset_template_fields : "templateId" + asset_template_sections |o..o{ asset_template_fields : "sectionId" + asset_template_sections |o..o{ asset_template_fields : "templateId, sectionId" + asset_definitions ||..o{ asset_template_sections : "templateId" + asset_brands |o..o{ assets : "brandId" + asset_definitions ||..o{ assets : "templateId" + companies ||..o{ assets : "companyId" + company_sites |o..o{ assets : "siteId" + certificate_configurations ||..o{ certificate_configuration_revisions : "configurationId" + resources |o..o{ certificate_configuration_revisions : "resourceId" + users ||..o{ certificate_configuration_revisions : "actorId" + work_profiles |o..o{ certificate_configuration_revisions : "profileId" + companies ||..o{ certificate_configurations : "companyId" + resources ||..o{ certificate_configurations : "resourceId" + work_profiles ||..o{ certificate_configurations : "profileId" + companies ||..o{ company_contacts : "companyId" + companies ||..o{ company_departments : "companyId" + companies ||..o| company_logos : "companyId" + companies ||..o{ company_sites : "companyId" + person_department_assignments ||..o{ department_assignment_revisions : "assignmentId" + access_items |o..o{ folder_mapping_revisions : "needId" + folder_mappings ||..o{ folder_mapping_revisions : "mappingId" + users ||..o{ folder_mapping_revisions : "actorId" + work_profiles |o..o{ folder_mapping_revisions : "profileId" + access_items ||..o{ folder_mappings : "needId" + companies ||..o{ folder_mappings : "companyId" + work_profiles ||..o{ folder_mappings : "profileId" + users |o..o{ http_sessions : "userId" + users |o..o{ identity_state : "initialAdminId" + intervention_procedures ||..o{ intervention_procedure_steps : "ownerId" + intervention_template_revisions ||..o{ intervention_procedures : "revisionId" + intervention_template_revisions ||..o{ intervention_procedures : "templateId, revisionId" + intervention_templates ||..o{ intervention_procedures : "templateId" + interventions ||..o| intervention_procedures : "interventionId" + intervention_revisions ||..o{ intervention_revision_steps : "ownerId" + assets |o..o{ intervention_revision_targets : "assetId" + company_contacts |o..o{ intervention_revision_targets : "personId" + intervention_revisions ||..o{ intervention_revision_targets : "ownerId" + resources |o..o{ intervention_revision_targets : "resourceId" + workstations |o..o{ intervention_revision_targets : "workstationId" + interventions ||..o{ intervention_revisions : "interventionId" + tickets |o..o{ intervention_revisions : "ticketId" + users ||..o{ intervention_revisions : "actorId" + users |o..o{ intervention_revisions : "assigneeId" + interventions ||..o{ intervention_steps : "ownerId" + assets |o..o{ intervention_targets : "assetId" + company_contacts |o..o{ intervention_targets : "personId" + interventions ||..o{ intervention_targets : "ownerId" + resources |o..o{ intervention_targets : "resourceId" + workstations |o..o{ intervention_targets : "workstationId" + intervention_template_revisions ||..o{ intervention_template_revision_steps : "ownerId" + intervention_templates ||..o{ intervention_template_revisions : "templateId" + users |o..o{ intervention_template_revisions : "actorId" + intervention_templates ||..o{ intervention_template_steps : "ownerId" + companies |o..o{ intervention_templates : "companyId" + companies ||..o{ interventions : "companyId" + tickets |o..o{ interventions : "ticketId" + users |o..o{ interventions : "assigneeId" + access_items |o..o{ mail_configuration_revisions : "accountId" + assets |o..o{ mail_configuration_revisions : "assetId" + company_contacts |o..o{ mail_configuration_revisions : "personId" + mail_configurations ||..o{ mail_configuration_revisions : "configurationId" + mailboxes |o..o{ mail_configuration_revisions : "mailboxId" + users ||..o{ mail_configuration_revisions : "actorId" + work_profiles |o..o{ mail_configuration_revisions : "profileId" + access_items |o..o{ mail_configurations : "accountId" + assets |o..o{ mail_configurations : "assetId" + companies ||..o{ mail_configurations : "companyId" + company_contacts |o..o{ mail_configurations : "personId" + mailboxes |o..o{ mail_configurations : "mailboxId" + work_profiles |o..o{ mail_configurations : "profileId" + access_items |o..o{ mailbox_revisions : "accountId" + mailboxes ||..o{ mailbox_revisions : "itemId" + users ||..o{ mailbox_revisions : "actorId" + company_contacts |o..o{ mailbox_use_revisions : "personId" + mailbox_uses ||..o{ mailbox_use_revisions : "itemId" + mailboxes |o..o{ mailbox_use_revisions : "mailboxId" + users ||..o{ mailbox_use_revisions : "actorId" + companies ||..o{ mailbox_uses : "companyId" + company_contacts ||..o{ mailbox_uses : "personId" + mailboxes ||..o{ mailbox_uses : "mailboxId" + access_items |o..o{ mailboxes : "accountId" + companies ||..o{ mailboxes : "companyId" + companies ||..o{ person_department_assignments : "companyId" + company_contacts ||..o{ person_department_assignments : "personId, companyId" + company_departments ||..o{ person_department_assignments : "departmentId, companyId" + company_contacts ||..o| person_photos : "personId, companyId" + person_process_revisions ||..o{ person_process_revision_tasks : "ownerId" + company_contacts |o..o{ person_process_revisions : "personId" + person_processes ||..o{ person_process_revisions : "itemId" + process_templates |o..o{ person_process_revisions : "templateId" + users |o..o{ person_process_revisions : "actorId" + person_processes ||..o{ person_process_tasks : "ownerId" + companies ||..o{ person_processes : "companyId" + company_contacts ||..o{ person_processes : "personId" + process_templates ||..o{ person_processes : "templateId" + assets |o..o{ printer_configuration_revisions : "printerId" + printer_configurations ||..o{ printer_configuration_revisions : "configurationId" + users ||..o{ printer_configuration_revisions : "actorId" + work_profiles |o..o{ printer_configuration_revisions : "profileId" + assets ||..o{ printer_configurations : "printerId" + companies ||..o{ printer_configurations : "companyId" + work_profiles ||..o{ printer_configurations : "profileId" + process_templates ||..o{ process_template_instructions : "ownerId" + process_template_revisions ||..o{ process_template_revision_instructions : "ownerId" + process_templates ||..o{ process_template_revisions : "itemId" + users |o..o{ process_template_revisions : "actorId" + assets |o..o{ remote_connection_revisions : "sourceAssetId" + assets |o..o{ remote_connection_revisions : "targetAssetId" + company_contacts |o..o{ remote_connection_revisions : "personId" + company_sites |o..o{ remote_connection_revisions : "siteId" + remote_connections ||..o{ remote_connection_revisions : "connectionId" + resources |o..o{ remote_connection_revisions : "targetResourceId" + users ||..o{ remote_connection_revisions : "actorId" + assets |o..o{ remote_connections : "sourceAssetId" + assets |o..o{ remote_connections : "targetAssetId" + companies ||..o{ remote_connections : "companyId" + company_contacts ||..o{ remote_connections : "personId" + company_sites |o..o{ remote_connections : "siteId" + resources |o..o{ remote_connections : "targetResourceId" + companies ||..o{ resource_availability : "companyId" + resource_catalogs ||..o{ resource_availability : "catalogId" + resource_definitions ||..o{ resource_availability : "definitionId" + resource_definitions ||..o{ resource_characteristics : "fieldId" + resources ||..o{ resource_characteristics : "resourceId" + companies |o..o{ resource_definitions : "companyId" + resource_catalogs ||..o{ resource_definitions : "catalogId" + resource_definitions ||..o{ resource_field_choices : "fieldId" + resource_definitions ||..o{ resource_revision_characteristics : "fieldId" + resource_revisions ||..o{ resource_revision_characteristics : "revisionId" + resources ||..o{ resource_revision_characteristics : "resourceId" + access_items |o..o{ resource_revisions : "accountId" + access_items |o..o{ resource_revisions : "environmentId" + access_items |o..o{ resource_revisions : "resourceTypeId" + access_items |o..o{ resource_revisions : "systemId" + resource_definitions |o..o{ resource_revisions : "addressFieldId" + resource_definitions |o..o{ resource_revisions : "contextFieldId" + resource_definitions |o..o{ resource_revisions : "referenceFieldId" + resource_definitions |o..o{ resource_revisions : "templateId" + resources ||..o{ resource_revisions : "resourceId" + users ||..o{ resource_revisions : "actorId" + resource_definitions ||..o{ resource_specification_field_choices : "fieldId" + resources ||..o{ resource_specification_field_choices : "resourceId" + resource_definitions ||..o{ resource_specification_fields : "fieldId" + resources ||..o{ resource_specification_fields : "resourceId" + resources ||..o{ resource_specification_sections : "resourceId" + resource_definitions ||..o{ resource_template_fields : "fieldId" + resource_definitions ||..o{ resource_template_fields : "templateId" + resource_template_sections |o..o{ resource_template_fields : "templateId, sectionId" + resource_definitions ||..o{ resource_template_sections : "templateId" + access_items |o..o{ resources : "accountId" + access_items |o..o{ resources : "environmentId" + access_items |o..o{ resources : "resourceTypeId" + access_items |o..o{ resources : "systemId" + companies ||..o{ resources : "companyId" + resource_definitions |o..o{ resources : "addressFieldId" + resource_definitions |o..o{ resources : "contextFieldId" + resource_definitions |o..o{ resources : "referenceFieldId" + resource_definitions ||..o{ resources : "templateId" + assets ||..o{ software_installation_revisions : "assetId" + resources |o..o{ software_installation_revisions : "licenseResourceId" + resources ||..o{ software_installation_revisions : "resourceId" + software_installations ||..o{ software_installation_revisions : "configurationId" + users ||..o{ software_installation_revisions : "actorId" + assets ||..o{ software_installations : "assetId" + companies ||..o{ software_installations : "companyId" + resources |o..o{ software_installations : "licenseResourceId" + resources ||..o{ software_installations : "resourceId" + company_contacts |o..o{ telework_authorization_revisions : "personId" + telework_authorizations ||..o{ telework_authorization_revisions : "authorizationId" + users ||..o{ telework_authorization_revisions : "actorId" + companies ||..o{ telework_authorizations : "companyId" + company_contacts ||..o| telework_authorizations : "personId" + assets |o..o{ ticket_revision_targets : "assetId" + company_contacts |o..o{ ticket_revision_targets : "personId" + resources |o..o{ ticket_revision_targets : "resourceId" + ticket_revisions ||..o{ ticket_revision_targets : "ownerId" + workstations |o..o{ ticket_revision_targets : "workstationId" + company_contacts |o..o{ ticket_revisions : "requesterId" + tickets ||..o{ ticket_revisions : "ticketId" + users ||..o{ ticket_revisions : "actorId" + users |o..o{ ticket_revisions : "assigneeId" + assets |o..o{ ticket_targets : "assetId" + company_contacts |o..o{ ticket_targets : "personId" + resources |o..o{ ticket_targets : "resourceId" + tickets ||..o{ ticket_targets : "ownerId" + workstations |o..o{ ticket_targets : "workstationId" + companies ||..o{ tickets : "companyId" + company_contacts |o..o{ tickets : "requesterId" + users |o..o{ tickets : "assigneeId" + access_items |o..o{ work_profile_revisions : "accountId" + assets |o..o{ work_profile_revisions : "assetId" + company_contacts |o..o{ work_profile_revisions : "personId" + users ||..o{ work_profile_revisions : "actorId" + work_profiles ||..o{ work_profile_revisions : "profileId" + access_items |o..o{ work_profiles : "accountId" + assets ||..o{ work_profiles : "assetId" + companies ||..o{ work_profiles : "companyId" + company_contacts ||..o{ work_profiles : "personId" + assets ||..o{ workstation_assets : "assetId" + assets ||..o{ workstation_assets : "companyId, assetId" + companies ||..o{ workstation_assets : "companyId" + workstations ||..o{ workstation_assets : "companyId, workstationId" + workstations ||..o{ workstation_assets : "workstationId" + assets ||..o{ workstation_members : "assetId" + workstations ||..o{ workstation_members : "workstationId" + assets ||..o{ workstation_revision_members : "assetId" + workstation_revisions ||..o{ workstation_revision_members : "revisionId" + assets |o..o{ workstation_revisions : "computerAssetId" + company_contacts |o..o{ workstation_revisions : "personId" + company_sites |o..o{ workstation_revisions : "siteId" + users ||..o{ workstation_revisions : "actorId" + workstations ||..o{ workstation_revisions : "workstationId" + assets |o..o{ workstations : "computerAssetId" + companies ||..o{ workstations : "companyId" + company_contacts |o..o{ workstations : "personId" + company_sites |o..o{ workstations : "siteId" diff --git a/docs/diagrama-entidad-relacion.md b/docs/diagrama-entidad-relacion.md new file mode 100644 index 0000000..e862796 --- /dev/null +++ b/docs/diagrama-entidad-relacion.md @@ -0,0 +1,1487 @@ +# Diagrama de entidad relación de la base de datos + +Esquema físico consultado en MariaDB el **14 de octubre de 2026** mediante `information_schema`. Esta instantánea refleja las tablas y restricciones existentes, no un diseño propuesto ni únicamente los modelos de Sequelize. No se han consultado registros de negocio ni modificado la base de datos. + +**130 tablas y 2 vistas SQL · 999 columnas · 1054 restricciones FK · 355 relaciones distintas** (agrupando restricciones con idénticas columnas, destino y reglas). + +- [Diagrama completo, con todas las columnas (Mermaid)](diagrama-entidad-relacion-completo.mmd). +- [Diccionario completo de tablas, columnas y claves foráneas](diccionario-del-esquema-actual.md). + +## Cómo leer los diagramas + +`||`: exactamente uno; `|o`: cero o uno; `o{`: cero o muchos; `o|`: cero o uno en el extremo hijo. La nulabilidad de la FK determina si el padre es opcional. Las restricciones PRIMARY/UNIQUE determinan si el hijo puede repetirse. Las líneas discontinuas representan asociaciones por FK; no se deduce composición de dominio a partir de ellas. La etiqueta indica las columnas del hijo. + +Los diagramas por área muestran las PK/FK y los padres externos necesarios para entender sus relaciones. Cada tabla pertenece a un área; los padres pueden repetirse como referencia. El archivo completo incluye todas las columnas, también las tablas sin FK. Los nombres son los reales de MariaDB: por ejemplo, las personas se almacenan en `company_contacts`. + +## Vista principal + +Vista reducida: omite tablas auxiliares e históricas; todas ellas aparecen en las áreas siguientes y en el diagrama completo. + +```mermaid +erDiagram + direction LR + asset_brands + asset_definitions + assets + companies + company_contacts + company_departments + company_sites + interventions + person_department_assignments + resource_definitions + resources + tickets + users + work_profiles + workstation_assets + workstation_members + workstations + companies |o..o{ asset_definitions : "companyId" + asset_brands |o..o{ assets : "brandId" + asset_definitions ||..o{ assets : "templateId" + companies ||..o{ assets : "companyId" + company_sites |o..o{ assets : "siteId" + companies ||..o{ company_contacts : "companyId" + companies ||..o{ company_departments : "companyId" + companies ||..o{ company_sites : "companyId" + companies ||..o{ interventions : "companyId" + tickets |o..o{ interventions : "ticketId" + users |o..o{ interventions : "assigneeId" + companies ||..o{ person_department_assignments : "companyId" + company_contacts ||..o{ person_department_assignments : "personId, companyId" + company_departments ||..o{ person_department_assignments : "departmentId, companyId" + companies |o..o{ resource_definitions : "companyId" + companies ||..o{ resources : "companyId" + resource_definitions |o..o{ resources : "addressFieldId" + resource_definitions |o..o{ resources : "contextFieldId" + resource_definitions |o..o{ resources : "referenceFieldId" + resource_definitions ||..o{ resources : "templateId" + companies ||..o{ tickets : "companyId" + company_contacts |o..o{ tickets : "requesterId" + users |o..o{ tickets : "assigneeId" + assets ||..o{ work_profiles : "assetId" + companies ||..o{ work_profiles : "companyId" + company_contacts ||..o{ work_profiles : "personId" + assets ||..o{ workstation_assets : "assetId" + assets ||..o{ workstation_assets : "companyId, assetId" + companies ||..o{ workstation_assets : "companyId" + workstations ||..o{ workstation_assets : "companyId, workstationId" + workstations ||..o{ workstation_assets : "workstationId" + assets ||..o{ workstation_members : "assetId" + workstations ||..o{ workstation_members : "workstationId" + assets |o..o{ workstations : "computerAssetId" + companies ||..o{ workstations : "companyId" + company_contacts |o..o{ workstations : "personId" + company_sites |o..o{ workstations : "siteId" +``` + +## Áreas del esquema + +| Área | Tablas | +| --- | ---: | +| Cuentas y accesos: histórico | 15 | +| Cuentas y accesos: estado actual | 18 | +| Vistas SQL de cuentas y accesos | 2 | +| Correo y configuraciones | 16 | +| Activos y catálogo | 18 | +| Compañías y personas | 8 | +| Usuarios y sesión | 3 | +| Tickets e intervenciones | 19 | +| Procesos de personas | 9 | +| Teletrabajo y conexiones remotas | 4 | +| Recursos y catálogo | 13 | +| Perfiles de usuario | 2 | +| Puestos de trabajo | 5 | + +## Cuentas y accesos: histórico + +Tablas propias: `access_account_person_revision_values`, `access_account_revision_values`, `access_coverage_revision_values`, `access_environment_revision_values`, `access_grant_revision_values`, `access_group_revision_values`, `access_membership_revision_values`, `access_need_revision_permissions`, `access_need_revision_values`, `access_permission_revision_values`, `access_resource_revision_values`, `access_resource_type_revision_values`, `access_revisions`, `access_system_revision_values`, `access_template_revision_values`. + +```mermaid +erDiagram + direction LR + access_account_person_revision_values { + char ownerId PK,FK + char accountId FK + char personId FK + } + access_account_revision_values { + char ownerId PK,FK + char systemId FK + char environmentId FK "nullable" + } + access_coverage_revision_values { + char ownerId PK,FK + char needId FK + char grantId FK + char accountPersonId FK + char membershipId FK "nullable" + } + access_environment_revision_values { + char ownerId PK,FK + char systemId FK + } + access_grant_revision_values { + char ownerId PK,FK + char resourceId FK + char permissionId FK + char accountId FK "nullable" + char groupId FK "nullable" + } + access_group_revision_values { + char ownerId PK,FK + char systemId FK + char environmentId FK "nullable" + char templateId FK "nullable" + } + access_items { + char id PK + char workspaceId FK + } + access_membership_revision_values { + char ownerId PK,FK + char accountId FK + char groupId FK + } + access_need_revision_permissions { + char ownerId PK,FK + int position PK + char permissionId FK + } + access_need_revision_values { + char ownerId PK,FK + char personId FK + char resourceId FK + } + access_permission_revision_values { + char ownerId PK,FK + char resourceTypeId FK + } + access_resource_revision_values { + char ownerId PK,FK + char resourceTypeId FK + char systemId FK "nullable" + char environmentId FK "nullable" + } + access_resource_type_revision_values { + char ownerId PK,FK + } + access_revisions { + char id PK + char itemId FK + char actorId FK + } + access_system_revision_values { + char ownerId PK,FK + } + access_template_revision_values { + char ownerId PK,FK + char systemId FK "nullable" + } + company_contacts { + char id PK + char companyId FK + } + users { + char id PK + } + access_items ||..o{ access_account_person_revision_values : "accountId" + access_revisions ||..o| access_account_person_revision_values : "ownerId" + company_contacts ||..o{ access_account_person_revision_values : "personId" + access_items |o..o{ access_account_revision_values : "environmentId" + access_items ||..o{ access_account_revision_values : "systemId" + access_revisions ||..o| access_account_revision_values : "ownerId" + access_items ||..o{ access_coverage_revision_values : "accountPersonId" + access_items ||..o{ access_coverage_revision_values : "grantId" + access_items |o..o{ access_coverage_revision_values : "membershipId" + access_items ||..o{ access_coverage_revision_values : "needId" + access_revisions ||..o| access_coverage_revision_values : "ownerId" + access_items ||..o{ access_environment_revision_values : "systemId" + access_revisions ||..o| access_environment_revision_values : "ownerId" + access_items |o..o{ access_grant_revision_values : "accountId" + access_items |o..o{ access_grant_revision_values : "groupId" + access_items ||..o{ access_grant_revision_values : "permissionId" + access_items ||..o{ access_grant_revision_values : "resourceId" + access_revisions ||..o| access_grant_revision_values : "ownerId" + access_items |o..o{ access_group_revision_values : "environmentId" + access_items ||..o{ access_group_revision_values : "systemId" + access_items |o..o{ access_group_revision_values : "templateId" + access_revisions ||..o| access_group_revision_values : "ownerId" + access_items ||..o{ access_membership_revision_values : "accountId" + access_items ||..o{ access_membership_revision_values : "groupId" + access_revisions ||..o| access_membership_revision_values : "ownerId" + access_items ||..o{ access_need_revision_permissions : "permissionId" + access_need_revision_values ||..o{ access_need_revision_permissions : "ownerId" + access_items ||..o{ access_need_revision_values : "resourceId" + access_revisions ||..o| access_need_revision_values : "ownerId" + company_contacts ||..o{ access_need_revision_values : "personId" + access_items ||..o{ access_permission_revision_values : "resourceTypeId" + access_revisions ||..o| access_permission_revision_values : "ownerId" + access_items |o..o{ access_resource_revision_values : "environmentId" + access_items ||..o{ access_resource_revision_values : "resourceTypeId" + access_items |o..o{ access_resource_revision_values : "systemId" + access_revisions ||..o| access_resource_revision_values : "ownerId" + access_revisions ||..o| access_resource_type_revision_values : "ownerId" + access_items ||..o{ access_revisions : "itemId" + users ||..o{ access_revisions : "actorId" + access_revisions ||..o| access_system_revision_values : "ownerId" + access_items |o..o{ access_template_revision_values : "systemId" + access_revisions ||..o| access_template_revision_values : "ownerId" +``` + +## Cuentas y accesos: estado actual + +Tablas propias: `access_account_person_values`, `access_account_values`, `access_coverage_values`, `access_environment_values`, `access_grant_values`, `access_group_values`, `access_items`, `access_membership_values`, `access_need_permissions`, `access_need_values`, `access_permission_values`, `access_person_references`, `access_references`, `access_resource_type_values`, `access_resource_values`, `access_system_values`, `access_template_values`, `access_workspaces`. + +```mermaid +erDiagram + direction LR + access_account_person_values { + char ownerId PK,FK + char accountId FK + char personId FK + } + access_account_values { + char ownerId PK,FK + char systemId FK + char environmentId FK "nullable" + } + access_coverage_values { + char ownerId PK,FK + char needId FK + char grantId FK + char accountPersonId FK + char membershipId FK "nullable" + } + access_environment_values { + char ownerId PK,FK + char systemId FK + } + access_grant_values { + char ownerId PK,FK + char resourceId FK + char permissionId FK + char accountId FK "nullable" + char groupId FK "nullable" + } + access_group_values { + char ownerId PK,FK + char systemId FK + char environmentId FK "nullable" + char templateId FK "nullable" + } + access_items { + char id PK + char workspaceId FK + } + access_membership_values { + char ownerId PK,FK + char accountId FK + char groupId FK + } + access_need_permissions { + char ownerId PK,FK + int position PK + char permissionId FK + } + access_need_values { + char ownerId PK,FK + char personId FK + char resourceId FK + } + access_permission_values { + char ownerId PK,FK + char resourceTypeId FK + } + access_person_references { + char itemId PK,FK + char personId PK,FK + } + access_references { + int id PK + char itemId FK + char targetId FK + } + access_resource_type_values { + char ownerId PK,FK + } + access_resource_values { + char ownerId PK,FK + char resourceTypeId FK + char systemId FK "nullable" + char environmentId FK "nullable" + } + access_system_values { + char ownerId PK,FK + } + access_template_values { + char ownerId PK,FK + char systemId FK "nullable" + } + access_workspaces { + char id PK + char companyId FK "nullable" + } + companies { + char id PK + } + company_contacts { + char id PK + char companyId FK + } + access_items ||..o{ access_account_person_values : "accountId" + access_items ||..o| access_account_person_values : "ownerId" + company_contacts ||..o{ access_account_person_values : "personId" + access_items |o..o{ access_account_values : "environmentId" + access_items ||..o| access_account_values : "ownerId" + access_items ||..o{ access_account_values : "systemId" + access_items ||..o{ access_coverage_values : "accountPersonId" + access_items ||..o{ access_coverage_values : "grantId" + access_items |o..o{ access_coverage_values : "membershipId" + access_items ||..o{ access_coverage_values : "needId" + access_items ||..o| access_coverage_values : "ownerId" + access_items ||..o| access_environment_values : "ownerId" + access_items ||..o{ access_environment_values : "systemId" + access_items |o..o{ access_grant_values : "accountId" + access_items |o..o{ access_grant_values : "groupId" + access_items ||..o| access_grant_values : "ownerId" + access_items ||..o{ access_grant_values : "permissionId" + access_items ||..o{ access_grant_values : "resourceId" + access_items |o..o{ access_group_values : "environmentId" + access_items ||..o| access_group_values : "ownerId" + access_items ||..o{ access_group_values : "systemId" + access_items |o..o{ access_group_values : "templateId" + access_workspaces ||..o{ access_items : "workspaceId" + access_items ||..o{ access_membership_values : "accountId" + access_items ||..o{ access_membership_values : "groupId" + access_items ||..o| access_membership_values : "ownerId" + access_items ||..o{ access_need_permissions : "permissionId" + access_need_values ||..o{ access_need_permissions : "ownerId" + access_items ||..o| access_need_values : "ownerId" + access_items ||..o{ access_need_values : "resourceId" + company_contacts ||..o{ access_need_values : "personId" + access_items ||..o| access_permission_values : "ownerId" + access_items ||..o{ access_permission_values : "resourceTypeId" + access_items ||..o{ access_person_references : "itemId" + company_contacts ||..o{ access_person_references : "personId" + access_items ||..o{ access_references : "itemId" + access_items ||..o{ access_references : "targetId" + access_items ||..o| access_resource_type_values : "ownerId" + access_items |o..o{ access_resource_values : "environmentId" + access_items ||..o| access_resource_values : "ownerId" + access_items ||..o{ access_resource_values : "resourceTypeId" + access_items |o..o{ access_resource_values : "systemId" + access_items ||..o| access_system_values : "ownerId" + access_items ||..o| access_template_values : "ownerId" + access_items |o..o{ access_template_values : "systemId" + companies |o..o{ access_workspaces : "companyId" +``` + +## Vistas SQL de cuentas y accesos + +Tablas propias: `access_current_values`, `access_historical_values`. + +```mermaid +erDiagram + direction LR + access_current_values + access_historical_values +``` + +## Correo y configuraciones + +Tablas propias: `application_configuration_revisions`, `application_configurations`, `certificate_configuration_revisions`, `certificate_configurations`, `folder_mapping_revisions`, `folder_mappings`, `mail_configuration_revisions`, `mail_configurations`, `mailbox_revisions`, `mailbox_use_revisions`, `mailbox_uses`, `mailboxes`, `printer_configuration_revisions`, `printer_configurations`, `software_installation_revisions`, `software_installations`. + +```mermaid +erDiagram + direction LR + access_items { + char id PK + char workspaceId FK + } + application_configuration_revisions { + char id PK + char configurationId FK + char actorId FK + char profileId FK "nullable" + char resourceId FK "nullable" + } + application_configurations { + char id PK + char companyId FK + char profileId FK + char resourceId FK + } + assets { + char id PK + char companyId FK + char templateId FK + char siteId FK "nullable" + char brandId FK "nullable" + } + certificate_configuration_revisions { + char id PK + char configurationId FK + char actorId FK + char profileId FK "nullable" + char resourceId FK "nullable" + } + certificate_configurations { + char id PK + char companyId FK + char profileId FK + char resourceId FK + } + companies { + char id PK + } + company_contacts { + char id PK + char companyId FK + } + folder_mapping_revisions { + char id PK + char mappingId FK + char actorId FK + char profileId FK "nullable" + char needId FK "nullable" + } + folder_mappings { + char id PK + char companyId FK + char profileId FK + char needId FK + } + mail_configuration_revisions { + char id PK + char configurationId FK + char actorId FK + char personId FK "nullable" + char accountId FK "nullable" + char assetId FK "nullable" + char profileId FK "nullable" + char mailboxId FK "nullable" + } + mail_configurations { + char id PK + char companyId FK + char personId FK "nullable" + char accountId FK "nullable" + char assetId FK "nullable" + char profileId FK "nullable" + char mailboxId FK "nullable" + } + mailbox_revisions { + char id PK + char itemId FK + char actorId FK + char accountId FK "nullable" + } + mailbox_use_revisions { + char id PK + char itemId FK + char actorId FK + char mailboxId FK "nullable" + char personId FK "nullable" + } + mailbox_uses { + char id PK + char companyId FK + char mailboxId FK + char personId FK + } + mailboxes { + char id PK + char companyId FK + char accountId FK "nullable" + } + printer_configuration_revisions { + char id PK + char configurationId FK + char actorId FK + char profileId FK "nullable" + char printerId FK "nullable" + } + printer_configurations { + char id PK + char companyId FK + char profileId FK + char printerId FK + } + resources { + char id PK + char companyId FK + char templateId FK + char addressFieldId FK "nullable" + char contextFieldId FK "nullable" + char accountId FK "nullable" + char referenceFieldId FK "nullable" + char resourceTypeId FK "nullable" + char systemId FK "nullable" + char environmentId FK "nullable" + } + software_installation_revisions { + char id PK + char configurationId FK + char assetId FK + char resourceId FK + char actorId FK + char licenseResourceId FK "nullable" + } + software_installations { + char id PK + char companyId FK + char assetId FK + char resourceId FK + char licenseResourceId FK "nullable" + } + users { + char id PK + } + work_profiles { + char id PK + char companyId FK + char personId FK + char accountId FK "nullable" + char assetId FK + } + application_configurations ||..o{ application_configuration_revisions : "configurationId" + resources |o..o{ application_configuration_revisions : "resourceId" + users ||..o{ application_configuration_revisions : "actorId" + work_profiles |o..o{ application_configuration_revisions : "profileId" + companies ||..o{ application_configurations : "companyId" + resources ||..o{ application_configurations : "resourceId" + work_profiles ||..o{ application_configurations : "profileId" + certificate_configurations ||..o{ certificate_configuration_revisions : "configurationId" + resources |o..o{ certificate_configuration_revisions : "resourceId" + users ||..o{ certificate_configuration_revisions : "actorId" + work_profiles |o..o{ certificate_configuration_revisions : "profileId" + companies ||..o{ certificate_configurations : "companyId" + resources ||..o{ certificate_configurations : "resourceId" + work_profiles ||..o{ certificate_configurations : "profileId" + access_items |o..o{ folder_mapping_revisions : "needId" + folder_mappings ||..o{ folder_mapping_revisions : "mappingId" + users ||..o{ folder_mapping_revisions : "actorId" + work_profiles |o..o{ folder_mapping_revisions : "profileId" + access_items ||..o{ folder_mappings : "needId" + companies ||..o{ folder_mappings : "companyId" + work_profiles ||..o{ folder_mappings : "profileId" + access_items |o..o{ mail_configuration_revisions : "accountId" + assets |o..o{ mail_configuration_revisions : "assetId" + company_contacts |o..o{ mail_configuration_revisions : "personId" + mail_configurations ||..o{ mail_configuration_revisions : "configurationId" + mailboxes |o..o{ mail_configuration_revisions : "mailboxId" + users ||..o{ mail_configuration_revisions : "actorId" + work_profiles |o..o{ mail_configuration_revisions : "profileId" + access_items |o..o{ mail_configurations : "accountId" + assets |o..o{ mail_configurations : "assetId" + companies ||..o{ mail_configurations : "companyId" + company_contacts |o..o{ mail_configurations : "personId" + mailboxes |o..o{ mail_configurations : "mailboxId" + work_profiles |o..o{ mail_configurations : "profileId" + access_items |o..o{ mailbox_revisions : "accountId" + mailboxes ||..o{ mailbox_revisions : "itemId" + users ||..o{ mailbox_revisions : "actorId" + company_contacts |o..o{ mailbox_use_revisions : "personId" + mailbox_uses ||..o{ mailbox_use_revisions : "itemId" + mailboxes |o..o{ mailbox_use_revisions : "mailboxId" + users ||..o{ mailbox_use_revisions : "actorId" + companies ||..o{ mailbox_uses : "companyId" + company_contacts ||..o{ mailbox_uses : "personId" + mailboxes ||..o{ mailbox_uses : "mailboxId" + access_items |o..o{ mailboxes : "accountId" + companies ||..o{ mailboxes : "companyId" + assets |o..o{ printer_configuration_revisions : "printerId" + printer_configurations ||..o{ printer_configuration_revisions : "configurationId" + users ||..o{ printer_configuration_revisions : "actorId" + work_profiles |o..o{ printer_configuration_revisions : "profileId" + assets ||..o{ printer_configurations : "printerId" + companies ||..o{ printer_configurations : "companyId" + work_profiles ||..o{ printer_configurations : "profileId" + assets ||..o{ software_installation_revisions : "assetId" + resources |o..o{ software_installation_revisions : "licenseResourceId" + resources ||..o{ software_installation_revisions : "resourceId" + software_installations ||..o{ software_installation_revisions : "configurationId" + users ||..o{ software_installation_revisions : "actorId" + assets ||..o{ software_installations : "assetId" + companies ||..o{ software_installations : "companyId" + resources |o..o{ software_installations : "licenseResourceId" + resources ||..o{ software_installations : "resourceId" +``` + +## Activos y catálogo + +Tablas propias: `asset_availability`, `asset_brands`, `asset_catalogs`, `asset_characteristics`, `asset_definitions`, `asset_field_choices`, `asset_relationships`, `asset_revision_characteristics`, `asset_revisions`, `asset_specification_field_choices`, `asset_specification_fields`, `asset_specification_sections`, `asset_support`, `asset_support_revisions`, `asset_template_fields`, `asset_template_sections`, `assets`, `brand_catalog_lock`. + +```mermaid +erDiagram + direction LR + asset_availability { + char id PK + char catalogId FK + char companyId FK + char definitionId FK + } + asset_brands { + char id PK + } + asset_catalogs { + char id PK + } + asset_characteristics { + char assetId PK,FK + char fieldId PK,FK + } + asset_definitions { + char id PK + char catalogId FK + char companyId FK "nullable" + } + asset_field_choices { + char fieldId PK,FK + int position PK + } + asset_relationships { + char id PK + char companyId FK + char sourceId FK + char targetId FK + char typeId FK + char createdBy FK + char endedBy FK "nullable" + } + asset_revision_characteristics { + char revisionId PK,FK + char assetId FK + char fieldId PK,FK + } + asset_revisions { + char id PK + char assetId FK + char actorId FK + char siteId FK "nullable" + char brandId FK "nullable" + char templateId FK + } + asset_specification_field_choices { + char assetId PK,FK + char fieldId PK,FK + int position PK + } + asset_specification_fields { + char assetId PK,FK + char fieldId PK,FK + char sectionId FK "nullable" + } + asset_specification_sections { + char assetId PK,FK + char id PK + } + asset_support { + char id PK + char companyId FK + char assetId FK + char resourceId FK + } + asset_support_revisions { + char id PK + char connectionId FK + char actorId FK + char resourceId FK + } + asset_template_fields { + char id PK + char templateId FK + char fieldId FK + char sectionId FK "nullable" + } + asset_template_sections { + char id PK + char templateId FK + } + assets { + char id PK + char companyId FK + char templateId FK + char siteId FK "nullable" + char brandId FK "nullable" + } + brand_catalog_lock { + int id PK + } + companies { + char id PK + } + company_sites { + char id PK + char companyId FK + } + resources { + char id PK + char companyId FK + char templateId FK + char addressFieldId FK "nullable" + char contextFieldId FK "nullable" + char accountId FK "nullable" + char referenceFieldId FK "nullable" + char resourceTypeId FK "nullable" + char systemId FK "nullable" + char environmentId FK "nullable" + } + users { + char id PK + } + asset_catalogs ||..o{ asset_availability : "catalogId" + asset_definitions ||..o{ asset_availability : "definitionId" + companies ||..o{ asset_availability : "companyId" + asset_definitions ||..o{ asset_characteristics : "fieldId" + asset_specification_fields ||..o| asset_characteristics : "assetId, fieldId" + assets ||..o{ asset_characteristics : "assetId" + asset_catalogs ||..o{ asset_definitions : "catalogId" + companies |o..o{ asset_definitions : "companyId" + asset_definitions ||..o{ asset_field_choices : "fieldId" + asset_definitions ||..o{ asset_relationships : "typeId" + assets ||..o{ asset_relationships : "companyId, sourceId" + assets ||..o{ asset_relationships : "companyId, targetId" + assets ||..o{ asset_relationships : "sourceId" + assets ||..o{ asset_relationships : "targetId" + companies ||..o{ asset_relationships : "companyId" + users ||..o{ asset_relationships : "createdBy" + users |o..o{ asset_relationships : "endedBy" + asset_definitions ||..o{ asset_revision_characteristics : "fieldId" + asset_revisions ||..o{ asset_revision_characteristics : "revisionId" + asset_revisions ||..o{ asset_revision_characteristics : "revisionId, assetId" + asset_specification_fields ||..o{ asset_revision_characteristics : "assetId, fieldId" + assets ||..o{ asset_revision_characteristics : "assetId" + asset_brands |o..o{ asset_revisions : "brandId" + asset_definitions ||..o{ asset_revisions : "templateId" + assets ||..o{ asset_revisions : "assetId" + company_sites |o..o{ asset_revisions : "siteId" + users ||..o{ asset_revisions : "actorId" + asset_definitions ||..o{ asset_specification_field_choices : "fieldId" + asset_specification_fields ||..o{ asset_specification_field_choices : "assetId, fieldId" + assets ||..o{ asset_specification_field_choices : "assetId" + asset_definitions ||..o{ asset_specification_fields : "fieldId" + asset_specification_sections |o..o{ asset_specification_fields : "assetId, sectionId" + assets ||..o{ asset_specification_fields : "assetId" + assets ||..o{ asset_specification_sections : "assetId" + assets ||..o{ asset_support : "assetId" + assets ||..o{ asset_support : "companyId, assetId" + companies ||..o{ asset_support : "companyId" + resources ||..o{ asset_support : "companyId, resourceId" + resources ||..o{ asset_support : "resourceId" + asset_support ||..o{ asset_support_revisions : "connectionId" + resources ||..o{ asset_support_revisions : "resourceId" + users ||..o{ asset_support_revisions : "actorId" + asset_definitions ||..o{ asset_template_fields : "fieldId" + asset_definitions ||..o{ asset_template_fields : "templateId" + asset_template_sections |o..o{ asset_template_fields : "sectionId" + asset_template_sections |o..o{ asset_template_fields : "templateId, sectionId" + asset_definitions ||..o{ asset_template_sections : "templateId" + asset_brands |o..o{ assets : "brandId" + asset_definitions ||..o{ assets : "templateId" + companies ||..o{ assets : "companyId" + company_sites |o..o{ assets : "siteId" +``` + +## Compañías y personas + +Tablas propias: `companies`, `company_contacts`, `company_departments`, `company_logos`, `company_sites`, `department_assignment_revisions`, `person_department_assignments`, `person_photos`. + +```mermaid +erDiagram + direction LR + companies { + char id PK + } + company_contacts { + char id PK + char companyId FK + } + company_departments { + char id PK + char companyId FK + } + company_logos { + char companyId PK,FK + } + company_sites { + char id PK + char companyId FK + } + department_assignment_revisions { + char id PK + char assignmentId FK + } + person_department_assignments { + char id PK + char companyId FK + char personId FK + char departmentId FK + } + person_photos { + char personId PK,FK + char companyId FK + } + companies ||..o{ company_contacts : "companyId" + companies ||..o{ company_departments : "companyId" + companies ||..o| company_logos : "companyId" + companies ||..o{ company_sites : "companyId" + person_department_assignments ||..o{ department_assignment_revisions : "assignmentId" + companies ||..o{ person_department_assignments : "companyId" + company_contacts ||..o{ person_department_assignments : "personId, companyId" + company_departments ||..o{ person_department_assignments : "departmentId, companyId" + company_contacts ||..o| person_photos : "personId, companyId" +``` + +## Usuarios y sesión + +Tablas propias: `http_sessions`, `identity_state`, `users`. + +```mermaid +erDiagram + direction LR + http_sessions { + varchar sid PK + char userId FK "nullable" + } + identity_state { + int id PK + char initialAdminId FK "nullable" + } + users { + char id PK + } + users |o..o{ http_sessions : "userId" + users |o..o{ identity_state : "initialAdminId" +``` + +## Tickets e intervenciones + +Tablas propias: `intervention_counter`, `intervention_procedure_steps`, `intervention_procedures`, `intervention_revision_steps`, `intervention_revision_targets`, `intervention_revisions`, `intervention_steps`, `intervention_targets`, `intervention_template_lock`, `intervention_template_revision_steps`, `intervention_template_revisions`, `intervention_template_steps`, `intervention_templates`, `interventions`, `ticket_counter`, `ticket_revision_targets`, `ticket_revisions`, `ticket_targets`, `tickets`. + +```mermaid +erDiagram + direction LR + assets { + char id PK + char companyId FK + char templateId FK + char siteId FK "nullable" + char brandId FK "nullable" + } + companies { + char id PK + } + company_contacts { + char id PK + char companyId FK + } + intervention_counter { + int id PK + } + intervention_procedure_steps { + char ownerId PK,FK + char stepId PK + } + intervention_procedures { + char interventionId PK,FK + char templateId FK + char revisionId FK + } + intervention_revision_steps { + char ownerId PK,FK + char stepId PK + } + intervention_revision_targets { + char ownerId PK,FK + int position PK + char personId FK "nullable" + char workstationId FK "nullable" + char assetId FK "nullable" + char resourceId FK "nullable" + } + intervention_revisions { + char id PK + char interventionId FK + char actorId FK + char ticketId FK "nullable" + char assigneeId FK "nullable" + } + intervention_steps { + char ownerId PK,FK + char stepId PK + } + intervention_targets { + char ownerId PK,FK + int position PK + char personId FK "nullable" + char workstationId FK "nullable" + char assetId FK "nullable" + char resourceId FK "nullable" + } + intervention_template_lock { + int id PK + } + intervention_template_revision_steps { + char ownerId PK,FK + char stepId PK + } + intervention_template_revisions { + char id PK + char templateId FK + char actorId FK "nullable" + } + intervention_template_steps { + char ownerId PK,FK + char stepId PK + } + intervention_templates { + char id PK + char companyId FK "nullable" + } + interventions { + char id PK + char companyId FK + char ticketId FK "nullable" + char assigneeId FK "nullable" + } + resources { + char id PK + char companyId FK + char templateId FK + char addressFieldId FK "nullable" + char contextFieldId FK "nullable" + char accountId FK "nullable" + char referenceFieldId FK "nullable" + char resourceTypeId FK "nullable" + char systemId FK "nullable" + char environmentId FK "nullable" + } + ticket_counter { + int id PK + } + ticket_revision_targets { + char ownerId PK,FK + int position PK + char personId FK "nullable" + char workstationId FK "nullable" + char assetId FK "nullable" + char resourceId FK "nullable" + } + ticket_revisions { + char id PK + char ticketId FK + char actorId FK + char requesterId FK "nullable" + char assigneeId FK "nullable" + } + ticket_targets { + char ownerId PK,FK + int position PK + char personId FK "nullable" + char workstationId FK "nullable" + char assetId FK "nullable" + char resourceId FK "nullable" + } + tickets { + char id PK + char companyId FK + char requesterId FK "nullable" + char assigneeId FK "nullable" + } + users { + char id PK + } + workstations { + char id PK + char companyId FK + char siteId FK "nullable" + char personId FK "nullable" + char computerAssetId FK "nullable" + } + intervention_procedures ||..o{ intervention_procedure_steps : "ownerId" + intervention_template_revisions ||..o{ intervention_procedures : "revisionId" + intervention_template_revisions ||..o{ intervention_procedures : "templateId, revisionId" + intervention_templates ||..o{ intervention_procedures : "templateId" + interventions ||..o| intervention_procedures : "interventionId" + intervention_revisions ||..o{ intervention_revision_steps : "ownerId" + assets |o..o{ intervention_revision_targets : "assetId" + company_contacts |o..o{ intervention_revision_targets : "personId" + intervention_revisions ||..o{ intervention_revision_targets : "ownerId" + resources |o..o{ intervention_revision_targets : "resourceId" + workstations |o..o{ intervention_revision_targets : "workstationId" + interventions ||..o{ intervention_revisions : "interventionId" + tickets |o..o{ intervention_revisions : "ticketId" + users ||..o{ intervention_revisions : "actorId" + users |o..o{ intervention_revisions : "assigneeId" + interventions ||..o{ intervention_steps : "ownerId" + assets |o..o{ intervention_targets : "assetId" + company_contacts |o..o{ intervention_targets : "personId" + interventions ||..o{ intervention_targets : "ownerId" + resources |o..o{ intervention_targets : "resourceId" + workstations |o..o{ intervention_targets : "workstationId" + intervention_template_revisions ||..o{ intervention_template_revision_steps : "ownerId" + intervention_templates ||..o{ intervention_template_revisions : "templateId" + users |o..o{ intervention_template_revisions : "actorId" + intervention_templates ||..o{ intervention_template_steps : "ownerId" + companies |o..o{ intervention_templates : "companyId" + companies ||..o{ interventions : "companyId" + tickets |o..o{ interventions : "ticketId" + users |o..o{ interventions : "assigneeId" + assets |o..o{ ticket_revision_targets : "assetId" + company_contacts |o..o{ ticket_revision_targets : "personId" + resources |o..o{ ticket_revision_targets : "resourceId" + ticket_revisions ||..o{ ticket_revision_targets : "ownerId" + workstations |o..o{ ticket_revision_targets : "workstationId" + company_contacts |o..o{ ticket_revisions : "requesterId" + tickets ||..o{ ticket_revisions : "ticketId" + users ||..o{ ticket_revisions : "actorId" + users |o..o{ ticket_revisions : "assigneeId" + assets |o..o{ ticket_targets : "assetId" + company_contacts |o..o{ ticket_targets : "personId" + resources |o..o{ ticket_targets : "resourceId" + tickets ||..o{ ticket_targets : "ownerId" + workstations |o..o{ ticket_targets : "workstationId" + companies ||..o{ tickets : "companyId" + company_contacts |o..o{ tickets : "requesterId" + users |o..o{ tickets : "assigneeId" +``` + +## Procesos de personas + +Tablas propias: `person_process_revision_tasks`, `person_process_revisions`, `person_process_tasks`, `person_processes`, `process_catalog_lock`, `process_template_instructions`, `process_template_revision_instructions`, `process_template_revisions`, `process_templates`. + +```mermaid +erDiagram + direction LR + companies { + char id PK + } + company_contacts { + char id PK + char companyId FK + } + person_process_revision_tasks { + char ownerId PK,FK + char taskId PK + } + person_process_revisions { + char id PK + char actorId FK "nullable" + char itemId FK + char personId FK "nullable" + char templateId FK "nullable" + } + person_process_tasks { + char ownerId PK,FK + char taskId PK + } + person_processes { + char id PK + char companyId FK + char personId FK + char templateId FK + } + process_catalog_lock { + int id PK + } + process_template_instructions { + char ownerId PK,FK + int position PK + } + process_template_revision_instructions { + char ownerId PK,FK + int position PK + } + process_template_revisions { + char id PK + char actorId FK "nullable" + char itemId FK + } + process_templates { + char id PK + } + users { + char id PK + } + person_process_revisions ||..o{ person_process_revision_tasks : "ownerId" + company_contacts |o..o{ person_process_revisions : "personId" + person_processes ||..o{ person_process_revisions : "itemId" + process_templates |o..o{ person_process_revisions : "templateId" + users |o..o{ person_process_revisions : "actorId" + person_processes ||..o{ person_process_tasks : "ownerId" + companies ||..o{ person_processes : "companyId" + company_contacts ||..o{ person_processes : "personId" + process_templates ||..o{ person_processes : "templateId" + process_templates ||..o{ process_template_instructions : "ownerId" + process_template_revisions ||..o{ process_template_revision_instructions : "ownerId" + process_templates ||..o{ process_template_revisions : "itemId" + users |o..o{ process_template_revisions : "actorId" +``` + +## Teletrabajo y conexiones remotas + +Tablas propias: `remote_connection_revisions`, `remote_connections`, `telework_authorization_revisions`, `telework_authorizations`. + +```mermaid +erDiagram + direction LR + assets { + char id PK + char companyId FK + char templateId FK + char siteId FK "nullable" + char brandId FK "nullable" + } + companies { + char id PK + } + company_contacts { + char id PK + char companyId FK + } + company_sites { + char id PK + char companyId FK + } + remote_connection_revisions { + char id PK + char connectionId FK + char actorId FK + char personId FK "nullable" + char sourceAssetId FK "nullable" + char targetAssetId FK "nullable" + char targetResourceId FK "nullable" + char siteId FK "nullable" + } + remote_connections { + char id PK + char companyId FK + char personId FK + char sourceAssetId FK "nullable" + char targetAssetId FK "nullable" + char targetResourceId FK "nullable" + char siteId FK "nullable" + } + resources { + char id PK + char companyId FK + char templateId FK + char addressFieldId FK "nullable" + char contextFieldId FK "nullable" + char accountId FK "nullable" + char referenceFieldId FK "nullable" + char resourceTypeId FK "nullable" + char systemId FK "nullable" + char environmentId FK "nullable" + } + telework_authorization_revisions { + char id PK + char authorizationId FK + char actorId FK + char personId FK "nullable" + } + telework_authorizations { + char id PK + char companyId FK + char personId FK + } + users { + char id PK + } + assets |o..o{ remote_connection_revisions : "sourceAssetId" + assets |o..o{ remote_connection_revisions : "targetAssetId" + company_contacts |o..o{ remote_connection_revisions : "personId" + company_sites |o..o{ remote_connection_revisions : "siteId" + remote_connections ||..o{ remote_connection_revisions : "connectionId" + resources |o..o{ remote_connection_revisions : "targetResourceId" + users ||..o{ remote_connection_revisions : "actorId" + assets |o..o{ remote_connections : "sourceAssetId" + assets |o..o{ remote_connections : "targetAssetId" + companies ||..o{ remote_connections : "companyId" + company_contacts ||..o{ remote_connections : "personId" + company_sites |o..o{ remote_connections : "siteId" + resources |o..o{ remote_connections : "targetResourceId" + company_contacts |o..o{ telework_authorization_revisions : "personId" + telework_authorizations ||..o{ telework_authorization_revisions : "authorizationId" + users ||..o{ telework_authorization_revisions : "actorId" + companies ||..o{ telework_authorizations : "companyId" + company_contacts ||..o| telework_authorizations : "personId" +``` + +## Recursos y catálogo + +Tablas propias: `resource_availability`, `resource_catalogs`, `resource_characteristics`, `resource_definitions`, `resource_field_choices`, `resource_revision_characteristics`, `resource_revisions`, `resource_specification_field_choices`, `resource_specification_fields`, `resource_specification_sections`, `resource_template_fields`, `resource_template_sections`, `resources`. + +```mermaid +erDiagram + direction LR + access_items { + char id PK + char workspaceId FK + } + companies { + char id PK + } + resource_availability { + char id PK + char catalogId FK + char companyId FK + char definitionId FK + } + resource_catalogs { + char id PK + } + resource_characteristics { + char resourceId PK,FK + char fieldId PK,FK + } + resource_definitions { + char id PK + char catalogId FK + char companyId FK "nullable" + } + resource_field_choices { + char fieldId PK,FK + int position PK + } + resource_revision_characteristics { + char revisionId PK,FK + char resourceId FK + char fieldId PK,FK + } + resource_revisions { + char id PK + char resourceId FK + char actorId FK + char addressFieldId FK "nullable" + char contextFieldId FK "nullable" + char accountId FK "nullable" + char referenceFieldId FK "nullable" + char resourceTypeId FK "nullable" + char systemId FK "nullable" + char environmentId FK "nullable" + char templateId FK "nullable" + } + resource_specification_field_choices { + char resourceId PK,FK + char fieldId PK,FK + int position PK + } + resource_specification_fields { + char resourceId PK,FK + char fieldId PK,FK + } + resource_specification_sections { + char resourceId PK,FK + char id PK + } + resource_template_fields { + char id PK + char templateId FK + char fieldId FK + char sectionId FK "nullable" + } + resource_template_sections { + char id PK + char templateId PK,FK + } + resources { + char id PK + char companyId FK + char templateId FK + char addressFieldId FK "nullable" + char contextFieldId FK "nullable" + char accountId FK "nullable" + char referenceFieldId FK "nullable" + char resourceTypeId FK "nullable" + char systemId FK "nullable" + char environmentId FK "nullable" + } + users { + char id PK + } + companies ||..o{ resource_availability : "companyId" + resource_catalogs ||..o{ resource_availability : "catalogId" + resource_definitions ||..o{ resource_availability : "definitionId" + resource_definitions ||..o{ resource_characteristics : "fieldId" + resources ||..o{ resource_characteristics : "resourceId" + companies |o..o{ resource_definitions : "companyId" + resource_catalogs ||..o{ resource_definitions : "catalogId" + resource_definitions ||..o{ resource_field_choices : "fieldId" + resource_definitions ||..o{ resource_revision_characteristics : "fieldId" + resource_revisions ||..o{ resource_revision_characteristics : "revisionId" + resources ||..o{ resource_revision_characteristics : "resourceId" + access_items |o..o{ resource_revisions : "accountId" + access_items |o..o{ resource_revisions : "environmentId" + access_items |o..o{ resource_revisions : "resourceTypeId" + access_items |o..o{ resource_revisions : "systemId" + resource_definitions |o..o{ resource_revisions : "addressFieldId" + resource_definitions |o..o{ resource_revisions : "contextFieldId" + resource_definitions |o..o{ resource_revisions : "referenceFieldId" + resource_definitions |o..o{ resource_revisions : "templateId" + resources ||..o{ resource_revisions : "resourceId" + users ||..o{ resource_revisions : "actorId" + resource_definitions ||..o{ resource_specification_field_choices : "fieldId" + resources ||..o{ resource_specification_field_choices : "resourceId" + resource_definitions ||..o{ resource_specification_fields : "fieldId" + resources ||..o{ resource_specification_fields : "resourceId" + resources ||..o{ resource_specification_sections : "resourceId" + resource_definitions ||..o{ resource_template_fields : "fieldId" + resource_definitions ||..o{ resource_template_fields : "templateId" + resource_template_sections |o..o{ resource_template_fields : "templateId, sectionId" + resource_definitions ||..o{ resource_template_sections : "templateId" + access_items |o..o{ resources : "accountId" + access_items |o..o{ resources : "environmentId" + access_items |o..o{ resources : "resourceTypeId" + access_items |o..o{ resources : "systemId" + companies ||..o{ resources : "companyId" + resource_definitions |o..o{ resources : "addressFieldId" + resource_definitions |o..o{ resources : "contextFieldId" + resource_definitions |o..o{ resources : "referenceFieldId" + resource_definitions ||..o{ resources : "templateId" +``` + +## Perfiles de usuario + +Tablas propias: `work_profile_revisions`, `work_profiles`. + +```mermaid +erDiagram + direction LR + access_items { + char id PK + char workspaceId FK + } + assets { + char id PK + char companyId FK + char templateId FK + char siteId FK "nullable" + char brandId FK "nullable" + } + companies { + char id PK + } + company_contacts { + char id PK + char companyId FK + } + users { + char id PK + } + work_profile_revisions { + char id PK + char profileId FK + char accountId FK "nullable" + char actorId FK + char personId FK "nullable" + char assetId FK "nullable" + } + work_profiles { + char id PK + char companyId FK + char personId FK + char accountId FK "nullable" + char assetId FK + } + access_items |o..o{ work_profile_revisions : "accountId" + assets |o..o{ work_profile_revisions : "assetId" + company_contacts |o..o{ work_profile_revisions : "personId" + users ||..o{ work_profile_revisions : "actorId" + work_profiles ||..o{ work_profile_revisions : "profileId" + access_items |o..o{ work_profiles : "accountId" + assets ||..o{ work_profiles : "assetId" + companies ||..o{ work_profiles : "companyId" + company_contacts ||..o{ work_profiles : "personId" +``` + +## Puestos de trabajo + +Tablas propias: `workstation_assets`, `workstation_members`, `workstation_revision_members`, `workstation_revisions`, `workstations`. + +```mermaid +erDiagram + direction LR + assets { + char id PK + char companyId FK + char templateId FK + char siteId FK "nullable" + char brandId FK "nullable" + } + companies { + char id PK + } + company_contacts { + char id PK + char companyId FK + } + company_sites { + char id PK + char companyId FK + } + users { + char id PK + } + workstation_assets { + char workstationId PK,FK + char companyId FK + char assetId PK,FK + } + workstation_members { + char workstationId PK,FK + char assetId PK,FK + } + workstation_revision_members { + char revisionId PK,FK + char assetId PK,FK + } + workstation_revisions { + char id PK + char workstationId FK + char actorId FK + char siteId FK "nullable" + char personId FK "nullable" + char computerAssetId FK "nullable" + } + workstations { + char id PK + char companyId FK + char siteId FK "nullable" + char personId FK "nullable" + char computerAssetId FK "nullable" + } + assets ||..o{ workstation_assets : "assetId" + assets ||..o{ workstation_assets : "companyId, assetId" + companies ||..o{ workstation_assets : "companyId" + workstations ||..o{ workstation_assets : "companyId, workstationId" + workstations ||..o{ workstation_assets : "workstationId" + assets ||..o{ workstation_members : "assetId" + workstations ||..o{ workstation_members : "workstationId" + assets ||..o{ workstation_revision_members : "assetId" + workstation_revisions ||..o{ workstation_revision_members : "revisionId" + assets |o..o{ workstation_revisions : "computerAssetId" + company_contacts |o..o{ workstation_revisions : "personId" + company_sites |o..o{ workstation_revisions : "siteId" + users ||..o{ workstation_revisions : "actorId" + workstations ||..o{ workstation_revisions : "workstationId" + assets |o..o{ workstations : "computerAssetId" + companies ||..o{ workstations : "companyId" + company_contacts |o..o{ workstations : "personId" + company_sites |o..o{ workstations : "siteId" +``` + +## Observaciones del esquema físico + +Los 132 objetos consultados son **130 tablas y 2 vistas SQL**: `access_current_values` y `access_historical_values`. Las vistas no tienen FK propias y se representan como objetos independientes; no se inventan relaciones físicas para ellas. + +Se han encontrado **17 relaciones con varias restricciones FK equivalentes**: 699 restricciones adicionales. Se han agrupado únicamente en la representación; no se han eliminado. El diccionario conserva todos los nombres de restricciones. Conviene revisar por separado el origen de estas duplicaciones. + +Solo se dibujan relaciones declaradas en MariaDB. Un identificador sin FK no implica una relación física garantizada; las referencias polimórficas y las validaciones de aplicación pueden tener relaciones adicionales. Las tablas de revisiones forman parte del esquema actual y se incluyen. Esta documentación no implica que exista ya la auditoría centralizada diseñada para una implementación futura. + +## Validación + +Generación verificada contra la misma captura de metadatos: 130 tablas y 2 vistas SQL, 999 columnas y 355 relaciones distintas incluidas en el archivo completo; todas las tablas asignadas a un área. Las reglas ON DELETE/ON UPDATE se recogen en el diccionario. No se han ejecutado migraciones ni cambios de datos. diff --git a/docs/diccionario-del-esquema-actual.md b/docs/diccionario-del-esquema-actual.md new file mode 100644 index 0000000..3dbd9a9 --- /dev/null +++ b/docs/diccionario-del-esquema-actual.md @@ -0,0 +1,2374 @@ +# Diccionario del esquema actual + +Instantánea de MariaDB del **14 de octubre de 2026**. Complementa el [diagrama de entidad relación](diagrama-entidad-relacion.md). `NULL` indica si la columna admite ausencia de valor. Se conservan los tipos SQL exactos y todos los nombres de las restricciones FK, incluidas las equivalentes. No se incluyen valores predeterminados ni registros de negocio. + +## access_account_person_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `accountId` | `char(36)` | No | FK | +| `personId` | `char(36)` | No | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | +| `configuration` | `varchar(20)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `access_account_person_revision_values_ibfk_2` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_account_person_revision_values_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `access_account_person_revision_values_ibfk_3` | + +## access_account_person_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `accountId` | `char(36)` | No | FK | +| `personId` | `char(36)` | No | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | +| `configuration` | `varchar(20)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `access_account_person_values_ibfk_2` | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_account_person_values_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `access_account_person_values_ibfk_3` | + +## access_account_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `systemId` | `char(36)` | No | FK | +| `environmentId` | `char(36)` | Sí | FK | +| `login` | `varchar(255)` | No | — | +| `usage` | `varchar(16)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `expiresOn` | `varchar(10)` | Sí | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `environmentId` | `access_items(id)` | RESTRICT / RESTRICT | `access_account_revision_values_ibfk_3` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_account_revision_values_ibfk_2` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_account_revision_values_ibfk_1` | + +## access_account_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `systemId` | `char(36)` | No | FK | +| `environmentId` | `char(36)` | Sí | FK | +| `login` | `varchar(255)` | No | — | +| `usage` | `varchar(16)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `expiresOn` | `varchar(10)` | Sí | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `environmentId` | `access_items(id)` | RESTRICT / RESTRICT | `access_account_values_ibfk_3` | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_account_values_ibfk_1` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_account_values_ibfk_2` | + +## access_coverage_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `needId` | `char(36)` | No | FK | +| `grantId` | `char(36)` | No | FK | +| `accountPersonId` | `char(36)` | No | FK | +| `membershipId` | `char(36)` | Sí | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountPersonId` | `access_items(id)` | RESTRICT / RESTRICT | `access_coverage_revision_values_ibfk_4` | +| `grantId` | `access_items(id)` | RESTRICT / RESTRICT | `access_coverage_revision_values_ibfk_3` | +| `membershipId` | `access_items(id)` | RESTRICT / RESTRICT | `access_coverage_revision_values_ibfk_5` | +| `needId` | `access_items(id)` | RESTRICT / RESTRICT | `access_coverage_revision_values_ibfk_2` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_coverage_revision_values_ibfk_1` | + +## access_coverage_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `needId` | `char(36)` | No | FK | +| `grantId` | `char(36)` | No | FK | +| `accountPersonId` | `char(36)` | No | FK | +| `membershipId` | `char(36)` | Sí | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountPersonId` | `access_items(id)` | RESTRICT / RESTRICT | `access_coverage_values_ibfk_4` | +| `grantId` | `access_items(id)` | RESTRICT / RESTRICT | `access_coverage_values_ibfk_3` | +| `membershipId` | `access_items(id)` | RESTRICT / RESTRICT | `access_coverage_values_ibfk_5` | +| `needId` | `access_items(id)` | RESTRICT / RESTRICT | `access_coverage_values_ibfk_2` | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_coverage_values_ibfk_1` | + +## access_current_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | — | +| `kind` | `varchar(14)` | No | — | +| `name` | `varchar(100)` | Sí | — | +| `active` | `tinyint(4)` | Sí | — | +| `resourceTypeId` | `char(36)` | Sí | — | +| `description` | `mediumtext` | Sí | — | +| `systemId` | `char(36)` | Sí | — | +| `environmentId` | `char(36)` | Sí | — | +| `login` | `varchar(255)` | Sí | — | +| `usage` | `varchar(16)` | Sí | — | +| `expiresOn` | `varchar(10)` | Sí | — | +| `notes` | `mediumtext` | Sí | — | +| `accountId` | `char(36)` | Sí | — | +| `personId` | `char(36)` | Sí | — | +| `startDate` | `varchar(10)` | Sí | — | +| `endDate` | `varchar(10)` | Sí | — | +| `configuration` | `varchar(20)` | Sí | — | +| `identifier` | `varchar(255)` | Sí | — | +| `templateId` | `char(36)` | Sí | — | +| `groupId` | `char(36)` | Sí | — | +| `reference` | `varchar(255)` | Sí | — | +| `resourceId` | `char(36)` | Sí | — | +| `permissionId` | `char(36)` | Sí | — | +| `needId` | `char(36)` | Sí | — | +| `grantId` | `char(36)` | Sí | — | +| `accountPersonId` | `char(36)` | Sí | — | +| `membershipId` | `char(36)` | Sí | — | + +## access_environment_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `systemId` | `char(36)` | No | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_environment_revision_values_ibfk_2` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_environment_revision_values_ibfk_1` | + +## access_environment_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `systemId` | `char(36)` | No | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_environment_values_ibfk_1` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_environment_values_ibfk_2` | + +## access_grant_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `resourceId` | `char(36)` | No | FK | +| `permissionId` | `char(36)` | No | FK | +| `accountId` | `char(36)` | Sí | FK | +| `groupId` | `char(36)` | Sí | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | +| `configuration` | `varchar(20)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `access_grant_revision_values_ibfk_4` | +| `groupId` | `access_items(id)` | RESTRICT / RESTRICT | `access_grant_revision_values_ibfk_5` | +| `permissionId` | `access_items(id)` | RESTRICT / RESTRICT | `access_grant_revision_values_ibfk_3` | +| `resourceId` | `access_items(id)` | RESTRICT / RESTRICT | `access_grant_revision_values_ibfk_2` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_grant_revision_values_ibfk_1` | + +## access_grant_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `resourceId` | `char(36)` | No | FK | +| `permissionId` | `char(36)` | No | FK | +| `accountId` | `char(36)` | Sí | FK | +| `groupId` | `char(36)` | Sí | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | +| `configuration` | `varchar(20)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `access_grant_values_ibfk_4` | +| `groupId` | `access_items(id)` | RESTRICT / RESTRICT | `access_grant_values_ibfk_5` | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_grant_values_ibfk_1` | +| `permissionId` | `access_items(id)` | RESTRICT / RESTRICT | `access_grant_values_ibfk_3` | +| `resourceId` | `access_items(id)` | RESTRICT / RESTRICT | `access_grant_values_ibfk_2` | + +## access_group_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `systemId` | `char(36)` | No | FK | +| `environmentId` | `char(36)` | Sí | FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `identifier` | `varchar(255)` | No | — | +| `description` | `text` | Sí | — | +| `templateId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `environmentId` | `access_items(id)` | RESTRICT / RESTRICT | `access_group_revision_values_ibfk_3` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_group_revision_values_ibfk_2` | +| `templateId` | `access_items(id)` | RESTRICT / RESTRICT | `access_group_revision_values_ibfk_4` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_group_revision_values_ibfk_1` | + +## access_group_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `systemId` | `char(36)` | No | FK | +| `environmentId` | `char(36)` | Sí | FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `identifier` | `varchar(255)` | No | — | +| `description` | `text` | Sí | — | +| `templateId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `environmentId` | `access_items(id)` | RESTRICT / RESTRICT | `access_group_values_ibfk_3` | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_group_values_ibfk_1` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_group_values_ibfk_2` | +| `templateId` | `access_items(id)` | RESTRICT / RESTRICT | `access_group_values_ibfk_4` | + +## access_historical_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | — | +| `kind` | `varchar(14)` | No | — | +| `name` | `varchar(100)` | Sí | — | +| `active` | `tinyint(4)` | Sí | — | +| `resourceTypeId` | `char(36)` | Sí | — | +| `description` | `mediumtext` | Sí | — | +| `systemId` | `char(36)` | Sí | — | +| `environmentId` | `char(36)` | Sí | — | +| `login` | `varchar(255)` | Sí | — | +| `usage` | `varchar(16)` | Sí | — | +| `expiresOn` | `varchar(10)` | Sí | — | +| `notes` | `mediumtext` | Sí | — | +| `accountId` | `char(36)` | Sí | — | +| `personId` | `char(36)` | Sí | — | +| `startDate` | `varchar(10)` | Sí | — | +| `endDate` | `varchar(10)` | Sí | — | +| `configuration` | `varchar(20)` | Sí | — | +| `identifier` | `varchar(255)` | Sí | — | +| `templateId` | `char(36)` | Sí | — | +| `groupId` | `char(36)` | Sí | — | +| `reference` | `varchar(255)` | Sí | — | +| `resourceId` | `char(36)` | Sí | — | +| `permissionId` | `char(36)` | Sí | — | +| `needId` | `char(36)` | Sí | — | +| `grantId` | `char(36)` | Sí | — | +| `accountPersonId` | `char(36)` | Sí | — | +| `membershipId` | `char(36)` | Sí | — | + +## access_items + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `workspaceId` | `char(36)` | No | FK | +| `kind` | `varchar(30)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `workspaceId` | `access_workspaces(id)` | RESTRICT / RESTRICT | `access_items_ibfk_1` | + +## access_membership_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `accountId` | `char(36)` | No | FK | +| `groupId` | `char(36)` | No | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | +| `configuration` | `varchar(20)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `access_membership_revision_values_ibfk_2` | +| `groupId` | `access_items(id)` | RESTRICT / RESTRICT | `access_membership_revision_values_ibfk_3` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_membership_revision_values_ibfk_1` | + +## access_membership_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `accountId` | `char(36)` | No | FK | +| `groupId` | `char(36)` | No | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | +| `configuration` | `varchar(20)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `access_membership_values_ibfk_2` | +| `groupId` | `access_items(id)` | RESTRICT / RESTRICT | `access_membership_values_ibfk_3` | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_membership_values_ibfk_1` | + +## access_need_permissions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `permissionId` | `char(36)` | No | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `permissionId` | `access_items(id)` | RESTRICT / RESTRICT | `access_need_permissions_ibfk_2` | +| `ownerId` | `access_need_values(ownerId)` | CASCADE / RESTRICT | `access_need_permissions_ibfk_1` | + +## access_need_revision_permissions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `permissionId` | `char(36)` | No | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `permissionId` | `access_items(id)` | RESTRICT / RESTRICT | `access_need_revision_permissions_ibfk_2` | +| `ownerId` | `access_need_revision_values(ownerId)` | CASCADE / RESTRICT | `access_need_revision_permissions_ibfk_1` | + +## access_need_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `personId` | `char(36)` | No | FK | +| `resourceId` | `char(36)` | No | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `resourceId` | `access_items(id)` | RESTRICT / RESTRICT | `access_need_revision_values_ibfk_3` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_need_revision_values_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `access_need_revision_values_ibfk_2` | + +## access_need_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `personId` | `char(36)` | No | FK | +| `resourceId` | `char(36)` | No | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_need_values_ibfk_1` | +| `resourceId` | `access_items(id)` | RESTRICT / RESTRICT | `access_need_values_ibfk_3` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `access_need_values_ibfk_2` | + +## access_permission_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `resourceTypeId` | `char(36)` | No | FK | +| `description` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `resourceTypeId` | `access_items(id)` | RESTRICT / RESTRICT | `access_permission_revision_values_ibfk_2` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_permission_revision_values_ibfk_1` | + +## access_permission_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `resourceTypeId` | `char(36)` | No | FK | +| `description` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_permission_values_ibfk_1` | +| `resourceTypeId` | `access_items(id)` | RESTRICT / RESTRICT | `access_permission_values_ibfk_2` | + +## access_person_references + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `itemId` | `char(36)` | No | PK, FK | +| `personId` | `char(36)` | No | PK, FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `itemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_person_references_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `access_person_references_ibfk_2` | + +## access_references + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `int(11)` | No | PK | +| `itemId` | `char(36)` | No | FK | +| `targetId` | `char(36)` | No | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `itemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_references_ibfk_1` | +| `targetId` | `access_items(id)` | RESTRICT / RESTRICT | `access_references_ibfk_2` | + +## access_resource_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `resourceTypeId` | `char(36)` | No | FK | +| `systemId` | `char(36)` | Sí | FK | +| `environmentId` | `char(36)` | Sí | FK | +| `reference` | `varchar(255)` | Sí | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `environmentId` | `access_items(id)` | RESTRICT / RESTRICT | `access_resource_revision_values_ibfk_4` | +| `resourceTypeId` | `access_items(id)` | RESTRICT / RESTRICT | `access_resource_revision_values_ibfk_2` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_resource_revision_values_ibfk_3` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_resource_revision_values_ibfk_1` | + +## access_resource_type_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_resource_type_revision_values_ibfk_1` | + +## access_resource_type_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_resource_type_values_ibfk_1` | + +## access_resource_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `resourceTypeId` | `char(36)` | No | FK | +| `systemId` | `char(36)` | Sí | FK | +| `environmentId` | `char(36)` | Sí | FK | +| `reference` | `varchar(255)` | Sí | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `environmentId` | `access_items(id)` | RESTRICT / RESTRICT | `access_resource_values_ibfk_4` | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_resource_values_ibfk_1` | +| `resourceTypeId` | `access_items(id)` | RESTRICT / RESTRICT | `access_resource_values_ibfk_2` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_resource_values_ibfk_3` | + +## access_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `itemId` | `char(36)` | No | FK | +| `sequence` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `itemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `access_revisions_ibfk_2` | + +## access_system_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_system_revision_values_ibfk_1` | + +## access_system_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_system_values_ibfk_1` | + +## access_template_revision_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `description` | `text` | Sí | — | +| `systemId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_template_revision_values_ibfk_2` | +| `ownerId` | `access_revisions(id)` | CASCADE / RESTRICT | `access_template_revision_values_ibfk_1` | + +## access_template_values + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `description` | `text` | Sí | — | +| `systemId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `access_items(id)` | CASCADE / RESTRICT | `access_template_values_ibfk_1` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `access_template_values_ibfk_2` | + +## access_workspaces + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | Sí | FK | +| `scopeKey` | `varchar(40)` | No | UNIQUE | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `access_workspaces_ibfk_1` | + +## application_configuration_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `configurationId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `profileId` | `char(36)` | Sí | FK | +| `resourceId` | `char(36)` | Sí | FK | +| `delivery` | `varchar(16)` | No | — | +| `version` | `varchar(100)` | Sí | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `configurationId` | `application_configurations(id)` | RESTRICT / RESTRICT | `application_configuration_revisions_ibfk_1` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `application_configuration_revisions_ibfk_10`, `application_configuration_revisions_ibfk_12`, `application_configuration_revisions_ibfk_14`, `application_configuration_revisions_ibfk_16`, `application_configuration_revisions_ibfk_18`, `application_configuration_revisions_ibfk_20`, `application_configuration_revisions_ibfk_22`, `application_configuration_revisions_ibfk_24`, `application_configuration_revisions_ibfk_26`, `application_configuration_revisions_ibfk_28`, `application_configuration_revisions_ibfk_30`, `application_configuration_revisions_ibfk_32`, `application_configuration_revisions_ibfk_34`, `application_configuration_revisions_ibfk_36`, `application_configuration_revisions_ibfk_38`, `application_configuration_revisions_ibfk_4`, `application_configuration_revisions_ibfk_40`, `application_configuration_revisions_ibfk_42`, `application_configuration_revisions_ibfk_44`, `application_configuration_revisions_ibfk_46`, `application_configuration_revisions_ibfk_48`, `application_configuration_revisions_ibfk_50`, `application_configuration_revisions_ibfk_52`, `application_configuration_revisions_ibfk_54`, `application_configuration_revisions_ibfk_56`, `application_configuration_revisions_ibfk_58`, `application_configuration_revisions_ibfk_6`, `application_configuration_revisions_ibfk_60`, `application_configuration_revisions_ibfk_62`, `application_configuration_revisions_ibfk_64`, `application_configuration_revisions_ibfk_66`, `application_configuration_revisions_ibfk_68`, `application_configuration_revisions_ibfk_70`, `application_configuration_revisions_ibfk_72`, `application_configuration_revisions_ibfk_74`, `application_configuration_revisions_ibfk_76`, `application_configuration_revisions_ibfk_78`, `application_configuration_revisions_ibfk_8`, `application_configuration_revisions_ibfk_80`, `application_configuration_revisions_ibfk_82`, `application_configuration_revisions_resourceId_foreign_idx` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `application_configuration_revisions_ibfk_2` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `application_configuration_revisions_ibfk_11`, `application_configuration_revisions_ibfk_13`, `application_configuration_revisions_ibfk_15`, `application_configuration_revisions_ibfk_17`, `application_configuration_revisions_ibfk_19`, `application_configuration_revisions_ibfk_21`, `application_configuration_revisions_ibfk_23`, `application_configuration_revisions_ibfk_25`, `application_configuration_revisions_ibfk_27`, `application_configuration_revisions_ibfk_29`, `application_configuration_revisions_ibfk_3`, `application_configuration_revisions_ibfk_31`, `application_configuration_revisions_ibfk_33`, `application_configuration_revisions_ibfk_35`, `application_configuration_revisions_ibfk_37`, `application_configuration_revisions_ibfk_39`, `application_configuration_revisions_ibfk_41`, `application_configuration_revisions_ibfk_43`, `application_configuration_revisions_ibfk_45`, `application_configuration_revisions_ibfk_47`, `application_configuration_revisions_ibfk_49`, `application_configuration_revisions_ibfk_5`, `application_configuration_revisions_ibfk_51`, `application_configuration_revisions_ibfk_53`, `application_configuration_revisions_ibfk_55`, `application_configuration_revisions_ibfk_57`, `application_configuration_revisions_ibfk_59`, `application_configuration_revisions_ibfk_61`, `application_configuration_revisions_ibfk_63`, `application_configuration_revisions_ibfk_65`, `application_configuration_revisions_ibfk_67`, `application_configuration_revisions_ibfk_69`, `application_configuration_revisions_ibfk_7`, `application_configuration_revisions_ibfk_71`, `application_configuration_revisions_ibfk_73`, `application_configuration_revisions_ibfk_75`, `application_configuration_revisions_ibfk_77`, `application_configuration_revisions_ibfk_79`, `application_configuration_revisions_ibfk_81`, `application_configuration_revisions_ibfk_9`, `application_configuration_revisions_profileId_foreign_idx` | + +## application_configurations + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `profileId` | `char(36)` | No | FK | +| `resourceId` | `char(36)` | No | FK | +| `currentKey` | `varchar(80)` | Sí | UNIQUE | +| `delivery` | `varchar(16)` | No | — | +| `version` | `varchar(100)` | Sí | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `application_configurations_ibfk_1` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `application_configurations_ibfk_3` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `application_configurations_ibfk_2` | + +## asset_availability + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `catalogId` | `char(36)` | No | FK | +| `companyId` | `char(36)` | No | FK | +| `definitionId` | `char(36)` | No | FK | +| `active` | `tinyint(1)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `catalogId` | `asset_catalogs(id)` | RESTRICT / RESTRICT | `asset_availability_ibfk_1` | +| `definitionId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_availability_ibfk_3` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `asset_availability_ibfk_2` | + +## asset_brands + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `name` | `varchar(100)` | No | UNIQUE | +| `active` | `tinyint(1)` | No | — | +| `builtin` | `tinyint(1)` | No | — | +| `logoSvg` | `text` | Sí | — | + +## asset_catalogs + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `catalogKey` | `varchar(20)` | No | UNIQUE | + +## asset_characteristics + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `assetId` | `char(36)` | No | PK, FK | +| `fieldId` | `char(36)` | No | PK, FK | +| `valueType` | `varchar(12)` | No | — | +| `textValue` | `text` | Sí | — | +| `numberValue` | `double` | Sí | — | +| `booleanValue` | `tinyint(1)` | Sí | — | +| `dateValue` | `date` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_characteristics_ibfk_2` | +| `assetId`, `fieldId` | `asset_specification_fields(assetId, fieldId)` | RESTRICT / RESTRICT | `asset_characteristics_snapshot` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `asset_characteristics_ibfk_1` | + +## asset_definitions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `catalogId` | `char(36)` | No | FK | +| `companyId` | `char(36)` | Sí | FK | +| `stockKey` | `varchar(100)` | Sí | UNIQUE | +| `active` | `tinyint(1)` | No | — | +| `kind` | `varchar(12)` | No | — | +| `name` | `varchar(100)` | No | — | +| `description` | `text` | No | — | +| `fieldType` | `varchar(12)` | Sí | — | +| `unit` | `varchar(30)` | Sí | — | +| `inverseName` | `varchar(100)` | Sí | — | +| `symmetric` | `tinyint(1)` | Sí | — | +| `sectionsDefined` | `tinyint(1)` | No | — | +| `icon` | `varchar(30)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `catalogId` | `asset_catalogs(id)` | RESTRICT / RESTRICT | `asset_definitions_ibfk_1` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `asset_definitions_ibfk_2` | + +## asset_field_choices + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `fieldId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `value` | `varchar(100)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_field_choices_ibfk_1` | + +## asset_relationships + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `sourceId` | `char(36)` | No | FK | +| `targetId` | `char(36)` | No | FK | +| `typeId` | `char(36)` | No | FK | +| `createdBy` | `char(36)` | No | FK | +| `createdAt` | `varchar(30)` | No | — | +| `endedBy` | `char(36)` | Sí | FK | +| `endedAt` | `varchar(30)` | Sí | — | +| `activeKey` | `varchar(150)` | Sí | UNIQUE | +| `name` | `varchar(100)` | No | — | +| `inverseName` | `varchar(100)` | No | — | +| `symmetric` | `tinyint(1)` | No | — | +| `notes` | `text` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `typeId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_relationships_ibfk_4` | +| `companyId`, `sourceId` | `assets(companyId, id)` | RESTRICT / RESTRICT | `relationship_source_company` | +| `companyId`, `targetId` | `assets(companyId, id)` | RESTRICT / RESTRICT | `relationship_target_company` | +| `sourceId` | `assets(id)` | RESTRICT / RESTRICT | `asset_relationships_ibfk_2` | +| `targetId` | `assets(id)` | RESTRICT / RESTRICT | `asset_relationships_ibfk_3` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `asset_relationships_ibfk_1` | +| `createdBy` | `users(id)` | RESTRICT / RESTRICT | `asset_relationships_ibfk_5` | +| `endedBy` | `users(id)` | RESTRICT / RESTRICT | `asset_relationships_ibfk_6` | + +## asset_revision_characteristics + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `revisionId` | `char(36)` | No | PK, FK | +| `assetId` | `char(36)` | No | FK | +| `fieldId` | `char(36)` | No | PK, FK | +| `valueType` | `varchar(12)` | No | — | +| `textValue` | `text` | Sí | — | +| `numberValue` | `double` | Sí | — | +| `booleanValue` | `tinyint(1)` | Sí | — | +| `dateValue` | `date` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_revision_characteristics_ibfk_3` | +| `revisionId` | `asset_revisions(id)` | RESTRICT / RESTRICT | `asset_revision_characteristics_ibfk_1` | +| `revisionId`, `assetId` | `asset_revisions(id, assetId)` | RESTRICT / RESTRICT | `asset_value_revision_owner` | +| `assetId`, `fieldId` | `asset_specification_fields(assetId, fieldId)` | RESTRICT / RESTRICT | `asset_revision_characteristics_snapshot` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `asset_revision_characteristics_ibfk_2` | + +## asset_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `assetId` | `char(36)` | No | FK | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(30)` | No | — | +| `position` | `int(11)` | No | — | +| `siteId` | `char(36)` | Sí | FK | +| `brandId` | `char(36)` | Sí | FK | +| `name` | `varchar(100)` | No | — | +| `status` | `varchar(15)` | No | — | +| `notes` | `text` | No | — | +| `templateId` | `char(36)` | No | FK | +| `brandDefined` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `brandId` | `asset_brands(id)` | RESTRICT / RESTRICT | `asset_revisions_brandId_foreign_idx` | +| `templateId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_revisions_templateId_foreign_idx` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `asset_revisions_ibfk_1` | +| `siteId` | `company_sites(id)` | RESTRICT / RESTRICT | `asset_revisions_siteId_foreign_idx` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `asset_revisions_ibfk_2` | + +## asset_specification_field_choices + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `assetId` | `char(36)` | No | PK, FK | +| `fieldId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `value` | `varchar(100)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_specification_field_choices_ibfk_2` | +| `assetId`, `fieldId` | `asset_specification_fields(assetId, fieldId)` | RESTRICT / RESTRICT | `asset_specification_field_choices_snapshot` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `asset_specification_field_choices_ibfk_1` | + +## asset_specification_fields + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `assetId` | `char(36)` | No | PK, FK | +| `fieldId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `fieldType` | `varchar(12)` | No | — | +| `unit` | `varchar(30)` | No | — | +| `required` | `tinyint(1)` | No | — | +| `sectionId` | `char(36)` | Sí | FK | +| `position` | `int(11)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_specification_fields_ibfk_2` | +| `assetId`, `sectionId` | `asset_specification_sections(assetId, id)` | RESTRICT / RESTRICT | `asset_snapshot_section` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `asset_specification_fields_ibfk_1` | + +## asset_specification_sections + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `assetId` | `char(36)` | No | PK, FK | +| `id` | `char(36)` | No | PK | +| `name` | `varchar(100)` | No | — | +| `description` | `text` | No | — | +| `position` | `int(11)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `asset_specification_sections_ibfk_1` | + +## asset_support + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `assetId` | `char(36)` | No | FK | +| `preferredKey` | `char(36)` | Sí | UNIQUE | +| `destination` | `varchar(300)` | No | — | +| `requiresVpn` | `tinyint(1)` | No | — | +| `preferred` | `tinyint(1)` | No | — | +| `state` | `varchar(12)` | No | — | +| `notes` | `text` | No | — | +| `resourceId` | `char(36)` | No | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `asset_support_ibfk_2` | +| `companyId`, `assetId` | `assets(companyId, id)` | RESTRICT / RESTRICT | `support_asset_company` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `asset_support_ibfk_1` | +| `companyId`, `resourceId` | `resources(companyId, id)` | RESTRICT / RESTRICT | `support_resource_company` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `asset_support_resource_reference` | + +## asset_support_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `connectionId` | `char(36)` | No | FK | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(30)` | No | — | +| `position` | `int(11)` | No | — | +| `destination` | `varchar(300)` | No | — | +| `requiresVpn` | `tinyint(1)` | No | — | +| `preferred` | `tinyint(1)` | No | — | +| `state` | `varchar(12)` | No | — | +| `notes` | `text` | No | — | +| `resourceId` | `char(36)` | No | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `connectionId` | `asset_support(id)` | RESTRICT / RESTRICT | `asset_support_revisions_ibfk_1` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `asset_support_revisions_resource_reference` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `asset_support_revisions_ibfk_2` | + +## asset_template_fields + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `templateId` | `char(36)` | No | FK | +| `fieldId` | `char(36)` | No | FK | +| `required` | `tinyint(1)` | No | — | +| `position` | `int(11)` | No | — | +| `sectionId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_template_fields_ibfk_2` | +| `templateId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_template_fields_ibfk_1` | +| `sectionId` | `asset_template_sections(id)` | RESTRICT / RESTRICT | `asset_template_fields_sectionId_foreign_idx` | +| `templateId`, `sectionId` | `asset_template_sections(templateId, id)` | RESTRICT / RESTRICT | `asset_binding_section_template` | + +## asset_template_sections + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `templateId` | `char(36)` | No | FK | +| `name` | `varchar(100)` | No | — | +| `description` | `text` | No | — | +| `position` | `int(11)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `templateId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `asset_template_sections_ibfk_1` | + +## assets + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `templateId` | `char(36)` | No | FK | +| `siteId` | `char(36)` | Sí | FK | +| `brandId` | `char(36)` | Sí | FK | +| `name` | `varchar(100)` | No | — | +| `status` | `varchar(15)` | No | — | +| `notes` | `text` | No | — | +| `templateName` | `varchar(100)` | No | — | +| `sectionsDefined` | `tinyint(1)` | No | — | +| `brandDefined` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `brandId` | `asset_brands(id)` | RESTRICT / RESTRICT | `assets_brandId_foreign_idx` | +| `templateId` | `asset_definitions(id)` | RESTRICT / RESTRICT | `assets_ibfk_2` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `assets_ibfk_1` | +| `siteId` | `company_sites(id)` | RESTRICT / RESTRICT | `assets_ibfk_3` | + +## brand_catalog_lock + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `int(11)` | No | PK | +| `legacyImported` | `tinyint(1)` | No | — | +| `logosImported` | `tinyint(1)` | No | — | + +## certificate_configuration_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `configurationId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `profileId` | `char(36)` | Sí | FK | +| `resourceId` | `char(36)` | Sí | FK | +| `location` | `varchar(255)` | Sí | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `configurationId` | `certificate_configurations(id)` | RESTRICT / RESTRICT | `certificate_configuration_revisions_ibfk_1` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `certificate_configuration_revisions_ibfk_10`, `certificate_configuration_revisions_ibfk_12`, `certificate_configuration_revisions_ibfk_14`, `certificate_configuration_revisions_ibfk_16`, `certificate_configuration_revisions_ibfk_18`, `certificate_configuration_revisions_ibfk_20`, `certificate_configuration_revisions_ibfk_22`, `certificate_configuration_revisions_ibfk_24`, `certificate_configuration_revisions_ibfk_26`, `certificate_configuration_revisions_ibfk_28`, `certificate_configuration_revisions_ibfk_30`, `certificate_configuration_revisions_ibfk_32`, `certificate_configuration_revisions_ibfk_34`, `certificate_configuration_revisions_ibfk_36`, `certificate_configuration_revisions_ibfk_38`, `certificate_configuration_revisions_ibfk_4`, `certificate_configuration_revisions_ibfk_40`, `certificate_configuration_revisions_ibfk_42`, `certificate_configuration_revisions_ibfk_44`, `certificate_configuration_revisions_ibfk_46`, `certificate_configuration_revisions_ibfk_48`, `certificate_configuration_revisions_ibfk_50`, `certificate_configuration_revisions_ibfk_52`, `certificate_configuration_revisions_ibfk_54`, `certificate_configuration_revisions_ibfk_56`, `certificate_configuration_revisions_ibfk_58`, `certificate_configuration_revisions_ibfk_6`, `certificate_configuration_revisions_ibfk_60`, `certificate_configuration_revisions_ibfk_62`, `certificate_configuration_revisions_ibfk_64`, `certificate_configuration_revisions_ibfk_66`, `certificate_configuration_revisions_ibfk_68`, `certificate_configuration_revisions_ibfk_70`, `certificate_configuration_revisions_ibfk_72`, `certificate_configuration_revisions_ibfk_74`, `certificate_configuration_revisions_ibfk_76`, `certificate_configuration_revisions_ibfk_78`, `certificate_configuration_revisions_ibfk_8`, `certificate_configuration_revisions_ibfk_80`, `certificate_configuration_revisions_ibfk_82`, `certificate_configuration_revisions_resourceId_foreign_idx` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `certificate_configuration_revisions_ibfk_2` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `certificate_configuration_revisions_ibfk_11`, `certificate_configuration_revisions_ibfk_13`, `certificate_configuration_revisions_ibfk_15`, `certificate_configuration_revisions_ibfk_17`, `certificate_configuration_revisions_ibfk_19`, `certificate_configuration_revisions_ibfk_21`, `certificate_configuration_revisions_ibfk_23`, `certificate_configuration_revisions_ibfk_25`, `certificate_configuration_revisions_ibfk_27`, `certificate_configuration_revisions_ibfk_29`, `certificate_configuration_revisions_ibfk_3`, `certificate_configuration_revisions_ibfk_31`, `certificate_configuration_revisions_ibfk_33`, `certificate_configuration_revisions_ibfk_35`, `certificate_configuration_revisions_ibfk_37`, `certificate_configuration_revisions_ibfk_39`, `certificate_configuration_revisions_ibfk_41`, `certificate_configuration_revisions_ibfk_43`, `certificate_configuration_revisions_ibfk_45`, `certificate_configuration_revisions_ibfk_47`, `certificate_configuration_revisions_ibfk_49`, `certificate_configuration_revisions_ibfk_5`, `certificate_configuration_revisions_ibfk_51`, `certificate_configuration_revisions_ibfk_53`, `certificate_configuration_revisions_ibfk_55`, `certificate_configuration_revisions_ibfk_57`, `certificate_configuration_revisions_ibfk_59`, `certificate_configuration_revisions_ibfk_61`, `certificate_configuration_revisions_ibfk_63`, `certificate_configuration_revisions_ibfk_65`, `certificate_configuration_revisions_ibfk_67`, `certificate_configuration_revisions_ibfk_69`, `certificate_configuration_revisions_ibfk_7`, `certificate_configuration_revisions_ibfk_71`, `certificate_configuration_revisions_ibfk_73`, `certificate_configuration_revisions_ibfk_75`, `certificate_configuration_revisions_ibfk_77`, `certificate_configuration_revisions_ibfk_79`, `certificate_configuration_revisions_ibfk_81`, `certificate_configuration_revisions_ibfk_9`, `certificate_configuration_revisions_profileId_foreign_idx` | + +## certificate_configurations + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `profileId` | `char(36)` | No | FK | +| `resourceId` | `char(36)` | No | FK | +| `currentKey` | `varchar(80)` | Sí | UNIQUE | +| `location` | `varchar(255)` | Sí | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `certificate_configurations_ibfk_1` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `certificate_configurations_ibfk_3` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `certificate_configurations_ibfk_2` | + +## companies + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `logoVersion` | `varchar(64)` | Sí | — | + +## company_contacts + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `name` | `varchar(100)` | No | — | +| `position` | `varchar(100)` | Sí | — | +| `email` | `varchar(254)` | Sí | — | +| `phone` | `varchar(50)` | Sí | — | +| `active` | `tinyint(1)` | No | — | +| `isContact` | `tinyint(1)` | No | — | +| `photoVersion` | `varchar(64)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `company_contacts_ibfk_1` | + +## company_departments + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `name` | `varchar(100)` | No | — | +| `nameKey` | `varchar(200)` | No | — | +| `active` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `company_departments_ibfk_1` | + +## company_logos + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `companyId` | `char(36)` | No | PK, FK | +| `data` | `mediumblob` | No | — | +| `version` | `varchar(64)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | CASCADE / RESTRICT | `company_logos_ibfk_1` | + +## company_sites + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `name` | `varchar(100)` | No | — | +| `address` | `text` | Sí | — | +| `phone` | `varchar(50)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `company_sites_ibfk_1` | + +## department_assignment_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `assignmentId` | `char(36)` | No | FK | +| `sequence` | `int(11)` | No | — | +| `position` | `varchar(100)` | Sí | — | +| `startDate` | `date` | No | — | +| `endDate` | `date` | Sí | — | +| `isPrimary` | `tinyint(1)` | No | — | +| `recordedAt` | `varchar(24)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assignmentId` | `person_department_assignments(id)` | RESTRICT / RESTRICT | `department_assignment_revisions_ibfk_1` | + +## folder_mapping_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `mappingId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `profileId` | `char(36)` | Sí | FK | +| `needId` | `char(36)` | Sí | FK | +| `path` | `varchar(255)` | No | — | +| `driveLetter` | `varchar(2)` | Sí | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `needId` | `access_items(id)` | RESTRICT / RESTRICT | `folder_mapping_revisions_ibfk_10`, `folder_mapping_revisions_ibfk_12`, `folder_mapping_revisions_ibfk_14`, `folder_mapping_revisions_ibfk_16`, `folder_mapping_revisions_ibfk_18`, `folder_mapping_revisions_ibfk_20`, `folder_mapping_revisions_ibfk_22`, `folder_mapping_revisions_ibfk_24`, `folder_mapping_revisions_ibfk_26`, `folder_mapping_revisions_ibfk_28`, `folder_mapping_revisions_ibfk_30`, `folder_mapping_revisions_ibfk_32`, `folder_mapping_revisions_ibfk_34`, `folder_mapping_revisions_ibfk_36`, `folder_mapping_revisions_ibfk_38`, `folder_mapping_revisions_ibfk_4`, `folder_mapping_revisions_ibfk_40`, `folder_mapping_revisions_ibfk_42`, `folder_mapping_revisions_ibfk_44`, `folder_mapping_revisions_ibfk_46`, `folder_mapping_revisions_ibfk_48`, `folder_mapping_revisions_ibfk_50`, `folder_mapping_revisions_ibfk_52`, `folder_mapping_revisions_ibfk_54`, `folder_mapping_revisions_ibfk_56`, `folder_mapping_revisions_ibfk_58`, `folder_mapping_revisions_ibfk_6`, `folder_mapping_revisions_ibfk_60`, `folder_mapping_revisions_ibfk_62`, `folder_mapping_revisions_ibfk_64`, `folder_mapping_revisions_ibfk_66`, `folder_mapping_revisions_ibfk_68`, `folder_mapping_revisions_ibfk_70`, `folder_mapping_revisions_ibfk_72`, `folder_mapping_revisions_ibfk_74`, `folder_mapping_revisions_ibfk_76`, `folder_mapping_revisions_ibfk_78`, `folder_mapping_revisions_ibfk_8`, `folder_mapping_revisions_ibfk_80`, `folder_mapping_revisions_ibfk_82`, `folder_mapping_revisions_needId_foreign_idx` | +| `mappingId` | `folder_mappings(id)` | RESTRICT / RESTRICT | `folder_mapping_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `folder_mapping_revisions_ibfk_2` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `folder_mapping_revisions_ibfk_11`, `folder_mapping_revisions_ibfk_13`, `folder_mapping_revisions_ibfk_15`, `folder_mapping_revisions_ibfk_17`, `folder_mapping_revisions_ibfk_19`, `folder_mapping_revisions_ibfk_21`, `folder_mapping_revisions_ibfk_23`, `folder_mapping_revisions_ibfk_25`, `folder_mapping_revisions_ibfk_27`, `folder_mapping_revisions_ibfk_29`, `folder_mapping_revisions_ibfk_3`, `folder_mapping_revisions_ibfk_31`, `folder_mapping_revisions_ibfk_33`, `folder_mapping_revisions_ibfk_35`, `folder_mapping_revisions_ibfk_37`, `folder_mapping_revisions_ibfk_39`, `folder_mapping_revisions_ibfk_41`, `folder_mapping_revisions_ibfk_43`, `folder_mapping_revisions_ibfk_45`, `folder_mapping_revisions_ibfk_47`, `folder_mapping_revisions_ibfk_49`, `folder_mapping_revisions_ibfk_5`, `folder_mapping_revisions_ibfk_51`, `folder_mapping_revisions_ibfk_53`, `folder_mapping_revisions_ibfk_55`, `folder_mapping_revisions_ibfk_57`, `folder_mapping_revisions_ibfk_59`, `folder_mapping_revisions_ibfk_61`, `folder_mapping_revisions_ibfk_63`, `folder_mapping_revisions_ibfk_65`, `folder_mapping_revisions_ibfk_67`, `folder_mapping_revisions_ibfk_69`, `folder_mapping_revisions_ibfk_7`, `folder_mapping_revisions_ibfk_71`, `folder_mapping_revisions_ibfk_73`, `folder_mapping_revisions_ibfk_75`, `folder_mapping_revisions_ibfk_77`, `folder_mapping_revisions_ibfk_79`, `folder_mapping_revisions_ibfk_81`, `folder_mapping_revisions_ibfk_9`, `folder_mapping_revisions_profileId_foreign_idx` | + +## folder_mappings + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `profileId` | `char(36)` | No | FK | +| `needId` | `char(36)` | No | FK | +| `currentKey` | `varchar(110)` | Sí | UNIQUE | +| `driveKey` | `varchar(40)` | Sí | UNIQUE | +| `path` | `varchar(255)` | No | — | +| `driveLetter` | `varchar(2)` | Sí | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `needId` | `access_items(id)` | RESTRICT / RESTRICT | `folder_mappings_ibfk_3` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `folder_mappings_ibfk_1` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `folder_mappings_ibfk_2` | + +## http_sessions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `sid` | `varchar(36)` | No | PK | +| `expires` | `datetime` | Sí | — | +| `createdAt` | `datetime` | No | — | +| `updatedAt` | `datetime` | No | — | +| `userId` | `char(36)` | Sí | FK | +| `authVersion` | `int(11)` | Sí | — | +| `csrfToken` | `varchar(128)` | Sí | — | +| `cookieExpires` | `varchar(30)` | Sí | — | +| `originalMaxAge` | `bigint(20)` | Sí | — | +| `cookiePath` | `varchar(255)` | Sí | — | +| `cookieDomain` | `varchar(255)` | Sí | — | +| `cookieSecure` | `varchar(10)` | Sí | — | +| `cookieHttpOnly` | `tinyint(1)` | Sí | — | +| `cookieSameSite` | `varchar(10)` | Sí | — | +| `cookiePartitioned` | `tinyint(1)` | Sí | — | +| `cookiePriority` | `varchar(10)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `userId` | `users(id)` | SET NULL / RESTRICT | `http_sessions_userId_foreign_idx` | + +## identity_state + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `int(11)` | No | PK | +| `initialAdminId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `initialAdminId` | `users(id)` | RESTRICT / RESTRICT | `identity_state_ibfk_1` | + +## intervention_counter + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `int(11)` | No | PK | +| `nextNumber` | `int(11)` | No | — | + +## intervention_procedure_steps + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `stepId` | `char(36)` | No | PK | +| `position` | `int(11)` | No | — | +| `title` | `varchar(200)` | No | — | +| `instructions` | `text` | No | — | +| `required` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `intervention_procedures(interventionId)` | CASCADE / RESTRICT | `intervention_procedure_steps_ibfk_1` | + +## intervention_procedures + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `interventionId` | `char(36)` | No | PK, FK | +| `templateId` | `char(36)` | No | FK | +| `revisionId` | `char(36)` | No | FK | +| `name` | `varchar(160)` | No | — | +| `description` | `text` | No | — | +| `active` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `revisionId` | `intervention_template_revisions(id)` | RESTRICT / RESTRICT | `intervention_procedures_ibfk_3` | +| `templateId`, `revisionId` | `intervention_template_revisions(templateId, id)` | RESTRICT / RESTRICT | `intervention_procedure_template_revision` | +| `templateId` | `intervention_templates(id)` | RESTRICT / RESTRICT | `intervention_procedures_ibfk_2` | +| `interventionId` | `interventions(id)` | CASCADE / RESTRICT | `intervention_procedures_ibfk_1` | + +## intervention_revision_steps + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `stepId` | `char(36)` | No | PK | +| `position` | `int(11)` | No | — | +| `title` | `varchar(200)` | No | — | +| `instructions` | `text` | No | — | +| `required` | `tinyint(1)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `intervention_revisions(id)` | CASCADE / RESTRICT | `intervention_revision_steps_ibfk_1` | + +## intervention_revision_targets + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `personId` | `char(36)` | Sí | FK | +| `workstationId` | `char(36)` | Sí | FK | +| `assetId` | `char(36)` | Sí | FK | +| `resourceId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `intervention_revision_targets_ibfk_4` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `intervention_revision_targets_ibfk_2` | +| `ownerId` | `intervention_revisions(id)` | CASCADE / RESTRICT | `intervention_revision_targets_ibfk_1` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `intervention_revision_targets_ibfk_5` | +| `workstationId` | `workstations(id)` | RESTRICT / RESTRICT | `intervention_revision_targets_ibfk_3` | + +## intervention_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `interventionId` | `char(36)` | No | FK | +| `actorId` | `char(36)` | No | FK | +| `ticketId` | `char(36)` | Sí | FK | +| `assigneeId` | `char(36)` | Sí | FK | +| `recordedAt` | `varchar(30)` | No | — | +| `position` | `int(11)` | No | — | +| `subject` | `varchar(200)` | No | — | +| `state` | `varchar(20)` | No | — | +| `modality` | `varchar(20)` | No | — | +| `plannedDate` | `date` | Sí | — | +| `startedAt` | `varchar(30)` | Sí | — | +| `endedAt` | `varchar(30)` | Sí | — | +| `minutes` | `int(11)` | Sí | — | +| `diagnosis` | `text` | No | — | +| `workPerformed` | `text` | No | — | +| `resolution` | `text` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `interventionId` | `interventions(id)` | RESTRICT / RESTRICT | `intervention_revisions_ibfk_1` | +| `ticketId` | `tickets(id)` | RESTRICT / RESTRICT | `intervention_revisions_ibfk_3` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `intervention_revisions_ibfk_2` | +| `assigneeId` | `users(id)` | RESTRICT / RESTRICT | `intervention_revisions_ibfk_4` | + +## intervention_steps + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `stepId` | `char(36)` | No | PK | +| `position` | `int(11)` | No | — | +| `title` | `varchar(200)` | No | — | +| `instructions` | `text` | No | — | +| `required` | `tinyint(1)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `interventions(id)` | CASCADE / RESTRICT | `intervention_steps_ibfk_1` | + +## intervention_targets + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `personId` | `char(36)` | Sí | FK | +| `workstationId` | `char(36)` | Sí | FK | +| `assetId` | `char(36)` | Sí | FK | +| `resourceId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `intervention_targets_ibfk_4` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `intervention_targets_ibfk_2` | +| `ownerId` | `interventions(id)` | CASCADE / RESTRICT | `intervention_targets_ibfk_1` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `intervention_targets_ibfk_5` | +| `workstationId` | `workstations(id)` | RESTRICT / RESTRICT | `intervention_targets_ibfk_3` | + +## intervention_template_lock + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `int(11)` | No | PK | + +## intervention_template_revision_steps + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `stepId` | `char(36)` | No | PK | +| `position` | `int(11)` | No | — | +| `title` | `varchar(200)` | No | — | +| `instructions` | `text` | No | — | +| `required` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `intervention_template_revisions(id)` | CASCADE / RESTRICT | `intervention_template_revision_steps_ibfk_1` | + +## intervention_template_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `templateId` | `char(36)` | No | FK | +| `actorId` | `char(36)` | Sí | FK | +| `recordedAt` | `varchar(30)` | No | — | +| `position` | `int(11)` | No | — | +| `name` | `varchar(160)` | No | — | +| `description` | `text` | No | — | +| `active` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `templateId` | `intervention_templates(id)` | RESTRICT / RESTRICT | `intervention_template_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `intervention_template_revisions_ibfk_2` | + +## intervention_template_steps + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `stepId` | `char(36)` | No | PK | +| `position` | `int(11)` | No | — | +| `title` | `varchar(200)` | No | — | +| `instructions` | `text` | No | — | +| `required` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `intervention_templates(id)` | CASCADE / RESTRICT | `intervention_template_steps_ibfk_1` | + +## intervention_templates + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | Sí | FK | +| `requestKey` | `char(36)` | No | UNIQUE | +| `name` | `varchar(160)` | No | — | +| `description` | `text` | No | — | +| `active` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `intervention_templates_ibfk_1` | + +## interventions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `reference` | `varchar(40)` | No | UNIQUE | +| `requestKey` | `char(36)` | No | UNIQUE | +| `ticketId` | `char(36)` | Sí | FK | +| `assigneeId` | `char(36)` | Sí | FK | +| `subject` | `varchar(200)` | No | — | +| `state` | `varchar(20)` | No | — | +| `modality` | `varchar(20)` | No | — | +| `plannedDate` | `date` | Sí | — | +| `startedAt` | `varchar(30)` | Sí | — | +| `endedAt` | `varchar(30)` | Sí | — | +| `minutes` | `int(11)` | Sí | — | +| `diagnosis` | `text` | No | — | +| `workPerformed` | `text` | No | — | +| `resolution` | `text` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `interventions_ibfk_1` | +| `ticketId` | `tickets(id)` | RESTRICT / RESTRICT | `interventions_ibfk_2` | +| `assigneeId` | `users(id)` | RESTRICT / RESTRICT | `interventions_ibfk_3` | + +## mail_configuration_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `configurationId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `snapshotKind` | `varchar(10)` | No | — | +| `personId` | `char(36)` | Sí | FK | +| `accountId` | `char(36)` | Sí | FK | +| `assetId` | `char(36)` | Sí | FK | +| `profileId` | `char(36)` | Sí | FK | +| `mailboxId` | `char(36)` | Sí | FK | +| `address` | `varchar(254)` | No | — | +| `method` | `varchar(16)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `mail_configuration_revisions_accountId_foreign_idx` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `mail_configuration_revisions_assetId_foreign_idx` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `mail_configuration_revisions_personId_foreign_idx` | +| `configurationId` | `mail_configurations(id)` | RESTRICT / RESTRICT | `mail_configuration_revisions_ibfk_1` | +| `mailboxId` | `mailboxes(id)` | RESTRICT / RESTRICT | `mail_configuration_revisions_mailboxId_foreign_idx` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `mail_configuration_revisions_ibfk_2` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `mail_configuration_revisions_profileId_foreign_idx` | + +## mail_configurations + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `personId` | `char(36)` | Sí | FK | +| `accountId` | `char(36)` | Sí | FK | +| `assetId` | `char(36)` | Sí | FK | +| `currentKey` | `varchar(110)` | Sí | UNIQUE | +| `profileId` | `char(36)` | Sí | FK | +| `mailboxId` | `char(36)` | Sí | FK | +| `snapshotKind` | `varchar(10)` | No | — | +| `address` | `varchar(254)` | No | — | +| `method` | `varchar(16)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `mail_configurations_ibfk_3` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `mail_configurations_ibfk_4` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `mail_configurations_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `mail_configurations_ibfk_2` | +| `mailboxId` | `mailboxes(id)` | RESTRICT / RESTRICT | `mail_configurations_mailboxId_foreign_idx` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `mail_configurations_profileId_foreign_idx` | + +## mailbox_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `itemId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `address` | `varchar(254)` | No | — | +| `kind` | `varchar(20)` | No | — | +| `context` | `varchar(255)` | Sí | — | +| `accountId` | `char(36)` | Sí | FK | +| `active` | `tinyint(1)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `mailbox_revisions_accountId_foreign_idx` | +| `itemId` | `mailboxes(id)` | RESTRICT / RESTRICT | `mailbox_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `mailbox_revisions_ibfk_2` | + +## mailbox_use_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `itemId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `mailboxId` | `char(36)` | Sí | FK | +| `personId` | `char(36)` | Sí | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `mailbox_use_revisions_ibfk_10`, `mailbox_use_revisions_ibfk_12`, `mailbox_use_revisions_ibfk_14`, `mailbox_use_revisions_ibfk_16`, `mailbox_use_revisions_ibfk_18`, `mailbox_use_revisions_ibfk_20`, `mailbox_use_revisions_ibfk_22`, `mailbox_use_revisions_ibfk_24`, `mailbox_use_revisions_ibfk_26`, `mailbox_use_revisions_ibfk_28`, `mailbox_use_revisions_ibfk_30`, `mailbox_use_revisions_ibfk_32`, `mailbox_use_revisions_ibfk_34`, `mailbox_use_revisions_ibfk_36`, `mailbox_use_revisions_ibfk_38`, `mailbox_use_revisions_ibfk_4`, `mailbox_use_revisions_ibfk_40`, `mailbox_use_revisions_ibfk_42`, `mailbox_use_revisions_ibfk_44`, `mailbox_use_revisions_ibfk_46`, `mailbox_use_revisions_ibfk_48`, `mailbox_use_revisions_ibfk_50`, `mailbox_use_revisions_ibfk_52`, `mailbox_use_revisions_ibfk_54`, `mailbox_use_revisions_ibfk_56`, `mailbox_use_revisions_ibfk_6`, `mailbox_use_revisions_ibfk_8`, `mailbox_use_revisions_personId_foreign_idx` | +| `itemId` | `mailbox_uses(id)` | RESTRICT / RESTRICT | `mailbox_use_revisions_ibfk_1` | +| `mailboxId` | `mailboxes(id)` | RESTRICT / RESTRICT | `mailbox_use_revisions_ibfk_11`, `mailbox_use_revisions_ibfk_13`, `mailbox_use_revisions_ibfk_15`, `mailbox_use_revisions_ibfk_17`, `mailbox_use_revisions_ibfk_19`, `mailbox_use_revisions_ibfk_21`, `mailbox_use_revisions_ibfk_23`, `mailbox_use_revisions_ibfk_25`, `mailbox_use_revisions_ibfk_27`, `mailbox_use_revisions_ibfk_29`, `mailbox_use_revisions_ibfk_3`, `mailbox_use_revisions_ibfk_31`, `mailbox_use_revisions_ibfk_33`, `mailbox_use_revisions_ibfk_35`, `mailbox_use_revisions_ibfk_37`, `mailbox_use_revisions_ibfk_39`, `mailbox_use_revisions_ibfk_41`, `mailbox_use_revisions_ibfk_43`, `mailbox_use_revisions_ibfk_45`, `mailbox_use_revisions_ibfk_47`, `mailbox_use_revisions_ibfk_49`, `mailbox_use_revisions_ibfk_5`, `mailbox_use_revisions_ibfk_51`, `mailbox_use_revisions_ibfk_53`, `mailbox_use_revisions_ibfk_55`, `mailbox_use_revisions_ibfk_7`, `mailbox_use_revisions_ibfk_9`, `mailbox_use_revisions_mailboxId_foreign_idx` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `mailbox_use_revisions_ibfk_2` | + +## mailbox_uses + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `mailboxId` | `char(36)` | No | FK | +| `personId` | `char(36)` | No | FK | +| `startDate` | `varchar(10)` | No | — | +| `endDate` | `varchar(10)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `mailbox_uses_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `mailbox_uses_ibfk_3` | +| `mailboxId` | `mailboxes(id)` | RESTRICT / RESTRICT | `mailbox_uses_ibfk_2` | + +## mailboxes + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `accountId` | `char(36)` | Sí | FK | +| `addressKey` | `varchar(300)` | No | UNIQUE | +| `address` | `varchar(254)` | No | — | +| `kind` | `varchar(20)` | No | — | +| `context` | `varchar(255)` | Sí | — | +| `active` | `tinyint(1)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `mailboxes_ibfk_2` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `mailboxes_ibfk_1` | + +## person_department_assignments + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `personId` | `char(36)` | No | FK | +| `departmentId` | `char(36)` | No | FK | +| `position` | `varchar(100)` | Sí | — | +| `startDate` | `date` | No | — | +| `endDate` | `date` | Sí | — | +| `isPrimary` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `person_department_assignments_ibfk_1` | +| `personId`, `companyId` | `company_contacts(id, companyId)` | RESTRICT / RESTRICT | `assignment_person_company_fk` | +| `departmentId`, `companyId` | `company_departments(id, companyId)` | RESTRICT / RESTRICT | `assignment_department_company_fk` | + +## person_photos + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `personId` | `char(36)` | No | PK, FK | +| `companyId` | `char(36)` | No | FK | +| `data` | `mediumblob` | No | — | +| `version` | `varchar(64)` | No | — | +| `createdAt` | `datetime` | No | — | +| `updatedAt` | `datetime` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `personId`, `companyId` | `company_contacts(id, companyId)` | CASCADE / RESTRICT | `person_photos_person_fk` | + +## person_process_revision_tasks + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | — | +| `title` | `varchar(160)` | No | — | +| `taskId` | `char(36)` | No | PK | +| `status` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | +| `referenceKind` | `varchar(20)` | Sí | — | +| `referenceId` | `char(36)` | Sí | — | +| `referenceLabel` | `varchar(500)` | Sí | — | +| `sourceKind` | `varchar(20)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `person_process_revisions(id)` | CASCADE / RESTRICT | `person_process_revision_tasks_ibfk_1` | + +## person_process_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | Sí | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `itemId` | `char(36)` | No | FK | +| `personId` | `char(36)` | Sí | FK | +| `templateId` | `char(36)` | Sí | FK | +| `templateName` | `varchar(160)` | No | — | +| `kind` | `varchar(20)` | No | — | +| `name` | `varchar(160)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `person_process_revisions_ibfk_11`, `person_process_revisions_ibfk_13`, `person_process_revisions_ibfk_15`, `person_process_revisions_ibfk_17`, `person_process_revisions_ibfk_19`, `person_process_revisions_ibfk_21`, `person_process_revisions_ibfk_23`, `person_process_revisions_ibfk_25`, `person_process_revisions_ibfk_27`, `person_process_revisions_ibfk_29`, `person_process_revisions_ibfk_3`, `person_process_revisions_ibfk_31`, `person_process_revisions_ibfk_33`, `person_process_revisions_ibfk_35`, `person_process_revisions_ibfk_37`, `person_process_revisions_ibfk_39`, `person_process_revisions_ibfk_5`, `person_process_revisions_ibfk_7`, `person_process_revisions_ibfk_9`, `person_process_revisions_personId_foreign_idx` | +| `itemId` | `person_processes(id)` | RESTRICT / RESTRICT | `person_process_revisions_ibfk_2` | +| `templateId` | `process_templates(id)` | RESTRICT / RESTRICT | `person_process_revisions_ibfk_10`, `person_process_revisions_ibfk_12`, `person_process_revisions_ibfk_14`, `person_process_revisions_ibfk_16`, `person_process_revisions_ibfk_18`, `person_process_revisions_ibfk_20`, `person_process_revisions_ibfk_22`, `person_process_revisions_ibfk_24`, `person_process_revisions_ibfk_26`, `person_process_revisions_ibfk_28`, `person_process_revisions_ibfk_30`, `person_process_revisions_ibfk_32`, `person_process_revisions_ibfk_34`, `person_process_revisions_ibfk_36`, `person_process_revisions_ibfk_38`, `person_process_revisions_ibfk_4`, `person_process_revisions_ibfk_40`, `person_process_revisions_ibfk_6`, `person_process_revisions_ibfk_8`, `person_process_revisions_templateId_foreign_idx` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `person_process_revisions_ibfk_1` | + +## person_process_tasks + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | — | +| `title` | `varchar(160)` | No | — | +| `taskId` | `char(36)` | No | PK | +| `status` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | +| `referenceKind` | `varchar(20)` | Sí | — | +| `referenceId` | `char(36)` | Sí | — | +| `referenceLabel` | `varchar(500)` | Sí | — | +| `sourceKind` | `varchar(20)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `person_processes(id)` | CASCADE / RESTRICT | `person_process_tasks_ibfk_1` | + +## person_processes + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `personId` | `char(36)` | No | FK | +| `templateId` | `char(36)` | No | FK | +| `currentKey` | `varchar(100)` | Sí | UNIQUE | +| `templateName` | `varchar(160)` | No | — | +| `kind` | `varchar(20)` | No | — | +| `name` | `varchar(160)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `person_processes_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `person_processes_ibfk_2` | +| `templateId` | `process_templates(id)` | RESTRICT / RESTRICT | `person_processes_ibfk_3` | + +## printer_configuration_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `configurationId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `profileId` | `char(36)` | Sí | FK | +| `printerId` | `char(36)` | Sí | FK | +| `connection` | `varchar(16)` | No | — | +| `isDefault` | `tinyint(1)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `printerId` | `assets(id)` | RESTRICT / RESTRICT | `printer_configuration_revisions_ibfk_10`, `printer_configuration_revisions_ibfk_12`, `printer_configuration_revisions_ibfk_14`, `printer_configuration_revisions_ibfk_16`, `printer_configuration_revisions_ibfk_18`, `printer_configuration_revisions_ibfk_20`, `printer_configuration_revisions_ibfk_22`, `printer_configuration_revisions_ibfk_24`, `printer_configuration_revisions_ibfk_26`, `printer_configuration_revisions_ibfk_28`, `printer_configuration_revisions_ibfk_30`, `printer_configuration_revisions_ibfk_32`, `printer_configuration_revisions_ibfk_34`, `printer_configuration_revisions_ibfk_36`, `printer_configuration_revisions_ibfk_38`, `printer_configuration_revisions_ibfk_4`, `printer_configuration_revisions_ibfk_40`, `printer_configuration_revisions_ibfk_42`, `printer_configuration_revisions_ibfk_44`, `printer_configuration_revisions_ibfk_46`, `printer_configuration_revisions_ibfk_48`, `printer_configuration_revisions_ibfk_50`, `printer_configuration_revisions_ibfk_52`, `printer_configuration_revisions_ibfk_54`, `printer_configuration_revisions_ibfk_56`, `printer_configuration_revisions_ibfk_58`, `printer_configuration_revisions_ibfk_6`, `printer_configuration_revisions_ibfk_60`, `printer_configuration_revisions_ibfk_62`, `printer_configuration_revisions_ibfk_64`, `printer_configuration_revisions_ibfk_66`, `printer_configuration_revisions_ibfk_68`, `printer_configuration_revisions_ibfk_70`, `printer_configuration_revisions_ibfk_72`, `printer_configuration_revisions_ibfk_74`, `printer_configuration_revisions_ibfk_76`, `printer_configuration_revisions_ibfk_78`, `printer_configuration_revisions_ibfk_8`, `printer_configuration_revisions_ibfk_80`, `printer_configuration_revisions_ibfk_82`, `printer_configuration_revisions_printerId_foreign_idx` | +| `configurationId` | `printer_configurations(id)` | RESTRICT / RESTRICT | `printer_configuration_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `printer_configuration_revisions_ibfk_2` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `printer_configuration_revisions_ibfk_11`, `printer_configuration_revisions_ibfk_13`, `printer_configuration_revisions_ibfk_15`, `printer_configuration_revisions_ibfk_17`, `printer_configuration_revisions_ibfk_19`, `printer_configuration_revisions_ibfk_21`, `printer_configuration_revisions_ibfk_23`, `printer_configuration_revisions_ibfk_25`, `printer_configuration_revisions_ibfk_27`, `printer_configuration_revisions_ibfk_29`, `printer_configuration_revisions_ibfk_3`, `printer_configuration_revisions_ibfk_31`, `printer_configuration_revisions_ibfk_33`, `printer_configuration_revisions_ibfk_35`, `printer_configuration_revisions_ibfk_37`, `printer_configuration_revisions_ibfk_39`, `printer_configuration_revisions_ibfk_41`, `printer_configuration_revisions_ibfk_43`, `printer_configuration_revisions_ibfk_45`, `printer_configuration_revisions_ibfk_47`, `printer_configuration_revisions_ibfk_49`, `printer_configuration_revisions_ibfk_5`, `printer_configuration_revisions_ibfk_51`, `printer_configuration_revisions_ibfk_53`, `printer_configuration_revisions_ibfk_55`, `printer_configuration_revisions_ibfk_57`, `printer_configuration_revisions_ibfk_59`, `printer_configuration_revisions_ibfk_61`, `printer_configuration_revisions_ibfk_63`, `printer_configuration_revisions_ibfk_65`, `printer_configuration_revisions_ibfk_67`, `printer_configuration_revisions_ibfk_69`, `printer_configuration_revisions_ibfk_7`, `printer_configuration_revisions_ibfk_71`, `printer_configuration_revisions_ibfk_73`, `printer_configuration_revisions_ibfk_75`, `printer_configuration_revisions_ibfk_77`, `printer_configuration_revisions_ibfk_79`, `printer_configuration_revisions_ibfk_81`, `printer_configuration_revisions_ibfk_9`, `printer_configuration_revisions_profileId_foreign_idx` | + +## printer_configurations + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `profileId` | `char(36)` | No | FK | +| `printerId` | `char(36)` | No | FK | +| `currentKey` | `varchar(80)` | Sí | UNIQUE | +| `defaultKey` | `varchar(40)` | Sí | UNIQUE | +| `connection` | `varchar(16)` | No | — | +| `isDefault` | `tinyint(1)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `printerId` | `assets(id)` | RESTRICT / RESTRICT | `printer_configurations_ibfk_3` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `printer_configurations_ibfk_1` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `printer_configurations_ibfk_2` | + +## process_catalog_lock + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `int(11)` | No | PK | + +## process_template_instructions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `title` | `varchar(160)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `process_templates(id)` | CASCADE / RESTRICT | `process_template_instructions_ibfk_1` | + +## process_template_revision_instructions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `title` | `varchar(160)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `ownerId` | `process_template_revisions(id)` | CASCADE / RESTRICT | `process_template_revision_instructions_ibfk_1` | + +## process_template_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | Sí | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `itemId` | `char(36)` | No | FK | +| `name` | `varchar(160)` | No | — | +| `kind` | `varchar(20)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `itemId` | `process_templates(id)` | RESTRICT / RESTRICT | `process_template_revisions_ibfk_2` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `process_template_revisions_ibfk_1` | + +## process_templates + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `nameKey` | `varchar(160)` | No | UNIQUE | +| `builtinKey` | `varchar(30)` | Sí | UNIQUE | +| `name` | `varchar(160)` | No | — | +| `kind` | `varchar(20)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `notes` | `text` | Sí | — | + +## remote_connection_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `connectionId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `personId` | `char(36)` | Sí | FK | +| `sourceKind` | `varchar(20)` | No | — | +| `sourceAssetId` | `char(36)` | Sí | FK | +| `targetKind` | `varchar(16)` | No | — | +| `targetAssetId` | `char(36)` | Sí | FK | +| `targetResourceId` | `char(36)` | Sí | FK | +| `siteId` | `char(36)` | Sí | FK | +| `method` | `varchar(80)` | No | — | +| `requiresVpn` | `tinyint(1)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `sourceAssetId` | `assets(id)` | RESTRICT / RESTRICT | `remote_connection_revisions_sourceAssetId_foreign_idx` | +| `targetAssetId` | `assets(id)` | RESTRICT / RESTRICT | `remote_connection_revisions_targetAssetId_foreign_idx` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `remote_connection_revisions_ibfk_10`, `remote_connection_revisions_ibfk_11`, `remote_connection_revisions_ibfk_12`, `remote_connection_revisions_ibfk_13`, `remote_connection_revisions_ibfk_14`, `remote_connection_revisions_ibfk_15`, `remote_connection_revisions_ibfk_16`, `remote_connection_revisions_ibfk_17`, `remote_connection_revisions_ibfk_18`, `remote_connection_revisions_ibfk_19`, `remote_connection_revisions_ibfk_20`, `remote_connection_revisions_ibfk_21`, `remote_connection_revisions_ibfk_22`, `remote_connection_revisions_ibfk_23`, `remote_connection_revisions_ibfk_24`, `remote_connection_revisions_ibfk_25`, `remote_connection_revisions_ibfk_26`, `remote_connection_revisions_ibfk_3`, `remote_connection_revisions_ibfk_4`, `remote_connection_revisions_ibfk_5`, `remote_connection_revisions_ibfk_6`, `remote_connection_revisions_ibfk_7`, `remote_connection_revisions_ibfk_8`, `remote_connection_revisions_ibfk_9`, `remote_connection_revisions_personId_foreign_idx` | +| `siteId` | `company_sites(id)` | RESTRICT / RESTRICT | `remote_connection_revisions_siteId_foreign_idx` | +| `connectionId` | `remote_connections(id)` | RESTRICT / RESTRICT | `remote_connection_revisions_ibfk_1` | +| `targetResourceId` | `resources(id)` | RESTRICT / RESTRICT | `remote_connection_revisions_targetResourceId_foreign_idx` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `remote_connection_revisions_ibfk_2` | + +## remote_connections + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `personId` | `char(36)` | No | FK | +| `sourceAssetId` | `char(36)` | Sí | FK | +| `targetAssetId` | `char(36)` | Sí | FK | +| `targetResourceId` | `char(36)` | Sí | FK | +| `siteId` | `char(36)` | Sí | FK | +| `currentKey` | `varchar(64)` | Sí | UNIQUE | +| `sourceKind` | `varchar(20)` | No | — | +| `targetKind` | `varchar(16)` | No | — | +| `method` | `varchar(80)` | No | — | +| `requiresVpn` | `tinyint(1)` | No | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `sourceAssetId` | `assets(id)` | RESTRICT / RESTRICT | `remote_connections_ibfk_3` | +| `targetAssetId` | `assets(id)` | RESTRICT / RESTRICT | `remote_connections_ibfk_4` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `remote_connections_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `remote_connections_ibfk_2` | +| `siteId` | `company_sites(id)` | RESTRICT / RESTRICT | `remote_connections_ibfk_6` | +| `targetResourceId` | `resources(id)` | RESTRICT / RESTRICT | `remote_connections_ibfk_5` | + +## resource_availability + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `catalogId` | `char(36)` | No | FK | +| `companyId` | `char(36)` | No | FK | +| `definitionId` | `char(36)` | No | FK | +| `active` | `tinyint(1)` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `resource_availability_ibfk_2` | +| `catalogId` | `resource_catalogs(id)` | RESTRICT / RESTRICT | `resource_availability_ibfk_1` | +| `definitionId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_availability_ibfk_3` | + +## resource_catalogs + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `catalogKey` | `varchar(20)` | No | UNIQUE | + +## resource_characteristics + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `resourceId` | `char(36)` | No | PK, FK | +| `fieldId` | `char(36)` | No | PK, FK | +| `valueType` | `varchar(12)` | No | — | +| `textValue` | `text` | Sí | — | +| `numberValue` | `double` | Sí | — | +| `booleanValue` | `tinyint(1)` | Sí | — | +| `dateValue` | `date` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_characteristics_ibfk_2` | +| `resourceId` | `resources(id)` | CASCADE / RESTRICT | `resource_characteristics_ibfk_1` | + +## resource_definitions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `catalogId` | `char(36)` | No | FK | +| `companyId` | `char(36)` | Sí | FK | +| `stockKey` | `varchar(100)` | Sí | UNIQUE | +| `active` | `tinyint(1)` | No | — | +| `kind` | `varchar(12)` | No | — | +| `name` | `varchar(100)` | No | — | +| `description` | `text` | No | — | +| `fieldType` | `varchar(12)` | Sí | — | +| `unit` | `varchar(30)` | Sí | — | +| `usage` | `varchar(15)` | Sí | — | +| `sectionsDefined` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `resource_definitions_ibfk_2` | +| `catalogId` | `resource_catalogs(id)` | RESTRICT / RESTRICT | `resource_definitions_ibfk_1` | + +## resource_field_choices + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `fieldId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `value` | `varchar(100)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_field_choices_ibfk_1` | + +## resource_revision_characteristics + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `revisionId` | `char(36)` | No | PK, FK | +| `resourceId` | `char(36)` | No | FK | +| `fieldId` | `char(36)` | No | PK, FK | +| `valueType` | `varchar(12)` | No | — | +| `textValue` | `text` | Sí | — | +| `numberValue` | `double` | Sí | — | +| `booleanValue` | `tinyint(1)` | Sí | — | +| `dateValue` | `date` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_revision_characteristics_ibfk_3` | +| `revisionId` | `resource_revisions(id)` | CASCADE / RESTRICT | `resource_revision_characteristics_ibfk_1` | +| `resourceId` | `resources(id)` | CASCADE / RESTRICT | `resource_revision_characteristics_ibfk_2` | + +## resource_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `resourceId` | `char(36)` | No | FK | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(30)` | No | — | +| `position` | `int(11)` | No | — | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `notes` | `text` | No | — | +| `integrationKind` | `varchar(10)` | Sí | — | +| `addressFieldId` | `char(36)` | Sí | FK | +| `contextFieldId` | `char(36)` | Sí | FK | +| `mailboxKind` | `varchar(10)` | Sí | — | +| `accountId` | `char(36)` | Sí | FK | +| `referenceFieldId` | `char(36)` | Sí | FK | +| `resourceTypeId` | `char(36)` | Sí | FK | +| `systemId` | `char(36)` | Sí | FK | +| `environmentId` | `char(36)` | Sí | FK | +| `templateId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `resource_revisions_accountId_restrict_fk` | +| `environmentId` | `access_items(id)` | RESTRICT / RESTRICT | `resource_revisions_environmentId_restrict_fk` | +| `resourceTypeId` | `access_items(id)` | RESTRICT / RESTRICT | `resource_revisions_resourceTypeId_restrict_fk` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `resource_revisions_systemId_restrict_fk` | +| `addressFieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_revisions_addressFieldId_foreign_idx` | +| `contextFieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_revisions_contextFieldId_foreign_idx` | +| `referenceFieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_revisions_referenceFieldId_foreign_idx` | +| `templateId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_revisions_ibfk_10`, `resource_revisions_ibfk_11`, `resource_revisions_ibfk_12`, `resource_revisions_ibfk_13`, `resource_revisions_ibfk_14`, `resource_revisions_ibfk_15`, `resource_revisions_ibfk_16`, `resource_revisions_ibfk_17`, `resource_revisions_ibfk_18`, `resource_revisions_ibfk_19`, `resource_revisions_ibfk_20`, `resource_revisions_ibfk_21`, `resource_revisions_ibfk_22`, `resource_revisions_ibfk_23`, `resource_revisions_ibfk_24`, `resource_revisions_ibfk_25`, `resource_revisions_ibfk_26`, `resource_revisions_ibfk_27`, `resource_revisions_ibfk_28`, `resource_revisions_ibfk_29`, `resource_revisions_ibfk_3`, `resource_revisions_ibfk_30`, `resource_revisions_ibfk_31`, `resource_revisions_ibfk_32`, `resource_revisions_ibfk_33`, `resource_revisions_ibfk_34`, `resource_revisions_ibfk_35`, `resource_revisions_ibfk_36`, `resource_revisions_ibfk_37`, `resource_revisions_ibfk_38`, `resource_revisions_ibfk_39`, `resource_revisions_ibfk_4`, `resource_revisions_ibfk_40`, `resource_revisions_ibfk_41`, `resource_revisions_ibfk_42`, `resource_revisions_ibfk_43`, `resource_revisions_ibfk_44`, `resource_revisions_ibfk_45`, `resource_revisions_ibfk_46`, `resource_revisions_ibfk_47`, `resource_revisions_ibfk_48`, `resource_revisions_ibfk_5`, `resource_revisions_ibfk_6`, `resource_revisions_ibfk_7`, `resource_revisions_ibfk_8`, `resource_revisions_ibfk_9`, `resource_revisions_templateId_foreign_idx` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `resource_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `resource_revisions_ibfk_2` | + +## resource_specification_field_choices + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `resourceId` | `char(36)` | No | PK, FK | +| `fieldId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `value` | `varchar(100)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_specification_field_choices_ibfk_2` | +| `resourceId` | `resources(id)` | CASCADE / RESTRICT | `resource_specification_field_choices_ibfk_1` | + +## resource_specification_fields + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `resourceId` | `char(36)` | No | PK, FK | +| `fieldId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `fieldType` | `varchar(12)` | No | — | +| `unit` | `varchar(30)` | No | — | +| `required` | `tinyint(1)` | No | — | +| `sectionId` | `char(36)` | Sí | — | +| `position` | `int(11)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_specification_fields_ibfk_2` | +| `resourceId` | `resources(id)` | CASCADE / RESTRICT | `resource_specification_fields_ibfk_1` | + +## resource_specification_sections + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `resourceId` | `char(36)` | No | PK, FK | +| `id` | `char(36)` | No | PK | +| `name` | `varchar(100)` | No | — | +| `description` | `text` | No | — | +| `position` | `int(11)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `resourceId` | `resources(id)` | CASCADE / RESTRICT | `resource_specification_sections_ibfk_1` | + +## resource_template_fields + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `templateId` | `char(36)` | No | FK | +| `fieldId` | `char(36)` | No | FK | +| `required` | `tinyint(1)` | No | — | +| `position` | `int(11)` | No | — | +| `sectionId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `fieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_template_fields_ibfk_2` | +| `templateId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_template_fields_ibfk_1` | +| `templateId`, `sectionId` | `resource_template_sections(templateId, id)` | RESTRICT / RESTRICT | `resource_template_field_section` | + +## resource_template_sections + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `templateId` | `char(36)` | No | PK, FK | +| `name` | `varchar(100)` | No | — | +| `description` | `text` | No | — | +| `position` | `int(11)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `templateId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resource_template_sections_ibfk_1` | + +## resources + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `templateId` | `char(36)` | No | FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `notes` | `text` | No | — | +| `integrationKind` | `varchar(10)` | Sí | — | +| `addressFieldId` | `char(36)` | Sí | FK | +| `contextFieldId` | `char(36)` | Sí | FK | +| `mailboxKind` | `varchar(10)` | Sí | — | +| `accountId` | `char(36)` | Sí | FK | +| `referenceFieldId` | `char(36)` | Sí | FK | +| `resourceTypeId` | `char(36)` | Sí | FK | +| `systemId` | `char(36)` | Sí | FK | +| `environmentId` | `char(36)` | Sí | FK | +| `templateName` | `varchar(100)` | No | — | +| `sectionsDefined` | `tinyint(1)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `resources_accountId_restrict_fk` | +| `environmentId` | `access_items(id)` | RESTRICT / RESTRICT | `resources_environmentId_restrict_fk` | +| `resourceTypeId` | `access_items(id)` | RESTRICT / RESTRICT | `resources_resourceTypeId_restrict_fk` | +| `systemId` | `access_items(id)` | RESTRICT / RESTRICT | `resources_systemId_restrict_fk` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `resources_ibfk_1` | +| `addressFieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resources_addressFieldId_foreign_idx` | +| `contextFieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resources_contextFieldId_foreign_idx` | +| `referenceFieldId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resources_referenceFieldId_foreign_idx` | +| `templateId` | `resource_definitions(id)` | RESTRICT / RESTRICT | `resources_ibfk_2` | + +## software_installation_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `configurationId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `assetId` | `char(36)` | No | FK | +| `resourceId` | `char(36)` | No | FK | +| `version` | `varchar(100)` | Sí | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `licenseResourceId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `software_installation_revisions_ibfk_2` | +| `licenseResourceId` | `resources(id)` | RESTRICT / RESTRICT | `software_installation_revisions_licenseResourceId_foreign_idx` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `software_installation_revisions_ibfk_3` | +| `configurationId` | `software_installations(id)` | RESTRICT / RESTRICT | `software_installation_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `software_installation_revisions_ibfk_4` | + +## software_installations + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `assetId` | `char(36)` | No | FK | +| `resourceId` | `char(36)` | No | FK | +| `version` | `varchar(100)` | Sí | — | +| `state` | `varchar(20)` | No | — | +| `notes` | `text` | Sí | — | +| `currentKey` | `varchar(80)` | Sí | UNIQUE | +| `licenseResourceId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `software_installations_ibfk_2` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `software_installations_ibfk_1` | +| `licenseResourceId` | `resources(id)` | RESTRICT / RESTRICT | `software_installations_licenseResourceId_foreign_idx` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `software_installations_ibfk_3` | + +## telework_authorization_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `authorizationId` | `char(36)` | No | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `personId` | `char(36)` | Sí | FK | +| `active` | `tinyint(1)` | No | — | +| `startsOn` | `varchar(10)` | Sí | — | +| `expiresOn` | `varchar(10)` | Sí | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `telework_authorization_revisions_ibfk_10`, `telework_authorization_revisions_ibfk_11`, `telework_authorization_revisions_ibfk_12`, `telework_authorization_revisions_ibfk_13`, `telework_authorization_revisions_ibfk_14`, `telework_authorization_revisions_ibfk_15`, `telework_authorization_revisions_ibfk_16`, `telework_authorization_revisions_ibfk_17`, `telework_authorization_revisions_ibfk_18`, `telework_authorization_revisions_ibfk_19`, `telework_authorization_revisions_ibfk_20`, `telework_authorization_revisions_ibfk_21`, `telework_authorization_revisions_ibfk_22`, `telework_authorization_revisions_ibfk_23`, `telework_authorization_revisions_ibfk_24`, `telework_authorization_revisions_ibfk_25`, `telework_authorization_revisions_ibfk_26`, `telework_authorization_revisions_ibfk_27`, `telework_authorization_revisions_ibfk_28`, `telework_authorization_revisions_ibfk_29`, `telework_authorization_revisions_ibfk_3`, `telework_authorization_revisions_ibfk_30`, `telework_authorization_revisions_ibfk_31`, `telework_authorization_revisions_ibfk_32`, `telework_authorization_revisions_ibfk_33`, `telework_authorization_revisions_ibfk_34`, `telework_authorization_revisions_ibfk_35`, `telework_authorization_revisions_ibfk_36`, `telework_authorization_revisions_ibfk_37`, `telework_authorization_revisions_ibfk_38`, `telework_authorization_revisions_ibfk_39`, `telework_authorization_revisions_ibfk_4`, `telework_authorization_revisions_ibfk_40`, `telework_authorization_revisions_ibfk_41`, `telework_authorization_revisions_ibfk_42`, `telework_authorization_revisions_ibfk_43`, `telework_authorization_revisions_ibfk_44`, `telework_authorization_revisions_ibfk_45`, `telework_authorization_revisions_ibfk_46`, `telework_authorization_revisions_ibfk_47`, `telework_authorization_revisions_ibfk_48`, `telework_authorization_revisions_ibfk_49`, `telework_authorization_revisions_ibfk_5`, `telework_authorization_revisions_ibfk_50`, `telework_authorization_revisions_ibfk_51`, `telework_authorization_revisions_ibfk_52`, `telework_authorization_revisions_ibfk_53`, `telework_authorization_revisions_ibfk_6`, `telework_authorization_revisions_ibfk_7`, `telework_authorization_revisions_ibfk_8`, `telework_authorization_revisions_ibfk_9`, `telework_authorization_revisions_personId_foreign_idx` | +| `authorizationId` | `telework_authorizations(id)` | RESTRICT / RESTRICT | `telework_authorization_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `telework_authorization_revisions_ibfk_2` | + +## telework_authorizations + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `personId` | `char(36)` | No | FK, UNIQUE | +| `active` | `tinyint(1)` | No | — | +| `startsOn` | `varchar(10)` | Sí | — | +| `expiresOn` | `varchar(10)` | Sí | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `telework_authorizations_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `telework_authorizations_ibfk_2` | + +## ticket_counter + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `int(11)` | No | PK | +| `nextNumber` | `int(11)` | No | — | + +## ticket_revision_targets + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `personId` | `char(36)` | Sí | FK | +| `workstationId` | `char(36)` | Sí | FK | +| `assetId` | `char(36)` | Sí | FK | +| `resourceId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `ticket_revision_targets_ibfk_4` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `ticket_revision_targets_ibfk_2` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `ticket_revision_targets_ibfk_5` | +| `ownerId` | `ticket_revisions(id)` | CASCADE / RESTRICT | `ticket_revision_targets_ibfk_1` | +| `workstationId` | `workstations(id)` | RESTRICT / RESTRICT | `ticket_revision_targets_ibfk_3` | + +## ticket_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `ticketId` | `char(36)` | No | FK | +| `actorId` | `char(36)` | No | FK | +| `requesterId` | `char(36)` | Sí | FK | +| `assigneeId` | `char(36)` | Sí | FK | +| `recordedAt` | `varchar(30)` | No | — | +| `position` | `int(11)` | No | — | +| `subject` | `varchar(200)` | No | — | +| `description` | `text` | No | — | +| `channel` | `varchar(20)` | No | — | +| `state` | `varchar(20)` | No | — | +| `priority` | `varchar(20)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `requesterId` | `company_contacts(id)` | RESTRICT / RESTRICT | `ticket_revisions_ibfk_3` | +| `ticketId` | `tickets(id)` | RESTRICT / RESTRICT | `ticket_revisions_ibfk_1` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `ticket_revisions_ibfk_2` | +| `assigneeId` | `users(id)` | RESTRICT / RESTRICT | `ticket_revisions_ibfk_4` | + +## ticket_targets + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `ownerId` | `char(36)` | No | PK, FK | +| `position` | `int(11)` | No | PK | +| `personId` | `char(36)` | Sí | FK | +| `workstationId` | `char(36)` | Sí | FK | +| `assetId` | `char(36)` | Sí | FK | +| `resourceId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `ticket_targets_ibfk_4` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `ticket_targets_ibfk_2` | +| `resourceId` | `resources(id)` | RESTRICT / RESTRICT | `ticket_targets_ibfk_5` | +| `ownerId` | `tickets(id)` | CASCADE / RESTRICT | `ticket_targets_ibfk_1` | +| `workstationId` | `workstations(id)` | RESTRICT / RESTRICT | `ticket_targets_ibfk_3` | + +## tickets + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `reference` | `varchar(40)` | No | UNIQUE | +| `requestKey` | `char(36)` | No | UNIQUE | +| `requesterId` | `char(36)` | Sí | FK | +| `assigneeId` | `char(36)` | Sí | FK | +| `subject` | `varchar(200)` | No | — | +| `description` | `text` | No | — | +| `channel` | `varchar(20)` | No | — | +| `state` | `varchar(20)` | No | — | +| `priority` | `varchar(20)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `tickets_ibfk_1` | +| `requesterId` | `company_contacts(id)` | RESTRICT / RESTRICT | `tickets_ibfk_2` | +| `assigneeId` | `users(id)` | RESTRICT / RESTRICT | `tickets_ibfk_3` | + +## users + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `name` | `varchar(100)` | No | — | +| `email` | `varchar(254)` | No | UNIQUE | +| `passwordHash` | `varchar(256)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `isInitialAdmin` | `tinyint(1)` | No | — | +| `authVersion` | `int(11)` | No | — | + +## work_profile_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `profileId` | `char(36)` | No | FK | +| `accountId` | `char(36)` | Sí | FK | +| `position` | `int(11)` | No | — | +| `actorId` | `char(36)` | No | FK | +| `recordedAt` | `varchar(24)` | No | — | +| `name` | `varchar(100)` | No | — | +| `personId` | `char(36)` | Sí | FK | +| `assetId` | `char(36)` | Sí | FK | +| `active` | `tinyint(1)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `work_profile_revisions_ibfk_2` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `work_profile_revisions_assetId_foreign_idx`, `work_profile_revisions_ibfk_101`, `work_profile_revisions_ibfk_103`, `work_profile_revisions_ibfk_105`, `work_profile_revisions_ibfk_107`, `work_profile_revisions_ibfk_109`, `work_profile_revisions_ibfk_11`, `work_profile_revisions_ibfk_111`, `work_profile_revisions_ibfk_113`, `work_profile_revisions_ibfk_115`, `work_profile_revisions_ibfk_117`, `work_profile_revisions_ibfk_119`, `work_profile_revisions_ibfk_121`, `work_profile_revisions_ibfk_123`, `work_profile_revisions_ibfk_125`, `work_profile_revisions_ibfk_127`, `work_profile_revisions_ibfk_129`, `work_profile_revisions_ibfk_13`, `work_profile_revisions_ibfk_131`, `work_profile_revisions_ibfk_133`, `work_profile_revisions_ibfk_135`, `work_profile_revisions_ibfk_137`, `work_profile_revisions_ibfk_139`, `work_profile_revisions_ibfk_141`, `work_profile_revisions_ibfk_143`, `work_profile_revisions_ibfk_145`, `work_profile_revisions_ibfk_147`, `work_profile_revisions_ibfk_149`, `work_profile_revisions_ibfk_15`, `work_profile_revisions_ibfk_151`, `work_profile_revisions_ibfk_153`, `work_profile_revisions_ibfk_155`, `work_profile_revisions_ibfk_157`, `work_profile_revisions_ibfk_159`, `work_profile_revisions_ibfk_162`, `work_profile_revisions_ibfk_163`, `work_profile_revisions_ibfk_165`, `work_profile_revisions_ibfk_167`, `work_profile_revisions_ibfk_169`, `work_profile_revisions_ibfk_17`, `work_profile_revisions_ibfk_19`, `work_profile_revisions_ibfk_21`, `work_profile_revisions_ibfk_23`, `work_profile_revisions_ibfk_26`, `work_profile_revisions_ibfk_27`, `work_profile_revisions_ibfk_30`, `work_profile_revisions_ibfk_31`, `work_profile_revisions_ibfk_33`, `work_profile_revisions_ibfk_35`, `work_profile_revisions_ibfk_37`, `work_profile_revisions_ibfk_39`, `work_profile_revisions_ibfk_41`, `work_profile_revisions_ibfk_43`, `work_profile_revisions_ibfk_45`, `work_profile_revisions_ibfk_47`, `work_profile_revisions_ibfk_49`, `work_profile_revisions_ibfk_5`, `work_profile_revisions_ibfk_51`, `work_profile_revisions_ibfk_53`, `work_profile_revisions_ibfk_55`, `work_profile_revisions_ibfk_57`, `work_profile_revisions_ibfk_59`, `work_profile_revisions_ibfk_61`, `work_profile_revisions_ibfk_63`, `work_profile_revisions_ibfk_65`, `work_profile_revisions_ibfk_67`, `work_profile_revisions_ibfk_69`, `work_profile_revisions_ibfk_7`, `work_profile_revisions_ibfk_71`, `work_profile_revisions_ibfk_73`, `work_profile_revisions_ibfk_75`, `work_profile_revisions_ibfk_77`, `work_profile_revisions_ibfk_79`, `work_profile_revisions_ibfk_81`, `work_profile_revisions_ibfk_83`, `work_profile_revisions_ibfk_85`, `work_profile_revisions_ibfk_87`, `work_profile_revisions_ibfk_89`, `work_profile_revisions_ibfk_9`, `work_profile_revisions_ibfk_91`, `work_profile_revisions_ibfk_93`, `work_profile_revisions_ibfk_95`, `work_profile_revisions_ibfk_97`, `work_profile_revisions_ibfk_99` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `work_profile_revisions_ibfk_10`, `work_profile_revisions_ibfk_100`, `work_profile_revisions_ibfk_102`, `work_profile_revisions_ibfk_104`, `work_profile_revisions_ibfk_106`, `work_profile_revisions_ibfk_108`, `work_profile_revisions_ibfk_110`, `work_profile_revisions_ibfk_112`, `work_profile_revisions_ibfk_114`, `work_profile_revisions_ibfk_116`, `work_profile_revisions_ibfk_118`, `work_profile_revisions_ibfk_12`, `work_profile_revisions_ibfk_120`, `work_profile_revisions_ibfk_122`, `work_profile_revisions_ibfk_124`, `work_profile_revisions_ibfk_126`, `work_profile_revisions_ibfk_128`, `work_profile_revisions_ibfk_130`, `work_profile_revisions_ibfk_132`, `work_profile_revisions_ibfk_134`, `work_profile_revisions_ibfk_136`, `work_profile_revisions_ibfk_138`, `work_profile_revisions_ibfk_14`, `work_profile_revisions_ibfk_140`, `work_profile_revisions_ibfk_142`, `work_profile_revisions_ibfk_144`, `work_profile_revisions_ibfk_146`, `work_profile_revisions_ibfk_148`, `work_profile_revisions_ibfk_150`, `work_profile_revisions_ibfk_152`, `work_profile_revisions_ibfk_154`, `work_profile_revisions_ibfk_156`, `work_profile_revisions_ibfk_158`, `work_profile_revisions_ibfk_16`, `work_profile_revisions_ibfk_160`, `work_profile_revisions_ibfk_161`, `work_profile_revisions_ibfk_164`, `work_profile_revisions_ibfk_166`, `work_profile_revisions_ibfk_168`, `work_profile_revisions_ibfk_18`, `work_profile_revisions_ibfk_20`, `work_profile_revisions_ibfk_22`, `work_profile_revisions_ibfk_24`, `work_profile_revisions_ibfk_25`, `work_profile_revisions_ibfk_28`, `work_profile_revisions_ibfk_29`, `work_profile_revisions_ibfk_32`, `work_profile_revisions_ibfk_34`, `work_profile_revisions_ibfk_36`, `work_profile_revisions_ibfk_38`, `work_profile_revisions_ibfk_4`, `work_profile_revisions_ibfk_40`, `work_profile_revisions_ibfk_42`, `work_profile_revisions_ibfk_44`, `work_profile_revisions_ibfk_46`, `work_profile_revisions_ibfk_48`, `work_profile_revisions_ibfk_50`, `work_profile_revisions_ibfk_52`, `work_profile_revisions_ibfk_54`, `work_profile_revisions_ibfk_56`, `work_profile_revisions_ibfk_58`, `work_profile_revisions_ibfk_6`, `work_profile_revisions_ibfk_60`, `work_profile_revisions_ibfk_62`, `work_profile_revisions_ibfk_64`, `work_profile_revisions_ibfk_66`, `work_profile_revisions_ibfk_68`, `work_profile_revisions_ibfk_70`, `work_profile_revisions_ibfk_72`, `work_profile_revisions_ibfk_74`, `work_profile_revisions_ibfk_76`, `work_profile_revisions_ibfk_78`, `work_profile_revisions_ibfk_8`, `work_profile_revisions_ibfk_80`, `work_profile_revisions_ibfk_82`, `work_profile_revisions_ibfk_84`, `work_profile_revisions_ibfk_86`, `work_profile_revisions_ibfk_88`, `work_profile_revisions_ibfk_90`, `work_profile_revisions_ibfk_92`, `work_profile_revisions_ibfk_94`, `work_profile_revisions_ibfk_96`, `work_profile_revisions_ibfk_98`, `work_profile_revisions_personId_foreign_idx` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `work_profile_revisions_ibfk_3` | +| `profileId` | `work_profiles(id)` | RESTRICT / RESTRICT | `work_profile_revisions_ibfk_1` | + +## work_profiles + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `personId` | `char(36)` | No | FK | +| `accountId` | `char(36)` | Sí | FK | +| `assetId` | `char(36)` | No | FK | +| `currentKey` | `varchar(110)` | Sí | UNIQUE | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `notes` | `text` | Sí | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `accountId` | `access_items(id)` | RESTRICT / RESTRICT | `work_profiles_ibfk_3` | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `work_profiles_ibfk_4` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `work_profiles_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `work_profiles_ibfk_2` | + +## workstation_assets + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `workstationId` | `char(36)` | No | PK, FK | +| `companyId` | `char(36)` | No | FK | +| `assetId` | `char(36)` | No | PK, FK | +| `activeKey` | `varchar(80)` | Sí | UNIQUE | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `workstation_assets_ibfk_3` | +| `companyId`, `assetId` | `assets(companyId, id)` | RESTRICT / RESTRICT | `workstation_asset_company` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `workstation_assets_ibfk_2` | +| `companyId`, `workstationId` | `workstations(companyId, id)` | RESTRICT / RESTRICT | `workstation_owner_company` | +| `workstationId` | `workstations(id)` | RESTRICT / RESTRICT | `workstation_assets_ibfk_1` | + +## workstation_members + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `workstationId` | `char(36)` | No | PK, FK | +| `assetId` | `char(36)` | No | PK, FK | +| `kind` | `varchar(10)` | No | — | +| `position` | `int(11)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `workstation_members_ibfk_2` | +| `workstationId` | `workstations(id)` | CASCADE / RESTRICT | `workstation_members_ibfk_1` | + +## workstation_revision_members + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `revisionId` | `char(36)` | No | PK, FK | +| `assetId` | `char(36)` | No | PK, FK | +| `kind` | `varchar(10)` | No | — | +| `position` | `int(11)` | No | — | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `assetId` | `assets(id)` | RESTRICT / RESTRICT | `workstation_revision_members_ibfk_2` | +| `revisionId` | `workstation_revisions(id)` | CASCADE / RESTRICT | `workstation_revision_members_ibfk_1` | + +## workstation_revisions + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `workstationId` | `char(36)` | No | FK | +| `actorId` | `char(36)` | No | FK | +| `siteId` | `char(36)` | Sí | FK | +| `personId` | `char(36)` | Sí | FK | +| `recordedAt` | `varchar(30)` | No | — | +| `position` | `int(11)` | No | — | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `computerAssetId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `computerAssetId` | `assets(id)` | RESTRICT / RESTRICT | `workstation_revisions_computerAssetId_foreign_idx` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `workstation_revisions_ibfk_4` | +| `siteId` | `company_sites(id)` | RESTRICT / RESTRICT | `workstation_revisions_ibfk_3` | +| `actorId` | `users(id)` | RESTRICT / RESTRICT | `workstation_revisions_ibfk_2` | +| `workstationId` | `workstations(id)` | RESTRICT / RESTRICT | `workstation_revisions_ibfk_1` | + +## workstations + +| Columna | Tipo SQL | NULL | Clave | +| --- | --- | --- | --- | +| `id` | `char(36)` | No | PK | +| `companyId` | `char(36)` | No | FK | +| `siteId` | `char(36)` | Sí | FK | +| `personId` | `char(36)` | Sí | FK | +| `name` | `varchar(100)` | No | — | +| `active` | `tinyint(1)` | No | — | +| `computerAssetId` | `char(36)` | Sí | FK | + +| FK: columnas | Referencia | DELETE / UPDATE | Restricciones | +| --- | --- | --- | --- | +| `computerAssetId` | `assets(id)` | RESTRICT / RESTRICT | `workstations_computerAssetId_foreign_idx` | +| `companyId` | `companies(id)` | RESTRICT / RESTRICT | `workstations_ibfk_1` | +| `personId` | `company_contacts(id)` | RESTRICT / RESTRICT | `workstations_ibfk_3` | +| `siteId` | `company_sites(id)` | RESTRICT / RESTRICT | `workstations_ibfk_2` | diff --git a/docs/entorno-actual.md b/docs/entorno-actual.md index 19ca289..3b23f17 100644 --- a/docs/entorno-actual.md +++ b/docs/entorno-actual.md @@ -4,11 +4,11 @@ Ficha de consulta disponible desde «Entorno actual» en cada perfil de usuario. ## Equipo y puesto -Muestra las características del equipo que tienen valor, conservando valores como No o cero. Consulta los puestos activos que contienen ese dispositivo y presenta sus activos exclusivos y compartidos sin duplicarlos. No infiere asignaciones ni titularidades entre persona, perfil y puesto. Las relaciones físicas y el mapa visual permanecen en sus consultas actuales; no se incorpora un mapa nuevo. +Muestra las características del equipo que tienen valor, conservando valores como No o cero. Consulta los puestos activos que contienen ese dispositivo y presenta sus activos sin duplicarlos. No infiere asignaciones ni titularidades entre persona, perfil y puesto. Las relaciones físicas y el mapa visual permanecen en sus consultas actuales; no se incorpora un mapa nuevo. ## Configuraciones -Los cinco bloques son correos, carpetas compartidas, impresoras, aplicaciones y certificados. Cada fila conserva método, ruta, permisos documentados, conexión, modalidad, versión o ubicación según corresponda; también estado, notas y aviso de revisión. +Las configuraciones aparecen después de los datos del equipo, en este orden: carpetas compartidas, correos electrónicos, aplicaciones, impresoras y certificados. Los bloques de teletrabajo, accesos remotos y asistencia técnica remota aparecen después. Cada fila conserva método, ruta, permisos documentados, conexión, modalidad, versión o ubicación según corresponda; también estado, notas y aviso de revisión. Una persona puede tener varios correos y varios perfiles. La ficha muestra solamente las configuraciones del perfil seleccionado; permite cambiar a otros perfiles vigentes de esa persona. Las configuraciones retiradas o nunca configuradas quedan en las consultas de gestión e historial. Los correos antiguos sin perfil requieren vinculación explícita: se avisa de los registros del mismo dispositivo y persona, sin asignarlos automáticamente. diff --git a/docs/evolucion-de-plantillas-de-activos.md b/docs/evolucion-de-plantillas-de-activos.md new file mode 100644 index 0000000..47fffc8 --- /dev/null +++ b/docs/evolucion-de-plantillas-de-activos.md @@ -0,0 +1,102 @@ +# Evolución de plantillas de activos + +## Estado y objetivo + +Propuesta pendiente de implementación. Define cómo deberían reflejarse las ediciones +de una plantilla en los activos existentes de todas las compañías que la utilizan. +No cambia el comportamiento actual ni las reglas del catálogo de recursos. + +Actualmente `mapAssetInput` captura la especificación al crear el activo y reutiliza +la anterior al editarlo. El formulario utiliza esa misma instantánea. Por tanto, +añadir campos, secciones o cambiar el orden solo afecta a las nuevas altas. +El icono ya se consulta desde la plantilla actual. + +Cada `AssetRevision` conserva valores, autor y fecha, pero no una especificación +propia. Sustituir la especificación del activo sin evolucionar este contrato haría +que las revisiones antiguas se interpretasen con campos y grupos posteriores. +Véanse [campos y plantillas](campos-y-plantillas-de-activos.md) y +[secciones de campos](secciones-de-campos-en-plantillas.md) para el contrato vigente. + +## Comportamiento propuesto + +La ficha y la edición del activo deben seguir la plantilla actual, conservando los +valores ya registrados. El historial debe mostrar la estructura y las reglas +vigentes cuando se registró cada revisión. + +| Cambio en la plantilla | Efecto en los activos existentes | +| --- | --- | +| Añadir un campo opcional | Aparece vacío en la ficha y disponible en la edición; no se inventa un valor. | +| Añadir un campo obligatorio o marcarlo obligatorio | Aparece pendiente de completar si falta un valor. El activo continúa siendo consultable. | +| Añadir, renombrar o reordenar secciones | Se aplica a la presentación actual; una sección vacía no se muestra en la ficha. | +| Mover o reordenar campos | Cambia su presentación, manteniendo identidad y valor. | +| Eliminar una sección | Sus campos pasan a «Sin sección», conservando los valores. | +| Retirar un campo | Deja de solicitarse; los valores existentes siguen consultables como características retiradas, separados de los campos actuales. | +| Reincorporar un campo | Recupera su valor anterior por UUID, si existe. | +| Cambiar nombre, descripción o unidad | Actualiza la presentación actual; no convierte ni modifica los valores. | +| Añadir opciones a un campo de lista | Las opciones nuevas quedan disponibles sin alterar la selección guardada. | +| Desactivar plantilla o campo | Conserva activos, asociaciones, valores e historial; mantiene las reglas de disponibilidad existentes. | + +La evolución no cambia el tipo de un campo ni elimina opciones existentes: ambas +operaciones continúan prohibidas por el catálogo. Un campo nuevo con el mismo nombre +que otro retirado tiene otra identidad y no hereda sus valores. + +Los nuevos activos cumplen desde el alta todas las reglas actuales. En los +existentes, la edición de características exige completar los campos obligatorios +actuales. Las operaciones independientes sobre estado, ubicación o notas no deben +quedar bloqueadas por un cambio posterior de plantilla; esto requiere distinguir +su validación de la edición de características. Nunca se rellenan valores por +defecto para disimular información pendiente. Un cambio a opcional elimina el +pendiente correspondiente sin borrar información. + +## Alcance entre compañías + +La dependencia se identifica por UUID de plantilla, nunca por nombre. Una edición +global afecta a los activos vinculados a esa plantilla en todas las compañías, +incluidos los retirados y los de compañías donde se haya desactivado su disponibilidad. +Una plantilla local solo afecta a sus propios activos. Una copia tiene identidad +propia y permanece independiente de la plantilla original. + +La propagación respeta la compañía propietaria de cada activo y la autorización del +servidor. El navegador no elige qué compañías pueden ser modificadas. La edición +de una plantilla no reasigna activos ni altera relaciones, puestos o perfiles. + +## Implementación prevista + +1. Introducir versiones inmutables de la especificación de plantilla, con identidad + UUID v7 y persistencia relacional. Una modificación semántica crea una versión; + un guardado equivalente conserva la existente. +2. Asociar cada revisión del activo a la especificación con la que se registró. + Una migración explícita conserva la instantánea existente para todas sus + revisiones anteriores: no puede reconstruirse qué plantilla histórica existía + si esa información nunca se almacenó. +3. Resolver la especificación actual desde la plantilla al consultar y editar el + activo, combinándola con sus valores por UUID de campo. Así el cambio se refleja + en todas las compañías sin reescribir masivamente sus activos. Conservar los + valores retirados y mantener la especificación histórica para el historial. +4. Hacer que el dominio y los mappers distingan validación de características, + cambios independientes y rehidratación histórica. Guardar una edición real + contra la versión actual en una transacción. Si la plantilla cambió mientras el + formulario estaba abierto, devolver un conflicto recuperable que permita + revisar los cambios conservando la entrada del usuario. +5. Mostrar en el editor de plantilla el alcance del cambio y, en cada activo, los + campos obligatorios pendientes y los valores retirados. Invalidar las consultas + afectadas del catálogo y del inventario en el cliente que realiza el cambio; + otros clientes reciben la estructura vigente al volver a consultar. + +Guardar una plantilla no debe generar revisiones técnicas ficticias por activo. +La versión de plantilla conserva autor y fecha para explicar el cambio estructural. +Una consulta tampoco escribe datos ni crea historial. Repetir una operación +equivalente no duplica versiones ni revisiones. La comparación es semántica. + +## Validación necesaria + +Probar propagación global en dos compañías y aislamiento local y de copias; +altas y activos anteriores; campos opcionales y obligatorios; valores `0` y `false`; +reordenación y eliminación de secciones; retirada y reincorporación por identidad; +historial sin cambios; disponibilidad desactivada; activos retirados; guardados +equivalentes; conflictos entre edición de plantilla y activo; autorización; +migración relacional repetible y caché del frontend. + +Esta evolución sustituirá, cuando esté implementada, la regla actual que congela +la estructura del activo. Hasta entonces las instantáneas siguen siendo inmutables, +como documenta el contrato vigente de secciones. diff --git a/docs/inventario-de-activos.md b/docs/inventario-de-activos.md index ee0b277..fd504c0 100644 --- a/docs/inventario-de-activos.md +++ b/docs/inventario-de-activos.md @@ -8,7 +8,7 @@ El listado ofrece una pestaña «Todo» y una por cada plantilla global o propia Un activo representa un elemento concreto, identificado mediante UUID v7. Su nombre sirve para reconocerlo (por ejemplo PORT-ANA-01), pero no sustituye al UUID y puede repetirse. La compañía y la plantilla son obligatorias. Sede, notas y características son opcionales salvo los campos marcados obligatorios en la plantilla. La persona se relaciona mediante un puesto de trabajo o un perfil de usuario. -Estados técnicos manuales: Operativo, Averiado, En mantenimiento y Retirado. Un activo operativo se muestra Disponible si ningún puesto activo lo utiliza, o En uso si figura como exclusivo o compartido en uno o varios puestos activos. La asignación se calcula desde la composición, independientemente del responsable. Los puestos inactivos y las revisiones históricas no cuentan como uso vigente. Para retirarlo se exige desvincularlo primero de todos los puestos activos, conservando sede e historial. La eliminación definitiva existente sigue protegida por referencias; no hay transferencia entre compañías. +Estados técnicos manuales: Operativo, Averiado, En mantenimiento y Retirado. Un activo operativo se muestra Disponible si ningún puesto activo lo utiliza, o En uso si figura en un puesto activo. La asignación se calcula desde la composición, independientemente del responsable. Los puestos inactivos y las revisiones históricas no cuentan como uso vigente. Para retirarlo se exige desvincularlo primero de todos los puestos activos, conservando sede e historial. La eliminación definitiva existente sigue protegida por referencias; no hay transferencia entre compañías. La sede y la persona pertenecen a la compañía. Solo se permiten nuevas asignaciones a personas activas. Una persona que se desactiva conserva su asignación anterior, para facilitar la revisión y retirada; puede desasignarse. Las personas no reciben acceso a Metis Desk. @@ -63,12 +63,12 @@ Los puestos de trabajo y la asistencia técnica se almacenan también de forma r `values.status` en la API representa el estado técnico. `assigned` y `displayStatus` son datos calculados al construir las respuestas de alta, edición y consulta; no se persisten como columnas. Listados, búsqueda, ordenación y fichas muestran `displayStatus`. El formulario solo ofrece estados técnicos y explica la distinción. -`assetTechnicalStatus`, `assetDisplayStatus` y `canAssignAsset` son funciones puras pequeñas del dominio. La selección de activos del puesto reutiliza la regla de asignación y muestra por qué un elemento está bloqueado. Una nueva asociación a un puesto activo, o su reactivación, requiere un activo Operativo. Las reservas exclusivas y compartidas mantienen sus restricciones y bloqueos transaccionales. Una asociación existente puede conservarse al editar un puesto si su activo se avería o entra en mantenimiento; cambiarlo de exclusivo a compartido o viceversa se considera una nueva asociación. +`assetTechnicalStatus`, `assetDisplayStatus` y `canAssignAsset` son funciones puras pequeñas del dominio. La selección de activos del puesto reutiliza la regla de asignación y muestra por qué un elemento está bloqueado. Una nueva asociación a un puesto activo, o su reactivación, requiere un activo Operativo. Las reservas mantienen sus restricciones y bloqueos transaccionales. Una asociación existente puede conservarse al editar un puesto si su activo se avería o entra en mantenimiento. -Una avería o mantenimiento no borra asignaciones, perfiles ni software, y prevalece visualmente sobre En uso. Se conserva la información del puesto para organizar la reparación. Al recuperar Operativo vuelve a mostrarse En uso si sigue asignado. Al dar de baja/eliminar un puesto o retirar un miembro se recalcula el uso; un activo compartido permanece En uso mientras otro puesto activo lo utilice. Un activo averiado o en mantenimiento conserva su estado al liberarse. +Una avería o mantenimiento no borra asignaciones, perfiles ni software, y prevalece visualmente sobre En uso. Se conserva la información del puesto para organizar la reparación. Al recuperar Operativo vuelve a mostrarse En uso si sigue asignado. Al desactivar/eliminar un puesto o retirar un miembro se recalcula el uso; el activo queda Disponible cuando deja de pertenecer a un puesto activo. Un activo averiado o en mantenimiento conserva su estado al liberarse. El servicio de inventario exporta `notRetired`, en sustitución del booleano genérico `available`. Perfiles, software, correo, impresoras, certificados, accesos remotos, procesos y consultas de soporte utilizan ese contrato explícito. Se conserva su criterio para registrar configuraciones: no retirado no significa libre ni operativo. La creación/activación de un perfil mantiene además los requisitos de persona y ordenador principal de un puesto activo. Las configuraciones existentes y sus avisos siguen documentando preparación/retirada manual, sin simular operaciones sobre equipos. La persistencia conserva los códigos originales Disponible/En uso en los registros existentes y sus revisiones. Se interpretan como estado técnico Operativo al consultar la situación actual; no se inventan asignaciones ni se reescribe el historial. Las nuevas entradas HTTP solo aceptan los cuatro estados técnicos. Una consulta y un guardado semánticamente equivalente no añaden revisiones de normalización. El mapper sigue comprobando que el estado original de raíz y última revisión coincida. La restricción SQL se amplía idempotentemente para admitir los nuevos estados, preservando la lectura de los registros originales; no se añade JSON ni nuevas columnas de estados calculados. -Pruebas de reglas puras, elegibilidad exclusiva/compartida, historial e idempotencia, uso compartido por varios puestos, avería/reparación conservando asignaciones, bloqueo de nuevas asociaciones, retirada con referencias activas, liberación por baja/eliminación y respuestas HTTP/MariaDB. Los escenarios de retirada en las pruebas desvinculan primero los puestos, como exige el nuevo comportamiento. +Pruebas de reglas puras, elegibilidad de activos, historial e idempotencia, avería/reparación conservando asignaciones, bloqueo de nuevas asociaciones, retirada con referencias activas, liberación por baja/eliminación y respuestas HTTP/MariaDB. Los escenarios de retirada en las pruebas desvinculan primero los puestos, como exige el nuevo comportamiento. diff --git a/docs/patron-de-listados-de-entidades.md b/docs/patron-de-listados-de-entidades.md index 88ea9b4..764325e 100644 --- a/docs/patron-de-listados-de-entidades.md +++ b/docs/patron-de-listados-de-entidades.md @@ -62,7 +62,7 @@ La aplicación navega por espacios y secciones mediante rutas URL independientes | Usuarios internos | [UserManagement](../modules/identity/src/web/users/list/ui/user-management.tsx) | Sí | | Activos | [AssetInventoryPage](../modules/assets/src/web/assets/list/ui/inventory-page.tsx) | Sí; conserva panel lateral | | Puestos de trabajo | [WorkstationsPage](../modules/assets/src/web/workstations/list/ui/workstations-page.tsx) | Sí | -| Recursos, incluidas aplicaciones y licencias | [ResourcesPage](../modules/resources/src/web/resources/list/ui/resources-page.tsx) | Sí | +| Recursos, incluidas aplicaciones y licencias | [ResourcesPage](../modules/resources/src/web/resources/list/ui/resources-page.tsx) | Sí; pestañas por plantilla y «Todo» | | Campos y plantillas de recursos, global/compañía | [ResourceCatalogPage](../modules/resources/src/web/catalog/list/ui/catalog-page.tsx) | Sí; dos secciones | | Campos, plantillas y relaciones de activos, global/compañía | [AssetCatalogPage](../modules/assets/src/web/catalog/list/ui/catalog-page.tsx) | Sí; tres secciones | | Cuentas y accesos / catálogos informáticos | [AccessWorkspacePage](../modules/companies/src/web/access/list/ui/access-workspace.tsx) | Sí; cuenta, titularidad, grupo, pertenencia, autorización, necesidad, cobertura, entorno, sistema, tipo, permiso y plantilla | @@ -125,3 +125,7 @@ La primera columna tiene anchura mínima para evitar nombres ilegibles. El despl Disparador solo con icono de tres puntos, etiqueta accesible y título conservados. Columna actions siempre al final, compacta, alineada y anclada a la derecha; no admite ocultación, búsqueda u ordenación. El fondo opaco respeta hover, menú abierto y selección del inventario. Verificación: 204 pruebas correctas, tipos y compilación correctos; los cinco archivos de código/estilos de este ajuste pasan Biome. Las comprobaciones globales conservan los errores anteriores. En navegador se han comprobado apertura/cancelación de edición, botón sin texto, posición de cabecera y celdas antes y después del desplazamiento horizontal a anchura móvil, y color del activo seleccionado. No se modificaron registros de negocio durante la revisión. + +### Selector de activos + +Los formularios que seleccionan activos usan un diálogo con el mismo `AssetList` del inventario. Reutiliza `EntityTable`, búsqueda, pestañas por plantilla, columnas y paginación. La selección provisional no cambia el formulario hasta confirmar, ni altera la URL o filtros del listado padre. Cada consumidor restringe los candidatos y declara selección única o múltiple con límite. Véase [componentes de interfaz](componentes-de-interfaz.md#selección-de-activos-en-diálogo). diff --git a/docs/perfiles-de-trabajo.md b/docs/perfiles-de-trabajo.md index cb8498b..514697f 100644 --- a/docs/perfiles-de-trabajo.md +++ b/docs/perfiles-de-trabajo.md @@ -16,7 +16,7 @@ Un puesto agrupa activos; el perfil pertenece a una persona y a un dispositivo. 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 agrupan un único conjunto de activos. Consultar [Puestos de trabajo](puestos-de-trabajo.md). ## Revisión y altas rápidas @@ -30,7 +30,7 @@ La ayuda aparece inmediatamente antes del pie con Guardar y Cancelar. Se mantien WorkProfile es un AggregateRoot con UniqueID; WorkProfileRevision es una DomainEntity con su propio UUID. WorkProfileValues es un Value Object inmutable con Name, UniqueID y Maybe para los opcionales. Crear genera UUID v7; actualizar conserva identidad, compara valores semánticamente y añade una revisión solo si cambian. Rehidratar conserva UUID e historial. -work_profiles conserva raíz, referencias y clave única nullable de la pareja vigente. work_profile_revisions conserva instantáneas ordenadas, cuenta histórica, actor autenticado y fecha UTC, con UUID propio y claves foráneas. Las migraciones explícitas son idempotentes. No hay borrado de perfiles ni de sus revisiones. +work_profiles conserva raíz, referencias y clave única nullable de la pareja vigente. work_profile_revisions conserva instantáneas ordenadas, cuenta histórica, actor autenticado y fecha UTC, con UUID propio y claves foráneas. Las migraciones explícitas son idempotentes. El borrado permitido elimina la raíz y sus revisiones en una única transacción. El repositorio bloquea la compañía antes de validar y guardar. Personas y cuentas usan ese mismo bloqueo, y el inventario se consulta mediante un servicio público de assets, sin importar sus modelos ORM. Dos altas concurrentes de la misma pareja no pueden producir dos perfiles vigentes. El mapper comprueba identidad, referencias, cuenta histórica, orden y fechas, última revisión y clave vigente. Una inconsistencia falla explícitamente. @@ -44,7 +44,7 @@ La base del perfil incorpora ahora [buzones y configuraciones de correo](correos El perfil incorpora también carpetas compartidas e [impresoras por perfil](impresoras-del-perfil.md), con acciones para abrir cada consulta filtrada. Las [aplicaciones por perfil](aplicaciones-del-perfil.md) están implementadas sobre recursos. Los [certificados por perfil](certificados-del-perfil.md) también están implementados. La ficha [Entorno actual](entorno-actual.md) reúne estas configuraciones. El mapa visual queda aplazado. -La comprobación en navegador verifica la nueva sección, los desplegables de persona y dispositivo, la apertura de las tres altas rápidas y la conservación del formulario al cancelarlas. En puestos se comprueba la carga de una composición anterior, el selector compartido, su incorporación visual y la cancelación sin persistir cambios. +La comprobación en navegador verifica la nueva sección, los desplegables de persona y dispositivo, la apertura de las tres altas rápidas y la conservación del formulario al cancelarlas. En puestos se comprueba la carga de una composición anterior, el selector de activos, su incorporación visual y la cancelación sin persistir cambios. ## Alta desde el puesto @@ -53,4 +53,30 @@ El formulario de alta pide puesto y persona. La persona no se toma automáticame 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. +El backend comprueba también esta condición al crear o reactivar un perfil vigente y al cambiar su cuenta. Los periféricos 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. + +## Desactivar y eliminar + +Las acciones de fila permiten desactivar y eliminar mediante un modal de confirmación. +Desactivar conserva valores, configuraciones e historial; solo cambia `active` y añade +una revisión si el perfil estaba vigente. Se permite aunque la persona, el ordenador +o la cuenta ya no estén disponibles. No retira permisos ni configuraciones externas. +Repetir la desactivación no genera revisiones adicionales. Los perfiles desactivados +se consultan mediante el filtro de archivados y pueden reactivarse desde el editor +con las comprobaciones habituales de disponibilidad y pareja vigente única. + +Eliminar exige que el perfil esté desactivado y que no existan referencias en correo, +carpetas, impresoras, aplicaciones o certificados, tanto en sus registros actuales +como en sus revisiones. Los estados retirado o nunca configurado también bloquean +el borrado mientras exista el registro vinculado. Primero deben eliminarse esos +registros mediante sus acciones existentes. No se borran configuraciones en cascada. +La eliminación definitiva incluye todo el historial del propio perfil. + +`POST /api/work-profiles/:companyId/:id/deactivate` y +`POST /api/work-profiles/:companyId/:id/delete` reciben un objeto vacío, requieren +sesión, origen válido y CSRF, y verifican pertenencia a la compañía. La desactivación +obtiene el actor de la sesión. El borrado devuelve `PROFILE_ACTIVE` o `PROFILE_IN_USE` +(409) cuando no cumple las condiciones; un perfil inexistente o de otra compañía +devuelve `PROFILE_NOT_FOUND` (404). Repetir un borrado devuelve 404 sin nuevos efectos. +El bloqueo de compañía serializa estas operaciones con altas y cambios de +configuraciones; las claves foráneas conservan la protección de referencias. diff --git a/docs/puestos-de-trabajo.md b/docs/puestos-de-trabajo.md index 755bc40..5839171 100644 --- a/docs/puestos-de-trabajo.md +++ b/docs/puestos-de-trabajo.md @@ -2,7 +2,7 @@ ## Uso -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, responsable opcional y hasta 200 activos sin duplicados. El alta exige al menos un activo y un ordenador principal. Los puestos antiguos o desactivados pueden conservar una composición vacía hasta completarlos. 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. Al crear, elegir responsable 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. @@ -10,7 +10,7 @@ La ayuda aparece inmediatamente antes de Guardar y Cancelar. El listado permite ## Reglas -Un activo exclusivo solo puede pertenecer a un puesto activo. Un recurso compartido puede utilizarse en varios puestos activos, por ejemplo una impresora de red. Un activo reservado como exclusivo no puede figurar como compartido en otro puesto activo; un recurso compartido debe retirarse de todos los otros puestos vigentes antes de reservarlo como exclusivo. El formulario marca como ocupados los activos de otros puestos y no permite seleccionarlos en una composición incompatible. Las reservas exclusivas y usos compartidos se validan bajo el mismo bloqueo de compañía, también frente a escrituras concurrentes. El servidor vuelve a comprobarlo dentro de una transacción y la base de datos protege la reserva con una clave única. Dos altas concurrentes no pueden reservar el mismo activo como exclusivo, ni mezclar un uso exclusivo con un uso compartido en otro puesto vigente. +Un activo solo puede pertenecer a un puesto activo. El formulario marca como ocupados los activos reservados por otros puestos. El servidor valida la reserva bajo bloqueo de compañía en una transacción y la base de datos la protege con una clave única. Dos altas concurrentes no pueden reservar el mismo activo. Desactivar un puesto conserva su composición y libera sus reservas. Un activo puede figurar en composiciones de puestos inactivos. Reactivar comprueba de nuevo la disponibilidad: si un activo está ocupado, hay que retirarlo de uno de los puestos antes de guardar. Retirar un activo del puesto no elimina el activo ni sus relaciones. @@ -24,7 +24,7 @@ La eliminación definitiva se realiza mediante una operación explícita con con Workstation es un agregado; WorkstationRevision es una entidad interna. WorkstationValues es un Value Object inmutable con nombres Name, referencias UniqueID y opcionales Maybe. Infrastructure reconstruye y valida los valores, la última revisión, la auditoría y el ámbito. -Las tablas workstations y workstation_revisions mantienen raíces y revisiones con claves foráneas a compañía, sede, persona y actor. workstation_assets es una proyección técnica de las referencias utilizadas en todo el historial y de sus reservas vigentes; no representa otra entidad de negocio. Conserva claves foráneas a activos históricos de ambas listas; solo la composición exclusiva genera la reserva vigente. Conserva claves compuestas para la pertenencia a compañía y una clave única nullable para la reserva. Las migraciones son idempotentes. +Las tablas workstations y workstation_revisions mantienen raíces y revisiones con claves foráneas a compañía, sede, persona y actor. workstation_assets es una proyección técnica de las referencias utilizadas en todo el historial y de sus reservas vigentes; no representa otra entidad de negocio. Conserva claves foráneas a activos históricos; la composición activa genera la reserva vigente. Conserva claves compuestas para la pertenencia a compañía y una clave única nullable para la reserva. Las migraciones son idempotentes. El repositorio del inventario carga los puestos y serializa sus escrituras con el bloqueo de catálogo y compañía ya utilizado por activos y relaciones. Conserva revisiones existentes y solo inserta las nuevas. @@ -40,27 +40,38 @@ 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. -`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 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. +La ficha reúne únicamente perfiles del ordenador principal, incluidos los archivados cuando corresponda. Los periféricos 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. La ficha consulta también el [software del ordenador](software-del-ordenador.md), incluido el sistema operativo actual, por `computerAssetId`. Las instalaciones permanecen vinculadas al activo al cambiar el ordenador principal. -## Baja y eliminación +## Desactivación y eliminación -El listado y la ficha completa ofrecen **Dar de baja** y **Eliminar definitivamente**, mediante un mismo diálogo reutilizable con confirmación y resultado de servidor. +El listado y la ficha completa ofrecen **Desactivar** y **Eliminar definitivamente**, mediante un mismo diálogo reutilizable con confirmación y resultado de servidor. -Dar de baja deja el puesto inactivo, vacía sus activos exclusivos y compartidos y retira el ordenador principal y responsable. Conserva nombre, sede, UUID e historial. Las asociaciones anteriores permanecen en las revisiones, pero no reservan activos ni representan usos vigentes. Repetir la baja no añade revisiones. También se puede aplicar a un puesto ya inactivo que conserve composición. Se puede editar posteriormente para reconstruirlo y reactivarlo, comprobando las reservas actuales. +Desactivar deja el puesto inactivo, vacía sus activos y retira el ordenador principal y responsable. Conserva nombre, sede, UUID e historial. Las asociaciones anteriores permanecen en las revisiones, pero no reservan activos ni representan usos vigentes. Repetir la desactivación no añade revisiones. También se puede aplicar a un puesto ya inactivo que conserve composición. Se puede editar posteriormente para reconstruirlo y reactivarlo, comprobando las reservas actuales. -Eliminar definitivamente borra el puesto, revisiones y asociaciones técnicas en una transacción y libera sus reservas, incluso si seguía activo. Conserva los activos, personas y sedes. Las referencias externas, incluidos tickets e intervenciones actuales o históricos, impiden la eliminación con `WORKSTATION_IN_USE`; la alternativa es dar de baja. La comprobación utiliza las claves foráneas declaradas para proteger también futuras referencias. Un error no elimina parcialmente la composición ni sus reservas. +Eliminar definitivamente borra el puesto, revisiones y asociaciones técnicas en una transacción y libera sus reservas, incluso si seguía activo. Conserva los activos, personas y sedes. Las referencias externas, incluidos tickets e intervenciones actuales o históricos, impiden la eliminación con `WORKSTATION_IN_USE`; la alternativa es desactivar. La comprobación utiliza las claves foráneas declaradas para proteger también futuras referencias. Un error no elimina parcialmente la composición ni sus reservas. -En este modelo los recursos compartidos del puesto son activos compartidos (por ejemplo impresoras). Los perfiles de usuario, correo, permisos, certificados y software pertenecen a personas, cuentas o al ordenador, y no son asignaciones directas del puesto. La baja libera la composición del puesto; conserva esos registros y no simula desinstalaciones ni retirada de accesos externos. Los perfiles sin un puesto vigente permanecen para revisión. La ficha actualiza los datos después de confirmar la operación; tras eliminar vuelve al listado. +El puesto mantiene un único conjunto de activos. Los perfiles de usuario, correo, permisos, certificados y software pertenecen a personas, cuentas o al ordenador, y no son asignaciones directas del puesto. La desactivación libera la composición del puesto; conserva esos registros y no simula desinstalaciones ni retirada de accesos externos. Los perfiles sin un puesto vigente permanecen para revisión. La ficha actualiza los datos después de confirmar la operación; tras eliminar vuelve al listado. -`POST /api/assets/:companyId/workstations/:id/decommission` y `POST /api/assets/:companyId/workstations/:id/delete` reciben cuerpo vacío `{}`. Requieren sesión, origen y CSRF; compañía y puesto se validan en servidor. La baja registra actor de sesión y fecha en la revisión. Ambas operaciones comparten los bloqueos de catálogo y compañía con las reservas de activos. La baja conserva las referencias históricas; la eliminación elimina las asociaciones internas mediante las claves foráneas y las revisiones, sin desactivar restricciones ni persistir JSON. +`POST /api/assets/:companyId/workstations/:id/decommission` y `POST /api/assets/:companyId/workstations/:id/delete` reciben cuerpo vacío `{}`. Requieren sesión, origen y CSRF; compañía y puesto se validan en servidor. La desactivación registra actor de sesión y fecha en la revisión. Ambas operaciones comparten los bloqueos de catálogo y compañía con las reservas de activos. La desactivación conserva las referencias históricas; la eliminación elimina las asociaciones internas mediante las claves foráneas y las revisiones, sin desactivar restricciones ni persistir JSON. -Las pruebas cubren baja activa/inactiva, liberación de reservas exclusivas y compartidas, ordenador y responsable, historial e idempotencia, eliminación activa/inactiva, referencias externas, rollback lógico, pertenencia a compañía, cuerpo estricto y protección CSRF/origen. +Las pruebas cubren desactivación activa/inactiva, liberación de reservas, ordenador y responsable, historial e idempotencia, eliminación activa/inactiva, referencias externas, rollback lógico, pertenencia a compañía, cuerpo estricto y protección CSRF/origen. -El estado mostrado del activo se calcula desde su estado técnico y los puestos activos, según [Inventario de activos](inventario-de-activos.md). La baja libera composición y reservas sin cambiar el estado técnico de sus activos. +El estado mostrado del activo se calcula desde su estado técnico y los puestos activos, según [Inventario de activos](inventario-de-activos.md). La desactivación libera composición y reservas sin cambiar el estado técnico de sus activos. + +## Alta en cuatro pasos + +1. Sede y responsable. Si existe una única sede se selecciona automáticamente. El responsable es opcional; las altas relacionadas conservan el formulario. +2. Activos. Un diálogo con listado, búsqueda rápida y pestañas por plantilla permite añadir varios a la vez. Es obligatorio añadir al menos uno; se impiden duplicados, activos ocupados y nuevas asociaciones no operativas. +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. + +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/docs/rutas-del-cliente.md b/docs/rutas-del-cliente.md index a94018b..1bdf591 100644 --- a/docs/rutas-del-cliente.md +++ b/docs/rutas-del-cliente.md @@ -103,7 +103,7 @@ La consulta conserva los filtros que ya están conectados a la URL: - Tickets: `companyId`, `pendingOnly`, `q`, `state`, `priority` y `assignee`. - Intervenciones: `companyId`, `pendingOnly`, `q`, `state`, `assignee`, `dateFrom` y `dateTo`. - Activos: `q` y `template` (UUID de plantilla), tanto en el listado como en la ficha seleccionada. -- Recursos: `q`. +- Recursos: `q` y `template` (UUID de plantilla), tanto en el listado como en la ficha seleccionada. - Perfiles y procesos: `q`, con `archived` o `closed`, respectivamente. - Catálogos de activos y recursos: `q` y `tab`; `relations` solo en activos. - Cuentas y accesos: `q` y `kind`, respetando los tipos disponibles en cada ámbito. diff --git a/docs/traducciones.md b/docs/traducciones.md index 4d51001..d036701 100644 --- a/docs/traducciones.md +++ b/docs/traducciones.md @@ -28,6 +28,10 @@ La biblioteca visual conserva sus textos en su propio paquete, sin depender de m No duplicar textos de componentes genéricos en los módulos. Los textos específicos de una entidad pertenecen al módulo consumidor. +## Terminología de desactivación y eliminación + +**Desactivar** (`Deactivate` en inglés) indica una baja lógica: el registro se conserva y queda inactivo. **Eliminar** (`Delete` en inglés) indica el borrado definitivo, sujeto a las restricciones de cada entidad. Las etiquetas, confirmaciones y errores utilizan estos términos de forma coherente; no se utiliza «Dar de baja» como etiqueta de desactivación. + ## Incorporar un idioma Crear un JSON junto a los existentes en cada módulo y en la biblioteca visual, conservando las claves y parámetros. Exportar las traducciones desde los adaptadores del módulo, registrarlas en `apps/web/src/i18n.ts` y ampliar `supportedLngs`, el selector de `apps/web/src/app.tsx` y los nombres de idioma de Core. Ampliar también la selección de idioma de `entityListLabels` y las pruebas de paridad. La carga sigue siendo explícita, sin nuevas dependencias. diff --git a/modules/assets/package.json b/modules/assets/package.json index 0972c45..ef8ea31 100644 --- a/modules/assets/package.json +++ b/modules/assets/package.json @@ -10,7 +10,7 @@ }, "scripts": { "typecheck": "tsc -p tsconfig.json", - "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/api/domain/asset-catalog.test.ts src/api/domain/asset.test.ts src/api/domain/workstation.test.ts src/api/domain/asset-relationship.test.ts src/web/catalog/shared/catalog.test.tsx src/web/assets/shared/inventory.test.tsx src/api/domain/support.test.ts src/web/support-connections/shared/support.test.tsx src/api/domain/brand.test.ts src/web/brands/shared/brands.test.tsx src/api/domain/template-sections.test.ts" + "test": "TSX_TSCONFIG_PATH=../../tsconfig.json node --import tsx --test src/api/domain/asset-catalog.test.ts src/api/domain/asset.test.ts src/api/domain/workstation.test.ts src/api/domain/asset-relationship.test.ts src/web/catalog/shared/catalog.test.tsx src/web/assets/shared/inventory.test.tsx src/api/domain/support.test.ts src/web/support-connections/shared/support.test.tsx src/api/domain/brand.test.ts src/web/brands/shared/brands.test.tsx src/api/domain/template-sections.test.ts src/web/workstations/shared/helpers/workstation-creation.test.ts src/web/assets/select/asset-selection.test.tsx" }, "dependencies": { "@hookform/resolvers": "5.2.2", diff --git a/modules/assets/src/api/application/asset-input.mapper.ts b/modules/assets/src/api/application/asset-input.mapper.ts index c418138..37b8514 100644 --- a/modules/assets/src/api/application/asset-input.mapper.ts +++ b/modules/assets/src/api/application/asset-input.mapper.ts @@ -23,9 +23,7 @@ export function mapAssetInput( context.workstations.some( (station) => station.values.active && - [...station.values.assetIds, ...station.values.sharedAssetIds].some( - (id) => id.toPrimitive() === previous?.id.toPrimitive() - ) + station.values.assetIds.some((id) => id.toPrimitive() === previous?.id.toPrimitive()) ) ) { return Result.fail(new AssetCatalogError("ASSET_ASSIGNED")); diff --git a/modules/assets/src/api/application/asset-response.builder.ts b/modules/assets/src/api/application/asset-response.builder.ts index 60b55e3..c7212ee 100644 --- a/modules/assets/src/api/application/asset-response.builder.ts +++ b/modules/assets/src/api/application/asset-response.builder.ts @@ -9,11 +9,7 @@ export function buildAssetResponse( workstations: readonly Workstation[] = [] ): AssetDTO { const assigned = workstations.some( - (station) => - station.values.active && - [...station.values.assetIds, ...station.values.sharedAssetIds].some((id) => - id.equals(asset.id) - ) + (station) => station.values.active && station.values.assetIds.some((id) => id.equals(asset.id)) ); return { diff --git a/modules/assets/src/api/application/workstation-input.mapper.ts b/modules/assets/src/api/application/workstation-input.mapper.ts index d4e01a2..f5be081 100644 --- a/modules/assets/src/api/application/workstation-input.mapper.ts +++ b/modules/assets/src/api/application/workstation-input.mapper.ts @@ -31,7 +31,7 @@ export function mapWorkstationInput( ) { return Result.fail(new AssetCatalogError("PERSON_UNAVAILABLE")); } - for (const id of [...values.assetIds, ...values.sharedAssetIds]) { + for (const id of values.assetIds) { const error = memberError(id, values, context, previous); if (error) { @@ -56,17 +56,11 @@ function memberError( if (!values.active) { return undefined; } - const exclusive = values.assetIds.includes(id); - const previousMembers = exclusive ? previous?.values.assetIds : previous?.values.sharedAssetIds; + const previousMembers = previous?.values.assetIds; const retained = previous?.values.active && previousMembers?.some((item) => item.toPrimitive() === id); - if ( - !( - retained || - canAssignAsset(asset.values.technicalStatus, exclusive ? "exclusive" : "shared", false, false) - ) - ) { + if (!(retained || canAssignAsset(asset.values.technicalStatus, false))) { return "ASSET_NOT_OPERATIONAL"; } const others = context.workstations.filter( @@ -75,11 +69,8 @@ function memberError( const exclusiveElsewhere = others.some((item) => item.values.assetIds.some((assetId) => assetId.toPrimitive() === id) ); - const sharedElsewhere = others.some((item) => - item.values.sharedAssetIds.some((assetId) => assetId.toPrimitive() === id) - ); - if (exclusiveElsewhere || (exclusive && sharedElsewhere)) { + if (exclusiveElsewhere) { return "ASSET_IN_WORKSTATION"; } diff --git a/modules/assets/src/api/domain/asset-state.ts b/modules/assets/src/api/domain/asset-state.ts index 89a8f67..56938dc 100644 --- a/modules/assets/src/api/domain/asset-state.ts +++ b/modules/assets/src/api/domain/asset-state.ts @@ -8,11 +8,6 @@ export function assetDisplayStatus(status: AssetInput["status"], assigned: boole return status === "operational" ? (assigned ? "in_use" : "available") : status; } -export function canAssignAsset( - status: AssetInput["status"], - kind: "exclusive" | "shared", - exclusiveElsewhere: boolean, - sharedElsewhere: boolean -) { - return status === "operational" && !exclusiveElsewhere && (kind === "shared" || !sharedElsewhere); +export function canAssignAsset(status: AssetInput["status"], assignedElsewhere: boolean) { + return status === "operational" && !assignedElsewhere; } diff --git a/modules/assets/src/api/domain/asset.test.ts b/modules/assets/src/api/domain/asset.test.ts index 036ce95..d3dfe20 100644 --- a/modules/assets/src/api/domain/asset.test.ts +++ b/modules/assets/src/api/domain/asset.test.ts @@ -149,15 +149,12 @@ test("estado técnico y uso: asignaciones calculadas, elegibilidad exclusiva/com assert.equal(assetTechnicalStatus("in_use"), "operational"); assert.equal(assetDisplayStatus("operational", false), "available"); assert.equal(assetDisplayStatus("operational", true), "in_use"); - assert.ok(canAssignAsset("operational", "exclusive", false, false)); - assert.ok(!canAssignAsset("operational", "exclusive", false, true)); - assert.ok(!canAssignAsset("operational", "shared", true, false)); - assert.ok(canAssignAsset("operational", "shared", false, true)); + assert.ok(canAssignAsset("operational", false)); + assert.ok(!canAssignAsset("operational", true)); for (const state of ["broken", "maintenance", "retired"] as const) { assert.equal(assetDisplayStatus(state, false), state); assert.equal(assetDisplayStatus(state, true), state); - assert.ok(!canAssignAsset(state, "exclusive", false, false)); - assert.ok(!canAssignAsset(state, "shared", false, false)); + assert.ok(!canAssignAsset(state, false)); } assert.equal(AssetInputSchema.safeParse({ ...input(), status: "in_use" }).success, false); assert.equal(AssetInputSchema.safeParse({ ...input(), status: "available" }).success, false); diff --git a/modules/assets/src/api/domain/value-objects/workstation-values.vo.ts b/modules/assets/src/api/domain/value-objects/workstation-values.vo.ts index 0699c6c..d559da6 100644 --- a/modules/assets/src/api/domain/value-objects/workstation-values.vo.ts +++ b/modules/assets/src/api/domain/value-objects/workstation-values.vo.ts @@ -12,8 +12,7 @@ export class WorkstationValues { public readonly active: boolean, public readonly siteId: Maybe, public readonly personId: Maybe, - public readonly assetIds: readonly UniqueID[], - public readonly sharedAssetIds: readonly UniqueID[] + public readonly assetIds: readonly UniqueID[] ) { Object.freeze(this); } @@ -37,7 +36,6 @@ export class WorkstationValues { return Result.fail(new AssetCatalogError("INVALID_REQUEST")); } const ids: UniqueID[] = []; - const shared: UniqueID[] = []; for (const value of parsed.data.assetIds) { const id = UniqueID.create(value); @@ -47,14 +45,6 @@ export class WorkstationValues { } ids.push(id.data); } - for (const value of parsed.data.sharedAssetIds) { - const id = UniqueID.create(value); - - if (!id.isSuccess || [...ids, ...shared].some((previous) => previous.equals(id.data))) { - return Result.fail(new AssetCatalogError("INVALID_MEMBERS")); - } - shared.push(id.data); - } if (input.computerAssetId && !input.assetIds.includes(input.computerAssetId)) { return Result.fail(new AssetCatalogError("INVALID_MEMBERS")); @@ -67,8 +57,7 @@ export class WorkstationValues { input.active, site.data, person.data, - Object.freeze(ids), - Object.freeze(shared) + Object.freeze(ids) ) ); } @@ -83,7 +72,6 @@ export class WorkstationValues { siteId: this.siteId.getOrUndefined()?.toPrimitive() ?? null, personId: this.personId.getOrUndefined()?.toPrimitive() ?? null, assetIds: this.assetIds.map((id) => id.toPrimitive()), - sharedAssetIds: this.sharedAssetIds.map((id) => id.toPrimitive()), }; } @@ -98,9 +86,7 @@ export class WorkstationValues { a.siteId === b.siteId && a.personId === b.personId && a.assetIds.length === b.assetIds.length && - a.assetIds.every((id) => b.assetIds.includes(id)) && - a.sharedAssetIds.length === b.sharedAssetIds.length && - a.sharedAssetIds.every((id) => b.sharedAssetIds.includes(id)) + a.assetIds.every((id) => b.assetIds.includes(id)) ); } } diff --git a/modules/assets/src/api/domain/workstation-values.ts b/modules/assets/src/api/domain/workstation-values.ts index 95caa8e..5a89045 100644 --- a/modules/assets/src/api/domain/workstation-values.ts +++ b/modules/assets/src/api/domain/workstation-values.ts @@ -7,7 +7,6 @@ export const WorkstationInputSchema = z.strictObject({ siteId: z.uuid().nullable(), personId: z.uuid().nullable(), assetIds: z.array(z.uuid()).max(200), - sharedAssetIds: z.array(z.uuid()).max(200), }); export type WorkstationInput = z.infer; diff --git a/modules/assets/src/api/domain/workstation.aggregate.ts b/modules/assets/src/api/domain/workstation.aggregate.ts index 2cb1bda..611929a 100644 --- a/modules/assets/src/api/domain/workstation.aggregate.ts +++ b/modules/assets/src/api/domain/workstation.aggregate.ts @@ -60,7 +60,6 @@ export class Workstation extends AggregateRoot { computerAssetId: null, personId: null, assetIds: [], - sharedAssetIds: [], }); return values.isSuccess ? this.update(values.data, actorId, recordedAt) : values; diff --git a/modules/assets/src/api/domain/workstation.test.ts b/modules/assets/src/api/domain/workstation.test.ts index 8671170..4739564 100644 --- a/modules/assets/src/api/domain/workstation.test.ts +++ b/modules/assets/src/api/domain/workstation.test.ts @@ -15,7 +15,6 @@ function input(): WorkstationInput { active: true, siteId: null, personId: null, - sharedAssetIds: [], assetIds, }; } @@ -88,36 +87,6 @@ test("Puestos: UUID estable e historial de composición, persona y estado; guard ); }); -test("Puestos: recursos compartidos separados, sin duplicados y cambios con historial", () => { - const sharedId = UniqueID.generateNewID().toPrimitive(); - const original = values({ ...input(), sharedAssetIds: [sharedId] }); - - assert.ok(Object.isFrozen(original.sharedAssetIds)); - assert.ok( - !WorkstationValues.create({ ...input(), sharedAssetIds: [assetIds[0] ?? ""] }).isSuccess - ); - assert.ok( - !WorkstationValues.create({ ...input(), sharedAssetIds: [sharedId, sharedId] }).isSuccess - ); - const created = Workstation.create( - UniqueID.generateNewID(), - original, - UniqueID.generateNewID(), - "2026-10-04T10:00:00.000Z" - ); - - assert.ok(created.isSuccess); - const changed = created.data.update( - values(), - UniqueID.generateNewID(), - "2026-10-04T11:00:00.000Z" - ); - - assert.ok(changed.isSuccess); - assert.deepEqual(changed.data.history[0]?.values.toPrimitive().sharedAssetIds, [sharedId]); - assert.equal(changed.data.history.length, 2); -}); - test("Puestos: ordenador principal exclusivo, único e independiente del responsable", () => { const computerId = assetIds[0] ?? ""; const first = values({ ...input(), computerAssetId: computerId }); @@ -135,7 +104,6 @@ test("Puestos: ordenador principal exclusivo, único e independiente del respons ...input(), computerAssetId: computerId, assetIds: [], - sharedAssetIds: [computerId], }).isSuccess ); assert.ok(!first.equals(values({ ...input(), computerAssetId: assetIds[1] }))); @@ -148,14 +116,12 @@ test("Puestos: ordenador principal exclusivo, único e independiente del respons test("baja del puesto: libera composición y responsable, conserva historial e identidad y es idempotente", () => { const actor = UniqueID.generateNewID(); - const shared = UniqueID.generateNewID().toPrimitive(); const created = Workstation.create( UniqueID.generateNewID(), values({ ...input(), computerAssetId: assetIds[0] ?? null, personId: actor.toPrimitive(), - sharedAssetIds: [shared], }), actor, "2026-10-13T10:00:00.000Z" @@ -171,9 +137,7 @@ test("baja del puesto: libera composición y responsable, conserva historial e i assert.equal(retired.data.values.toPrimitive().personId, null); assert.equal(retired.data.values.computerAssetId.getOrUndefined(), undefined); assert.deepEqual(retired.data.values.toPrimitive().assetIds, []); - assert.deepEqual(retired.data.values.toPrimitive().sharedAssetIds, []); assert.deepEqual(retired.data.history[0]?.values.toPrimitive().assetIds, assetIds); - assert.deepEqual(retired.data.history[0]?.values.toPrimitive().sharedAssetIds, [shared]); assert.ok(retired.data.history[1]?.actorId.equals(actor)); const repeated = retired.data.decommission(actor, "2026-10-13T12:00:00.000Z"); diff --git a/modules/assets/src/api/infrastructure/persistence/load-workstations.ts b/modules/assets/src/api/infrastructure/persistence/load-workstations.ts index 4ad6515..0eedf1a 100644 --- a/modules/assets/src/api/infrastructure/persistence/load-workstations.ts +++ b/modules/assets/src/api/infrastructure/persistence/load-workstations.ts @@ -42,9 +42,7 @@ export async function loadWorkstations( if ( (values.siteId && !sites.some((site) => site.id === values.siteId)) || (values.personId && !people.some((person) => person.id === values.personId)) || - [...values.assetIds, ...values.sharedAssetIds].some( - (id) => !assets.some((asset) => asset.id.toPrimitive() === id) - ) + values.assetIds.some((id) => !assets.some((asset) => asset.id.toPrimitive() === id)) ) { throw new Error("Puesto de trabajo fuera de su compañía."); } diff --git a/modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets.ts b/modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets.ts new file mode 100644 index 0000000..49a9f51 --- /dev/null +++ b/modules/assets/src/api/infrastructure/persistence/remove-shared-workstation-assets.ts @@ -0,0 +1,20 @@ +import type { Sequelize } from "sequelize"; + +/** Retira el vínculo compartido de instantáneas y proyecciones, sin borrar activos ni revisiones. */ +export async function removeSharedWorkstationAssets(database: Sequelize) { + await database.transaction(async (transaction) => { + await database.query("DELETE FROM workstation_members WHERE kind='shared'", { transaction }); + await database.query("DELETE FROM workstation_revision_members WHERE kind='shared'", { + transaction, + }); + await database.query( + `DELETE reference FROM workstation_assets AS reference + WHERE NOT EXISTS ( + SELECT 1 FROM workstation_revision_members AS member + INNER JOIN workstation_revisions AS revision ON revision.id=member.revisionId + WHERE revision.workstationId=reference.workstationId AND member.assetId=reference.assetId + )`, + { transaction } + ); + }); +} diff --git a/modules/assets/src/api/infrastructure/persistence/workstation-persistence.mapper.ts b/modules/assets/src/api/infrastructure/persistence/workstation-persistence.mapper.ts index eea23a4..1c0c99d 100644 --- a/modules/assets/src/api/infrastructure/persistence/workstation-persistence.mapper.ts +++ b/modules/assets/src/api/infrastructure/persistence/workstation-persistence.mapper.ts @@ -53,12 +53,7 @@ export class WorkstationPersistenceMapper { if (!(history.length && history.at(-1)?.values.equals(values))) { throw new Error("Última revisión de puesto inconsistente."); } - const expected = new Set( - history.flatMap((item) => [ - ...item.values.toPrimitive().assetIds, - ...item.values.toPrimitive().sharedAssetIds, - ]) - ); + const expected = new Set(history.flatMap((item) => item.values.toPrimitive().assetIds)); if ( references.length !== expected.size || @@ -99,12 +94,7 @@ export class WorkstationPersistenceMapper { personId: revision.values.toPrimitive().personId, })), references: [ - ...new Set( - item.history.flatMap((revision) => [ - ...revision.values.toPrimitive().assetIds, - ...revision.values.toPrimitive().sharedAssetIds, - ]) - ), + ...new Set(item.history.flatMap((revision) => revision.values.toPrimitive().assetIds)), ].map((assetId) => ({ workstationId: id, companyId, diff --git a/modules/assets/src/api/infrastructure/persistence/workstation-relational.ts b/modules/assets/src/api/infrastructure/persistence/workstation-relational.ts index 582eb98..5a16210 100644 --- a/modules/assets/src/api/infrastructure/persistence/workstation-relational.ts +++ b/modules/assets/src/api/infrastructure/persistence/workstation-relational.ts @@ -47,7 +47,7 @@ function reconstruct(row: StoredRow, members: readonly Member[]): Values { .filter((member) => member.ownerId === row.id) .sort((a, b) => a.position - b.position); - if (ordered.some((member) => !["exclusive", "shared"].includes(member.kind))) { + if (ordered.some((member) => member.kind !== "exclusive")) { throw new Error("Tipo de asignación de activo inconsistente."); } @@ -60,9 +60,6 @@ function reconstruct(row: StoredRow, members: readonly Member[]): Values { assetIds: ordered .filter((member) => member.kind === "exclusive") .map((member) => member.assetId), - sharedAssetIds: ordered - .filter((member) => member.kind === "shared") - .map((member) => member.assetId), }); } @@ -139,13 +136,13 @@ async function writeMembers( replacements: { id }, transaction, }); - for (const [kind, ids] of [ - ["exclusive", values.assetIds], - ["shared", values.sharedAssetIds], - ] as const) { - for (const [position, assetId] of ids.entries()) { - await writeAssetRow(database, table, { [owner]: id, assetId, kind, position }, transaction); - } + for (const [position, assetId] of values.assetIds.entries()) { + await writeAssetRow( + database, + table, + { [owner]: id, assetId, kind: "exclusive", position }, + transaction + ); } } diff --git a/modules/assets/src/api/infrastructure/persistence/workstation.model.ts b/modules/assets/src/api/infrastructure/persistence/workstation.model.ts index 1e1162a..366985b 100644 --- a/modules/assets/src/api/infrastructure/persistence/workstation.model.ts +++ b/modules/assets/src/api/infrastructure/persistence/workstation.model.ts @@ -1,6 +1,7 @@ import type { InferAttributes, InferCreationAttributes, Sequelize } from "sequelize"; import { DataTypes, Model, QueryTypes } from "sequelize"; +import { removeSharedWorkstationAssets } from "./remove-shared-workstation-assets"; import { initializeWorkstationMembers } from "./workstation-relational"; /** Proyección técnica de referencias históricas y reserva vigente; no representa otra entidad de negocio. */ @@ -104,4 +105,5 @@ export async function migrateWorkstations(database: Sequelize) { timestamps: false, }); await initializeWorkstationMembers(database); + await removeSharedWorkstationAssets(database); } diff --git a/modules/assets/src/api/module.ts b/modules/assets/src/api/module.ts index fcc3e2c..f87654c 100644 --- a/modules/assets/src/api/module.ts +++ b/modules/assets/src/api/module.ts @@ -120,7 +120,7 @@ function inventoryAssignments(context: InventoryContext) { if (!(values.active && values.personId)) { continue; } - for (const assetId of [...values.assetIds, ...values.sharedAssetIds]) { + for (const assetId of values.assetIds) { const people = peopleByAsset.get(assetId) ?? new Set(); people.add(values.personId); diff --git a/modules/assets/src/common/locales/en.json b/modules/assets/src/common/locales/en.json index edc9534..d10cc39 100644 --- a/modules/assets/src/common/locales/en.json +++ b/modules/assets/src/common/locales/en.json @@ -147,10 +147,6 @@ }, "workstations": "Workstations", "newWorkstation": "New workstation", - "addSharedWorkstationAsset": "Add a shared resource", - "exclusiveWorkstationAssets": "Exclusive assets", - "sharedWorkstationAssets": "Shared resources", - "noSharedWorkstationAssets": "No shared resources.", "editWorkstation": "Edit workstation", "workstationPerson": "Workstation custodian", "workstationSuggestedName": "{{person}}'s workstation at {{site}}", @@ -158,7 +154,7 @@ "workstationAssets": "Workstation assets", "noWorkstations": "This company has no workstations yet.", "noWorkstationAssets": "This workstation has no assets yet.", - "addWorkstationAsset": "Add an exclusive asset", + "addWorkstationAsset": "Add assets", "workstationOccupied": "{{name}} · used in another workstation", "removeWorkstationAsset": "Remove {{name}} from workstation", "workstationCount": "{{count}} assets", @@ -166,7 +162,7 @@ "workstationCount_other": "{{count}} assets", "workstationDescription": "A workstation groups a computer and its peripherals. Its optional custodian is independent of the people with profiles on it.", "workstationExamples": "Ana's workstation: laptop, two monitors and a printer. One person can have several workstations.", - "workstationGuidance": "Exclusive assets can belong to one active workstation. Shared resources can be used in several workstations unless they are reserved as exclusive. Deactivation preserves composition and releases its assets; reactivation checks availability. Workstation assignment does not change individual asset assignments.", + "workstationGuidance": "Select the site, add at least one asset and designate the main computer. Each asset can belong to only one active workstation. Deactivation keeps its composition and releases its assets; reactivation checks availability.", "workstationScope": "All elements must belong to this company. Person, site and composition changes retain history.", "workstationInAsset": "Workstation", "noWorkstation": "No active workstation", @@ -249,6 +245,7 @@ "selectField": "Add a field to the template", "choose": "Select a field", "addField": "New field", + "clear": "Clear selection", "required": "Required", "chooseType": "Select a type", "view": "View", @@ -301,6 +298,7 @@ "moveDown": "Move down", "remove": "Remove section", "removeField": "Remove field", + "clear": "Clear selection", "required": "Required", "assign": "Section", "help": "Organize fields without changing their values or requirements. Removing a section moves its fields to Unsectioned.", @@ -354,7 +352,7 @@ "notFound": "This workstation does not exist in this company.", "assets": "Workstation assets", "profiles": "User profiles", - "profilesHint": "Profiles associated with the exclusive and shared assets of this workstation.", + "profilesHint": "Profiles associated with the assets of this workstation.", "profilesError": "Unable to load user profiles.", "noProfiles": "No profiles are linked to this workstation’s assets.", "people": "Related people", @@ -365,24 +363,54 @@ "missingAsset": "Asset unavailable." }, "workstationComputer": "Main computer", - "workstationComputerHelp": "Select the computer among this workstation’s exclusive assets. Profiles will reside on it; the remaining assets are peripherals. Use two workstations for two computers.", + "workstationComputerHelp": "Select the computer from the workstation assets. Profiles will be created on it; the other assets are peripherals.", "workstationLifecycle": { "decommission": { - "title": "Decommission", - "description": "Decommission “{{name}}” and unlink its responsible person, computer, exclusive and shared assets. History is retained. Assets, profiles and computer software retain their records.", - "pending": "Decommissioning…", - "error": "Could not decommission the workstation. Please try again." + "title": "Deactivate", + "description": "Deactivate “{{name}}” and unlink its responsible person, computer, assets. History is retained. Assets, profiles and computer software retain their records.", + "pending": "Deactivating…", + "error": "Could not deactivate the workstation. Please try again." }, "delete": { "title": "Delete permanently", - "description": "Delete “{{name}}”, its history and its associations. Assets, people, profiles and software are retained. This cannot be undone. If tickets or interventions refer to it, use Decommission.", + "description": "Delete “{{name}}”, its history and its associations. Assets, people, profiles and software are retained. This cannot be undone. If tickets or interventions refer to it, use Deactivate.", "pending": "Deleting…", "error": "Could not delete the workstation. Please try again." }, - "inUse": "Other records, such as tickets or interventions, reference this workstation. Decommission it to retain those references." + "inUse": "Other records, such as tickets or interventions, reference this workstation. Deactivate it to retain those references." }, "operational": "Operational", "broken": "Broken", "technicalStatus": "Technical state", - "technicalStatusHelp": "Available and In use are calculated from active workstations. Breakdowns and maintenance retain existing assignments." + "technicalStatusHelp": "Available and In use are calculated from active workstations. Breakdowns and maintenance retain existing assignments.", + "workstationPersonOptional": "Workstation owner (optional)", + "workstationNameBySite": "Workstation at {{site}}", + "workstationSteps": { + "label": "New workstation steps", + "progress": "Step {{step}} of {{total}}", + "completed": "Completed", + "location": "Site and owner", + "assets": "Assets", + "computer": "Main computer", + "summary": "Summary", + "locationDescription": "Select a site. You can leave the owner unassigned.", + "assetsDescription": "Add at least one asset to the workstation.", + "computerDescription": "Identify the asset that will host user profiles.", + "summaryDescription": "Review the details and workstation name before saving.", + "singleComputer": "The only selected asset is automatically assigned as the main computer.", + "previous": "Previous", + "next": "Next" + }, + "assetPicker": { + "single": "Select asset", + "multiple": "Select assets", + "select": "Selection", + "selectAsset": "Select {{name}}", + "selected": "{{count}} assets selected", + "selected_one": "{{count}} asset selected", + "selected_other": "{{count}} assets selected", + "confirm": "Select", + "clear": "Clear selection", + "required": "Required" + } } diff --git a/modules/assets/src/common/locales/es.json b/modules/assets/src/common/locales/es.json index 3d69e63..8fdf3bf 100644 --- a/modules/assets/src/common/locales/es.json +++ b/modules/assets/src/common/locales/es.json @@ -147,10 +147,6 @@ }, "workstations": "Puestos de trabajo", "newWorkstation": "Nuevo puesto", - "addSharedWorkstationAsset": "Añadir un recurso compartido", - "exclusiveWorkstationAssets": "Activos exclusivos", - "sharedWorkstationAssets": "Recursos compartidos", - "noSharedWorkstationAssets": "Sin recursos compartidos.", "editWorkstation": "Editar puesto", "workstationPerson": "Responsable del puesto", "workstationSuggestedName": "Puesto de {{person}} en {{site}}", @@ -158,7 +154,7 @@ "workstationAssets": "Activos del puesto", "noWorkstations": "Esta compañía todavía no tiene puestos de trabajo.", "noWorkstationAssets": "Este puesto todavía no tiene activos.", - "addWorkstationAsset": "Añadir un activo exclusivo", + "addWorkstationAsset": "Añadir activos", "workstationOccupied": "{{name}} · ocupado en otro puesto", "removeWorkstationAsset": "Retirar {{name}} del puesto", "workstationCount": "{{count}} activos", @@ -166,7 +162,7 @@ "workstationCount_other": "{{count}} activos", "workstationDescription": "Un puesto reúne un ordenador y sus periféricos. Su responsable es opcional e independiente de las personas que tienen perfiles en él.", "workstationExamples": "Puesto de Ana: portátil, dos monitores y una impresora. Una persona puede tener varios puestos.", - "workstationGuidance": "Los activos exclusivos solo pueden estar en un puesto activo. Los recursos compartidos pueden utilizarse en varios puestos, siempre que no estén reservados como exclusivos. Desactivar conserva la composición y libera sus activos; reactivar comprueba su disponibilidad. El puesto no cambia las asignaciones individuales de los activos.", + "workstationGuidance": "Selecciona la sede, añade al menos un activo y designa el ordenador principal. Cada activo solo puede pertenecer a un puesto activo. Desactivar conserva la composición y libera sus activos; reactivar comprueba su disponibilidad.", "workstationScope": "Todos los elementos deben pertenecer a esta compañía. Los cambios de persona, sede y composición conservan historial.", "workstationInAsset": "Puesto de trabajo", "noWorkstation": "Sin puesto activo", @@ -249,6 +245,7 @@ "selectField": "Añadir un campo a la plantilla", "choose": "Selecciona un campo", "addField": "Nuevo campo", + "clear": "Quitar selección", "required": "Obligatorio", "chooseType": "Selecciona un tipo", "view": "Ver", @@ -301,6 +298,7 @@ "moveDown": "Mover hacia abajo", "remove": "Eliminar sección", "removeField": "Quitar campo", + "clear": "Quitar selección", "required": "Obligatorio", "assign": "Sección", "help": "Organiza los campos sin cambiar sus valores ni su obligatoriedad. Al eliminar una sección, sus campos pasan a «Sin sección».", @@ -354,7 +352,7 @@ "notFound": "Este puesto de trabajo no existe en esta compañía.", "assets": "Activos del puesto", "profiles": "Perfiles de usuario", - "profilesHint": "Perfiles asociados a los activos exclusivos y compartidos de este puesto.", + "profilesHint": "Perfiles asociados a los activos de este puesto.", "profilesError": "No se han podido cargar los perfiles de usuario.", "noProfiles": "No hay perfiles vinculados a los activos de este puesto.", "people": "Personas relacionadas", @@ -365,24 +363,54 @@ "missingAsset": "Activo no disponible." }, "workstationComputer": "Ordenador principal", - "workstationComputerHelp": "Selecciona el ordenador entre los activos exclusivos del puesto. Los perfiles se crearán en él; el resto de activos son periféricos. Si hay dos ordenadores, crea dos puestos.", + "workstationComputerHelp": "Selecciona el ordenador entre los activos del puesto. Los perfiles se crearán en él; el resto de activos son periféricos.", "workstationLifecycle": { "decommission": { - "title": "Dar de baja", - "description": "Se dará de baja «{{name}}» y se desvincularán su responsable, ordenador y activos exclusivos y compartidos. Se conservará el historial. Los activos, perfiles y software del ordenador conservarán sus registros.", - "pending": "Dando de baja…", - "error": "No se pudo dar de baja el puesto. Inténtalo de nuevo." + "title": "Desactivar", + "description": "Se desactivará «{{name}}» y se desvincularán su responsable, ordenador y activos. Se conservará el historial. Los activos, perfiles y software del ordenador conservarán sus registros.", + "pending": "Desactivando…", + "error": "No se pudo desactivar el puesto. Inténtalo de nuevo." }, "delete": { "title": "Eliminar definitivamente", - "description": "Se eliminarán «{{name}}», su historial y sus asociaciones. Los activos, personas, perfiles y software se conservarán. Esta acción no se puede deshacer. Si hay tickets o intervenciones vinculados, utiliza Dar de baja.", + "description": "Se eliminarán «{{name}}», su historial y sus asociaciones. Los activos, personas, perfiles y software se conservarán. Esta acción no se puede deshacer. Si hay tickets o intervenciones vinculados, utiliza Desactivar.", "pending": "Eliminando…", "error": "No se pudo eliminar el puesto. Inténtalo de nuevo." }, - "inUse": "El puesto está referenciado por otros registros, por ejemplo tickets o intervenciones. Da de baja el puesto para conservar esas referencias." + "inUse": "El puesto está referenciado por otros registros, por ejemplo tickets o intervenciones. Desactiva el puesto para conservar esas referencias." }, "operational": "Operativo", "broken": "Averiado", "technicalStatus": "Estado técnico", - "technicalStatusHelp": "Disponible y En uso se calculan según los puestos activos. Una avería o mantenimiento conserva las asignaciones existentes." + "technicalStatusHelp": "Disponible y En uso se calculan según los puestos activos. Una avería o mantenimiento conserva las asignaciones existentes.", + "workstationPersonOptional": "Responsable del puesto (opcional)", + "workstationNameBySite": "Puesto en {{site}}", + "workstationSteps": { + "label": "Pasos del nuevo puesto", + "progress": "Paso {{step}} de {{total}}", + "completed": "Completado", + "location": "Sede y responsable", + "assets": "Activos", + "computer": "Ordenador principal", + "summary": "Resumen", + "locationDescription": "Selecciona una sede. Puedes dejar el responsable sin asignar.", + "assetsDescription": "Añade al menos un activo al puesto.", + "computerDescription": "Identifica el activo que alojará los perfiles de usuario.", + "summaryDescription": "Revisa los datos y el nombre del puesto antes de guardar.", + "singleComputer": "El único activo seleccionado se asigna automáticamente como ordenador principal.", + "previous": "Anterior", + "next": "Siguiente" + }, + "assetPicker": { + "single": "Seleccionar activo", + "multiple": "Seleccionar activos", + "select": "Selección", + "selectAsset": "Seleccionar {{name}}", + "selected": "{{count}} activos seleccionados", + "selected_one": "{{count}} activo seleccionado", + "selected_other": "{{count}} activos seleccionados", + "confirm": "Seleccionar", + "clear": "Quitar selección", + "required": "Obligatorio" + } } diff --git a/modules/assets/src/web/asset-relationships/list/types/asset-relationships-props.ts b/modules/assets/src/web/asset-relationships/list/types/asset-relationships-props.ts index 59d3501..ab8b5a8 100644 --- a/modules/assets/src/web/asset-relationships/list/types/asset-relationships-props.ts +++ b/modules/assets/src/web/asset-relationships/list/types/asset-relationships-props.ts @@ -1,5 +1,6 @@ export type AssetRelationshipsProps = { compact?: boolean; companyId: string; + sites?: readonly { id: string; name: string }[] | undefined; assetId: string; }; diff --git a/modules/assets/src/web/asset-relationships/list/ui/asset-relationships.tsx b/modules/assets/src/web/asset-relationships/list/ui/asset-relationships.tsx index 92e4efc..e3c97bd 100644 --- a/modules/assets/src/web/asset-relationships/list/ui/asset-relationships.tsx +++ b/modules/assets/src/web/asset-relationships/list/ui/asset-relationships.tsx @@ -95,6 +95,7 @@ export function AssetRelationships(props: AssetRelationshipsProps) { companyId={companyId} onOpenChange={setAdding} onSaved={() => setAdding(false)} + sites={props.sites} /> )} {ending && ( diff --git a/modules/assets/src/web/asset-relationships/shared/types/relationship-form-props.ts b/modules/assets/src/web/asset-relationships/shared/types/relationship-form-props.ts index b90a76e..ae405cd 100644 --- a/modules/assets/src/web/asset-relationships/shared/types/relationship-form-props.ts +++ b/modules/assets/src/web/asset-relationships/shared/types/relationship-form-props.ts @@ -1,5 +1,6 @@ export type RelationshipFormProps = { companyId: string; + sites?: readonly { id: string; name: string }[] | undefined; assetId: string; onSaved: () => void; }; diff --git a/modules/assets/src/web/asset-relationships/shared/ui/relationship-editor-dialog.tsx b/modules/assets/src/web/asset-relationships/shared/ui/relationship-editor-dialog.tsx index 5e37875..fb1b4de 100644 --- a/modules/assets/src/web/asset-relationships/shared/ui/relationship-editor-dialog.tsx +++ b/modules/assets/src/web/asset-relationships/shared/ui/relationship-editor-dialog.tsx @@ -6,10 +6,12 @@ import { RelationshipForm } from "./relationship-form"; export function RelationshipEditorDialog({ companyId, assetId, + sites, onSaved, onOpenChange, }: { companyId: string; + sites?: readonly { id: string; name: string }[] | undefined; assetId: string; onSaved: () => void; onOpenChange: (open: boolean) => void; @@ -24,7 +26,7 @@ export function RelationshipEditorDialog({ open title={t("linkAsset")} > - + ); } diff --git a/modules/assets/src/web/asset-relationships/shared/ui/relationship-form.tsx b/modules/assets/src/web/asset-relationships/shared/ui/relationship-form.tsx index bcefd0c..a446502 100644 --- a/modules/assets/src/web/asset-relationships/shared/ui/relationship-form.tsx +++ b/modules/assets/src/web/asset-relationships/shared/ui/relationship-form.tsx @@ -7,13 +7,14 @@ import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { useId } from "react"; import { useTranslation } from "react-i18next"; +import { AssetSelect } from "../../../assets/select/ui/asset-select"; import { CatalogRelationCreateDialog } from "../../../catalog-relations"; import { InventoryFeedback } from "../../../shared"; import { useRelationshipFormController } from "../controllers"; import type { RelationshipFormProps } from "../types"; export function RelationshipForm(props: RelationshipFormProps) { - const { companyId } = props; + const { companyId, sites } = props; const { typeId, setTypeId, @@ -56,7 +57,8 @@ export function RelationshipForm(props: RelationshipFormProps) {
- ({ value: asset.id, label: asset.values.name }))} placeholder={t("chooseAsset")} required + sites={sites} value={targetId} />
diff --git a/modules/assets/src/web/assets/detail/controllers/use-asset-detail-controller.ts b/modules/assets/src/web/assets/detail/controllers/use-asset-detail-controller.ts index 6d621cc..f9cdcb2 100644 --- a/modules/assets/src/web/assets/detail/controllers/use-asset-detail-controller.ts +++ b/modules/assets/src/web/assets/detail/controllers/use-asset-detail-controller.ts @@ -10,9 +10,7 @@ export function useAssetDetailController({ asset }: AssetDetailProps) { queryFn: () => getAssetInventory(asset.companyId), }); const workstations = inventory.data?.workstations.filter( - (item) => - item.values.active && - (item.values.assetIds.includes(asset.id) || item.values.sharedAssetIds.includes(asset.id)) + (item) => item.values.active && item.values.assetIds.includes(asset.id) ); return { workstations }; diff --git a/modules/assets/src/web/assets/detail/ui/asset-detail-dialog.tsx b/modules/assets/src/web/assets/detail/ui/asset-detail-dialog.tsx index 4d40962..69a0437 100644 --- a/modules/assets/src/web/assets/detail/ui/asset-detail-dialog.tsx +++ b/modules/assets/src/web/assets/detail/ui/asset-detail-dialog.tsx @@ -33,7 +33,7 @@ export function AssetDetailDialog({ wide > - + - + asset.id === editor); const viewed = assets.find((asset) => asset.id === viewId); const deleted = assets.find((asset) => asset.id === deleteId); - const filtered = assets.filter((asset) => { - if (templateId && asset.values.templateId !== templateId) { - return false; - } - const site = lookups.sites.find((site) => site.id === asset.values.siteId); - - return [ - asset.values.name, - asset.specification.templateName, - site?.name ?? "", - t(asset.displayStatus), - ].some((value) => value.toLocaleLowerCase().includes(search.trim().toLocaleLowerCase())); - }); return { query, @@ -122,6 +106,5 @@ export function useAssetInventoryPageController({ selected, edited, viewed, - filtered, }; } diff --git a/modules/assets/src/web/assets/list/ui/inventory-page.tsx b/modules/assets/src/web/assets/list/ui/inventory-page.tsx index 5b108a5..e47c3d3 100644 --- a/modules/assets/src/web/assets/list/ui/inventory-page.tsx +++ b/modules/assets/src/web/assets/list/ui/inventory-page.tsx @@ -3,18 +3,15 @@ import { EntityAction, EntityListHeader, EntityRowActions, - EntityStatus, - EntityTable, } from "@repo/rdx-ui/components/entity-list"; import { Button } from "@repo/shadcn-ui/components/button"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/shadcn-ui/components/tabs"; import { useTranslation } from "react-i18next"; import { CatalogError } from "../../../catalog"; -import { AssetTemplateIcon } from "../../../catalog-templates/shared/ui/asset-template-icon"; import { InventoryFeedback } from "../../../shared"; import { AssetDetailDialog, AssetSelectionPanel } from "../../detail"; import { AssetEditorDialog, DeleteAssetDialog } from "../../shared"; +import { AssetList } from "../../shared/ui/asset-list"; import { useAssetInventoryPageController } from "../controllers"; import type { AssetInventoryPageProps } from "../types"; @@ -29,7 +26,6 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { const { query, catalog, - templates, templateId, setTemplateId, clearFilters, @@ -48,7 +44,6 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { selected, edited, viewed, - filtered, missingView, entityHref, } = useAssetInventoryPageController(props); @@ -103,127 +98,56 @@ export function AssetInventoryPage(props: AssetInventoryPageProps) { )} {query.data && ( - -
- - {t("allAssets")} - {templates.map((template) => ( - - {template.values.name} - - ))} - -
- - asset.values.name, - hideable: false, - render: (asset) => ( - setDetailOpen(true)} + ( + + {" "} +
+ setViewId(asset.id)} > - {asset.values.name} - - ), - }, - { - id: "template", - label: t("template"), - value: (asset) => asset.specification.templateName, - render: (asset) => { - const template = catalog.data?.items.find( - (item) => item.id === asset.values.templateId - ); - - return ( - - - {asset.specification.templateName} - - ); - }, - }, - { - id: "state", - label: t("status"), - value: (asset) => t(asset.displayStatus), - render: (asset) => ( - - {t(asset.displayStatus)} - - ), - }, - { - id: "site", - label: t("site"), - value: (asset) => - lookups.sites.find((site) => site.id === asset.values.siteId)?.name ?? - t("noAssignment"), - render: (asset) => ( - <> - {lookups.sites.find((site) => site.id === asset.values.siteId)?.name ?? - t("noAssignment")} - - ), - }, - { - id: "actions", - label: t("actions"), - hideable: false, - render: (asset) => ( - - {" "} -
- setViewId(asset.id)} - > - {t("viewAsset")} - - setEditor(asset.id)} - > - {t("edit")} - - setDeleteId(asset.id)} - > - {t("deleteAsset")} - -
{" "} -
- ), - }, - ]} - emptyMessage={t("noAssets")} - filterKey={templateId ?? "all"} - filtersApplied={templateId !== null} - items={filtered} - locale={i18n.language} - onClearFilters={clearFilters} - onSearchChange={setSearch} - rowClassName={(asset) => selectedRowClass(asset.id, selected?.id, detailOpen)} - search={search} - searchPlaceholder={t("search")} - title={t("inventory")} - /> - - + {t("viewAsset")} +
+ setEditor(asset.id)}> + {t("edit")} + + setDeleteId(asset.id)} + > + {t("deleteAsset")} + +
{" "} +
+ ), + }, + ]} + onClearFilters={clearFilters} + onSearchChange={setSearch} + onTemplateChange={setTemplateId} + renderName={(asset) => ( + setDetailOpen(true)} + > + {asset.values.name} + + )} + rowClassName={(asset) => selectedRowClass(asset.id, selected?.id, detailOpen)} + search={search} + sites={lookups.sites} + templateId={templateId} + /> )} `0199b200-0000-7000-8000-${String(n).padStart(12, "0")}`; + +const assets = [2, 3].map((n) => { + const values = { + name: `Equipo ${n}`, + templateId: id(n + 10), + siteId: id(4), + status: "operational", + notes: "", + characteristics: {}, + }; + + return AssetSchema.parse({ + id: id(n), + companyId: id(1), + values, + displayStatus: "available", + assigned: false, + specification: { + templateId: values.templateId, + templateName: n === 2 ? "Portátiles" : "Monitores", + fields: [ + { fieldId: id(5), name: "Serie", type: "text", unit: "", choices: [], required: false }, + ], + }, + history: [ + { + id: id(n + 20), + values, + actorId: id(6), + actorName: "Técnico", + recordedAt: "2026-10-14T10:00:00.000Z", + }, + ], + }); +}); + +async function render(language: string, node: ReactNode) { + const i18n = createInstance(); + + await i18n.init({ + lng: language, + resources: { es: { assets: assetsSpanish }, en: { assets: assetsEnglish } }, + }); + + return renderToStaticMarkup({node}); +} + +const listProps = { + assets, + catalog: [], + sites: [{ id: id(4), name: "Sede Central" }], + templateId: null, + onTemplateChange: () => undefined, + search: "", + onSearchChange: () => undefined, + onClearFilters: () => undefined, +}; + +test("selector: comparte búsqueda, pestañas por identidad y columnas del inventario en es/en", async () => { + for (const language of ["es", "en"]) { + const html = await render( + language, + value === id(3)} + mode="multiple" + onToggle={() => undefined} + selectedIds={[id(2)]} + unavailableLabels={new Map([[id(3), "Ocupado"]])} + /> + ); + + assert.equal((html.match(/role="tab"/g) ?? []).length, 3); + assert.ok(html.includes("Portátiles")); + assert.ok(html.includes("Monitores")); + assert.ok(html.includes("Sede Central")); + assert.ok(html.includes(language === "es" ? "Seleccionar Equipo 2" : "Select Equipo 2")); + assert.ok(html.includes('role="checkbox"')); + assert.ok(html.includes('aria-checked="true"')); + assert.ok(html.includes("Ocupado")); + assert.ok(html.includes(language === "es" ? "Buscar" : "Search")); + } +}); + +test("selector: filtra conjuntamente por plantilla, nombre, sede y estado; distingue vacío", async () => { + for (const [search, templateId, expected] of [ + ["equipo 2", null, ["Equipo 2"]], + ["SEDE CENTRAL", id(13), ["Equipo 3"]], + ["disponible", id(12), ["Equipo 2"]], + ["no existe", null, []], + ] satisfies [string, string | null, string[]][]) { + const html = await render( + "es", + + ); + const table = html.match(//)?.[0]; + + assert.ok(table); + for (const asset of assets) { + assert.equal(table.includes(asset.values.name), expected.includes(asset.values.name)); + } + if (!expected.length) { + assert.ok(table.includes("No hay resultados para estos criterios.")); + } + } +}); + +test("selector único: controles radio y disparador de diálogo; conserva el alta rápida y el bloqueo", async () => { + const table = await render( + "es", + false} + mode="single" + onToggle={() => undefined} + selectedIds={[id(2)]} + unavailableLabels={new Map()} + /> + ); + + assert.ok(table.includes('role="radio"')); + assert.equal(table.includes('role="checkbox"'), false); + const trigger = await render( + "es", + undefined }} + companyId={id(1)} + disabled + id="asset" + label="Ordenador principal" + onValueChange={() => undefined} + options={assets.map((asset) => ({ value: asset.id, label: asset.values.name }))} + placeholder="Elegir" + value={id(2)} + /> + ); + + assert.ok(trigger.includes('aria-haspopup="dialog"')); + assert.ok(trigger.includes("Equipo 2")); + assert.ok(trigger.includes('aria-label="Nuevo activo"')); + assert.ok(trigger.includes("disabled")); + assert.equal(trigger.includes('role="combobox"'), false); +}); diff --git a/modules/assets/src/web/assets/select/controllers/use-asset-picker-controller.ts b/modules/assets/src/web/assets/select/controllers/use-asset-picker-controller.ts new file mode 100644 index 0000000..f97776e --- /dev/null +++ b/modules/assets/src/web/assets/select/controllers/use-asset-picker-controller.ts @@ -0,0 +1,89 @@ +import { useQuery } from "@tanstack/react-query"; +import { useState } from "react"; + +import { catalogApi, catalogKeys } from "../../../catalog/shared/api"; +import { inventoryKeys } from "../../../shared/queries"; +import { getAssetInventory } from "../../list/api"; +import type { AssetSelectProps } from "../types/asset-select-props"; + +export function useAssetPickerController(props: AssetSelectProps, onClose: () => void) { + const [selectedIds, setSelectedIds] = useState( + props.mode === "multiple" ? props.value : props.value ? [props.value] : [] + ); + const [search, setSearch] = useState(""); + const [templateId, setTemplateId] = useState(null); + const inventory = useQuery({ + queryKey: inventoryKeys.company(props.companyId), + queryFn: () => getAssetInventory(props.companyId), + }); + const catalog = useQuery({ + queryKey: catalogKeys.scope(props.companyId), + queryFn: () => catalogApi.get(props.companyId), + }); + const options = new Map(props.options.map((option) => [option.value, option])); + const assets = (inventory.data?.assets ?? []).filter( + (asset) => asset.companyId === props.companyId && options.has(asset.id) + ); + const availableIds = new Set( + assets.filter((asset) => !options.get(asset.id)?.disabled).map((asset) => asset.id) + ); + const withinLimit = props.mode !== "multiple" || selectedIds.length <= props.maxSelection; + const validSelection = + withinLimit && selectedIds.length > 0 && selectedIds.every((id) => availableIds.has(id)); + const ready = Boolean(inventory.data && catalog.data && !inventory.isError && !catalog.isError); + + const disabled = (id: string) => + !availableIds.has(id) || + (props.mode === "multiple" && + selectedIds.length >= props.maxSelection && + !selectedIds.includes(id)); + + const toggle = (id: string) => { + if (disabled(id)) { + return; + } + setSelectedIds((previous) => + props.mode === "multiple" + ? previous.includes(id) + ? previous.filter((value) => value !== id) + : [...previous, id] + : [id] + ); + }; + + const confirm = () => { + if (!(ready && validSelection)) { + return; + } + if (props.mode === "multiple") { + props.onValueChange([...selectedIds]); + } else { + const id = selectedIds[0]; + + if (!id) { + return; + } + props.onValueChange(id); + } + onClose(); + }; + + const retry = () => Promise.all([inventory.refetch(), catalog.refetch()]); + + return { + inventory, + catalog, + assets, + options, + selectedIds, + search, + setSearch, + templateId, + setTemplateId, + disabled, + toggle, + confirm, + retry, + canConfirm: ready && validSelection, + }; +} diff --git a/modules/assets/src/web/assets/select/types/asset-select-props.ts b/modules/assets/src/web/assets/select/types/asset-select-props.ts new file mode 100644 index 0000000..7c792ea --- /dev/null +++ b/modules/assets/src/web/assets/select/types/asset-select-props.ts @@ -0,0 +1,26 @@ +import type { SelectAction, SelectOption } from "@repo/rdx-ui/components/select-with-action"; + +type AssetSelectBaseProps = { + companyId: string; + id: string; + label: string; + placeholder: string; + options: readonly SelectOption[]; + sites?: readonly { id: string; name: string }[] | undefined; + disabled?: boolean | undefined; + action?: SelectAction | undefined; + required?: boolean | undefined; + invalid?: boolean | undefined; + describedBy?: string | undefined; +}; + +export type AssetSelectProps = AssetSelectBaseProps & + ( + | { mode?: "single"; value: string; onValueChange: (id: string) => void } + | { + mode: "multiple"; + value: readonly string[]; + onValueChange: (ids: string[]) => void; + maxSelection: number; + } + ); diff --git a/modules/assets/src/web/assets/select/ui/asset-picker-dialog.tsx b/modules/assets/src/web/assets/select/ui/asset-picker-dialog.tsx new file mode 100644 index 0000000..9a500f5 --- /dev/null +++ b/modules/assets/src/web/assets/select/ui/asset-picker-dialog.tsx @@ -0,0 +1,86 @@ +import { FormActions, FormBody, FormDialog } from "@repo/rdx-ui/components/form-dialog"; +import { Button } from "@repo/shadcn-ui/components/button"; +import { useTranslation } from "react-i18next"; + +import { InventoryFeedback } from "../../../shared"; +import { useAssetPickerController } from "../controllers/use-asset-picker-controller"; +import type { AssetSelectProps } from "../types/asset-select-props"; + +import { AssetPickerList } from "./asset-picker-list"; + +export function AssetPickerDialog({ + selection, + onClose, +}: { + selection: AssetSelectProps; + onClose: () => void; +}) { + const { t } = useTranslation("assets"); + const c = useAssetPickerController(selection, onClose); + + return ( + { + if (!open) { + onClose(); + } + }} + open + title={t(selection.mode === "multiple" ? "assetPicker.multiple" : "assetPicker.single")} + wide + > + +

{selection.label}

+ {(c.inventory.isPending || c.catalog.isPending) &&

{t("loading")}

} + + {(c.inventory.isError || c.catalog.isError) && ( + + )} + {c.inventory.data && ( + { + c.setSearch(""); + c.setTemplateId(null); + }} + onSearchChange={c.setSearch} + onTemplateChange={(id) => c.setTemplateId(id === "all" ? null : id)} + onToggle={c.toggle} + search={c.search} + selectedIds={c.selectedIds} + sites={selection.sites} + templateId={c.templateId} + unavailableLabels={ + new Map( + selection.options + .filter((option) => option.disabled) + .map((option) => [option.value, option.label]) + ) + } + /> + )} +

+ {t("assetPicker.selected", { count: c.selectedIds.length })} +

+
+ + + +
+ ); +} diff --git a/modules/assets/src/web/assets/select/ui/asset-picker-list.tsx b/modules/assets/src/web/assets/select/ui/asset-picker-list.tsx new file mode 100644 index 0000000..579ffcf --- /dev/null +++ b/modules/assets/src/web/assets/select/ui/asset-picker-list.tsx @@ -0,0 +1,72 @@ +import { Checkbox } from "@repo/shadcn-ui/components/checkbox"; +import { RadioGroup, RadioGroupItem } from "@repo/shadcn-ui/components/radio-group"; +import type { ComponentProps } from "react"; +import { useTranslation } from "react-i18next"; + +import { AssetList } from "../../shared/ui/asset-list"; + +export function AssetPickerList({ + mode, + selectedIds, + disabled, + onToggle, + unavailableLabels, + ...listProps +}: ComponentProps & { + mode: "single" | "multiple"; + selectedIds: readonly string[]; + disabled: (id: string) => boolean; + onToggle: (id: string) => void; + unavailableLabels: ReadonlyMap; +}) { + const { t } = useTranslation("assets"); + const list = ( + ( +
+ {mode === "multiple" ? ( + onToggle(asset.id)} + /> + ) : ( + + )} + {unavailableLabels.has(asset.id) && ( +

{unavailableLabels.get(asset.id)}

+ )} +
+ ), + }, + ]} + /> + ); + + return mode === "multiple" ? ( + list + ) : ( + { + if (typeof id === "string") { + onToggle(id); + } + }} + value={selectedIds[0] ?? ""} + > + {list} + + ); +} diff --git a/modules/assets/src/web/assets/select/ui/asset-select.tsx b/modules/assets/src/web/assets/select/ui/asset-select.tsx new file mode 100644 index 0000000..b67ebc3 --- /dev/null +++ b/modules/assets/src/web/assets/select/ui/asset-select.tsx @@ -0,0 +1,70 @@ +import { ButtonGroup } from "@repo/rdx-ui/components/button-group"; +import { Button } from "@repo/shadcn-ui/components/button"; +import { useState } from "react"; +import { useTranslation } from "react-i18next"; + +import type { AssetSelectProps } from "../types/asset-select-props"; + +import { AssetPickerDialog } from "./asset-picker-dialog"; + +/** La elección se confirma en un diálogo; cancelar conserva el valor del formulario padre. */ +export function AssetSelect(props: AssetSelectProps) { + const { id, label, placeholder, options, disabled, action, invalid, describedBy, required } = + props; + const { t } = useTranslation("assets"); + const [open, setOpen] = useState(false); + const displayValue = + props.mode === "multiple" + ? props.value.length + ? t("assetPicker.selected", { count: props.value.length }) + : placeholder + : (options.find((option) => option.value === props.value)?.label ?? + (props.value || placeholder)); + + return ( + <> + + + {props.mode !== "multiple" && !required && props.value && ( + + )} + {action && ( + + )} + + {open && setOpen(false)} selection={props} />} + + ); +} diff --git a/modules/assets/src/web/assets/shared/inventory.test.tsx b/modules/assets/src/web/assets/shared/inventory.test.tsx index 4b7179c..f6cfffb 100644 --- a/modules/assets/src/web/assets/shared/inventory.test.tsx +++ b/modules/assets/src/web/assets/shared/inventory.test.tsx @@ -313,10 +313,13 @@ test("Puestos de trabajo: altas rápidas y ayuda inferior; un activo anidado no undefined} /> ); - for (const label of ["Nueva sede", "Nueva persona", "Nuevo activo"]) { + for (const label of ["Nueva sede", "Nueva persona"]) { assert.ok(html.includes(`aria-label="${label}"`)); } - assert.ok(html.indexOf('name="active"') < html.indexOf("Guardar<"), false); + assert.equal(html.includes('aria-label="Nuevo activo"'), false); assert.ok(html.indexOf(" { + const siteId = UniqueID.generateNewID().toPrimitive(); + + for (const language of ["es", "en"]) { + const html = await render( + language, + undefined} + /> + ); + + assert.ok(html.includes("Sede única test")); + assert.ok( + html.includes( + language === "es" ? "Responsable del puesto (opcional)" : "Workstation owner (optional)" + ) + ); + assert.ok(html.includes(language === "es" ? "Paso 1 de 4" : "Step 1 of 4")); + assert.ok(html.includes('aria-current="step"')); + assert.equal(html.includes(language === "es" ? ">Guardar<" : ">Save<"), false); + assert.equal( + html.includes(language === "es" ? "Recursos compartidos" : "Shared assets"), + false + ); + } +}); + test("la ficha lateral agrupa información y ofrece acciones superiores y enlaces reales", async () => { const fieldId = UniqueID.generateNewID().toPrimitive(); const personId = UniqueID.generateNewID().toPrimitive(); @@ -401,8 +433,7 @@ test("la ficha lateral agrupa información y ofrece acciones superiores y enlace active: true, siteId, personId, - assetIds: [], - sharedAssetIds: [asset.id], + assetIds: [asset.id], }, history: [ { @@ -415,8 +446,7 @@ test("la ficha lateral agrupa información y ofrece acciones superiores y enlace active: true, siteId, personId, - assetIds: [], - sharedAssetIds: [asset.id], + assetIds: [asset.id], }, }, ], diff --git a/modules/assets/src/web/assets/shared/ui/asset-list.tsx b/modules/assets/src/web/assets/shared/ui/asset-list.tsx new file mode 100644 index 0000000..dab6fa0 --- /dev/null +++ b/modules/assets/src/web/assets/shared/ui/asset-list.tsx @@ -0,0 +1,138 @@ +import type { EntityColumn } from "@repo/rdx-ui/components/entity-list"; +import { EntityStatus, EntityTable } from "@repo/rdx-ui/components/entity-list"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/shadcn-ui/components/tabs"; +import type { ReactNode } from "react"; +import { useTranslation } from "react-i18next"; + +import type { CatalogItemDTO } from "../../../../common"; +import type { AssetDTO } from "../../../../common/inventory"; +import { AssetTemplateIcon } from "../../../catalog-templates/shared/ui/asset-template-icon"; + +/** Inventario y selectores comparten columnas, búsqueda, pestañas y paginación. */ +export function AssetList({ + assets, + catalog, + sites = [], + templateId, + onTemplateChange, + search, + onSearchChange, + onClearFilters, + renderName, + extraColumns = [], + rowClassName = () => "", +}: { + assets: readonly AssetDTO[]; + catalog: readonly CatalogItemDTO[]; + sites?: readonly { id: string; name: string }[] | undefined; + templateId: string | null; + onTemplateChange: (id: string) => void; + search: string; + onSearchChange: (search: string) => void; + onClearFilters: () => void; + renderName?: ((asset: AssetDTO) => ReactNode) | undefined; + extraColumns?: readonly EntityColumn[] | undefined; + rowClassName?: ((asset: AssetDTO) => string) | undefined; +}) { + const { t, i18n } = useTranslation("assets"); + const templates = new Map( + catalog + .filter((item) => item.values.kind === "template") + .map((item) => [item.id, item.values.name]) + ); + + for (const asset of assets) { + if (!templates.has(asset.values.templateId)) { + templates.set(asset.values.templateId, asset.specification.templateName); + } + } + + const siteName = (asset: AssetDTO) => + sites.find((site) => site.id === asset.values.siteId)?.name ?? + (asset.values.siteId ? t("noValue") : t("noAssignment")); + + const term = search.trim().toLocaleLowerCase(); + const items = assets.filter( + (asset) => + (!templateId || asset.values.templateId === templateId) && + [ + asset.values.name, + asset.specification.templateName, + siteName(asset), + t(asset.displayStatus), + ].some((value) => value.toLocaleLowerCase().includes(term)) + ); + + return ( + +
+ + {t("allAssets")} + {[...templates].map(([id, name]) => ( + + {name} + + ))} + +
+ + asset.values.name, + hideable: false, + render: (asset) => + renderName ? ( + renderName(asset) + ) : ( + {asset.values.name} + ), + }, + { + id: "template", + label: t("template"), + value: (asset) => asset.specification.templateName, + render: (asset) => { + const template = catalog.find((item) => item.id === asset.values.templateId); + + return ( + + + {asset.specification.templateName} + + ); + }, + }, + { + id: "state", + label: t("status"), + value: (asset) => t(asset.displayStatus), + render: (asset) => ( + + {t(asset.displayStatus)} + + ), + }, + { id: "site", label: t("site"), value: siteName, render: siteName }, + ...extraColumns, + ]} + emptyMessage={t("noAssets")} + filterKey={templateId ?? "all"} + filtersApplied={templateId !== null} + items={items} + locale={i18n.language} + onClearFilters={onClearFilters} + onSearchChange={onSearchChange} + rowClassName={rowClassName} + search={search} + searchPlaceholder={t("search")} + title={t("inventory")} + /> + +
+ ); +} diff --git a/modules/assets/src/web/index.ts b/modules/assets/src/web/index.ts index d45c04c..c7c4e82 100644 --- a/modules/assets/src/web/index.ts +++ b/modules/assets/src/web/index.ts @@ -1,4 +1,5 @@ export { AssetForm, AssetInventoryPage } from "./assets"; +export { AssetSelect } from "./assets/select/ui/asset-select"; export { BrandCatalogPage } from "./brands"; export { AssetCatalogPage } from "./catalog"; export { catalogApi, catalogKeys } from "./catalog/shared/api"; diff --git a/modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx b/modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx index 109f706..1c529ee 100644 --- a/modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx +++ b/modules/assets/src/web/workstations/history/ui/workstation-history-dialog.tsx @@ -47,11 +47,7 @@ export function WorkstationHistoryDialog({ {siteName(revision.values.siteId)} · {personName(revision.values.personId)}

- {t("exclusiveWorkstationAssets")}: {members(revision.values.assetIds)} -

-

- {t("sharedWorkstationAssets")}:{" "} - {members(revision.values.sharedAssetIds, "noSharedWorkstationAssets")} + {t("workstationAssets")}: {members(revision.values.assetIds)}

))} diff --git a/modules/assets/src/web/workstations/list/ui/workstations-page.tsx b/modules/assets/src/web/workstations/list/ui/workstations-page.tsx index baea369..40ce5f8 100644 --- a/modules/assets/src/web/workstations/list/ui/workstations-page.tsx +++ b/modules/assets/src/web/workstations/list/ui/workstations-page.tsx @@ -106,24 +106,17 @@ export function WorkstationsPage(props: WorkstationsPageProps) { }, { id: "exclusive", - label: t("exclusiveWorkstationAssets"), + label: t("workstationAssets"), render: (item) => <>{members(item.values.assetIds)}, }, - { - id: "shared", - label: t("sharedWorkstationAssets"), - render: (item) => ( - <>{members(item.values.sharedAssetIds, "noSharedWorkstationAssets")} - ), - }, { id: "count", label: t("characteristics"), - value: (item) => item.values.assetIds.length + item.values.sharedAssetIds.length, + value: (item) => item.values.assetIds.length, render: (item) => ( <> {t("workstationCount", { - count: item.values.assetIds.length + item.values.sharedAssetIds.length, + count: item.values.assetIds.length, })} ), @@ -142,7 +135,6 @@ export function WorkstationsPage(props: WorkstationsPageProps) { setEditor(item.id)}>{t("edit")} {(item.values.active || item.values.assetIds.length > 0 || - item.values.sharedAssetIds.length > 0 || item.values.personId) && ( setOperation({ id: item.id, action: "decommission" })} diff --git a/modules/assets/src/web/workstations/shared/controllers/use-workstation-form-controller.ts b/modules/assets/src/web/workstations/shared/controllers/use-workstation-form-controller.ts index ce069b4..e480801 100644 --- a/modules/assets/src/web/workstations/shared/controllers/use-workstation-form-controller.ts +++ b/modules/assets/src/web/workstations/shared/controllers/use-workstation-form-controller.ts @@ -1,5 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery, 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"; @@ -11,6 +12,7 @@ import { getAssetInventory } from "../../../assets/list/api"; import { catalogApi, catalogKeys } from "../../../catalog/shared/api"; import { inventoryKeys } from "../../../shared/queries"; import { saveWorkstation } from "../api"; +import { workstationCreationReady, workstationMainComputer } from "../helpers/workstation-creation"; import type { WorkstationFormProps } from "../types"; import { suggestedWorkstationName } from "../workstation-name"; @@ -23,6 +25,22 @@ export function useWorkstationFormController({ const { t } = useTranslation("assets"); const client = useQueryClient(); const mounted = useRef(true); + const isWizard = !workstation; + const [step, setStep] = useState<1 | 2 | 3 | 4>(1); + const initialSite = lookups.sites.length === 1 ? lookups.sites[0] : undefined; + const form = useForm({ + resolver: zodResolver(WorkstationInputSchema), + defaultValues: workstation?.values ?? { + name: initialSite ? t("workstationNameBySite", { site: initialSite.name }) : "", + active: true, + siteId: initialSite?.id ?? null, + personId: null, + assetIds: [], + computerAssetId: null, + }, + }); + const values = form.watch(); + const [quick, setQuick] = useState(false); useEffect(() => { mounted.current = true; @@ -31,20 +49,6 @@ export function useWorkstationFormController({ mounted.current = false; }; }, []); - const form = useForm({ - resolver: zodResolver(WorkstationInputSchema), - defaultValues: workstation?.values ?? { - name: "", - active: true, - siteId: null, - personId: null, - assetIds: [], - sharedAssetIds: [], - }, - }); - const values = form.watch(); - const [quick, setQuick] = useState(false); - const [quickField, setQuickField] = useState<"assetIds" | "sharedAssetIds">("assetIds"); const inventory = useQuery({ queryKey: inventoryKeys.company(companyId), queryFn: () => getAssetInventory(companyId), @@ -54,61 +58,57 @@ export function useWorkstationFormController({ queryFn: () => catalogApi.get(companyId), }); const assets = inventory.data?.assets ?? []; - const stations = inventory.data?.workstations ?? []; const occupied = new Set( - stations + (inventory.data?.workstations ?? []) .filter((item) => item.values.active && item.id !== workstation?.id) .flatMap((item) => item.values.assetIds) ); - const sharedUsed = new Set( - stations - .filter((item) => item.values.active && item.id !== workstation?.id) - .flatMap((item) => item.values.sharedAssetIds) - ); - const unavailableReason = (field: "assetIds" | "sharedAssetIds", id: string) => { + const unavailableReason = (id: string) => { if (!values.active) { return undefined; } - const retained = workstation?.values.active && workstation.values[field].includes(id); const asset = assets.find((item) => item.id === id); if (!asset) { return "ASSET_NOT_FOUND"; } - if ( - !( - retained || - canAssignAsset( - asset.values.status, - field === "assetIds" ? "exclusive" : "shared", - false, - false - ) - ) - ) { + const retained = workstation?.values.active && workstation.values.assetIds.includes(id); + + if (!(retained || canAssignAsset(asset.values.status, false))) { return "ASSET_NOT_OPERATIONAL"; } - if (occupied.has(id) || (field === "assetIds" && sharedUsed.has(id))) { + if (occupied.has(id)) { return "ASSET_IN_WORKSTATION"; } return undefined; }; - const unavailable = (field: "assetIds" | "sharedAssetIds", id: string) => - Boolean(unavailableReason(field, id)); + const changeAssets = (ids: string[]) => { + form.setValue("assetIds", ids, { shouldDirty: true }); + form.setValue( + "computerAssetId", + workstationMainComputer(ids, form.getValues("computerAssetId")), + { shouldDirty: true } + ); + }; - const add = (field: "assetIds" | "sharedAssetIds", id: string) => { - if ( - id && - mounted.current && - ![...form.getValues("assetIds"), ...form.getValues("sharedAssetIds")].includes(id) - ) { - form.setValue(field, [...form.getValues(field), id]); + const addMany = (ids: readonly string[]) => { + const current = form.getValues("assetIds"); + const additions = ids.filter((id) => id && !current.includes(id)); + + if (mounted.current && current.length + additions.length <= 200) { + changeAssets([...new Set([...current, ...additions])]); } }; + const add = (id: string) => addMany([id]); + + const remove = (id: string) => { + changeAssets(form.getValues("assetIds").filter((item) => item !== id)); + }; + const selectAssociation = (field: "siteId" | "personId", id: string) => { const selectedId = id || null; @@ -118,15 +118,14 @@ export function useWorkstationFormController({ } const siteId = field === "siteId" ? selectedId : form.getValues("siteId"); const personId = field === "personId" ? selectedId : form.getValues("personId"); + const siteName = lookups.sites.find((item) => item.id === siteId)?.name; const name = suggestedWorkstationName( lookups.people.find((item) => item.id === personId)?.name, - lookups.sites.find((item) => item.id === siteId)?.name, + siteName, (person, site) => t("workstationSuggestedName", { person, site }) ); - if (name) { - form.setValue("name", name, { shouldDirty: false }); - } + form.setValue("name", name ?? (siteName ? t("workstationNameBySite", { site: siteName }) : "")); }; const mutation = useMutation({ @@ -138,7 +137,34 @@ export function useWorkstationFormController({ } }, }); - const submit = form.handleSubmit((input) => mutation.mutate(input)); + const assetsReady = !!inventory.data && values.assetIds.every((id) => !unavailableReason(id)); + const canContinue = assetsReady && workstationCreationReady(step, values); + const save = form.handleSubmit((input) => { + if (assetsReady && (!isWizard || workstationCreationReady(4, input))) { + mutation.mutate(input); + } + }); + + const submit = async (event: FormEvent) => { + if (mutation.isPending) { + event.preventDefault(); + + return; + } + if (isWizard && step !== 4) { + event.preventDefault(); + if (canContinue) { + setStep(NEXT_STEPS[step]); + } + + return; + } + await save(event); + }; + + const previousStep = () => { + setStep(PREVIOUS_STEPS[step]); + }; const siteCreated = (id: string) => { if (mounted.current) { @@ -153,18 +179,22 @@ export function useWorkstationFormController({ }; return { + isWizard, + step, + previousStep, + canContinue, + assetsReady, form, values, quick, setQuick, - quickField, - setQuickField, inventory, catalog, assets, - unavailable, unavailableReason, add, + addMany, + remove, selectAssociation, mutation, submit, @@ -172,3 +202,6 @@ export function useWorkstationFormController({ personCreated, }; } + +const NEXT_STEPS = { 1: 2, 2: 3, 3: 4, 4: 4 } as const; +const PREVIOUS_STEPS = { 1: 1, 2: 1, 3: 2, 4: 3 } as const; diff --git a/modules/assets/src/web/workstations/shared/helpers/workstation-creation.test.ts b/modules/assets/src/web/workstations/shared/helpers/workstation-creation.test.ts new file mode 100644 index 0000000..a52cb9d --- /dev/null +++ b/modules/assets/src/web/workstations/shared/helpers/workstation-creation.test.ts @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; + +import type { WorkstationInput } from "../../../../common"; + +import { workstationCreationReady, workstationMainComputer } from "./workstation-creation"; + +const values: WorkstationInput = { + name: "Puesto", + active: true, + siteId: "0199b200-0000-7000-8000-000000000001", + personId: null, + assetIds: ["0199b200-0000-7000-8000-000000000002"], + computerAssetId: "0199b200-0000-7000-8000-000000000002", +}; + +test("alta: sede necesaria, responsable opcional, composición mínima y ordenador perteneciente al puesto", () => { + assert.equal(workstationCreationReady(1, { ...values, siteId: null }), false); + assert.equal(workstationCreationReady(1, { ...values, assetIds: [] }), true); + assert.equal(workstationCreationReady(2, { ...values, assetIds: [] }), false); + assert.equal(workstationCreationReady(2, { ...values, computerAssetId: null }), true); + assert.equal(workstationCreationReady(3, { ...values, computerAssetId: null }), false); + assert.equal(workstationCreationReady(3, { ...values, computerAssetId: values.siteId }), false); + assert.equal(workstationCreationReady(4, { ...values, name: " " }), false); + assert.equal(workstationCreationReady(4, values), true); +}); + +test("composición: asigna el activo único, conserva una elección válida y limpia el ordenador retirado", () => { + assert.equal(workstationMainComputer(["a"], null), "a"); + assert.equal(workstationMainComputer(["a"], "b"), "a"); + assert.equal(workstationMainComputer(["a", "b"], "a"), "a"); + assert.equal(workstationMainComputer(["a", "b"], "c"), null); + assert.equal(workstationMainComputer(["a", "b"], null), null); + assert.equal(workstationMainComputer([], "a"), null); +}); diff --git a/modules/assets/src/web/workstations/shared/helpers/workstation-creation.ts b/modules/assets/src/web/workstations/shared/helpers/workstation-creation.ts new file mode 100644 index 0000000..f45a174 --- /dev/null +++ b/modules/assets/src/web/workstations/shared/helpers/workstation-creation.ts @@ -0,0 +1,29 @@ +import type { WorkstationInput } from "../../../../common"; + +/** Una composición unitaria identifica el ordenador sin pedir una segunda selección. */ +export function workstationMainComputer( + ids: readonly string[], + current: string | null | undefined +) { + return ids.length === 1 ? (ids[0] ?? null) : current && ids.includes(current) ? current : null; +} + +export function workstationCreationReady(step: 1 | 2 | 3 | 4, values: WorkstationInput) { + if (!values.siteId) { + return false; + } + if (step === 1) { + return true; + } + if (!values.assetIds.length) { + return false; + } + if (step === 2) { + return true; + } + if (!(values.computerAssetId && values.assetIds.includes(values.computerAssetId))) { + return false; + } + + return step === 3 || values.name.trim().length > 0; +} diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx index e1b4da9..25eb4dc 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-actions.tsx @@ -22,7 +22,6 @@ export function WorkstationActions({ const canDecommission = workstation.values.active || workstation.values.assetIds.length > 0 || - workstation.values.sharedAssetIds.length > 0 || Boolean(workstation.values.personId); return ( 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 new file mode 100644 index 0000000..6b6d713 --- /dev/null +++ b/modules/assets/src/web/workstations/shared/ui/workstation-asset-fields.tsx @@ -0,0 +1,102 @@ +import { Button } from "@repo/shadcn-ui/components/button"; +import { Label } from "@repo/shadcn-ui/components/label"; +import { useTranslation } from "react-i18next"; + +import type { AssetDTO } from "../../../../common/inventory"; +import { AssetSelect } from "../../../assets/select/ui/asset-select"; + +export function WorkstationAssetFields({ + assets, + companyId, + sites, + selectedIds, + prefix, + disabled, + canCreate, + unavailableReason, + onAdd, + onRemove, + onCreate, +}: { + assets: readonly AssetDTO[]; + companyId: string; + sites: readonly { id: string; name: string }[]; + selectedIds: readonly string[]; + prefix: string; + disabled: boolean; + canCreate: boolean; + unavailableReason: ( + id: string + ) => "ASSET_NOT_FOUND" | "ASSET_NOT_OPERATIONAL" | "ASSET_IN_WORKSTATION" | undefined; + onAdd: (ids: string[]) => void; + onRemove: (id: string) => void; + onCreate: () => void; +}) { + const { t } = useTranslation("assets"); + + return ( +
+ + = 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 ( +
  • +
    +

    {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 new file mode 100644 index 0000000..a3bcd23 --- /dev/null +++ b/modules/assets/src/web/workstations/shared/ui/workstation-association-fields.tsx @@ -0,0 +1,66 @@ +import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; +import { Label } from "@repo/shadcn-ui/components/label"; +import { useTranslation } from "react-i18next"; + +import type { WorkstationInput } from "../../../../common"; +import type { WorkstationFormProps } from "../types"; + +export function WorkstationAssociationFields({ + lookups, + workstation, + values, + prefix, + required, + onSelect, + onSiteCreated, + onPersonCreated, +}: { + lookups: WorkstationFormProps["lookups"]; + workstation: WorkstationFormProps["workstation"]; + values: WorkstationInput; + prefix: string; + required: boolean; + onSelect: (field: "siteId" | "personId", id: string) => void; + onSiteCreated: (id: string) => void; + onPersonCreated: (id: string) => void; +}) { + const { t } = useTranslation("assets"); + + return ( +
+
+ + lookups.onCreateSite(onSiteCreated), + }} + id={`${prefix}-site`} + label={t("site")} + onValueChange={(id) => onSelect("siteId", id)} + options={lookups.sites.map((item) => ({ value: item.id, label: item.name }))} + placeholder={t("noAssignment")} + required={required} + value={values.siteId ?? ""} + /> +
+
+ + 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 new file mode 100644 index 0000000..b18cbf9 --- /dev/null +++ b/modules/assets/src/web/workstations/shared/ui/workstation-computer-field.tsx @@ -0,0 +1,51 @@ +import { Label } from "@repo/shadcn-ui/components/label"; +import { useTranslation } from "react-i18next"; + +import type { WorkstationInput } from "../../../../common"; +import type { AssetDTO } from "../../../../common/inventory"; +import { AssetSelect } from "../../../assets/select/ui/asset-select"; + +export function WorkstationComputerField({ + values, + assets, + companyId, + sites, + prefix, + singleComputer, + required, + onChange, +}: { + values: WorkstationInput; + assets: readonly AssetDTO[]; + companyId: string; + sites: readonly { id: string; name: string }[]; + prefix: string; + singleComputer: boolean; + required: boolean; + onChange: (id: string | null) => void; +}) { + const { t } = useTranslation("assets"); + + return ( +
+ + onChange(id || null)} + options={assets + .filter((asset) => values.assetIds.includes(asset.id)) + .map((asset) => ({ value: asset.id, label: asset.values.name }))} + placeholder={t("noAssignment")} + required={required} + sites={sites} + value={values.computerAssetId ?? ""} + /> +

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

+
+ ); +} diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-form-actions.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-form-actions.tsx new file mode 100644 index 0000000..16d70d6 --- /dev/null +++ b/modules/assets/src/web/workstations/shared/ui/workstation-form-actions.tsx @@ -0,0 +1,31 @@ +import { FormActions } from "@repo/rdx-ui/components/form-dialog"; +import { Button } from "@repo/shadcn-ui/components/button"; +import { useTranslation } from "react-i18next"; + +export function WorkstationFormActions({ + step, + pending, + canSubmit, + onPrevious, +}: { + step: 1 | 2 | 3 | 4 | undefined; + pending: boolean; + canSubmit: boolean; + onPrevious: () => void; +}) { + const { t } = useTranslation("assets"); + const action = step && step < 4 ? "workstationSteps.next" : "save"; + + return ( + + {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 8e2d546..0be3b39 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-form.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-form.tsx @@ -1,8 +1,7 @@ import { ContextHelp } from "@repo/rdx-ui/components/context-help"; -import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; -import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; +import { FormBody } from "@repo/rdx-ui/components/form-dialog"; +import { FormProgress } from "@repo/rdx-ui/components/form-progress"; import { SwitchField } from "@repo/rdx-ui/components/switch-field"; -import { Button } from "@repo/shadcn-ui/components/button"; import { Input } from "@repo/shadcn-ui/components/input"; import { Label } from "@repo/shadcn-ui/components/label"; import { useId } from "react"; @@ -13,21 +12,31 @@ import { InventoryFeedback } from "../../../shared"; import { useWorkstationFormController } from "../controllers"; import type { WorkstationFormProps } from "../types"; +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 { WorkstationSummary } from "./workstation-summary"; + export function WorkstationForm(props: WorkstationFormProps) { const { companyId, lookups, workstation } = props; const { + isWizard, + step, + previousStep, + canContinue, + assetsReady, form, values, quick, setQuick, - quickField, - setQuickField, inventory, catalog, assets, - unavailable, unavailableReason, add, + addMany, + remove, selectAssociation, mutation, submit, @@ -36,153 +45,82 @@ export function WorkstationForm(props: WorkstationFormProps) { } = useWorkstationFormController(props); const { t } = useTranslation("assets"); const prefix = useId(); + const stepKeys = ["location", "assets", "computer", "summary"] as const; + const layout = isWizard ? STEP_LAYOUTS[step] : FULL_LAYOUT; + const singleComputer = isWizard && values.assetIds.length === 1; return ( <>
-
-
- - -
- form.setValue("active", active)} + {isWizard && ( + t(`workstationSteps.${key}`))} + progress={t("workstationSteps.progress", { step, total: 4 })} + step={step} /> -
-
- - lookups.onCreateSite(siteCreated), - }} - id={`${prefix}-site`} - label={t("site")} - onValueChange={(id) => selectAssociation("siteId", id)} - options={lookups.sites.map((item) => ({ value: item.id, label: item.name }))} - placeholder={t("noAssignment")} - value={values.siteId ?? ""} - /> -
-
- - lookups.onCreatePerson(personCreated), - }} - id={`${prefix}-person`} - label={t("workstationPerson")} - 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 }))} - placeholder={t("noAssignment")} - value={values.personId ?? ""} - /> -
-
- -
- - form.setValue("computerAssetId", id || null)} - options={assets - .filter((asset) => values.assetIds.includes(asset.id)) - .map((asset) => ({ value: asset.id, label: asset.values.name }))} - placeholder={t("noAssignment")} - value={values.computerAssetId ?? ""} + )} +
+ {layout.location && ( + -

{t("workstationComputerHelp")}

-
- - {(["assetIds", "sharedAssetIds"] as const).map((field) => ( -
- - { - setQuickField(field); - setQuick(true); - }, - disabled: !catalog.data, - }} - disabled={inventory.isPending || values[field].length >= 200} - id={`${prefix}-${field}`} - label={t( - field === "assetIds" ? "addWorkstationAsset" : "addSharedWorkstationAsset" - )} - onValueChange={(id) => add(field, id)} - options={assets - .filter( - (item) => ![...values.assetIds, ...values.sharedAssetIds].includes(item.id) - ) - .map((item) => ({ - value: item.id, - label: - unavailableReason(field, item.id) === "ASSET_NOT_OPERATIONAL" - ? `${item.values.name} · ${t(item.values.status)}` - : unavailable(field, item.id) - ? t("workstationOccupied", { name: item.values.name }) - : item.values.name, - disabled: unavailable(field, item.id), - }))} - placeholder={t("chooseAsset")} - value="" - /> - {values[field].length ? ( -
    - {values[field].map((id) => ( -
  • -
    -

    {assets.find((item) => item.id === id)?.values.name ?? id}

    - {unavailable(field, id) && ( -

    - {t(`inventoryErrors.${unavailableReason(field, id)}`)} -

    - )} -
    - -
  • - ))} -
+ )} + {layout.assets && ( + setQuick(true)} + onRemove={remove} + prefix={prefix} + selectedIds={values.assetIds} + sites={lookups.sites} + unavailableReason={unavailableReason} + /> + )} + {layout.computer && ( + form.setValue("computerAssetId", id)} + prefix={prefix} + required={isWizard} + singleComputer={singleComputer} + sites={lookups.sites} + values={values} + /> + )} + {layout.summary && ( + <> +
+ + +
+ {isWizard ? ( + ) : ( -

- {t(field === "assetIds" ? "noWorkstationAssets" : "noSharedWorkstationAssets")} -

+ form.setValue("active", active)} + /> )} -
- ))} + + )}
{Object.keys(form.formState.errors).length > 0 && (

@@ -197,21 +135,15 @@ export function WorkstationForm(props: WorkstationFormProps) { guidance={t("workstationGuidance")} scopeNote={t("workstationScope")} title={t("helpTitle")} + variant="plain" /> - - - + {quick && catalog.data && ( { - add(quickField, id); + add(id); setQuick(false); }} /> @@ -228,3 +160,11 @@ export function WorkstationForm(props: WorkstationFormProps) { ); } + +const FULL_LAYOUT = { location: true, assets: true, computer: true, summary: true }; +const STEP_LAYOUTS = { + 1: { location: true, assets: false, computer: false, summary: false }, + 2: { location: false, assets: true, computer: false, summary: false }, + 3: { location: false, assets: false, computer: true, summary: false }, + 4: { location: false, assets: false, computer: false, summary: true }, +} as const; diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-summary.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-summary.tsx new file mode 100644 index 0000000..8743bc1 --- /dev/null +++ b/modules/assets/src/web/workstations/shared/ui/workstation-summary.tsx @@ -0,0 +1,52 @@ +import { useTranslation } from "react-i18next"; + +import type { WorkstationInput } from "../../../../common"; +import type { AssetDTO } from "../../../../common/inventory"; +import type { WorkstationFormProps } from "../types"; + +export function WorkstationSummary({ + values, + assets, + lookups, +}: { + values: WorkstationInput; + assets: readonly AssetDTO[]; + lookups: WorkstationFormProps["lookups"]; +}) { + 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")} +
+
+
+ ); +} diff --git a/modules/companies/src/api/application/work-profile.repository.ts b/modules/companies/src/api/application/work-profile.repository.ts index 7e4ce10..f6eff71 100644 --- a/modules/companies/src/api/application/work-profile.repository.ts +++ b/modules/companies/src/api/application/work-profile.repository.ts @@ -22,6 +22,7 @@ export type WorkProfileContext = Readonly<{ export interface IWorkProfileRepository { get(companyId: UniqueID): Promise>; + remove(companyId: UniqueID, id: UniqueID): Promise>; save( companyId: UniqueID, action: (context: WorkProfileContext) => Result diff --git a/modules/companies/src/api/application/work-profile.use-cases.ts b/modules/companies/src/api/application/work-profile.use-cases.ts index b85e0af..113e085 100644 --- a/modules/companies/src/api/application/work-profile.use-cases.ts +++ b/modules/companies/src/api/application/work-profile.use-cases.ts @@ -19,6 +19,35 @@ export class WorkProfileUseCases { : Promise.resolve(Result.fail(new WorkProfileError("INVALID_REQUEST"))); } + remove(companyId: string, id: string) { + const company = UniqueID.create(companyId); + const identity = UniqueID.create(id); + + if (!(company.isSuccess && identity.isSuccess)) { + return Promise.resolve(Result.fail(new WorkProfileError("INVALID_REQUEST"))); + } + + return this.repository.remove(company.data, identity.data); + } + + deactivate(companyId: string, id: string, actorId: string) { + const company = UniqueID.create(companyId); + const identity = UniqueID.create(id); + const actor = UniqueID.create(actorId); + + if (!(company.isSuccess && identity.isSuccess && actor.isSuccess)) { + return Promise.resolve(Result.fail(new WorkProfileError("INVALID_REQUEST"))); + } + + return this.repository.save(company.data, (context) => { + const previous = context.profiles.find((item) => item.id.equals(identity.data)); + + return previous + ? previous.deactivate(actor.data, new Date().toISOString()) + : Result.fail(new WorkProfileError("PROFILE_NOT_FOUND")); + }); + } + save(companyId: string, id: string | undefined, input: WorkProfileInput, actorId: string) { const company = UniqueID.create(companyId); const actor = UniqueID.create(actorId); diff --git a/modules/companies/src/api/domain/work-profile.aggregate.ts b/modules/companies/src/api/domain/work-profile.aggregate.ts index 1763724..8eef5cb 100644 --- a/modules/companies/src/api/domain/work-profile.aggregate.ts +++ b/modules/companies/src/api/domain/work-profile.aggregate.ts @@ -3,7 +3,7 @@ import { AggregateRoot } from "@repo/rdx-ddd"; import { Result } from "@repo/rdx-utils"; import { z } from "zod/v4"; -import type { WorkProfileValues } from "./value-objects/work-profile-values.vo"; +import { WorkProfileValues } from "./value-objects/work-profile-values.vo"; import { WorkProfileRevision } from "./work-profile-revision.entity"; import { WorkProfileError } from "./work-profile-values"; @@ -53,6 +53,27 @@ export class WorkProfile extends AggregateRoot { return this.props.history; } + deactivate(actorId: UniqueID, recordedAt: string): Result { + const values = WorkProfileValues.create({ ...this.values.toPrimitive(), active: false }); + + return values.isSuccess ? this.update(values.data, actorId, recordedAt) : values; + } + + checkRemoval({ + hasConfigurations, + }: { + hasConfigurations: boolean; + }): Result { + if (this.values.active) { + return Result.fail(new WorkProfileError("PROFILE_ACTIVE")); + } + if (hasConfigurations) { + return Result.fail(new WorkProfileError("PROFILE_IN_USE")); + } + + return Result.ok(undefined); + } + update( values: WorkProfileValues, actorId: UniqueID, diff --git a/modules/companies/src/api/domain/work-profile.test.ts b/modules/companies/src/api/domain/work-profile.test.ts index 95ea27d..717dbdb 100644 --- a/modules/companies/src/api/domain/work-profile.test.ts +++ b/modules/companies/src/api/domain/work-profile.test.ts @@ -93,6 +93,32 @@ test("Perfil: cuenta opcional, UUID, historial, identidad fija e idempotencia", assert.ok(!WorkProfileValues.create({ ...input(), name: " " }).isSuccess); assert.ok(!WorkProfileValues.create({ ...input(), assetId: "invalid" }).isSuccess); }); + +test("Perfil: desactivar conserva valores e historial, es idempotente y precede al borrado", () => { + const item = profile(); + const blocked = item.checkRemoval({ hasConfigurations: false }); + + assert.ok(!blocked.isSuccess); + assert.equal(blocked.error.code, "PROFILE_ACTIVE"); + const result = item.deactivate(actor, now); + + assert.ok(result.isSuccess); + const inactive = result.data; + + assert.deepEqual(inactive.values.toPrimitive(), { ...item.values.toPrimitive(), active: false }); + assert.ok(inactive.id.equals(item.id)); + assert.equal(inactive.history.length, 2); + assert.equal(inactive.history[0], item.history[0]); + const repeated = inactive.deactivate(actor, now); + + assert.ok(repeated.isSuccess); + assert.equal(repeated.data, inactive); + assert.ok(inactive.checkRemoval({ hasConfigurations: false }).isSuccess); + const linked = inactive.checkRemoval({ hasConfigurations: true }); + + assert.ok(!linked.isSuccess); + assert.equal(linked.error.code, "PROFILE_IN_USE"); +}); test("Perfil: misma persona/dispositivo único vigente, múltiples dispositivos/personas y revisión sin pérdida de datos", () => { const previous = profile(); const ctx = { ...context(), profiles: [previous] }; diff --git a/modules/companies/src/api/infrastructure/http/work-profile.routes.ts b/modules/companies/src/api/infrastructure/http/work-profile.routes.ts index 6a5cbfe..fc774f7 100644 --- a/modules/companies/src/api/infrastructure/http/work-profile.routes.ts +++ b/modules/companies/src/api/infrastructure/http/work-profile.routes.ts @@ -85,5 +85,63 @@ export function registerWorkProfileRoutes( .catch(next); }); } + router.post("/:companyId/:id/deactivate", (request, response, next) => { + const params = ProfileItemParamsSchema.safeParse(request.params); + const actor = z + .object({ userId: z.uuid() }) + .safeParse("session" in request ? request.session : undefined); + + if ( + !( + params.success && + actor.success && + z.strictObject({}).safeParse(request.body).success && + z.strictObject({}).safeParse(request.query).success + ) + ) { + fail(response, "INVALID_REQUEST"); + + return; + } + useCases + .deactivate(params.data.companyId, params.data.id, actor.data.userId) + .then((result) => { + if (!result.isSuccess) { + fail(response, result.error.code); + + return; + } + response.json({ + profile: buildWorkProfileResponse(result.data.profile, result.data.context), + }); + }) + .catch(next); + }); + router.post("/:companyId/:id/delete", (request, response, next) => { + const params = ProfileItemParamsSchema.safeParse(request.params); + + if ( + !( + params.success && + z.strictObject({}).safeParse(request.body).success && + z.strictObject({}).safeParse(request.query).success + ) + ) { + fail(response, "INVALID_REQUEST"); + + return; + } + useCases + .remove(params.data.companyId, params.data.id) + .then((result) => { + if (!result.isSuccess) { + fail(response, result.error.code); + + return; + } + response.json({ deleted: true }); + }) + .catch(next); + }); app.use("/api/work-profiles", authenticate, router); } diff --git a/modules/companies/src/api/infrastructure/persistence/sequelize-work-profile.repository.ts b/modules/companies/src/api/infrastructure/persistence/sequelize-work-profile.repository.ts index 832c73b..fe9df0e 100644 --- a/modules/companies/src/api/infrastructure/persistence/sequelize-work-profile.repository.ts +++ b/modules/companies/src/api/infrastructure/persistence/sequelize-work-profile.repository.ts @@ -102,6 +102,53 @@ export class SequelizeWorkProfileRepository implements IWorkProfileRepository { return this.database.transaction((transaction) => this.load(companyId, transaction, false)); } + remove(companyId: UniqueID, id: UniqueID): Promise> { + return this.database.transaction(async (transaction) => { + const context = await this.load(companyId, transaction, true); + + if (!context.isSuccess) { + return Result.fail(context.error); + } + const profile = context.data.profiles.find((item) => item.id.equals(id)); + + if (!profile) { + return Result.fail(new WorkProfileError("PROFILE_NOT_FOUND")); + } + // También las instantáneas históricas impiden borrar una referencia utilizada. + const tables = [ + "mail_configurations", + "mail_configuration_revisions", + "folder_mappings", + "folder_mapping_revisions", + "printer_configurations", + "printer_configuration_revisions", + "application_configurations", + "application_configuration_revisions", + "certificate_configurations", + "certificate_configuration_revisions", + ]; + const references = await this.database.query<{ profileId: string }>( + `${tables.map((table) => `SELECT profileId FROM ${table} WHERE profileId=:id`).join(" UNION ALL ")} LIMIT 1`, + { replacements: { id: id.toPrimitive() }, transaction, type: QueryTypes.SELECT } + ); + const allowed = profile.checkRemoval({ hasConfigurations: references.length > 0 }); + + if (!allowed.isSuccess) { + return allowed; + } + await this.database.query("DELETE FROM work_profile_revisions WHERE profileId=:id", { + replacements: { id: id.toPrimitive() }, + transaction, + }); + await this.database.query("DELETE FROM work_profiles WHERE id=:id AND companyId=:companyId", { + replacements: { id: id.toPrimitive(), companyId: companyId.toPrimitive() }, + transaction, + }); + + return Result.ok(undefined); + }); + } + save(companyId: UniqueID, action: Parameters[1]) { return this.database.transaction(async (transaction) => { const context = await this.load(companyId, transaction, true); diff --git a/modules/companies/src/common/locales/en.json b/modules/companies/src/common/locales/en.json index 7deb29f..1b31a90 100644 --- a/modules/companies/src/common/locales/en.json +++ b/modules/companies/src/common/locales/en.json @@ -453,8 +453,6 @@ "authorization": "Authorization", "expiry": "Expiry", "validity": "Validity", - "shared": "Shared within workstation", - "exclusive": "Exclusive within workstation", "retiredAsset": "Retired asset", "profileNotFound": "This profile is unavailable in this company.", "loadError": "Could not load the complete environment. Retry to view all configurations.", @@ -735,10 +733,18 @@ "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_TARGET_IMMUTABLE": "A profile's person and device cannot change.", + "PROFILE_ACTIVE": "Deactivate the profile before deleting it.", + "PROFILE_IN_USE": "The profile has linked mail, folder, printer, application or certificate configurations, including revisions. Delete those records before deleting the profile.", "PROFILE_NOT_FOUND": "This profile is unavailable.", "COMPANY_NOT_FOUND": "This company is unavailable.", "INTERNAL_ERROR": "The operation could not be completed. Please try again." }, + "deactivate": "Deactivate profile", + "delete": "Delete profile", + "deactivateDescription": "Deactivate “{{name}}”? Its data, history and configurations are preserved. This action does not remove permissions or configurations from the device.", + "deleteDescription": "Permanently delete “{{name}}” and its history? It must be inactive and have no linked current or historical mail, folder, printer, application or certificate configurations.", + "lifecyclePending": "Processing…", + "lifecycleHelp": "To delete a profile, deactivate it first and delete its linked configuration records. Deactivating preserves history and does not revoke external access.", "workstation": "Workstation", "chooseWorkstation": "Select a workstation", "workstationNeedsComputer": "Edit this workstation and identify an available main computer before creating the profile." diff --git a/modules/companies/src/common/locales/es.json b/modules/companies/src/common/locales/es.json index 16efbcc..a342dd3 100644 --- a/modules/companies/src/common/locales/es.json +++ b/modules/companies/src/common/locales/es.json @@ -453,8 +453,6 @@ "authorization": "Autorización", "expiry": "Caducidad", "validity": "Vigencia", - "shared": "Compartido en el puesto", - "exclusive": "Uso exclusivo en el puesto", "retiredAsset": "Activo retirado", "profileNotFound": "El perfil no está disponible en esta compañía.", "loadError": "No se ha podido cargar el entorno completo. Reintenta para consultar todas sus configuraciones.", @@ -735,10 +733,18 @@ "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_TARGET_IMMUTABLE": "La persona y el dispositivo de un perfil no pueden cambiar.", + "PROFILE_ACTIVE": "Primero desactiva el perfil antes de eliminarlo.", + "PROFILE_IN_USE": "El perfil tiene configuraciones vinculadas de correo, carpetas, impresoras, aplicaciones o certificados, incluidas sus revisiones. Elimina esos registros antes de borrar el perfil.", "PROFILE_NOT_FOUND": "El perfil ya no está disponible.", "COMPANY_NOT_FOUND": "La compañía ya no está disponible.", "INTERNAL_ERROR": "No se pudo completar la operación. Inténtalo de nuevo." }, + "deactivate": "Desactivar perfil", + "delete": "Eliminar perfil", + "deactivateDescription": "¿Desactivar «{{name}}»? Se conservan sus datos, historial y configuraciones. Esta acción no retira permisos ni configuraciones del equipo.", + "deleteDescription": "¿Eliminar definitivamente «{{name}}» y su historial? Debe estar desactivado y no tener configuraciones vinculadas, ni actuales ni históricas, de correo, carpetas, impresoras, aplicaciones o certificados.", + "lifecyclePending": "Procesando…", + "lifecycleHelp": "Para eliminar un perfil, primero desactívalo y elimina sus configuraciones vinculadas. Desactivar conserva el historial y no retira accesos externos.", "workstation": "Puesto de trabajo", "chooseWorkstation": "Selecciona un puesto de trabajo", "workstationNeedsComputer": "Edita el puesto e identifica un ordenador principal disponible antes de crear el perfil." diff --git a/modules/companies/src/web/people/detail/ui/current-workstations.tsx b/modules/companies/src/web/people/detail/ui/current-workstations.tsx index 6087ada..7e03d21 100644 --- a/modules/companies/src/web/people/detail/ui/current-workstations.tsx +++ b/modules/companies/src/web/people/detail/ui/current-workstations.tsx @@ -72,17 +72,12 @@ export function CurrentWorkstations({ ?.name ?? t("noAssignment")}

- {[...station.values.assetIds, ...station.values.sharedAssetIds].map( - (id) => ( - - {inventory.data?.assets.find((asset) => asset.id === id)?.values - .name ?? id} - - ) - )} + {station.values.assetIds.map((id) => ( + + {inventory.data?.assets.find((asset) => asset.id === id)?.values + .name ?? id} + + ))}
))} diff --git a/modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-form.tsx b/modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-form.tsx index 5348db7..bb1bade 100644 --- a/modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-form.tsx +++ b/modules/companies/src/web/printer-configurations/shared/ui/printer-configuration-form.tsx @@ -1,3 +1,4 @@ +import { AssetSelect } from "@metis/assets/client"; import { ContextHelp } from "@repo/rdx-ui/components/context-help"; import { FormActions, FormBody } from "@repo/rdx-ui/components/form-dialog"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; @@ -75,12 +76,13 @@ export function PrinterConfigurationForm(props: PrinterConfigurationFormProps) {
- setQuick("printer") } } + companyId={company.id} disabled={!!configuration || !ready || !profile} id={`${prefix}-printer`} label={t("printers.printer")} @@ -92,6 +94,7 @@ export function PrinterConfigurationForm(props: PrinterConfigurationFormProps) { )} placeholder={t("mail.choose")} required + sites={company.sites} value={values.printerId} />
diff --git a/modules/companies/src/web/remote-connections/shared/ui/remote-endpoint-fields.tsx b/modules/companies/src/web/remote-connections/shared/ui/remote-endpoint-fields.tsx index 54bfb58..000bb44 100644 --- a/modules/companies/src/web/remote-connections/shared/ui/remote-endpoint-fields.tsx +++ b/modules/companies/src/web/remote-connections/shared/ui/remote-endpoint-fields.tsx @@ -1,3 +1,4 @@ +import { AssetSelect } from "@metis/assets/client"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { Label } from "@repo/shadcn-ui/components/label"; import type { UseFormReturn } from "react-hook-form"; @@ -8,13 +9,9 @@ import type { RemoteConnectionInput } from "../../../../common/remote-connection import { RemoteConnectionInputSchema } from "../../../../common/remote-connections"; import type { RemoteQuickKind } from "../../create"; -type Option = { value: string; label: string }; +import { RemoteTargetField } from "./remote-target-field"; -const targetActions = { - network: { label: "newSite", kind: "site" }, - asset: { label: "remote.newAsset", kind: "target" }, - resource: { label: "remote.newResource", kind: "resource" }, -} satisfies Record; +type Option = { value: string; label: string }; export function RemoteEndpointFields({ company, @@ -37,15 +34,6 @@ export function RemoteEndpointFields({ }) { const { t } = useTranslation("companies"); const v = form.watch(); - const targetAction = targetActions[v.targetKind]; - const targetOptions = { - network: [ - { value: "", label: t("remote.allCompany") }, - ...company.sites.map((s) => ({ value: s.id, label: s.name })), - ], - asset: assetOptions.filter((a) => a.value !== v.sourceAssetId), - resource: resourceOptions, - }; return ( <> @@ -74,10 +62,11 @@ export function RemoteEndpointFields({ {v.sourceKind === "company_asset" && (
- setQuick("source") } } + companyId={company.id} disabled={fixed || !ready} id={`${prefix}-source`} label={t("remote.sourceAsset")} @@ -85,6 +74,7 @@ export function RemoteEndpointFields({ options={assetOptions} placeholder={t("mail.choose")} required + sites={company.sites} value={v.sourceAssetId ?? ""} />
@@ -112,31 +102,16 @@ export function RemoteEndpointFields({ value={v.targetKind} /> -
- - setQuick(targetAction.kind), - } - } - disabled={fixed || !ready} - id={`${prefix}-target`} - label={t(v.targetKind === "network" ? "remote.site" : "remote.target")} - onValueChange={(id) => - form.setValue(v.targetKind === "network" ? "siteId" : "targetId", id || null) - } - options={targetOptions[v.targetKind]} - placeholder={t(v.targetKind === "network" ? "remote.allCompany" : "mail.choose")} - required={v.targetKind !== "network"} - value={(v.targetKind === "network" ? v.siteId : v.targetId) ?? ""} - /> -
+ ); } diff --git a/modules/companies/src/web/remote-connections/shared/ui/remote-target-field.tsx b/modules/companies/src/web/remote-connections/shared/ui/remote-target-field.tsx new file mode 100644 index 0000000..a0cf349 --- /dev/null +++ b/modules/companies/src/web/remote-connections/shared/ui/remote-target-field.tsx @@ -0,0 +1,95 @@ +import { AssetSelect } from "@metis/assets/client"; +import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; +import { Label } from "@repo/shadcn-ui/components/label"; +import type { UseFormReturn } from "react-hook-form"; +import { useTranslation } from "react-i18next"; + +import type { CompanyDTO } from "../../../../common"; +import type { RemoteConnectionInput } from "../../../../common/remote-connections"; +import type { RemoteQuickKind } from "../../create"; + +const targetActions = { + network: { label: "newSite", kind: "site" }, + asset: { label: "remote.newAsset", kind: "target" }, + resource: { label: "remote.newResource", kind: "resource" }, +} satisfies Record; + +export function RemoteTargetField({ + company, + form, + prefix, + fixed, + ready, + assetOptions, + resourceOptions, + setQuick, +}: { + company: CompanyDTO; + form: UseFormReturn; + prefix: string; + fixed: boolean; + ready: boolean; + assetOptions: readonly { value: string; label: string }[]; + resourceOptions: readonly { value: string; label: string }[]; + setQuick: (kind: RemoteQuickKind) => void; +}) { + const { t } = useTranslation("companies"); + const v = form.watch(); + const targetAction = targetActions[v.targetKind]; + const targetOptions = { + network: [ + { value: "", label: t("remote.allCompany") }, + ...company.sites.map((s) => ({ value: s.id, label: s.name })), + ], + asset: assetOptions.filter((a) => a.value !== v.sourceAssetId), + resource: resourceOptions, + }; + + const disabled = fixed || !ready; + + return ( +
+ + {v.targetKind === "asset" ? ( + setQuick("target") } + } + companyId={company.id} + disabled={disabled} + id={`${prefix}-target`} + label={t("remote.target")} + onValueChange={(id) => form.setValue("targetId", id || null)} + options={targetOptions.asset} + placeholder={t("mail.choose")} + required + sites={company.sites} + value={v.targetId ?? ""} + /> + ) : ( + setQuick(targetAction.kind), + } + } + disabled={disabled} + id={`${prefix}-target`} + label={t(v.targetKind === "network" ? "remote.site" : "remote.target")} + onValueChange={(id) => + form.setValue(v.targetKind === "network" ? "siteId" : "targetId", id || null) + } + options={targetOptions[v.targetKind]} + placeholder={t(v.targetKind === "network" ? "remote.allCompany" : "mail.choose")} + required={v.targetKind !== "network"} + value={(v.targetKind === "network" ? v.siteId : v.targetId) ?? ""} + /> + )} +
+ ); +} diff --git a/modules/companies/src/web/tickets/shared/ui/ticket-targets.tsx b/modules/companies/src/web/tickets/shared/ui/ticket-targets.tsx index c4891b1..c8f1478 100644 --- a/modules/companies/src/web/tickets/shared/ui/ticket-targets.tsx +++ b/modules/companies/src/web/tickets/shared/ui/ticket-targets.tsx @@ -1,3 +1,4 @@ +import { AssetSelect } from "@metis/assets/client"; import { SelectWithAction } from "@repo/rdx-ui/components/select-with-action"; import { Button } from "@repo/shadcn-ui/components/button"; import { Label } from "@repo/shadcn-ui/components/label"; @@ -67,26 +68,57 @@ export function TicketTargets({
- setQuick(kind), - disabled: values.length >= 200, - }} - disabled={values.length >= 200} - id={`${prefix}-item`} - label={t("tickets.addTarget")} - onValueChange={(id) => add(id)} - options={ticketTargetOptions(lookups, kind) - .filter( - (item) => - item.active && - !values.some((target) => target.kind === kind && target.targetId === item.id) - ) - .map((item) => ({ value: item.id, label: item.name }))} - placeholder={t("tickets.choose")} - value="" - /> + {kind === "asset" ? ( + setQuick("asset"), + disabled: values.length >= 200, + }} + companyId={companyId} + disabled={values.length >= 200} + id={`${prefix}-item`} + label={t("tickets.addTarget")} + maxSelection={200 - values.length} + mode="multiple" + onValueChange={(ids) => + onChange([ + ...values, + ...ids.map((targetId) => ({ kind: "asset" as const, targetId })), + ]) + } + options={lookups.assets + .filter( + (item) => + item.active && + !values.some((target) => target.kind === "asset" && target.targetId === item.id) + ) + .map((item) => ({ value: item.id, label: item.name }))} + placeholder={t("tickets.choose")} + value={[]} + /> + ) : ( + setQuick(kind), + disabled: values.length >= 200, + }} + disabled={values.length >= 200} + id={`${prefix}-item`} + label={t("tickets.addTarget")} + onValueChange={(id) => add(id)} + options={ticketTargetOptions(lookups, kind) + .filter( + (item) => + item.active && + !values.some((target) => target.kind === kind && target.targetId === item.id) + ) + .map((item) => ({ value: item.id, label: item.name }))} + placeholder={t("tickets.choose")} + value="" + /> + )}
    diff --git a/modules/companies/src/web/work-environments/detail/ui/environment-content.tsx b/modules/companies/src/web/work-environments/detail/ui/environment-content.tsx index 094b794..947ebc3 100644 --- a/modules/companies/src/web/work-environments/detail/ui/environment-content.tsx +++ b/modules/companies/src/web/work-environments/detail/ui/environment-content.tsx @@ -120,17 +120,10 @@ export function EnvironmentContent({ {model.stationAssets.length > 0 && (
      {model.stationAssets.map((asset) => { - const shared = model.stations.some((station) => - station.values.sharedAssetIds.includes(asset.id) - ); - return (
    • {asset.values.name} -

      - {asset.specification.templateName} ·{" "} - {t(shared ? "environment.shared" : "environment.exclusive")} -

      +

      {asset.specification.templateName}

      {asset.values.status === "retired" && (

      {t("environment.retiredAsset")}

      )} @@ -144,6 +137,45 @@ export function EnvironmentContent({ )} +
      +
      + {(["configured", "pending", "removal", "review"] as const).map((key) => ( +
      +
      {t(`environment.${key}`)}
      +
      {model.counts[key]}
      +
      + ))} +
      + + {model.unlinkedMail > 0 && ( +

      + {t("environment.unlinkedMail", { count: model.unlinkedMail })} +

      + )} +
      + {(["folders", "mail", "applications", "printers", "certificates"] as const).map( + (kind) => ( + onManage(kind)} + rows={model.rows.filter( + (item) => + item.kind === kind && (!attention || item.state !== "configured" || item.review) + )} + /> + ) + )} +
      +
      @@ -190,45 +222,6 @@ export function EnvironmentContent({ companyId={company.id} retired={model.host?.values.status === "retired"} /> -
      -
      - {(["configured", "pending", "removal", "review"] as const).map((key) => ( -
      -
      {t(`environment.${key}`)}
      -
      {model.counts[key]}
      -
      - ))} -
      - - {model.unlinkedMail > 0 && ( -

      - {t("environment.unlinkedMail", { count: model.unlinkedMail })} -

      - )} -
      - {(["mail", "folders", "printers", "applications", "certificates"] as const).map( - (kind) => ( - onManage(kind)} - rows={model.rows.filter( - (item) => - item.kind === kind && (!attention || item.state !== "configured" || item.review) - )} - /> - ) - )} -
      -

      {t("environment.unknown")}

      ); diff --git a/modules/companies/src/web/work-environments/shared/helpers/work-environment.model.ts b/modules/companies/src/web/work-environments/shared/helpers/work-environment.model.ts index 53a3171..a02aaf6 100644 --- a/modules/companies/src/web/work-environments/shared/helpers/work-environment.model.ts +++ b/modules/companies/src/web/work-environments/shared/helpers/work-environment.model.ts @@ -89,15 +89,13 @@ export function buildEnvironment(profile: WorkProfileDTO, sources: EnvironmentSo item.values.active && item.values.computerAssetId === profile.values.assetId ); - const stationAssets = [ - ...new Set( - stations.flatMap((item) => [...item.values.assetIds, ...item.values.sharedAssetIds]) - ), - ].flatMap((id) => { - const asset = assets.get(id); + const stationAssets = [...new Set(stations.flatMap((item) => item.values.assetIds))].flatMap( + (id) => { + const asset = assets.get(id); - return asset ? [asset] : []; - }); + return asset ? [asset] : []; + } + ); const unlinkedMail = sources.mail.filter( (item) => item.companyId === profile.companyId && diff --git a/modules/companies/src/web/work-environments/shared/work-environment.test.tsx b/modules/companies/src/web/work-environments/shared/work-environment.test.tsx index 6e3879c..8a72130 100644 --- a/modules/companies/src/web/work-environments/shared/work-environment.test.tsx +++ b/modules/companies/src/web/work-environments/shared/work-environment.test.tsx @@ -186,8 +186,7 @@ const station = WorkstationSchema.parse( siteId: null, personId: null, computerAssetId: host.id, - assetIds: [host.id], - sharedAssetIds: [monitor.id], + assetIds: [host.id, monitor.id], }) ); const sources: EnvironmentSources = { @@ -424,6 +423,22 @@ test("entorno: ficha es/en con estados y acciones, sin formularios; muestra fals assert.ok(html.includes(text.environment.title)); assert.ok(html.includes(text.environment.onlyAttention)); assert.ok(html.includes(text.environment.samePerson)); + const headings = [ + text.environment.equipment, + text.folders.title, + text.mail.title, + text.applications.title, + text.printers.title, + text.certificates.title, + text.telework.title, + text.remote.title, + ].map((title) => html.indexOf(`

      ${title}

      `)); + + assert.ok(headings.every((position) => position >= 0)); + assert.deepEqual( + headings, + [...headings].sort((left, right) => left - right) + ); assert.ok(html.includes("ana@example.test") && html.includes("pedidos@example.test")); assert.ok(html.includes("BitLocker") && html.includes(text.no)); assert.ok(html.includes(text.environment.needsReview)); diff --git a/modules/companies/src/web/work-profiles/list/controllers/use-work-profiles-page-controller.ts b/modules/companies/src/web/work-profiles/list/controllers/use-work-profiles-page-controller.ts index e948ce5..80244d2 100644 --- a/modules/companies/src/web/work-profiles/list/controllers/use-work-profiles-page-controller.ts +++ b/modules/companies/src/web/work-profiles/list/controllers/use-work-profiles-page-controller.ts @@ -92,6 +92,7 @@ export function useWorkProfilesPageController({ }; const [editor, setEditor] = useState(); + const [lifecycle, setLifecycle] = useState<{ id: string; action: "deactivate" | "delete" }>(); const [view, setView] = useState(); const { searchParams, setSearchParams } = useBrowserSearchParams(); const search = searchParams.get("q") ?? ""; @@ -164,6 +165,8 @@ export function useWorkProfilesPageController({ setConfiguration, closeConfiguration, editor, + lifecycle, + setLifecycle, setEditor, setView, search, diff --git a/modules/companies/src/web/work-profiles/list/ui/work-profiles-page.tsx b/modules/companies/src/web/work-profiles/list/ui/work-profiles-page.tsx index b5a9299..e248fe5 100644 --- a/modules/companies/src/web/work-profiles/list/ui/work-profiles-page.tsx +++ b/modules/companies/src/web/work-profiles/list/ui/work-profiles-page.tsx @@ -12,6 +12,7 @@ import { useTranslation } from "react-i18next"; import { ProfileConfigurations } from "../../../work-environments"; import { WorkProfileHistoryDialog } from "../../history"; import { ProfileFeedback, WorkProfileEditorDialog } from "../../shared"; +import { WorkProfileLifecycleDialog } from "../../shared/ui/work-profile-lifecycle-dialog"; import { useWorkProfilesPageController } from "../controllers"; import type { WorkProfilesPageProps } from "../types"; @@ -22,6 +23,8 @@ export function WorkProfilesPage(props: WorkProfilesPageProps) { setConfiguration, closeConfiguration, editor, + lifecycle, + setLifecycle, setEditor, setView, search, @@ -42,6 +45,7 @@ export function WorkProfilesPage(props: WorkProfilesPageProps) { missingProfile, } = useWorkProfilesPageController(props); const { t, i18n } = useTranslation("companies"); + const lifecycleProfile = profiles.find((item) => item.id === lifecycle?.id); if (missingProfile) { return ; @@ -182,6 +186,18 @@ export function WorkProfilesPage(props: WorkProfilesPageProps) { {t("profiles.history")} setEditor(item.id)}>{t("edit")} + setLifecycle({ id: item.id, action: "deactivate" })} + > + {t("profiles.deactivate")} + + setLifecycle({ id: item.id, action: "delete" })} + > + {t("profiles.delete")} + {" "} ), @@ -209,6 +225,7 @@ export function WorkProfilesPage(props: WorkProfilesPageProps) { title={t("profiles.title")} /> )} + {ready &&

      {t("profiles.lifecycleHelp")}

      } {editor && ( )} + {lifecycle && lifecycleProfile && ( + setLifecycle(undefined)} + profile={lifecycleProfile} + /> + )} {viewed && ( { @@ -93,7 +94,6 @@ test("perfiles: el puesto fijado aporta su ordenador y no selecciona al responsa siteId: null, computerAssetId: computerId, assetIds: [computerId], - sharedAssetIds: [], }, history: [], }; diff --git a/modules/companies/src/web/work-profiles/shared/ui/work-profile-lifecycle-dialog.tsx b/modules/companies/src/web/work-profiles/shared/ui/work-profile-lifecycle-dialog.tsx new file mode 100644 index 0000000..b1dddc2 --- /dev/null +++ b/modules/companies/src/web/work-profiles/shared/ui/work-profile-lifecycle-dialog.tsx @@ -0,0 +1,59 @@ +import { ConfirmDeleteDialog } from "@repo/rdx-ui/components/confirm-delete-dialog"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; + +import type { WorkProfileDTO } from "../../../../common/work-profiles"; +import { dashboardKeys } from "../../../dashboard/shared/api"; +import { processKeys } from "../../../processes/shared/queries"; +import { WorkProfileApiError, profileApi, profileKeys } from "../api"; + +export function WorkProfileLifecycleDialog({ + profile, + action, + onClose, +}: { + profile: WorkProfileDTO; + action: "deactivate" | "delete"; + onClose: () => void; +}) { + const { t } = useTranslation("companies"); + const client = useQueryClient(); + const mutation = useMutation({ + mutationFn: () => profileApi.changeLifecycle(profile.companyId, profile.id, action), + onSuccess: async () => { + await Promise.all([ + client.invalidateQueries({ queryKey: profileKeys.company(profile.companyId) }), + client.invalidateQueries({ + queryKey: processKeys.review(profile.companyId, profile.values.personId), + }), + ...[profile.companyId, undefined].flatMap((companyId) => + ["summary", "attention", "details"].map((block) => + client.invalidateQueries({ + queryKey: dashboardKeys.block(block, companyId).slice(0, 3), + }) + ) + ), + ]); + }, + }); + + return ( + mutation.mutateAsync()} + onOpenChange={(open) => { + if (!open) { + onClose(); + } + }} + open + pendingLabel={t("profiles.lifecyclePending")} + title={t(`profiles.${action}`)} + /> + ); +} diff --git a/modules/companies/src/web/workspace/shared/helpers/workspace-routes.ts b/modules/companies/src/web/workspace/shared/helpers/workspace-routes.ts index db248cb..2c1170b 100644 --- a/modules/companies/src/web/workspace/shared/helpers/workspace-routes.ts +++ b/modules/companies/src/web/workspace/shared/helpers/workspace-routes.ts @@ -182,7 +182,13 @@ function queryMatchesLocation( if (location.area === "company" && query.companyId && query.companyId !== location.id) { return false; } - if (query.template && !(location.area === "company" && location.section === "inventory")) { + if ( + query.template && + !( + location.area === "company" && + (location.section === "inventory" || location.section === "resources") + ) + ) { return false; } diff --git a/modules/companies/src/web/workspace/shared/workspace-routes.test.ts b/modules/companies/src/web/workspace/shared/workspace-routes.test.ts index 9a9cf5f..8e8e19e 100644 --- a/modules/companies/src/web/workspace/shared/workspace-routes.test.ts +++ b/modules/companies/src/web/workspace/shared/workspace-routes.test.ts @@ -12,17 +12,24 @@ const COMPANY_ID = "019b8838-44af-776d-a406-af74452b6f26"; const ENTITY_ID = "019b8838-44af-776d-a406-af74452b6f27"; const TEMPLATE_ID = "01a1276d-2138-711e-b3d2-99258a02a80f"; -test("el filtro de plantilla permite abrir el listado y las fichas de activos", () => { - for (const suffix of ["", `/${ENTITY_ID}`]) { - assert.deepEqual( - parseWorkspaceUrl(`/companies/${COMPANY_ID}/assets${suffix}?template=${TEMPLATE_ID}&q=Alpha`), - { - area: "company", - id: COMPANY_ID, - section: "inventory", - ...(suffix ? { entityId: ENTITY_ID } : {}), - } - ); +test("el filtro de plantilla permite abrir listados y fichas de activos y recursos", () => { + for (const [path, section] of [ + ["assets", "inventory"], + ["resources", "resources"], + ]) { + for (const suffix of ["", `/${ENTITY_ID}`]) { + assert.deepEqual( + parseWorkspaceUrl( + `/companies/${COMPANY_ID}/${path}${suffix}?template=${TEMPLATE_ID}&q=Alpha` + ), + { + area: "company", + id: COMPANY_ID, + section, + ...(suffix ? { entityId: ENTITY_ID } : {}), + } + ); + } } assert.deepEqual( parseWorkspaceUrl( @@ -35,6 +42,16 @@ test("el filtro de plantilla permite abrir el listado y las fichas de activos", entityId: "01a127a0-190b-70fc-bf70-4e41990367b8", } ); + assert.deepEqual( + parseWorkspaceUrl( + "http://192.168.0.2:5173/companies/01a1016a-289c-761d-9d11-bf10359c0b4e/resources?template=01a10ba8-31cb-70a1-9950-1cc51a827295" + ), + { + area: "company", + id: "01a1016a-289c-761d-9d11-bf10359c0b4e", + section: "resources", + } + ); }); test("el filtro de plantilla rechaza UUID inválidos, parámetros repetidos y otros ámbitos", () => { @@ -42,7 +59,10 @@ test("el filtro de plantilla rechaza UUID inválidos, parámetros repetidos y ot `/companies/${COMPANY_ID}/assets?template=invalid`, `/companies/${COMPANY_ID}/assets?template=`, `/companies/${COMPANY_ID}/assets?template=${TEMPLATE_ID}&template=${TEMPLATE_ID}`, - `/companies/${COMPANY_ID}/resources?template=${TEMPLATE_ID}`, + `/companies/${COMPANY_ID}/resources?template=invalid`, + `/companies/${COMPANY_ID}/resources?template=`, + `/companies/${COMPANY_ID}/resources?template=${TEMPLATE_ID}&template=${TEMPLATE_ID}`, + `/companies/${COMPANY_ID}/catalogs/resources?template=${TEMPLATE_ID}`, `/companies/${COMPANY_ID}/catalogs/assets?template=${TEMPLATE_ID}`, `/catalogs/assets?template=${TEMPLATE_ID}`, `/tickets?template=${TEMPLATE_ID}`, diff --git a/modules/companies/src/web/workstations/detail/helpers/workstation-relations.test.ts b/modules/companies/src/web/workstations/detail/helpers/workstation-relations.test.ts index a396dba..d8102c4 100644 --- a/modules/companies/src/web/workstations/detail/helpers/workstation-relations.test.ts +++ b/modules/companies/src/web/workstations/detail/helpers/workstation-relations.test.ts @@ -17,7 +17,6 @@ const values = { personId: id(3), computerAssetId: id(4), assetIds: [id(4)], - sharedAssetIds: [id(5)], }; const history = { id: id(9), @@ -83,7 +82,7 @@ test("puesto inexistente o sin composición no hereda perfiles de otros activos" ); const empty = WorkstationSchema.parse({ ...workstation, - values: { ...values, computerAssetId: null, assetIds: [], sharedAssetIds: [] }, + values: { ...values, computerAssetId: null, assetIds: [] }, }); const result = workstationRelations(empty, [profile(11, id(4), id(6))]); diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx index e56b1a4..f852963 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-assets.tsx @@ -16,7 +16,7 @@ export function WorkstationAssets({ }) { const { t } = useTranslation("assets"); const root = `/companies/${companyId}`; - const assetIds = new Set([...workstation.values.assetIds, ...workstation.values.sharedAssetIds]); + const assetIds = new Set(workstation.values.assetIds); const assetName = (id: string) => assets.find((asset) => asset.id === id)?.values.name ?? t("noValue"); @@ -44,11 +44,7 @@ export function WorkstationAssets({

      {id === workstation.values.computerAssetId ? t("workstationComputer") - : t( - workstation.values.sharedAssetIds.includes(id) - ? "sharedWorkstationAssets" - : "exclusiveWorkstationAssets" - )} + : t("workstationAssets")}

      {asset ? ( diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx index b4a2f17..a7c18b5 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-detail.test.tsx @@ -33,7 +33,6 @@ const values = { siteId: null, personId: null, assetIds: [], - sharedAssetIds: [], }; const workstation = WorkstationSchema.parse({ id: workstationId, @@ -99,7 +98,7 @@ test("ficha de puesto es/en: cabecera, retorno, bloques vacíos y edición modal assert.ok(html.includes("Administración")); assert.ok(html.includes(`href="/companies/${companyId}/workstations"`)); assert.ok(html.includes(language === "es" ? "Editar puesto" : "Edit workstation")); - assert.ok(html.includes(language === "es" ? "Dar de baja" : "Decommission")); + assert.ok(html.includes(language === "es" ? "Desactivar" : "Deactivate")); assert.ok(html.includes(language === "es" ? "Eliminar definitivamente" : "Delete permanently")); assert.ok(html.includes(language === "es" ? "Perfiles de usuario" : "User profiles")); assert.ok(html.includes(language === "es" ? "Personas relacionadas" : "Related people")); diff --git a/modules/resources/src/common/locales/en.json b/modules/resources/src/common/locales/en.json index 88b37f0..d54af8b 100644 --- a/modules/resources/src/common/locales/en.json +++ b/modules/resources/src/common/locales/en.json @@ -1,4 +1,6 @@ { + "allResources": "All", + "filterByTemplate": "Filter by template", "inventory": "Resources", "mailboxKind": "Mailbox type", "corporate": "Corporate", diff --git a/modules/resources/src/common/locales/es.json b/modules/resources/src/common/locales/es.json index 03120f3..2278547 100644 --- a/modules/resources/src/common/locales/es.json +++ b/modules/resources/src/common/locales/es.json @@ -1,4 +1,6 @@ { + "allResources": "Todo", + "filterByTemplate": "Filtrar por plantilla", "inventory": "Recursos", "mailboxKind": "Tipo de buzón", "corporate": "Corporativo", diff --git a/modules/resources/src/web/resources/create/ui/resource-create-progress.tsx b/modules/resources/src/web/resources/create/ui/resource-create-progress.tsx index 7043df6..ffb06c6 100644 --- a/modules/resources/src/web/resources/create/ui/resource-create-progress.tsx +++ b/modules/resources/src/web/resources/create/ui/resource-create-progress.tsx @@ -1,3 +1,4 @@ +import { FormProgress } from "@repo/rdx-ui/components/form-progress"; import { useTranslation } from "react-i18next"; export function ResourceCreateProgress({ @@ -8,71 +9,23 @@ export function ResourceCreateProgress({ templateName?: string | undefined; }) { const { t } = useTranslation("resources"); - const currentStep = STEP_KEYS[step - 1]; + const keys = ["template", "fields", "name"] as const; return ( -
      -
        - {STEP_KEYS.map((key, index) => { - const current = step === index + 1; - const completed = step > index + 1; - - return ( -
      1. - - - {t(`creationSteps.${key}`)} - {completed && {t("creationSteps.completed")}} - -
      2. - ); - })} -
      -
      -
      -

      - {t(`creationSteps.${currentStep}`)} -

      -

      - {t("creationSteps.progress", { step, total: 3 })} -

      -
      -

      - {t(`creationSteps.${currentStep}Description`)} +

      + t(`creationSteps.${key}`))} + progress={t("creationSteps.progress", { step, total: 3 })} + step={step} + /> + {templateName && step > 1 && ( +

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

      - {templateName && step > 1 && ( -

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

      - )} -
      + )}
      ); } - -const STEP_KEYS = ["template", "fields", "name"] as const; - -function focusStep(element: HTMLParagraphElement | null) { - element?.focus(); -} diff --git a/modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts b/modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts index 8bc596c..6d27e60 100644 --- a/modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts +++ b/modules/resources/src/web/resources/list/controllers/use-resources-page-controller.ts @@ -40,14 +40,39 @@ export function useResourcesPageController({ queryKey: catalogKeys.scope(companyId), queryFn: () => catalogApi.get(companyId), }); + const templates = (catalog.data?.items ?? []).filter((item) => item.values.kind === "template"); + const requestedTemplateId = searchParams.get("template"); + const templateId = templates.some((item) => item.id === requestedTemplateId) + ? requestedTemplateId + : null; + + const setTemplateId = (value: string) => + setSearchParams((params) => { + if (value === "all") { + params.delete("template"); + } else { + params.set("template", value); + } + }); + + const clearFilters = () => + setSearchParams((params) => { + params.delete("q"); + params.delete("template"); + }); + const edited = query.data?.resources.find((item) => item.id === editor); const viewed = query.data?.resources.find((item) => item.id === viewId); const visible = - query.data?.resources.filter((item) => - `${item.values.name} ${item.specification.templateName}` + query.data?.resources.filter((item) => { + if (templateId && item.values.templateId !== templateId) { + return false; + } + + return `${item.values.name} ${item.specification.templateName}` .toLocaleLowerCase() - .includes(search.toLocaleLowerCase()) - ) ?? []; + .includes(search.toLocaleLowerCase()); + }) ?? []; return { search, @@ -57,6 +82,10 @@ export function useResourcesPageController({ setViewId, query, catalog, + templates, + templateId, + setTemplateId, + clearFilters, edited, viewed, entityHref, diff --git a/modules/resources/src/web/resources/list/ui/resources-page.tsx b/modules/resources/src/web/resources/list/ui/resources-page.tsx index 5cf8a1c..93253f6 100644 --- a/modules/resources/src/web/resources/list/ui/resources-page.tsx +++ b/modules/resources/src/web/resources/list/ui/resources-page.tsx @@ -7,6 +7,7 @@ import { EntityTable, } from "@repo/rdx-ui/components/entity-list"; import { Button } from "@repo/shadcn-ui/components/button"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@repo/shadcn-ui/components/tabs"; import { useTranslation } from "react-i18next"; import { InventoryFeedback } from "../../../shared"; @@ -25,6 +26,10 @@ export function ResourcesPage(props: ResourcesPageProps) { setViewId, query, catalog, + templates, + templateId, + setTemplateId, + clearFilters, edited, viewed, entityHref, @@ -32,13 +37,14 @@ export function ResourcesPage(props: ResourcesPageProps) { visible, } = useResourcesPageController(props); const { t, i18n } = useTranslation("resources"); + const tabValue = templateId ?? "all"; if (missingView) { return ; } return ( -
      +
      @@ -65,67 +71,84 @@ export function ResourcesPage(props: ResourcesPageProps) { {t("retry")} )} - item.values.name, - hideable: false, - render: (item) => ( - - {item.values.name} - - ), - }, - { - id: "template", - label: t("template"), - value: (item) => item.specification.templateName, - render: (item) => <>{item.specification.templateName}, - }, - { - id: "state", - label: t("status"), - value: (item) => t(item.values.active ? "active" : "inactive"), - render: (item) => ( - - {t(item.values.active ? "active" : "inactive")} - - ), - }, - { - id: "actions", - label: t("actions"), - hideable: false, - render: (item) => ( - - {" "} -
      - setViewId(item.id)}> - {t("viewResource")} - - setEditor(item.id)}> - {t("edit")} - -
      {" "} -
      - ), - }, - ]} - emptyMessage={t("empty")} - items={visible} - loading={query.isPending || catalog.isPending} - locale={i18n.language} - onSearchChange={setSearch} - search={search} - searchPlaceholder={t("search")} - title={t("inventory")} - unavailable={(!query.data && query.isError) || (!catalog.data && catalog.isError)} - /> + +
      + + {t("allResources")} + {templates.map((template) => ( + + {template.values.name} + + ))} + +
      + + item.values.name, + hideable: false, + render: (item) => ( + + {item.values.name} + + ), + }, + { + id: "template", + label: t("template"), + value: (item) => item.specification.templateName, + render: (item) => <>{item.specification.templateName}, + }, + { + id: "state", + label: t("status"), + value: (item) => t(item.values.active ? "active" : "inactive"), + render: (item) => ( + + {t(item.values.active ? "active" : "inactive")} + + ), + }, + { + id: "actions", + label: t("actions"), + hideable: false, + render: (item) => ( + + {" "} +
      + setViewId(item.id)}> + {t("viewResource")} + + setEditor(item.id)}> + {t("edit")} + +
      {" "} +
      + ), + }, + ]} + emptyMessage={t("empty")} + filterKey={tabValue} + filtersApplied={templateId !== null} + items={visible} + loading={query.isPending || catalog.isPending} + locale={i18n.language} + onClearFilters={clearFilters} + onSearchChange={setSearch} + search={search} + searchPlaceholder={t("search")} + title={t("inventory")} + unavailable={(!query.data && query.isError) || (!catalog.data && catalog.isError)} + /> +
      +
      {editor && catalog.data && ( @@ -56,9 +64,107 @@ test("recursos: listado con alta modal y sin formulario visible, en español e i const html = await render(language, ); assert.ok(html.includes(label)); + assert.ok(html.includes('role="tablist"')); + assert.ok(html.includes(language === "es" ? ">Todo<" : ">All<")); + assert.ok( + html.includes( + language === "es" ? 'aria-label="Filtrar por plantilla"' : 'aria-label="Filter by template"' + ) + ); assert.equal(html.includes(" { + const catalog = structuredClone( + buildCatalogResponse(seedResourceCatalog(ResourceCatalog.create()), Maybe.none()) + ); + const templates = catalog.items.filter((item) => item.values.kind === "template"); + const [first, second, empty] = templates; + + assert.ok(first && second && empty); + // Dos nombres iguales siguen representando filtros distintos por identidad. + second.values.name = first.values.name; + empty.active = false; + empty.effectiveActive = false; + const resources = [first, second].flatMap((template, index) => + ["Alpha", "Beta"].map((name) => { + const values = { + name: `${name}-${index}`, + templateId: template.id, + active: true, + notes: "", + characteristics: {}, + }; + + return ResourceSchema.parse({ + id: UniqueID.generateNewID().toPrimitive(), + companyId, + specification: { + templateId: template.id, + templateName: template.values.name, + fields: [ + { + fieldId: UniqueID.generateNewID().toPrimitive(), + name: "Número de serie", + type: "text", + unit: "", + choices: [], + required: false, + }, + ], + }, + values, + history: [ + { + id: UniqueID.generateNewID().toPrimitive(), + values, + actorId: UniqueID.generateNewID().toPrimitive(), + actorName: "Técnico", + recordedAt: "2026-10-14T10:00:00.000Z", + }, + ], + }); + }) + ); + const descriptor = Object.getOwnPropertyDescriptor(globalThis, "window"); + + try { + for (const [search, expected] of [ + ["", ["Alpha-0", "Beta-0", "Alpha-1", "Beta-1"]], + [`?template=${first.id}`, ["Alpha-0", "Beta-0"]], + [`?template=${second.id}&q=Alpha`, ["Alpha-1"]], + [`?template=${empty.id}`, []], + ["?template=missing", ["Alpha-0", "Beta-0", "Alpha-1", "Beta-1"]], + ] satisfies [string, string[]][]) { + Object.defineProperty(globalThis, "window", { + configurable: true, + value: { location: { pathname: `/companies/${companyId}/resources`, search } }, + }); + const html = await render("es", , { + resources, + catalog, + }); + const table = html.match(//)?.[0]; + + assert.ok(table); + for (const resource of resources) { + assert.equal(table.includes(resource.values.name), expected.includes(resource.values.name)); + } + assert.equal((html.match(/role="tab"/g) ?? []).length, templates.length + 1); + assert.ok(html.includes(empty.values.name)); + if (expected.length === 0) { + assert.ok(html.includes("No hay resultados para estos criterios.")); + } + } + } finally { + if (descriptor) { + Object.defineProperty(globalThis, "window", descriptor); + } else { + Reflect.deleteProperty(globalThis, "window"); + } + } +}); + test("recurso: ayuda inferior, plantilla con alta rápida y sin asignaciones de recursos", async () => { const html = await render( "es", diff --git a/packages/rdx-ui/src/components/form-progress.tsx b/packages/rdx-ui/src/components/form-progress.tsx new file mode 100644 index 0000000..6da5dae --- /dev/null +++ b/packages/rdx-ui/src/components/form-progress.tsx @@ -0,0 +1,68 @@ +/** Indicador de pasos sin conocimiento del formulario o de sus datos. */ +export function FormProgress({ + step, + labels, + label, + progress, + completedLabel, + description, +}: { + step: number; + labels: readonly string[]; + label: string; + progress: string; + completedLabel: string; + description: string; +}) { + return ( +
      +
        + {labels.map((title, index) => { + const current = step === index + 1; + const completed = step > index + 1; + + return ( +
      1. + + + {title} + {completed && {completedLabel}} + +
      2. + ); + })} +
      +
      +
      +

      {labels[step - 1]}

      +

      + {progress} +

      +
      +

      {description}

      +
      +
      + ); +} + +function focusStep(element: HTMLParagraphElement | null) { + element?.focus(); +}