diff --git a/apps/server/src/identity.test.ts b/apps/server/src/identity.test.ts index 4c9a9cf..6cd96c3 100644 --- a/apps/server/src/identity.test.ts +++ b/apps/server/src/identity.test.ts @@ -55,6 +55,7 @@ import { verifySupport } from "./support.integration"; import { verifyTelework } from "./telework.integration"; import { verifyTickets } from "./tickets.integration"; import { verifyWorkProfiles } from "./work-profiles.integration"; +import { verifyWorkstationLifecycle } from "./workstation-lifecycle.integration"; import { verifyWorkstations } from "./workstations.integration"; test("HTTP y MariaDB: configuración única, usuarios, sesiones y protección del administrador", async () => { @@ -1000,6 +1001,12 @@ test("HTTP y MariaDB: configuración única, usuarios, sesiones y protección de } await verifyPersistence(database); await verifyRecordDeletion((path, body) => operator.request(path, body), database); + await verifyWorkstationLifecycle( + (...args) => operator.request(...args), + customer.id, + secondCompany.id, + database + ); const countAfterAccess = await database.query<{ total: number }>( "SELECT COUNT(*) AS total FROM users", { type: QueryTypes.SELECT } @@ -1007,6 +1014,15 @@ test("HTTP y MariaDB: configuración única, usuarios, sesiones y protección de assert.equal(Number(countAfterAccess[0]?.total), 2); assert.equal((await operator.request("/auth/logout", {})).status, 200); + // Sesión anónima con CSRF válido: se prueba autenticación, no rechazo del token caducado. + assert.equal((await operator.session()).user, null); + for (const action of ["decommission", "delete"]) { + await expectError( + await operator.request(`/assets/${customer.id}/workstations/${customer.id}/${action}`, {}), + 401, + "UNAUTHENTICATED" + ); + } await expectError(await operator.request("/access/global"), 401, "UNAUTHENTICATED"); await expectError( await operator.request(`/work-profiles/${customer.id}`), diff --git a/apps/server/src/workstation-lifecycle.integration.ts b/apps/server/src/workstation-lifecycle.integration.ts new file mode 100644 index 0000000..3d6001b --- /dev/null +++ b/apps/server/src/workstation-lifecycle.integration.ts @@ -0,0 +1,209 @@ +import assert from "node:assert/strict"; +import { randomUUID } from "node:crypto"; + +import { + AssetResponseSchema, + InventoryResponseSchema, + WorkstationResponseSchema, +} from "@metis/assets/common"; +import { + CompanyResponseSchema, + TicketListSchema, + TicketResponseSchema, +} from "@metis/companies/common"; +import type { Sequelize } from "sequelize"; +import { QueryTypes } from "sequelize"; +import { z } from "zod/v4"; + +type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise; + +export async function verifyWorkstationLifecycle( + request: Request, + companyId: string, + otherId: string, + database: Sequelize +) { + const path = `/assets/${companyId}/workstations`; + const inventory = InventoryResponseSchema.parse( + await (await request(`/assets/${companyId}`)).json() + ); + const source = inventory.assets[0]; + + assert.ok(source); + const assets = []; + + for (const name of ["Equipo de baja", "Impresora compartida de baja"]) { + const response = await request(`/assets/${companyId}`, { + values: { ...source.values, name, status: "available" }, + }); + + assert.equal(response.status, 201, await response.clone().text()); + assets.push(AssetResponseSchema.parse(await response.json()).asset); + } + const [computer, shared] = assets; + + assert.ok(computer && shared); + const personResponse = await request(`/companies/${companyId}/people`, { + name: "Responsable baja", + position: null, + email: null, + phone: null, + active: true, + isContact: false, + }); + + assert.equal(personResponse.status, 201); + const person = CompanyResponseSchema.parse(await personResponse.json()).company.people.at(-1); + + assert.ok(person); + const values = { + name: "Puesto para baja", + active: true, + siteId: source.values.siteId, + personId: person.id, + computerAssetId: computer.id, + assetIds: [computer.id], + sharedAssetIds: [shared.id], + }; + + const create = async (data = values) => { + const response = await request(path, { values: data }); + + assert.equal(response.status, 201, await response.clone().text()); + + return WorkstationResponseSchema.parse(await response.json()).workstation; + }; + + const station = await create(); + const retirePath = `${path}/${station.id}/decommission`; + const deletePath = `${path}/${station.id}/delete`; + + for (const action of ["decommission", "delete"]) { + assert.equal( + (await request(`/assets/${otherId}/workstations/${station.id}/${action}`, {})).status, + 404 + ); + assert.equal( + (await request(`${path}/${station.id}/${action}`, {}, "https://foreign.example")).status, + 403 + ); + assert.equal( + (await request(`${path}/${station.id}/${action}`, {}, undefined, "a".repeat(64))).status, + 403 + ); + assert.equal( + (await request(`${path}/${station.id}/${action}`, { actorId: person.id })).status, + 400 + ); + } + const response = await request(retirePath, {}); + + assert.equal(response.status, 200); + const retired = WorkstationResponseSchema.parse(await response.json()).workstation; + + assert.equal(retired.values.active, false); + 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( + WorkstationResponseSchema.parse(await (await request(retirePath, {})).json()).workstation, + retired + ); + // Ambas clases de 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: { + subject: "Conservar referencia a puesto", + description: "", + requesterId: null, + assigneeId: null, + state: "open", + priority: "normal", + channel: "internal", + affected: [{ kind: "workstation", targetId: replacement.id }], + }, + requestKey: randomUUID(), + expectedRevisionId: null, + }); + + assert.equal(protectedResponse.status, 201, await protectedResponse.clone().text()); + const ticket = TicketResponseSchema.parse(await protectedResponse.json()).ticket; + const blocked = await request(`${path}/${replacement.id}/delete`, {}); + + assert.equal(blocked.status, 409); + assert.equal( + z.object({ code: z.string() }).parse(await blocked.json()).code, + "WORKSTATION_IN_USE" + ); + const retained = InventoryResponseSchema.parse( + await (await request(`/assets/${companyId}`)).json() + ).workstations.find((item) => item.id === replacement.id); + + assert.deepEqual(retained, replacement); + assert.equal((await request(`${path}/${replacement.id}/decommission`, {})).status, 200); + const retainedTicket = TicketListSchema.parse( + await (await request(`/tickets/${companyId}`)).json() + ).tickets.find((item) => item.id === ticket.id); + + assert.ok(retainedTicket); + assert.deepEqual(retainedTicket.values.affected, ticket.values.affected); + assert.equal((await request(deletePath, {})).status, 200); + assert.equal((await request(deletePath, {})).status, 404); + const after = InventoryResponseSchema.parse(await (await request(`/assets/${companyId}`)).json()); + + assert.equal( + after.workstations.some((item) => item.id === station.id), + false + ); + assert.ok(after.assets.some((item) => item.id === computer.id)); + assert.ok(after.assets.some((item) => item.id === shared.id)); + const rows = await database.query<{ total: number }>( + "SELECT COUNT(*) AS total FROM workstation_revisions WHERE workstationId=:id", + { replacements: { id: station.id }, type: QueryTypes.SELECT } + ); + + assert.equal(Number(rows[0]?.total), 0); + for (const table of ["workstation_assets", "workstation_members"]) { + const remaining = await database.query<{ total: number }>( + `SELECT COUNT(*) AS total FROM ${table} WHERE workstationId=:id`, + { replacements: { id: station.id }, type: QueryTypes.SELECT } + ); + + 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: [] }); + + 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 [retirement, reservation] = await Promise.all([ + request(`${path}/${concurrent.id}/decommission`, {}), + request(path, { values: candidate }), + ]); + + assert.equal(retirement.status, 200); + assert.ok([201, 409].includes(reservation.status)); + const newStation = + reservation.status === 201 + ? WorkstationResponseSchema.parse(await reservation.json()).workstation + : await create(candidate); + + assert.equal((await request(`${path}/${newStation.id}/delete`, {})).status, 200); + assert.equal((await request(`${path}/${concurrent.id}/delete`, {})).status, 200); +} diff --git a/docs/puestos-de-trabajo.md b/docs/puestos-de-trabajo.md index 25b2cc2..e479582 100644 --- a/docs/puestos-de-trabajo.md +++ b/docs/puestos-de-trabajo.md @@ -18,7 +18,7 @@ Persona, sede y activos deben pertenecer a la misma compañía. Una persona debe El responsable del puesto no cambia automáticamente la sede ni el estado de cada activo, ni identifica a todos sus usuarios. Tampoco se crean conexiones entre equipos automáticamente: se registran mediante relaciones entre activos. Los activos retirados pueden permanecer en una composición para revisión. -No se eliminan puestos ni revisiones. Guardar valores normalizados equivalentes no añade una revisión; cambiar solo el orden de selección de activos tampoco. Cambiar nombre, estado, sede, persona o composición registra una instantánea nueva con UUID propio y conserva el UUID del puesto. +La eliminación definitiva se realiza mediante una operación explícita con confirmación; véase Baja y eliminación. Guardar valores normalizados equivalentes no añade una revisión; cambiar solo el orden de selección de activos tampoco. Cambiar nombre, estado, sede, persona o composición registra una instantánea nueva con UUID propio y conserva el UUID del puesto. ## Arquitectura y API @@ -47,3 +47,18 @@ La ficha reúne únicamente perfiles del ordenador principal, incluidos los arch 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 + +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. + +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. + +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. + +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. + +`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. + +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. diff --git a/modules/assets/src/api/domain/workstation.test.ts b/modules/assets/src/api/domain/workstation.test.ts index 60007bb..8671170 100644 --- a/modules/assets/src/api/domain/workstation.test.ts +++ b/modules/assets/src/api/domain/workstation.test.ts @@ -145,3 +145,49 @@ 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" + ); + + assert.ok(created.isSuccess); + const retired = created.data.decommission(actor, "2026-10-13T11:00:00.000Z"); + + assert.ok(retired.isSuccess); + assert.ok(retired.data.id.equals(created.data.id)); + assert.equal(retired.data.history.length, 2); + assert.equal(retired.data.values.active, false); + 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"); + + assert.ok(repeated.isSuccess); + assert.equal(repeated.data, retired.data); + const previouslyInactive = created.data.update( + values({ ...created.data.values.toPrimitive(), active: false }), + actor, + "2026-10-13T10:30:00.000Z" + ); + + assert.ok(previouslyInactive.isSuccess); + const cleared = previouslyInactive.data.decommission(actor, "2026-10-13T11:00:00.000Z"); + + assert.ok(cleared.isSuccess); + assert.deepEqual(cleared.data.values.assetIds, []); +}); diff --git a/modules/assets/src/api/infrastructure/http/workstation.routes.ts b/modules/assets/src/api/infrastructure/http/workstation.routes.ts index fa703b0..5c10e50 100644 --- a/modules/assets/src/api/infrastructure/http/workstation.routes.ts +++ b/modules/assets/src/api/infrastructure/http/workstation.routes.ts @@ -63,33 +63,50 @@ export function registerWorkstationRoutes( for (const action of ["decommission", "delete"] as const) { router.post(`/:companyId/workstations/:id/${action}`, (request, response, next) => { const params = AssetParamsSchema.safeParse(request.params); - const actor = z.object({ userId: z.uuid() }).safeParse("session" in request ? request.session : undefined); + 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)) { + if ( + !( + params.success && + actor.success && + z.strictObject({}).safeParse(request.body).success && + z.strictObject({}).safeParse(request.query).success + ) + ) { fail(response, "INVALID_REQUEST"); return; } if (action === "delete") { - useCases.remove(params.data.companyId, params.data.id).then((result) => { + 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); + + return; + } + useCases + .decommission(params.data.companyId, params.data.id, actor.data.userId) + .then((result) => { if (!result.isSuccess) { fail(response, result.error.code); return; } - response.json({ deleted: true }); - }).catch(next); - - return; - } - useCases.decommission(params.data.companyId, params.data.id, actor.data.userId).then((result) => { - if (!result.isSuccess) { - fail(response, result.error.code); - - return; - } - response.json({ workstation: buildWorkstationResponse(result.data.workstation, result.data.actorNames) }); - }).catch(next); + response.json({ + workstation: buildWorkstationResponse(result.data.workstation, result.data.actorNames), + }); + }) + .catch(next); }); } app.use("/api/assets", authenticate, router); diff --git a/modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts b/modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts index af5a5cb..cb55c05 100644 --- a/modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts +++ b/modules/assets/src/api/infrastructure/persistence/sequelize-asset.repository.ts @@ -203,9 +203,18 @@ export class SequelizeAssetRepository implements IAssetRepository { } } // Las asociaciones de composición se eliminan; sus activos no se borran. - await this.database.query("DELETE FROM workstation_assets WHERE workstationId=:id", { replacements, transaction }); - await this.database.query("DELETE FROM workstation_revisions WHERE workstationId=:id", { replacements, transaction }); - await this.database.query("DELETE FROM workstations WHERE companyId=:companyId AND id=:id", { replacements, transaction }); + await this.database.query("DELETE FROM workstation_assets WHERE workstationId=:id", { + replacements, + transaction, + }); + await this.database.query("DELETE FROM workstation_revisions WHERE workstationId=:id", { + replacements, + transaction, + }); + await this.database.query("DELETE FROM workstations WHERE companyId=:companyId AND id=:id", { + replacements, + transaction, + }); return Result.ok(undefined); }); diff --git a/modules/assets/src/web/index.ts b/modules/assets/src/web/index.ts index b1597e0..d45c04c 100644 --- a/modules/assets/src/web/index.ts +++ b/modules/assets/src/web/index.ts @@ -7,4 +7,5 @@ export { inventoryApi } from "./shared/api"; export { inventoryKeys } from "./shared/queries"; export { AssetSupport } from "./support-connections"; export { WorkstationEditorDialog, WorkstationForm, WorkstationsPage } from "./workstations"; +export { WorkstationActions } from "./workstations/shared/ui/workstation-actions"; export { WorkstationLifecycleDialog } from "./workstations/shared/ui/workstation-lifecycle-dialog"; 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 e774f4c..baea369 100644 --- a/modules/assets/src/web/workstations/list/ui/workstations-page.tsx +++ b/modules/assets/src/web/workstations/list/ui/workstations-page.tsx @@ -10,10 +10,10 @@ import { Button } from "@repo/shadcn-ui/components/button"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { WorkstationLifecycleDialog } from "../../shared/ui/workstation-lifecycle-dialog"; import { InventoryFeedback } from "../../../shared"; import { WorkstationHistoryDialog } from "../../history"; import { WorkstationEditorDialog } from "../../shared"; +import { WorkstationLifecycleDialog } from "../../shared/ui/workstation-lifecycle-dialog"; import { useWorkstationsPageController } from "../controllers"; import type { WorkstationsPageProps } from "../types"; @@ -140,8 +140,22 @@ export function WorkstationsPage(props: WorkstationsPageProps) { {t("workstationHistory")} 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" })}>{t("workstationLifecycle.decommission.title")}} - setOperation({ id: item.id, action: "delete" })}>{t("workstationLifecycle.delete.title")} + {(item.values.active || + item.values.assetIds.length > 0 || + item.values.sharedAssetIds.length > 0 || + item.values.personId) && ( + setOperation({ id: item.id, action: "decommission" })} + > + {t("workstationLifecycle.decommission.title")} + + )} + setOperation({ id: item.id, action: "delete" })} + > + {t("workstationLifecycle.delete.title")} + {" "} ), @@ -156,7 +170,13 @@ export function WorkstationsPage(props: WorkstationsPageProps) { title={t("workstations")} /> )} - {targeted && operation && setOperation(undefined)} />} + {targeted && operation && ( + setOperation(undefined)} + workstation={targeted} + /> + )} {editor && ( void; + onDeleted: () => void; + onDecommissioned: () => void; +}) { + const { t } = useTranslation("assets"); + const [action, setAction] = useState<"decommission" | "delete">(); + const canDecommission = + workstation.values.active || + workstation.values.assetIds.length > 0 || + workstation.values.sharedAssetIds.length > 0 || + Boolean(workstation.values.personId); + + return ( + <> +
+ + {canDecommission && ( + + )} + +
+ {action && ( + setAction(undefined)} + onCompleted={action === "delete" ? onDeleted : onDecommissioned} + workstation={workstation} + /> + )} + + ); +} diff --git a/modules/assets/src/web/workstations/shared/ui/workstation-lifecycle-dialog.tsx b/modules/assets/src/web/workstations/shared/ui/workstation-lifecycle-dialog.tsx index 70f0f26..3b6a73a 100644 --- a/modules/assets/src/web/workstations/shared/ui/workstation-lifecycle-dialog.tsx +++ b/modules/assets/src/web/workstations/shared/ui/workstation-lifecycle-dialog.tsx @@ -10,7 +10,12 @@ import { InventoryApiError } from "../../../shared/api/inventory-api-error"; import { inventoryRequest } from "../../../shared/api/inventory-request"; import { inventoryKeys } from "../../../shared/queries"; -export function WorkstationLifecycleDialog({ workstation, action, onClose, onCompleted }: { +export function WorkstationLifecycleDialog({ + workstation, + action, + onClose, + onCompleted, +}: { workstation: WorkstationDTO; action: "decommission" | "delete"; onClose: () => void; @@ -21,31 +26,44 @@ export function WorkstationLifecycleDialog({ workstation, action, onClose, onCom const [errorCode, setErrorCode] = useState(); const prefix = `workstationLifecycle.${action}`; - return { if (!open) { onClose(); } }} - onConfirm={async () => { - setErrorCode(undefined); - try { - const body = await inventoryRequest(`${workstation.companyId}/workstations/${workstation.id}/${action}`, {}); + return ( + { + setErrorCode(undefined); + try { + const body = await inventoryRequest( + `${workstation.companyId}/workstations/${workstation.id}/${action}`, + {} + ); - if (action === "delete") { - z.strictObject({ deleted: z.literal(true) }).parse(body); - } else { - WorkstationResponseSchema.parse(body); + if (action === "delete") { + z.strictObject({ deleted: z.literal(true) }).parse(body); + } else { + WorkstationResponseSchema.parse(body); + } + await client.invalidateQueries({ + queryKey: inventoryKeys.company(workstation.companyId), + }); + onCompleted?.(); + } catch (error) { + setErrorCode(error instanceof InventoryApiError ? error.code : "INTERNAL_ERROR"); + throw error; } - await client.invalidateQueries({ queryKey: inventoryKeys.company(workstation.companyId) }); - onCompleted?.(); - } catch (error) { - setErrorCode(error instanceof InventoryApiError ? error.code : "INTERNAL_ERROR"); - throw error; - } - }} - />; + }} + onOpenChange={(open) => { + if (!open) { + onClose(); + } + }} + open + pendingLabel={t(`${prefix}.pending`)} + title={t(`${prefix}.title`)} + /> + ); } diff --git a/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx b/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx index 7ff1213..1ef9c72 100644 --- a/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx +++ b/modules/companies/src/web/workstations/detail/ui/workstation-detail-page.tsx @@ -1,10 +1,9 @@ -import { WorkstationEditorDialog, WorkstationLifecycleDialog } from "@metis/assets/client"; +import { WorkstationActions, WorkstationEditorDialog } from "@metis/assets/client"; import { BrowserLink, navigateTo } from "@metis/core/client"; import { EntityStatus } from "@repo/rdx-ui/components/entity-list"; import { BriefcaseIcon, ChevronLeftIcon, UsersIcon } from "@repo/rdx-ui/lib/icons"; import { Button } from "@repo/shadcn-ui/components/button"; import type { ComponentProps } from "react"; -import { useState } from "react"; import { useTranslation } from "react-i18next"; import type { CompanyDTO } from "../../../../common"; @@ -25,7 +24,6 @@ export function WorkstationDetailPage({ lookups: ComponentProps["lookups"]; }) { const { t } = useTranslation("assets"); - const [operation, setOperation] = useState<"decommission" | "delete">(); const { t: profileTranslation } = useTranslation("companies"); const { creatingProfile, @@ -98,11 +96,14 @@ export function WorkstationDetailPage({

{t("workstationDetail.subtitle")}

-
- - {(workstation.values.active || workstation.values.assetIds.length > 0 || workstation.values.sharedAssetIds.length > 0 || workstation.values.personId) && } - -
+ { + profiles.refetch().catch(() => undefined); + }} + onDeleted={() => navigateTo(`${root}/workstations`, { replace: true })} + onEdit={() => setEditing(true)} + workstation={workstation} + />
@@ -230,15 +231,6 @@ export function WorkstationDetailPage({
- {operation && setOperation(undefined)} - onCompleted={() => { - if (operation === "delete") { navigateTo(`${root}/workstations`, { replace: true }); } - else { profiles.refetch().catch(() => undefined); } - }} - />} {creatingProfile && (