Refactorización

This commit is contained in:
David Arranz 2026-10-08 16:43:34 +02:00
parent 6bb9845c8c
commit 2a9d810d31
329 changed files with 5194 additions and 2842 deletions

View File

@ -2,8 +2,8 @@ import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common"; import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import type { ApplicationConfigurationInput } from "@metis/companies/common";
import { import {
type ApplicationConfigurationInput,
ApplicationConfigurationResponseSchema, ApplicationConfigurationResponseSchema,
ApplicationConfigurationsResponseSchema, ApplicationConfigurationsResponseSchema,
CompanyResponseSchema, CompanyResponseSchema,

View File

@ -7,7 +7,8 @@ import {
BrandsResponseSchema, BrandsResponseSchema,
CatalogResponseSchema, CatalogResponseSchema,
} from "@metis/assets/common"; } from "@metis/assets/common";
import { QueryTypes, type Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { import {

View File

@ -2,8 +2,8 @@ import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common"; import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import type { CertificateConfigurationInput } from "@metis/companies/common";
import { import {
type CertificateConfigurationInput,
CertificateConfigurationResponseSchema, CertificateConfigurationResponseSchema,
CertificateConfigurationsResponseSchema, CertificateConfigurationsResponseSchema,
CompanyResponseSchema, CompanyResponseSchema,

View File

@ -1,8 +1,8 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { InterventionTemplateDTO } from "@metis/companies/common";
import { import {
type InterventionTemplateDTO,
InterventionTemplateResponseSchema, InterventionTemplateResponseSchema,
InterventionTemplatesSchema, InterventionTemplatesSchema,
} from "@metis/companies/common"; } from "@metis/companies/common";

View File

@ -1,9 +1,8 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { InterventionDTO, InterventionInput } from "@metis/companies/common";
import { import {
type InterventionDTO,
type InterventionInput,
InterventionListSchema, InterventionListSchema,
InterventionLookupsSchema, InterventionLookupsSchema,
InterventionResponseSchema, InterventionResponseSchema,

View File

@ -2,18 +2,21 @@ import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { InventoryResponseSchema } from "@metis/assets/common"; import { InventoryResponseSchema } from "@metis/assets/common";
import type {
MailConfigurationInput,
MailboxInput,
MailboxUseInput,
} from "@metis/companies/common";
import { import {
AccessResponseSchema, AccessResponseSchema,
CompanyResponseSchema, CompanyResponseSchema,
type MailConfigurationInput,
MailConfigurationResponseSchema, MailConfigurationResponseSchema,
MailConfigurationsResponseSchema, MailConfigurationsResponseSchema,
type MailboxInput,
type MailboxUseInput,
MailboxesResponseSchema, MailboxesResponseSchema,
WorkProfileResponseSchema, WorkProfileResponseSchema,
} from "@metis/companies/common"; } from "@metis/companies/common";
import { QueryTypes, type Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
import { z } from "zod/v4"; import { z } from "zod/v4";
type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>; type Request = (path: string, body?: unknown, origin?: string, token?: string) => Promise<Response>;

View File

@ -2,17 +2,16 @@ import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common"; import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import type { ProcessTemplateInput, UpdateProcessInput } from "@metis/companies/common";
import { import {
ApplicationConfigurationResponseSchema, ApplicationConfigurationResponseSchema,
CompanyResponseSchema, CompanyResponseSchema,
ProcessResponseSchema, ProcessResponseSchema,
ProcessReviewResponseSchema, ProcessReviewResponseSchema,
type ProcessTemplateInput,
ProcessTemplateResponseSchema, ProcessTemplateResponseSchema,
ProcessTemplatesResponseSchema, ProcessTemplatesResponseSchema,
ProcessesResponseSchema, ProcessesResponseSchema,
RemoteConnectionResponseSchema, RemoteConnectionResponseSchema,
type UpdateProcessInput,
WorkProfileResponseSchema, WorkProfileResponseSchema,
} from "@metis/companies/common"; } from "@metis/companies/common";
import { InventoryResponseSchema as ResourcesSchema } from "@metis/resources/common"; import { InventoryResponseSchema as ResourcesSchema } from "@metis/resources/common";

View File

@ -2,9 +2,9 @@ import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common"; import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import type { RemoteConnectionInput } from "@metis/companies/common";
import { import {
CompanyResponseSchema, CompanyResponseSchema,
type RemoteConnectionInput,
RemoteConnectionResponseSchema, RemoteConnectionResponseSchema,
RemoteConnectionsResponseSchema, RemoteConnectionsResponseSchema,
TeleworkAuthorizationResponseSchema, TeleworkAuthorizationResponseSchema,

View File

@ -4,7 +4,8 @@ import { AccessResponseSchema, MailboxesResponseSchema } from "@metis/companies/
import type { ResourceDTO } from "@metis/resources/common"; import type { ResourceDTO } from "@metis/resources/common";
import { InventoryResponseSchema, ResourceResponseSchema } from "@metis/resources/common"; import { InventoryResponseSchema, ResourceResponseSchema } from "@metis/resources/common";
import { createResourceModule } from "@metis/resources/module"; import { createResourceModule } from "@metis/resources/module";
import { QueryTypes, type Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
import { z } from "zod/v4"; import { z } from "zod/v4";
type Request = (path: string, body?: unknown) => Promise<Response>; type Request = (path: string, body?: unknown) => Promise<Response>;

View File

@ -1,9 +1,9 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import type { TeleworkAuthorizationInput } from "@metis/companies/common";
import { import {
CompanyResponseSchema, CompanyResponseSchema,
type TeleworkAuthorizationInput,
TeleworkAuthorizationResponseSchema, TeleworkAuthorizationResponseSchema,
TeleworkAuthorizationsResponseSchema, TeleworkAuthorizationsResponseSchema,
} from "@metis/companies/common"; } from "@metis/companies/common";

View File

@ -2,10 +2,9 @@ import assert from "node:assert/strict";
import { randomUUID } from "node:crypto"; import { randomUUID } from "node:crypto";
import { InventoryResponseSchema } from "@metis/assets/common"; import { InventoryResponseSchema } from "@metis/assets/common";
import type { TicketDTO, TicketInput } from "@metis/companies/common";
import { import {
CompanyResponseSchema, CompanyResponseSchema,
type TicketDTO,
type TicketInput,
TicketListSchema, TicketListSchema,
TicketLookupsSchema, TicketLookupsSchema,
TicketResponseSchema, TicketResponseSchema,

View File

@ -1,8 +1,8 @@
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { InventoryResponseSchema } from "@metis/assets/common"; import { InventoryResponseSchema } from "@metis/assets/common";
import type { AccessFieldsDTO } from "@metis/companies/common";
import { import {
type AccessFieldsDTO,
AccessResponseSchema, AccessResponseSchema,
CompanyResponseSchema, CompanyResponseSchema,
WorkProfileResponseSchema, WorkProfileResponseSchema,

View File

@ -203,3 +203,66 @@ test("los enlaces del dashboard mantienen la compañía y la ficha exacta", () =
entityId: "entry", entityId: "entry",
}); });
}); });
test("los bloques del dashboard presentan datos, avisos y actividad reales en ambos idiomas", async () => {
const session = { user, csrfToken: "a".repeat(64), setupRequired: false };
for (const language of ["es", "en"]) {
const html = await renderPage(language, session, (client) => {
client.setQueryData(companyKeys.list, { companies: [workspaceCompany] });
client.setQueryData(dashboardKeys.block("summary"), {
companies: 1,
tickets: 2,
interventions: 3,
assets: 4,
resources: 5,
});
const entry = {
id: workspaceCompany.id,
companyId: workspaceCompany.id,
companyName: workspaceCompany.name,
name: "Revisar acceso",
kind: "ticket",
reference: "T-2026-001",
state: "open",
priority: "high",
recordedAt: "2026-10-01T10:30:00.000Z",
actorName: "Operador test",
};
client.setQueryData(dashboardKeys.block("work"), { items: [entry], total: 1 });
client.setQueryData(dashboardKeys.block("activity"), { items: [entry], total: 1 });
client.setQueryData(dashboardKeys.block("attention"), {
items: [{ kind: "missing_site", count: 2 }],
});
client.setQueryData(dashboardKeys.block("companies"), {
items: [
{
id: workspaceCompany.id,
name: workspaceCompany.name,
companies: 1,
tickets: 2,
interventions: 3,
assets: 4,
resources: 5,
people: 6,
},
],
total: 1,
});
});
assert.ok(html.includes("Revisar acceso"));
assert.ok(html.includes("T-2026-001"));
assert.ok(html.includes("Operador test"));
assert.ok(html.includes(workspaceCompany.name));
assert.ok(html.includes('dateTime="2026-10-01T10:30:00.000Z"'));
assert.ok(html.includes('data-state="high"'));
assert.ok(html.includes('data-state="open"'));
assert.ok(
html.includes(
`aria-label="${language === "es" ? "Tickets abiertos" : "Open tickets"} · ${workspaceCompany.name}: 2"`
)
);
assert.equal(html.includes("dashboard.issues.missing_site"), false);
assert.equal(html.includes('role="alert"'), false);
assert.equal(html.includes("<form"), false);
}
});

View File

@ -105,11 +105,28 @@ Se declara @tanstack/react-table en el paquete compartido porque es quien consum
## Catálogo de marcas y selector funcional ## Catálogo de marcas y selector funcional
BrandCatalogPage reutiliza EntityListHeader/EntityTable con búsqueda, filtro de estado, columnas, ordenación y paginación. BrandForm reutiliza FormDialog, ContextHelp y FormActions tanto para la gestión global como para las altas relacionadas. BrandCatalogPage reutiliza EntityListHeader/EntityTable con búsqueda, filtro de estado, columnas, ordenación y paginación. BrandEditorDialog compone FormDialog y BrandForm tanto para la gestión global como para las altas relacionadas; el formulario conserva ContextHelp y FormActions.
BrandSelect, dentro del módulo assets, adapta SelectWithAction al catálogo remoto: filtra marcas activas, conserva la selección inactiva actual y textos anteriores, ofrece alta rápida y respeta una sola anidación. El borrador padre se conserva y la nueva marca se selecciona por UUID. El componente genérico no conoce BrandDTO, consultas ni endpoints. Véase [Catálogo global de marcas](catalogo-global-de-marcas.md). BrandSelect, dentro del módulo assets, adapta SelectWithAction al catálogo remoto: filtra marcas activas, conserva la selección inactiva actual y textos anteriores, ofrece alta rápida y respeta una sola anidación. El borrador padre se conserva y la nueva marca se selecciona por UUID. El componente genérico no conoce BrandDTO, consultas ni endpoints. Véase [Catálogo global de marcas](catalogo-global-de-marcas.md).
## Componentes modales de los módulos
El [inventario de modales](inventario-de-modales.md) localiza las interacciones de alta, edición,
copia, confirmación, detalle, historial, selección, ayuda y configuración, incluyendo las altas
rápidas dentro de otros formularios. La regla normativa de organización reside en AGENTS.md.
Las páginas coordinan apertura, selección y callbacks. El componente modal posee su estructura
y la lógica que le pertenece; el formulario mantiene validación, borrador, errores y guardado.
Se reutilizan FormDialog y FormActions sin modificar su implementación vendor. Los diálogos
específicos de entidades viven junto a sus consumidores, no en la biblioteca visual genérica.
CompanyEditorDialog, PersonEditorDialog, DepartmentEditorDialog, SiteEditorDialog,
BrandEditorDialog y SupportToolEditorDialog se reutilizan donde ya existen operaciones
equivalentes. Los callbacks de guardado actualizan la selección del consumidor; no se cierra
un formulario por el mero inicio de una mutación. Los historiales de consulta se derivan de
los datos remotos actuales, mientras que los borradores de edición permanecen locales.
## Acciones de fila ancladas ## Acciones de fila ancladas
EntityRowActions muestra un botón icon-sm con EllipsisIcon, etiqueta accesible y título traducidos, sin texto visible repetido. EntityTable reserva la identidad de columna actions para operaciones: siempre visible, sin búsqueda/ordenación y fijada a la derecha mediante columnPinning de TanStack Table. EntityTableContent renderiza primero las columnas de datos y después la columna anclada, con cabecera/celdas sticky right-0, alineación derecha, anchura compacta y fondo opaco para evitar solapamientos al desplazar la tabla. Todos los listados consumidores heredan este comportamiento. EntityRowActions muestra un botón icon-sm con EllipsisIcon, etiqueta accesible y título traducidos, sin texto visible repetido. EntityTable reserva la identidad de columna actions para operaciones: siempre visible, sin búsqueda/ordenación y fijada a la derecha mediante columnPinning de TanStack Table. EntityTableContent renderiza primero las columnas de datos y después la columna anclada, con cabecera/celdas sticky right-0, alineación derecha, anchura compacta y fondo opaco para evitar solapamientos al desplazar la tabla. Todos los listados consumidores heredan este comportamiento.

View File

@ -0,0 +1,182 @@
# Inventario de interacciones modales
Revisión de 2026-10-08. Se inspeccionaron las primitivas y wrappers importados, sus consumidores,
los bloques condicionales y las implementaciones manuales posibles: Dialog, AlertDialog, Sheet,
Drawer, FormDialog, portales, elementos dialog, aria-modal y roles dialog/alertdialog.
La búsqueda no dependió del nombre de los componentes.
El código propio contiene **81 componentes que construyen una interacción modal**. Todos
usan FormDialog de @repo/shadcn-ui y tienen un componente principal por fichero. Un componente
puede representar variantes de alta, edición o copia, y catálogos con distintos tipos; el número
no es un recuento de todos los estados posibles de las ventanas. No se encontraron otras
implementaciones propias con AlertDialog, Sheet, Drawer ni diálogos HTML/manuales.
La implementación vendor de packages/shadcn-ui queda fuera de la refactorización según AGENTS.md.
Sus primitivas y el diálogo de configuración de tablas se consultan y consumen, no se duplican
ni reorganizan. Los popovers de selectores y menús de acciones no se contabilizan como formularios
o ventanas modales funcionales.
## Responsabilidades y consumidores
Las páginas y listados conservan apertura, selección y callbacks. Los componentes enumerados
encapsulan título, disposición, anchura, contenido y cierre. Los formularios existentes mantienen
sus schemas, borradores, mutaciones, ayuda y errores. EndRelationshipDialog posee además la
mutación de confirmación y sus estados pendientes y de error.
CompanyEditorDialog, PersonEditorDialog, DepartmentEditorDialog y SiteEditorDialog se reutilizan
desde listados y altas rápidas equivalentes. BrandEditorDialog y SupportToolEditorDialog comparten
el patrón entre catálogo y selector. Los callbacks de selección permanecen en el consumidor que
posee el borrador. Las altas anidadas conservan sus restricciones existentes.
Se integró el contenido exclusivo de los antiguos SupportHistory, ApplicationHistory,
CertificateHistory y PrinterHistory en sus respectivos componentes de diálogo, tras comprobar
sus referencias. No se eliminaron historiales ni acciones. Los archivos anteriores se pueden
recuperar desde Git.
## Componentes por área
### modules/assets/src/web
- [AssetDetailDialog](../modules/assets/src/web/asset-detail-dialog.tsx)
- [AssetEditorDialog](../modules/assets/src/web/asset-editor-dialog.tsx)
- [BrandEditorDialog](../modules/assets/src/web/brand-editor-dialog.tsx)
- [CatalogDefinitionDialog](../modules/assets/src/web/catalog-definition-dialog.tsx)
- [CatalogEditorDialog](../modules/assets/src/web/catalog-editor-dialog.tsx)
- [CatalogFieldCreateDialog](../modules/assets/src/web/catalog-field-create-dialog.tsx)
- [CatalogRelationCreateDialog](../modules/assets/src/web/catalog-relation-create-dialog.tsx)
- [CatalogTemplateCreateDialog](../modules/assets/src/web/catalog-template-create-dialog.tsx)
- [EndRelationshipDialog](../modules/assets/src/web/end-relationship-dialog.tsx)
- [RelationshipEditorDialog](../modules/assets/src/web/relationship-editor-dialog.tsx)
- [SupportConnectionEditorDialog](../modules/assets/src/web/support-connection-editor-dialog.tsx)
- [SupportHistoryDialog](../modules/assets/src/web/support-history-dialog.tsx)
- [SupportToolEditorDialog](../modules/assets/src/web/support-tool-editor-dialog.tsx)
- [SupportToolsDialog](../modules/assets/src/web/support-tools-dialog.tsx)
- [WorkstationAssetCreateDialog](../modules/assets/src/web/workstation-asset-create-dialog.tsx)
- [WorkstationEditorDialog](../modules/assets/src/web/workstation-editor-dialog.tsx)
- [WorkstationHistoryDialog](../modules/assets/src/web/workstation-history-dialog.tsx)
### modules/companies/src/web/access
- [AccessEditorDialog](../modules/companies/src/web/access/access-editor-dialog.tsx)
- [AccessHelpDialog](../modules/companies/src/web/access/access-help-dialog.tsx)
- [AccessHistory](../modules/companies/src/web/access/access-history.tsx)
- [AccessQuickEditor](../modules/companies/src/web/access/access-quick-editor.tsx)
- [QuickCreateDialog](../modules/companies/src/web/access/quick-create-dialog.tsx)
### modules/companies/src/web/applications
- [ApplicationConfigurationEditorDialog](../modules/companies/src/web/applications/application-configuration-editor-dialog.tsx)
- [ApplicationHistoryDialog](../modules/companies/src/web/applications/application-history-dialog.tsx)
- [ApplicationQuickEditor](../modules/companies/src/web/applications/application-quick-editor.tsx)
### modules/companies/src/web/certificates
- [CertificateConfigurationEditorDialog](../modules/companies/src/web/certificates/certificate-configuration-editor-dialog.tsx)
- [CertificateHistoryDialog](../modules/companies/src/web/certificates/certificate-history-dialog.tsx)
- [CertificateQuickEditor](../modules/companies/src/web/certificates/certificate-quick-editor.tsx)
### modules/companies/src/web/components
- [AssignmentEditorDialog](../modules/companies/src/web/components/assignment-editor-dialog.tsx)
- [AssignmentHistoryDialog](../modules/companies/src/web/components/assignment-history-dialog.tsx)
- [CompanyEditorDialog](../modules/companies/src/web/components/company-editor-dialog.tsx)
- [ConfigurationQuickEditor](../modules/companies/src/web/components/configuration-quick-editor.tsx)
- [DashboardDetailsDialog](../modules/companies/src/web/components/dashboard-details-dialog.tsx)
- [DashboardQuickCreate](../modules/companies/src/web/components/dashboard-quick-create.tsx)
- [DepartmentEditorDialog](../modules/companies/src/web/components/department-editor-dialog.tsx)
- [InventoryQuickCreateDialog](../modules/companies/src/web/components/inventory-quick-create-dialog.tsx)
- [PersonEditorDialog](../modules/companies/src/web/components/person-editor-dialog.tsx)
- [SiteEditorDialog](../modules/companies/src/web/components/site-editor-dialog.tsx)
- [WorkspaceSettingsDialog](../modules/companies/src/web/components/workspace-settings-dialog.tsx)
### modules/companies/src/web/folders
- [FolderMappingEditorDialog](../modules/companies/src/web/folders/folder-mapping-editor-dialog.tsx)
- [FolderMappingHistoryDialog](../modules/companies/src/web/folders/folder-mapping-history-dialog.tsx)
- [FolderQuickEditor](../modules/companies/src/web/folders/folder-quick-editor.tsx)
### modules/companies/src/web/intervention-templates
- [InterventionTemplateEditorDialog](../modules/companies/src/web/intervention-templates/intervention-template-editor-dialog.tsx)
### modules/companies/src/web/interventions
- [InterventionDetailDialog](../modules/companies/src/web/interventions/intervention-detail-dialog.tsx)
- [InterventionEditorDialog](../modules/companies/src/web/interventions/intervention-editor-dialog.tsx)
- [InterventionTemplateCreateDialog](../modules/companies/src/web/interventions/intervention-template-create-dialog.tsx)
- [InterventionTicketCreateDialog](../modules/companies/src/web/interventions/intervention-ticket-create-dialog.tsx)
### modules/companies/src/web/mail
- [MailboxCreateDialog](../modules/companies/src/web/mail/mailbox-create-dialog.tsx)
- [MailboxUseEditorDialog](../modules/companies/src/web/mail/mailbox-use-editor-dialog.tsx)
- [MailConfigurationEditorDialog](../modules/companies/src/web/mail/mail-configuration-editor-dialog.tsx)
- [MailConfigurationHistoryDialog](../modules/companies/src/web/mail/mail-configuration-history-dialog.tsx)
- [MailQuickEditor](../modules/companies/src/web/mail/mail-quick-editor.tsx)
### modules/companies/src/web/printers
- [PrinterConfigurationEditorDialog](../modules/companies/src/web/printers/printer-configuration-editor-dialog.tsx)
- [PrinterHistoryDialog](../modules/companies/src/web/printers/printer-history-dialog.tsx)
- [PrinterProfileCreateDialog](../modules/companies/src/web/printers/printer-profile-create-dialog.tsx)
### modules/companies/src/web/processes
- [ProcessEditDialog](../modules/companies/src/web/processes/process-edit-dialog.tsx)
- [ProcessQuickCreateDialog](../modules/companies/src/web/processes/process-quick-create-dialog.tsx)
- [ProcessStartDialog](../modules/companies/src/web/processes/process-start-dialog.tsx)
- [ProcessTemplateEditorDialog](../modules/companies/src/web/processes/process-template-editor-dialog.tsx)
### modules/companies/src/web/profiles
- [WorkProfileEditorDialog](../modules/companies/src/web/profiles/work-profile-editor-dialog.tsx)
- [WorkProfileHistoryDialog](../modules/companies/src/web/profiles/work-profile-history-dialog.tsx)
### modules/companies/src/web/remote
- [RemoteConnectionEditorDialog](../modules/companies/src/web/remote/remote-connection-editor-dialog.tsx)
- [RemoteConnectionHistoryDialog](../modules/companies/src/web/remote/remote-connection-history-dialog.tsx)
- [RemoteQuickEditor](../modules/companies/src/web/remote/remote-quick-editor.tsx)
### modules/companies/src/web/telework
- [TeleworkAuthorizationEditorDialog](../modules/companies/src/web/telework/telework-authorization-editor-dialog.tsx)
- [TeleworkAuthorizationHistoryDialog](../modules/companies/src/web/telework/telework-authorization-history-dialog.tsx)
### modules/companies/src/web/tickets
- [TicketDetailDialog](../modules/companies/src/web/tickets/ticket-detail-dialog.tsx)
- [TicketEditorDialog](../modules/companies/src/web/tickets/ticket-editor-dialog.tsx)
- [TicketInterventionDialog](../modules/companies/src/web/tickets/ticket-intervention-dialog.tsx)
- [TicketQuickEditor](../modules/companies/src/web/tickets/ticket-quick-editor.tsx)
- [TicketQuickForms](../modules/companies/src/web/tickets/ticket-quick-forms.tsx)
- [TicketTargetCreateDialog](../modules/companies/src/web/tickets/ticket-target-create-dialog.tsx)
### modules/identity/src/web/components
- [AccountPasswordDialog](../modules/identity/src/web/components/account-password-dialog.tsx)
- [UserEditorDialog](../modules/identity/src/web/components/user-editor-dialog.tsx)
- [UserManagementDialog](../modules/identity/src/web/components/user-management-dialog.tsx)
### modules/resources/src/web
- [CatalogDefinitionDialog](../modules/resources/src/web/catalog-definition-dialog.tsx)
- [CatalogEditorDialog](../modules/resources/src/web/catalog-editor-dialog.tsx)
- [CatalogFieldCreateDialog](../modules/resources/src/web/catalog-field-create-dialog.tsx)
- [CatalogTemplateCreateDialog](../modules/resources/src/web/catalog-template-create-dialog.tsx)
- [ResourceDetailDialog](../modules/resources/src/web/resource-detail-dialog.tsx)
- [ResourceEditorDialog](../modules/resources/src/web/resource-editor-dialog.tsx)
## Comprobaciones y límites
Typecheck, lint, pruebas de los módulos y validaciones globales correctos. Las pruebas existentes
comprueban traducciones es/en, listados sin formularios visibles, ayuda, restricciones de altas
anidadas, estados y contratos. Se amplió la prueba del dashboard con datos no vacíos de los
paneles extraídos.
No se ejecutó una sesión interactiva de navegador. Queda por comprobar manualmente apertura,
Escape, restauración del foco, cancelación, envío y error en modales simples y anidados, en español,
inglés y anchura móvil. El render estático y el lint no sustituyen esa comprobación.
Véase [revisión de buenas prácticas](revision-de-buenas-practicas.md) para el alcance y los resultados
globales, y [componentes de interfaz](componentes-de-interfaz.md) para los patrones compartidos.

View File

@ -0,0 +1,149 @@
# Revisión de buenas prácticas
Revisión realizada el 2026-10-08 conforme a AGENTS.md y a los documentos de arquitectura
de capas, composición, flujo HTTP, contratos, pruebas y modelo DDD e identificadores.
Se consultaron también el patrón de listados y los componentes de interfaz.
## Alcance y método
Se inventariaron estructura, dependencias, configuración y patrones antes de modificar código.
La revisión combinó inspección estructural de todos los archivos TypeScript propios con lectura
por áreas, fronteras y patrones repetidos, comprobaciones de referencias y pruebas dirigidas.
No equivale a una certificación de seguridad ni a una comprobación manual de cada interacción.
El inventario final contiene 756 archivos TypeScript/TSX propios, incluidas pruebas:
| Área | Archivos | Responsabilidades inspeccionadas |
| --- | ---: | --- |
| apps/server | 31 | Arranque, configuración, cierre, HTTP, logging e integraciones |
| apps/web | 8 | Composición React, idioma, proveedores, dashboard integrado y pruebas |
| modules/core | 11 | Contratos de salud, rutas, acceso HTTP y pantalla técnica |
| modules/identity | 38 | Usuarios, sesiones, autenticación, contraseñas y formularios |
| modules/companies | 460 | Compañías, personas, departamentos, sedes, accesos, perfiles, correo, carpetas, aplicaciones, certificados, impresoras, teletrabajo, remotos, procesos, tickets, intervenciones, plantillas y dashboard |
| modules/assets | 136 | Catálogos, marcas, inventario, relaciones, puestos y asistencia |
| modules/resources | 63 | Catálogos, inventario, características e integraciones |
| packages/rdx-ddd y rdx-utils | 9 | Identidad, entidades, agregados, Value Objects, Result y Maybe |
Se inspeccionaron además scripts, configuración TypeScript/Biome/PNPM, composition roots y
exports para entender sus contratos. No se alteraron dependencias, APIs públicas, schemas HTTP,
lógica de migraciones ni persistencia como parte de esta refactorización. Los errores de entrada HTTP
malformada sí se corrigieron en el middleware común.
La implementación de packages/shadcn-ui está excluida de cambios por AGENTS.md. Sus APIs se
consultaron para consumir los patrones existentes. Se preservó el cambio local del usuario en
packages/shadcn-ui/src/styles/globals.css. No se imprimieron ni versionaron archivos .env.
## Orden de trabajo
1. Inspección global y lectura normativa, con clasificación de patrones y riesgos.
2. Componentes propios compartidos, composición de identidad y formularios de accesos.
3. Catálogos e inventarios de activos y recursos; asistencia y relaciones.
4. Compañías, perfiles, configuraciones, procesos, tickets e intervenciones.
5. Fronteras HTTP, contraseñas, datos persistidos, logging y ciclo de vida del servidor.
6. Inventario completo y separación de modales, incluidos wrappers y altas anidadas.
7. Paneles del dashboard, navegación lateral y búsqueda global.
8. Convenciones de imports de tipos, revisión del diff y validaciones globales.
Los primeros cambios ya quedaron incorporados en el commit 7f336df durante la revisión.
La extracción de modales, las últimas convenciones y la documentación se entregan en el
working tree posterior a 6bb9845; no se ha creado otro commit automáticamente.
## Incumplimientos corregidos
- Varios componentes significativos en un mismo TSX: se separaron responsabilidades reales,
sin convertir fragmentos triviales de JSX en componentes artificiales.
- Formularios, historiales y confirmaciones implementados dentro de listados: ahora viven
en componentes modales propios. El [inventario](inventario-de-modales.md) enumera los 81
componentes que construyen una interacción modal y sus ficheros, incluidas variantes
ya independientes antes de esta revisión.
- Estructuras modales repetidas para una misma entidad: se reutilizan diálogos de compañía,
persona, departamento, sede, marca y herramienta de asistencia entre sus consumidores.
- Copias locales de datos de consulta usados solo para visualizar: se conservan identificadores
y se deriva el dato actual desde TanStack Query. Los borradores de edición deliberados siguen
siendo locales. No se agregaron efectos ni memoización preventiva.
- Dashboard y shell con responsabilidades mezcladas: paneles independientes para trabajo,
avisos, actividad, compañías e indicadores; búsqueda local en WorkspaceHeader y navegación
en WorkspaceSidebar. Se mantienen query keys, cancelación de consultas y navegación.
- Imports de valores y tipos mezclados: se separaron 237 declaraciones mixtas en import type
y los imports de runtime correspondientes. Una comparación de tokens y bindings verificó
que 193 ficheros de esta última fase solo cambian imports, no lógica ni imports de runtime.
- Cast de JSON persistido: se usa unknown antes de la validación del dashboard.
- Promesa de envío en AccessForm: se utiliza la mutación con gestión explícita del resultado
y del error, sin descartar una promesa rechazada de mutateAsync.
- Código sin consumidores y variables no utilizadas: retirada tras comprobar referencias.
- Logging SQL habilitado: desactivado para no emitir parámetros sensibles de sesiones o usuarios.
- Cuerpos HTTP inválidos tratados como errores internos: clasificación de errores del parser
con respuestas seguras 400/413/415; errores inesperados conservan respuesta genérica 500.
- Hash scrypt con segmentos sobrantes aceptado: rechazo explícito y prueba de regresión.
- Cierre del servidor repetible y errores de cierre: protección contra señales duplicadas,
observación del cierre y conservación del error original cuando falla la limpieza.
Se conservaron los encadenamientos con catch(next) en Express 4: convertirlos a handlers async
sin propagación explícita habría introducido una regresión. Los schemas de dominio existentes
y sus Value Objects no se sustituyeron por DTOs de transporte ni casts. El ORM permanece en
Infrastructure; las referencias transversales de pruebas no son dependencias del dominio.
El análisis final del código propio no encontró any, non-null assertions, casts distintos de
as const, import("...").Tipo, imports mixtos pendientes ni ficheros de producción con varios
componentes principales. Esto describe las comprobaciones estructurales ejecutadas, no una
garantía de ausencia de cualquier defecto posible.
## Archivos especialmente modificados
- modules/companies/src/web/components/companies-workspace.tsx, dashboard-page.tsx y sus
componentes de navegación, búsqueda y paneles.
- modules/companies/src/web/access/access-form.tsx, access-form-fields.tsx,
access-quick-editor.tsx y access-workspace.tsx.
- Listados y formularios de companies, assets y resources: diálogos editor/detail/history/create
próximos a cada funcionalidad, enumerados en el inventario de modales.
- modules/identity/src/web/components: composición del espacio, gestión de usuarios y
formularios de contraseña con FormActions.
- apps/server/src/app.ts, index.ts y config/database.ts.
- modules/identity/src/api/infrastructure/security/scrypt-password-hasher.ts.
- modules/companies/src/api/infrastructure/persistence/sequelize-dashboard.repository.ts.
- apps/server/src/app.test.ts, scrypt-password-hasher.test.ts y apps/web/src/app.test.tsx.
Se integró el contenido exclusivo de los antiguos support-history.tsx, application-history.tsx,
certificate-history.tsx y printer-history.tsx en sus respectivos componentes *-history-dialog.tsx.
No se eliminó la funcionalidad; las versiones anteriores son recuperables desde Git.
## Validaciones ejecutadas
Entorno comprobado: WSL/bash, Node.js v24.20.0 y PNPM 11.25.0, conforme a package.json.
| Comando | Resultado |
| --- | --- |
| pnpm --filter @metis/companies typecheck y test | Correctos |
| pnpm --filter @metis/assets typecheck y test | Correctos |
| pnpm --filter @metis/resources typecheck y test | Correctos |
| pnpm --filter @metis/identity typecheck y test | Correctos |
| pnpm --filter @metis/web test | Correcto; se amplió la cobertura del dashboard con datos reales en es/en |
| pnpm --filter @metis/server test | Correcto; HTTP real e integración MariaDB en la base exclusiva de pruebas |
| pnpm typecheck | Correcto: 10 tareas; última ejecución con 9 resultados de caché y web recompilado |
| pnpm lint | Correcto, sin diagnósticos |
| pnpm test | Correcto: 9 tareas sin caché; incluye HTTP real e integración MariaDB |
| pnpm check | Correcto, sin diagnósticos |
| pnpm build | Correcto: servidor y web; primera ejecución compiló, repetición usó caché |
| git diff --check | Correcto |
Se aplicaron comprobaciones Biome dirigidas tras cada grupo de cambios. El sandbox no permite
abrir sockets para las pruebas HTTP; esas pruebas se ejecutaron con permiso ampliado, utilizando
la base metis_desk_test que el propio runner configura y verifica. No se modificó una base de
producción. Los errores intermedios de tipos introducidos durante las extracciones se corrigieron
antes de las validaciones finales.
## Pendientes y revisión humana
1. **Prueba interactiva de modales y navegación.** Área: componentes enumerados en el inventario.
Las pruebas de render estático y lint no verifican foco, Escape, portales ni interacción real.
No se ejecutó navegador ni E2E interactivo. Propuesta: comprobar alta/edición/cancelación,
envío pendiente, error y selección anidada en es/en y anchura móvil. No requiere cambiar
contratos ni introducir una librería de pruebas automáticamente.
2. **Tamaño del bundle web.** Área: apps/web/vite.config.ts y composición de los módulos web.
Vite advierte de un chunk JS de aproximadamente 1,20 MB minificado (330 kB gzip), por encima
de 500 kB. La compilación termina correctamente. No se añadió code splitting sin medir la
carga y decidir fronteras de carga diferida y fallbacks. Propuesta: evaluar el perfil de carga
y acordar después qué espacios cargar de manera diferida; no silenciar el aviso aumentando
el límite.
No se detectó un bloqueo que requiriera cambiar contratos, modelo de datos o arquitectura.

View File

@ -1,12 +1,8 @@
import { Name } from "@repo/rdx-ddd"; import { Name } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { import type { BrandInput, BrandProps } from "../domain/brand.aggregate";
BrandError, import { BrandError, BrandInputSchema } from "../domain/brand.aggregate";
type BrandInput,
BrandInputSchema,
type BrandProps,
} from "../domain/brand.aggregate";
export function mapBrandInput(input: BrandInput, builtin = false): Result<BrandProps, BrandError> { export function mapBrandInput(input: BrandInput, builtin = false): Result<BrandProps, BrandError> {
const parsed = BrandInputSchema.safeParse(input); const parsed = BrandInputSchema.safeParse(input);
if (!parsed.success) { if (!parsed.success) {

View File

@ -1,7 +1,8 @@
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { Brand, BrandError, type BrandInput } from "../domain/brand.aggregate"; import type { BrandInput } from "../domain/brand.aggregate";
import { Brand, BrandError } from "../domain/brand.aggregate";
import type { IBrandRepository } from "./brand.repository"; import type { IBrandRepository } from "./brand.repository";
import { mapBrandInput } from "./brand-input.mapper"; import { mapBrandInput } from "./brand-input.mapper";

View File

@ -1,7 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { SupportConnection } from "../domain/support-connection.aggregate"; import type { SupportConnection } from "../domain/support-connection.aggregate";
import { SupportError, type SupportInput } from "../domain/support-values"; import type { SupportInput } from "../domain/support-values";
import { SupportError } from "../domain/support-values";
import { SupportValues } from "../domain/value-objects/support-values"; import { SupportValues } from "../domain/value-objects/support-values";
import type { SupportContext } from "./support.repository"; import type { SupportContext } from "./support.repository";

View File

@ -3,7 +3,8 @@ import { Result } from "@repo/rdx-utils";
import { SupportConnection } from "../domain/support-connection.aggregate"; import { SupportConnection } from "../domain/support-connection.aggregate";
import { SupportTool } from "../domain/support-tool.aggregate"; import { SupportTool } from "../domain/support-tool.aggregate";
import { SupportError, type SupportInput, type SupportToolInput } from "../domain/support-values"; import type { SupportInput, SupportToolInput } from "../domain/support-values";
import { SupportError } from "../domain/support-values";
import type { ISupportRepository } from "./support.repository"; import type { ISupportRepository } from "./support.repository";
import { mapSupportInput } from "./support-input.mapper"; import { mapSupportInput } from "./support-input.mapper";

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { AssetCatalogError } from "./asset-definition"; import { AssetCatalogError } from "./asset-definition";

View File

@ -1,4 +1,5 @@
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { DomainEntity } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { AssetCatalogError } from "./asset-definition"; import { AssetCatalogError } from "./asset-definition";

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";

View File

@ -8,10 +8,8 @@ import { AssetCatalog } from "./asset-catalog.aggregate";
import { seedAssetCatalog } from "./asset-catalog-seed"; import { seedAssetCatalog } from "./asset-catalog-seed";
import { AssetRelationship } from "./asset-relationship.aggregate"; import { AssetRelationship } from "./asset-relationship.aggregate";
import { AssetDefinitionValues } from "./value-objects/asset-definition-values"; import { AssetDefinitionValues } from "./value-objects/asset-definition-values";
import { import type { RelationshipValuesData } from "./value-objects/asset-relationship-values";
AssetRelationshipValues, import { AssetRelationshipValues } from "./value-objects/asset-relationship-values";
type RelationshipValuesData,
} from "./value-objects/asset-relationship-values";
const sourceId = UniqueID.generateNewID().toPrimitive(); const sourceId = UniqueID.generateNewID().toPrimitive();
const targetId = UniqueID.generateNewID().toPrimitive(); const targetId = UniqueID.generateNewID().toPrimitive();

View File

@ -1,4 +1,5 @@
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { DomainEntity } from "@repo/rdx-ddd";
import type { AssetValues } from "./value-objects/asset-values"; import type { AssetValues } from "./value-objects/asset-values";
export type AssetRevisionProps = Readonly<{ export type AssetRevisionProps = Readonly<{

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { AssetCatalogError } from "./asset-definition"; import { AssetCatalogError } from "./asset-definition";

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type Name, type UniqueID } from "@repo/rdx-ddd"; import type { Name, UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";

View File

@ -1,4 +1,5 @@
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { DomainEntity } from "@repo/rdx-ddd";
import type { SupportValues } from "./value-objects/support-values"; import type { SupportValues } from "./value-objects/support-values";
export type SupportConnectionRevisionProps = Readonly<{ export type SupportConnectionRevisionProps = Readonly<{

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";

View File

@ -1,7 +1,9 @@
import { AggregateRoot, Name, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot, Name } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { SupportError, type SupportToolInput, SupportToolInputSchema } from "./support-values"; import type { SupportToolInput } from "./support-values";
import { SupportError, SupportToolInputSchema } from "./support-values";
type Props = Readonly<{ name: Name; active: boolean; builtin: boolean }>; type Props = Readonly<{ name: Name; active: boolean; builtin: boolean }>;
export class SupportTool extends AggregateRoot<Props> { export class SupportTool extends AggregateRoot<Props> {

View File

@ -1,7 +1,9 @@
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { SupportError, type SupportInput, SupportValuesSchema } from "../support-values"; import type { SupportInput } from "../support-values";
import { SupportError, SupportValuesSchema } from "../support-values";
export class SupportValues { export class SupportValues {
private constructor( private constructor(
public readonly toolId: UniqueID, public readonly toolId: UniqueID,

View File

@ -2,7 +2,8 @@ import { Name, UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { AssetCatalogError } from "../asset-definition"; import { AssetCatalogError } from "../asset-definition";
import { type WorkstationInput, WorkstationInputSchema } from "../workstation-values"; import type { WorkstationInput } from "../workstation-values";
import { WorkstationInputSchema } from "../workstation-values";
export class WorkstationValues { export class WorkstationValues {
private constructor( private constructor(
public readonly name: Name, public readonly name: Name,

View File

@ -1,4 +1,5 @@
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { DomainEntity } from "@repo/rdx-ddd";
import type { WorkstationValues } from "./value-objects/workstation-values"; import type { WorkstationValues } from "./value-objects/workstation-values";
export type WorkstationRevisionProps = Readonly<{ export type WorkstationRevisionProps = Readonly<{

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";

View File

@ -1,4 +1,5 @@
import { QueryTypes, type Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { QueryTypes } from "sequelize";
type Entry = { type Entry = {
eventId?: string; eventId?: string;

View File

@ -1,5 +1,6 @@
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { DataTypes, QueryTypes, type Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { DataTypes, QueryTypes } from "sequelize";
import { z } from "zod/v4"; import { z } from "zod/v4";
import { mapBrandInput } from "../../application/brand-input.mapper"; import { mapBrandInput } from "../../application/brand-input.mapper";

View File

@ -1,5 +1,6 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { QueryTypes, type Sequelize, type Transaction } from "sequelize"; import type { Sequelize, Transaction } from "sequelize";
import { QueryTypes } from "sequelize";
import type { IBrandRepository } from "../../application/brand.repository"; import type { IBrandRepository } from "../../application/brand.repository";

View File

@ -1,6 +1,7 @@
import type { UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { QueryTypes, type Sequelize, type Transaction } from "sequelize"; import type { Sequelize, Transaction } from "sequelize";
import { QueryTypes } from "sequelize";
import type { ISupportRepository, SupportContext } from "../../application/support.repository"; import type { ISupportRepository, SupportContext } from "../../application/support.repository";
import { AssetInputSchema } from "../../domain/asset-values"; import { AssetInputSchema } from "../../domain/asset-values";

View File

@ -1,5 +1,6 @@
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { DataTypes, QueryTypes, type Sequelize } from "sequelize"; import type { Sequelize } from "sequelize";
import { DataTypes, QueryTypes } from "sequelize";
export async function migrateSupport(database: Sequelize) { export async function migrateSupport(database: Sequelize) {
const query = database.getQueryInterface(); const query = database.getQueryInterface();
const tables = await database.query<{ name: string }>( const tables = await database.query<{ name: string }>(

View File

@ -0,0 +1,47 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { AssetDetail } from "./asset-detail";
import { AssetRelationships } from "./asset-relationships";
import { AssetSupport } from "./asset-support";
export function AssetDetailDialog({
asset,
lookups,
companyId,
csrfToken,
onClose,
}: {
asset: ComponentProps<typeof AssetDetail>["asset"];
lookups: ComponentProps<typeof AssetDetail>["lookups"];
companyId: string;
csrfToken: string;
onClose: () => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("close")}
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
open
title={asset.values.name}
wide
>
<AssetDetail asset={asset} lookups={lookups}>
<AssetRelationships assetId={asset.id} companyId={companyId} csrfToken={csrfToken} />
<AssetSupport
assetId={asset.id}
companyId={companyId}
csrfToken={csrfToken}
retired={asset.values.status === "retired"}
/>
</AssetDetail>
</FormDialog>
);
}

View File

@ -0,0 +1,49 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { AssetForm } from "./asset-form";
export function AssetEditorDialog({
asset,
companyId,
csrfToken,
items,
lookups,
onSaved,
onClose,
}: {
asset: ComponentProps<typeof AssetForm>["asset"];
companyId: string;
csrfToken: string;
items: ComponentProps<typeof AssetForm>["items"];
lookups: ComponentProps<typeof AssetForm>["lookups"];
onSaved: (id: string) => void;
onClose: () => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
open
title={t(asset ? "editAsset" : "newAsset")}
wide
>
<AssetForm
asset={asset}
companyId={companyId}
csrfToken={csrfToken}
items={items}
lookups={lookups}
onSaved={onSaved}
/>
</FormDialog>
);
}

View File

@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { ContextHelp } from "@repo/shadcn-ui/components/context-help"; import { ContextHelp } from "@repo/shadcn-ui/components/context-help";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog"; import { FormActions } from "@repo/shadcn-ui/components/form-dialog";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action"; import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
@ -17,7 +17,7 @@ import { AssetInputSchema, AssetStatusSchema } from "../common/inventory";
import { AssetCharacteristicControl } from "./asset-characteristic-control"; import { AssetCharacteristicControl } from "./asset-characteristic-control";
import { BrandSelect } from "./brand-select"; import { BrandSelect } from "./brand-select";
import { CatalogTemplateForm } from "./catalog-template-form"; import { CatalogTemplateCreateDialog } from "./catalog-template-create-dialog";
import { inventoryApi, inventoryKeys } from "./inventory-api"; import { inventoryApi, inventoryKeys } from "./inventory-api";
import { InventoryFeedback } from "./inventory-feedback"; import { InventoryFeedback } from "./inventory-feedback";
@ -294,32 +294,22 @@ export function AssetForm({
</FormActions> </FormActions>
</form> </form>
{creatingTemplate && ( {creatingTemplate && (
<FormDialog <CatalogTemplateCreateDialog
cancelLabel={t("cancel")} csrfToken={csrfToken}
formActions items={items}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setCreatingTemplate(false); setCreatingTemplate(false);
} }
}} }}
open onSaved={(id) => {
title={t("newTemplate")} form.setValue("templateId", id);
wide form.setValue("characteristics", {});
> form.setValue("brandId", null);
<p className="text-sm text-muted-foreground">{t("relatedNotice")}</p> setCreatingTemplate(false);
<CatalogTemplateForm }}
allowQuickCreate={false} scope={companyId}
csrfToken={csrfToken} />
items={items}
onSaved={(id) => {
form.setValue("templateId", id);
form.setValue("characteristics", {});
form.setValue("brandId", null);
setCreatingTemplate(false);
}}
scope={companyId}
/>
</FormDialog>
)} )}
</> </>
); );

View File

@ -1,14 +1,13 @@
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog"; import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { RelationshipDTO } from "../common"; import type { RelationshipDTO } from "../common";
import { EndRelationshipDialog } from "./end-relationship-dialog";
import { inventoryApi, inventoryKeys } from "./inventory-api"; import { inventoryApi, inventoryKeys } from "./inventory-api";
import { InventoryFeedback } from "./inventory-feedback"; import { RelationshipEditorDialog } from "./relationship-editor-dialog";
import { RelationshipForm } from "./relationship-form";
export function AssetRelationships({ export function AssetRelationships({
companyId, companyId,
@ -20,20 +19,13 @@ export function AssetRelationships({
assetId: string; assetId: string;
}) { }) {
const { t, i18n } = useTranslation("assets"); const { t, i18n } = useTranslation("assets");
const client = useQueryClient();
const query = useQuery({ const query = useQuery({
queryKey: inventoryKeys.company(companyId), queryKey: inventoryKeys.company(companyId),
queryFn: () => inventoryApi.get(companyId), queryFn: () => inventoryApi.get(companyId),
}); });
const [adding, setAdding] = useState(false); const [adding, setAdding] = useState(false);
const [ending, setEnding] = useState<RelationshipDTO>(); const [endingId, setEndingId] = useState<string>();
const mutation = useMutation({ const ending = query.data?.relationships.find((relationship) => relationship.id === endingId);
mutationFn: (id: string) => inventoryApi.endRelationship(companyId, id, csrfToken),
onSuccess: async () => {
await client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) });
setEnding(undefined);
},
});
const links = (query.data?.relationships ?? []).filter( const links = (query.data?.relationships ?? []).filter(
(link) => link.values.sourceId === assetId || link.values.targetId === assetId (link) => link.values.sourceId === assetId || link.values.targetId === assetId
); );
@ -66,8 +58,7 @@ export function AssetRelationships({
) : ( ) : (
<Button <Button
onClick={() => { onClick={() => {
mutation.reset(); setEndingId(link.id);
setEnding(link);
}} }}
size="sm" size="sm"
variant="outline" variant="outline"
@ -100,45 +91,23 @@ export function AssetRelationships({
</details> </details>
)} )}
{adding && ( {adding && (
<FormDialog <RelationshipEditorDialog
cancelLabel={t("cancel")} assetId={assetId}
formActions companyId={companyId}
csrfToken={csrfToken}
onOpenChange={setAdding} onOpenChange={setAdding}
open onSaved={() => setAdding(false)}
title={t("linkAsset")} />
>
<RelationshipForm
assetId={assetId}
companyId={companyId}
csrfToken={csrfToken}
onSaved={() => setAdding(false)}
/>
</FormDialog>
)} )}
{ending && ( {ending && (
<FormDialog <EndRelationshipDialog
cancelLabel={t("cancel")} companyId={companyId}
formActions csrfToken={csrfToken}
onOpenChange={(open) => { onClose={() => setEndingId(undefined)}
if (!open) { relationship={ending}
setEnding(undefined); sourceName={nameOf(ending.values.sourceId)}
} targetName={nameOf(ending.values.targetId)}
}} />
open
title={t("endRelationship")}
>
<p className="text-sm">
{ending.values.name} · {nameOf(ending.values.sourceId)} →{" "}
{nameOf(ending.values.targetId)}
</p>
<p className="text-sm text-muted-foreground">{t("endRelationshipConfirm")}</p>
<InventoryFeedback error={mutation.error} />
<FormActions>
<Button disabled={mutation.isPending} onClick={() => mutation.mutate(ending.id)}>
{t("endRelationship")}
</Button>
</FormActions>
</FormDialog>
)} )}
</section> </section>
); );

View File

@ -1,5 +1,4 @@
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -7,10 +6,10 @@ import { useTranslation } from "react-i18next";
import type { SupportConnectionDTO } from "../common/support"; import type { SupportConnectionDTO } from "../common/support";
import { supportApi, supportKeys } from "./support-api"; import { supportApi, supportKeys } from "./support-api";
import { SupportForm } from "./support-form"; import { SupportConnectionEditorDialog } from "./support-connection-editor-dialog";
import { SupportHistory } from "./support-history"; import { SupportHistoryDialog } from "./support-history-dialog";
import { SupportSummary } from "./support-summary"; import { SupportSummary } from "./support-summary";
import { SupportTools } from "./support-tools"; import { SupportToolsDialog } from "./support-tools-dialog";
export function AssetSupport({ export function AssetSupport({
companyId, companyId,
@ -32,7 +31,8 @@ export function AssetSupport({
const [edit, setEdit] = useState<SupportConnectionDTO | undefined>(); const [edit, setEdit] = useState<SupportConnectionDTO | undefined>();
const [toolsOpen, setToolsOpen] = useState(false); const [toolsOpen, setToolsOpen] = useState(false);
const [showRetired, setShowRetired] = useState(false); const [showRetired, setShowRetired] = useState(false);
const [history, setHistory] = useState<SupportConnectionDTO | undefined>(); const [historyId, setHistoryId] = useState<string>();
const history = query.data?.connections.find((connection) => connection.id === historyId);
const items = const items =
query.data?.connections.filter((item) => showRetired || item.values.state !== "retired") ?? []; query.data?.connections.filter((item) => showRetired || item.values.state !== "retired") ?? [];
const tools = query.data?.tools ?? []; const tools = query.data?.tools ?? [];
@ -102,7 +102,7 @@ export function AssetSupport({
> >
{t("edit")} {t("edit")}
</Button> </Button>
<Button onClick={() => setHistory(item)} size="sm" variant="outline"> <Button onClick={() => setHistoryId(item.id)} size="sm" variant="outline">
{t("support.history")} {t("support.history")}
</Button> </Button>
</div> </div>
@ -111,43 +111,26 @@ export function AssetSupport({
</ul> </ul>
</> </>
)} )}
<FormDialog <SupportConnectionEditorDialog
cancelLabel={t("cancel")} assetId={assetId}
formActions companyId={companyId}
csrfToken={csrfToken}
edit={edit}
onOpenChange={setOpen} onOpenChange={setOpen}
onSaved={() => setOpen(false)}
open={open} open={open}
title={t(edit ? "support.edit" : "support.new")} tools={tools}
> />
<SupportForm <SupportToolsDialog csrfToken={csrfToken} onOpenChange={setToolsOpen} open={toolsOpen} />
assetId={assetId} <SupportHistoryDialog
companyId={companyId} history={history}
csrfToken={csrfToken}
key={edit?.id ?? "new"}
tools={tools}
{...(edit ? { connection: edit } : {})}
onSaved={() => setOpen(false)}
/>
</FormDialog>
<FormDialog
cancelLabel={t("cancel")}
onOpenChange={setToolsOpen}
open={toolsOpen}
title={t("support.tools")}
>
<SupportTools csrfToken={csrfToken} />
</FormDialog>
<FormDialog
cancelLabel={t("cancel")}
onOpenChange={(value) => { onOpenChange={(value) => {
if (!value) { if (!value) {
setHistory(undefined); setHistoryId(undefined);
} }
}} }}
open={Boolean(history)} tools={tools}
title={t("support.history")} />
>
{history && <SupportHistory connection={history} tools={tools} />}
</FormDialog>
</section> </section>
); );
} }

View File

@ -1,6 +1,7 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import { type BrandInput, BrandResponseSchema, BrandsResponseSchema } from "../common/brands"; import type { BrandInput } from "../common/brands";
import { BrandResponseSchema, BrandsResponseSchema } from "../common/brands";
export const brandKeys = { all: ["asset-brands"] as const }; export const brandKeys = { all: ["asset-brands"] as const };
export class BrandApiError extends Error { export class BrandApiError extends Error {
constructor(public readonly code: string) { constructor(public readonly code: string) {

View File

@ -0,0 +1,30 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { BrandForm } from "./brand-form";
export function BrandEditorDialog({
open,
onOpenChange,
...formProps
}: ComponentProps<typeof BrandForm> & {
open: boolean;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={onOpenChange}
open={open}
title={t(formProps.brand ? "brands.edit" : "brands.new")}
>
{formProps.quickCreate && (
<p className="text-sm text-muted-foreground">{t("brands.quickNotice")}</p>
)}
<BrandForm {...formProps} key={formProps.brand?.id ?? "new"} />
</FormDialog>
);
}

View File

@ -9,7 +9,8 @@ import { useEffect, useId, useRef } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { type BrandDTO, type BrandInput, BrandInputSchema } from "../common/brands"; import type { BrandDTO, BrandInput } from "../common/brands";
import { BrandInputSchema } from "../common/brands";
import { BrandApiError, brandApi, brandKeys } from "./brand-api"; import { BrandApiError, brandApi, brandKeys } from "./brand-api";

View File

@ -1,5 +1,4 @@
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action"; import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
@ -9,7 +8,7 @@ import { useTranslation } from "react-i18next";
import type { BrandDTO } from "../common/brands"; import type { BrandDTO } from "../common/brands";
import { brandApi, brandKeys } from "./brand-api"; import { brandApi, brandKeys } from "./brand-api";
import { BrandForm } from "./brand-form"; import { BrandEditorDialog } from "./brand-editor-dialog";
const LEGACY = "legacy"; const LEGACY = "legacy";
export function BrandSelect({ export function BrandSelect({
@ -88,23 +87,16 @@ export function BrandSelect({
</div> </div>
)} )}
{open && ( {open && (
<FormDialog <BrandEditorDialog
cancelLabel={t("cancel")} csrfToken={csrfToken}
formActions
onOpenChange={setOpen} onOpenChange={setOpen}
onSaved={(brand) => {
onChange(brand);
setOpen(false);
}}
open open
title={t("brands.new")} quickCreate
> />
<p className="text-sm text-muted-foreground">{t("brands.quickNotice")}</p>
<BrandForm
csrfToken={csrfToken}
onSaved={(brand) => {
onChange(brand);
setOpen(false);
}}
quickCreate
/>
</FormDialog>
)} )}
</div> </div>
); );

View File

@ -6,7 +6,6 @@ import {
EntityStatus, EntityStatus,
EntityTable, EntityTable,
} from "@repo/shadcn-ui/components/entity-list"; } from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action"; import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useId, useState } from "react"; import { useId, useState } from "react";
@ -15,7 +14,7 @@ import { useTranslation } from "react-i18next";
import type { BrandDTO } from "../common/brands"; import type { BrandDTO } from "../common/brands";
import { brandApi, brandKeys } from "./brand-api"; import { brandApi, brandKeys } from "./brand-api";
import { BrandForm } from "./brand-form"; import { BrandEditorDialog } from "./brand-editor-dialog";
export function BrandCatalogPage({ csrfToken }: { csrfToken: string }) { export function BrandCatalogPage({ csrfToken }: { csrfToken: string }) {
const { t, i18n } = useTranslation("assets"); const { t, i18n } = useTranslation("assets");
@ -125,20 +124,13 @@ export function BrandCatalogPage({ csrfToken }: { csrfToken: string }) {
title={t("brands.title")} title={t("brands.title")}
unavailable={query.isError} unavailable={query.isError}
/> />
<FormDialog <BrandEditorDialog
cancelLabel={t("cancel")} csrfToken={csrfToken}
formActions
onOpenChange={setOpen} onOpenChange={setOpen}
open={open} open={open}
title={t(edit ? "brands.edit" : "brands.new")} {...(edit ? { brand: edit } : {})}
> onSaved={() => setOpen(false)}
<BrandForm />
csrfToken={csrfToken}
key={edit?.id ?? "new"}
{...(edit ? { brand: edit } : {})}
onSaved={() => setOpen(false)}
/>
</FormDialog>
</div> </div>
); );
} }

View File

@ -0,0 +1,79 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useTranslation } from "react-i18next";
import type { CatalogItemDTO } from "../common";
import { CatalogFieldForm } from "./catalog-form";
import { CatalogRelationForm } from "./catalog-relation-form";
import { CatalogTemplateForm } from "./catalog-template-form";
export type CatalogEditor = {
kind: "field" | "template" | "relation";
item?: CatalogItemDTO;
copy?: CatalogItemDTO;
};
function editorTitle(editor: CatalogEditor) {
if (editor.kind === "field") {
return editor.item ? "editField" : "newField";
}
if (editor.kind === "relation") {
return editor.item ? "editRelation" : "newRelation";
}
return editor.item ? "editTemplate" : "newTemplate";
}
export function CatalogEditorDialog({
editor,
items,
scope,
csrfToken,
onClose,
}: {
editor: CatalogEditor;
items: readonly CatalogItemDTO[];
scope: string;
csrfToken: string;
onClose: () => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
open
title={t(editorTitle(editor))}
wide={editor.kind === "template"}
>
{editor.kind === "field" ? (
<CatalogFieldForm
csrfToken={csrfToken}
item={editor.item}
onSaved={() => onClose()}
scope={scope}
/>
) : editor.kind === "relation" ? (
<CatalogRelationForm
csrfToken={csrfToken}
item={editor.item}
onSaved={() => onClose()}
scope={scope}
/>
) : (
<CatalogTemplateForm
copy={editor.copy}
csrfToken={csrfToken}
item={editor.item}
items={items}
onSaved={() => onClose()}
scope={scope}
/>
)}
</FormDialog>
);
}

View File

@ -0,0 +1,31 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useTranslation } from "react-i18next";
import { CatalogFieldForm } from "./catalog-form";
export function CatalogFieldCreateDialog({
scope,
csrfToken,
onSaved,
onOpenChange,
}: {
scope: string;
csrfToken: string;
onSaved: (id: string) => void;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={onOpenChange}
open
title={t("newField")}
>
<p className="text-sm text-muted-foreground">{t("quickNotice")}</p>
<CatalogFieldForm csrfToken={csrfToken} onSaved={onSaved} scope={scope} />
</FormDialog>
);
}

View File

@ -1,6 +1,5 @@
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { EntityListHeader, EntityTable } from "@repo/shadcn-ui/components/entity-list"; import { EntityListHeader, EntityTable } from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -10,16 +9,9 @@ import type { CatalogItemDTO } from "../common";
import { catalogApi, catalogKeys } from "./catalog-api"; import { catalogApi, catalogKeys } from "./catalog-api";
import { CatalogCell } from "./catalog-cell"; import { CatalogCell } from "./catalog-cell";
import { CatalogDefinitionDialog } from "./catalog-definition-dialog"; import { CatalogDefinitionDialog } from "./catalog-definition-dialog";
import type { CatalogEditor } from "./catalog-editor-dialog";
import { CatalogEditorDialog } from "./catalog-editor-dialog";
import { CatalogError } from "./catalog-error"; import { CatalogError } from "./catalog-error";
import { CatalogFieldForm } from "./catalog-form";
import { CatalogRelationForm } from "./catalog-relation-form";
import { CatalogTemplateForm } from "./catalog-template-form";
type Editor = {
kind: "field" | "template" | "relation";
item?: CatalogItemDTO;
copy?: CatalogItemDTO;
};
export function AssetCatalogPage({ scope, csrfToken }: { scope: string; csrfToken: string }) { export function AssetCatalogPage({ scope, csrfToken }: { scope: string; csrfToken: string }) {
const { t, i18n } = useTranslation("assets"); const { t, i18n } = useTranslation("assets");
@ -27,7 +19,7 @@ export function AssetCatalogPage({ scope, csrfToken }: { scope: string; csrfToke
const [tab, setTab] = useState<"field" | "template" | "relation">("template"); const [tab, setTab] = useState<"field" | "template" | "relation">("template");
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [viewId, setViewId] = useState<string>(); const [viewId, setViewId] = useState<string>();
const [editor, setEditor] = useState<Editor>(); const [editor, setEditor] = useState<CatalogEditor>();
const query = useQuery({ const query = useQuery({
queryKey: catalogKeys.scope(scope), queryKey: catalogKeys.scope(scope),
queryFn: () => catalogApi.get(scope), queryFn: () => catalogApi.get(scope),
@ -196,54 +188,14 @@ export function AssetCatalogPage({ scope, csrfToken }: { scope: string; csrfToke
/> />
<CatalogDefinitionDialog item={view} items={items} onClose={() => setViewId(undefined)} /> <CatalogDefinitionDialog item={view} items={items} onClose={() => setViewId(undefined)} />
{editor && ( {editor && (
<FormDialog <CatalogEditorDialog
cancelLabel={t("cancel")} csrfToken={csrfToken}
formActions editor={editor}
onOpenChange={(open) => { items={items}
if (!open) { onClose={() => setEditor(undefined)}
setEditor(undefined); scope={scope}
} />
}}
open
title={t(editorTitle(editor))}
wide={editor.kind === "template"}
>
{editor.kind === "field" ? (
<CatalogFieldForm
csrfToken={csrfToken}
item={editor.item}
onSaved={() => setEditor(undefined)}
scope={scope}
/>
) : editor.kind === "relation" ? (
<CatalogRelationForm
csrfToken={csrfToken}
item={editor.item}
onSaved={() => setEditor(undefined)}
scope={scope}
/>
) : (
<CatalogTemplateForm
copy={editor.copy}
csrfToken={csrfToken}
item={editor.item}
items={items}
onSaved={() => setEditor(undefined)}
scope={scope}
/>
)}
</FormDialog>
)} )}
</div> </div>
); );
} }
function editorTitle(editor: Editor) {
if (editor.kind === "field") {
return editor.item ? "editField" : "newField";
}
if (editor.kind === "relation") {
return editor.item ? "editRelation" : "newRelation";
}
return editor.item ? "editTemplate" : "newTemplate";
}

View File

@ -0,0 +1,31 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useTranslation } from "react-i18next";
import { CatalogRelationForm } from "./catalog-relation-form";
export function CatalogRelationCreateDialog({
companyId,
csrfToken,
onSaved,
onOpenChange,
}: {
companyId: string;
csrfToken: string;
onSaved: (id: string) => void;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={onOpenChange}
open
title={t("newRelation")}
>
<p className="text-sm text-muted-foreground">{t("quickRelationNotice")}</p>
<CatalogRelationForm csrfToken={csrfToken} onSaved={onSaved} scope={companyId} />
</FormDialog>
);
}

View File

@ -9,7 +9,8 @@ import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { z } from "zod/v4"; import type { z } from "zod/v4";
import { AssetDefinitionSchema, type CatalogItemDTO } from "../common"; import type { CatalogItemDTO } from "../common";
import { AssetDefinitionSchema } from "../common";
import { CatalogError } from "./catalog-error"; import { CatalogError } from "./catalog-error";
import { CatalogHelp } from "./catalog-help"; import { CatalogHelp } from "./catalog-help";

View File

@ -0,0 +1,41 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { CatalogTemplateForm } from "./catalog-template-form";
export function CatalogTemplateCreateDialog({
scope,
csrfToken,
items,
onSaved,
onOpenChange,
}: {
scope: string;
csrfToken: string;
items: ComponentProps<typeof CatalogTemplateForm>["items"];
onSaved: (id: string) => void;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={onOpenChange}
open
title={t("newTemplate")}
wide
>
<p className="text-sm text-muted-foreground">{t("relatedNotice")}</p>
<CatalogTemplateForm
allowQuickCreate={false}
csrfToken={csrfToken}
items={items}
onSaved={onSaved}
scope={scope}
/>
</FormDialog>
);
}

View File

@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { Checkbox } from "@repo/shadcn-ui/components/checkbox"; import { Checkbox } from "@repo/shadcn-ui/components/checkbox";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog"; import { FormActions } from "@repo/shadcn-ui/components/form-dialog";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action"; import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
@ -15,7 +15,7 @@ import type { CatalogItemDTO } from "../common";
import { AssetDefinitionSchema } from "../common"; import { AssetDefinitionSchema } from "../common";
import { CatalogError } from "./catalog-error"; import { CatalogError } from "./catalog-error";
import { CatalogFieldForm } from "./catalog-form"; import { CatalogFieldCreateDialog } from "./catalog-field-create-dialog";
import { CatalogHelp } from "./catalog-help"; import { CatalogHelp } from "./catalog-help";
import type { CatalogSaved } from "./use-catalog-save"; import type { CatalogSaved } from "./use-catalog-save";
import { useCatalogSave } from "./use-catalog-save"; import { useCatalogSave } from "./use-catalog-save";
@ -173,27 +173,19 @@ export function CatalogTemplateForm({
</FormActions> </FormActions>
</form> </form>
{creatingField && ( {creatingField && (
<FormDialog <CatalogFieldCreateDialog
cancelLabel={t("cancel")} csrfToken={csrfToken}
formActions
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setCreatingField(false); setCreatingField(false);
} }
}} }}
open onSaved={(fieldId) => {
title={t("newField")} add(fieldId);
> setCreatingField(false);
<p className="text-sm text-muted-foreground">{t("quickNotice")}</p> }}
<CatalogFieldForm scope={scope}
csrfToken={csrfToken} />
onSaved={(fieldId) => {
add(fieldId);
setCreatingField(false);
}}
scope={scope}
/>
</FormDialog>
)} )}
</> </>
); );

View File

@ -0,0 +1,60 @@
import { Button } from "@repo/shadcn-ui/components/button";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useTranslation } from "react-i18next";
import type { RelationshipDTO } from "../common";
import { inventoryApi, inventoryKeys } from "./inventory-api";
import { InventoryFeedback } from "./inventory-feedback";
export function EndRelationshipDialog({
relationship,
companyId,
csrfToken,
sourceName,
targetName,
onClose,
}: {
relationship: RelationshipDTO;
companyId: string;
csrfToken: string;
sourceName: string;
targetName: string;
onClose: () => void;
}) {
const { t } = useTranslation("assets");
const client = useQueryClient();
const mutation = useMutation({
mutationFn: () => inventoryApi.endRelationship(companyId, relationship.id, csrfToken),
onSuccess: () => client.invalidateQueries({ queryKey: inventoryKeys.company(companyId) }),
});
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
open
title={t("endRelationship")}
>
<p className="text-sm">
{relationship.values.name} · {sourceName} → {targetName}
</p>
<p className="text-sm text-muted-foreground">{t("endRelationshipConfirm")}</p>
<InventoryFeedback error={mutation.error} />
<FormActions>
<Button
disabled={mutation.isPending}
onClick={() => mutation.mutate(undefined, { onSuccess: onClose })}
type="button"
>
{t("endRelationship")}
</Button>
</FormActions>
</FormDialog>
);
}

View File

@ -2,8 +2,10 @@ import { z } from "zod/v4";
import type { AssetInput } from "../common/inventory"; import type { AssetInput } from "../common/inventory";
import { AssetResponseSchema, InventoryResponseSchema } from "../common/inventory"; import { AssetResponseSchema, InventoryResponseSchema } from "../common/inventory";
import { type RelationshipInputDTO, RelationshipResponseSchema } from "../common/relationships"; import type { RelationshipInputDTO } from "../common/relationships";
import { type WorkstationInput, WorkstationResponseSchema } from "../common/workstations"; import { RelationshipResponseSchema } from "../common/relationships";
import type { WorkstationInput } from "../common/workstations";
import { WorkstationResponseSchema } from "../common/workstations";
export const inventoryKeys = { company: (id: string) => ["inventory", id] }; export const inventoryKeys = { company: (id: string) => ["inventory", id] };
export class InventoryApiError extends Error { export class InventoryApiError extends Error {
constructor(public readonly code: string) { constructor(public readonly code: string) {

View File

@ -6,17 +6,14 @@ import {
EntityStatus, EntityStatus,
EntityTable, EntityTable,
} from "@repo/shadcn-ui/components/entity-list"; } from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { AssetDetail } from "./asset-detail"; import { AssetDetailDialog } from "./asset-detail-dialog";
import { AssetEditorDialog } from "./asset-editor-dialog";
import type { InventoryLookups } from "./asset-form"; import type { InventoryLookups } from "./asset-form";
import { AssetForm } from "./asset-form";
import { AssetRelationships } from "./asset-relationships";
import { AssetSelectionPanel } from "./asset-selection-panel"; import { AssetSelectionPanel } from "./asset-selection-panel";
import { AssetSupport } from "./asset-support";
import { catalogApi, catalogKeys } from "./catalog-api"; import { catalogApi, catalogKeys } from "./catalog-api";
import { CatalogError } from "./catalog-error"; import { CatalogError } from "./catalog-error";
import { inventoryApi, inventoryKeys } from "./inventory-api"; import { inventoryApi, inventoryKeys } from "./inventory-api";
@ -216,54 +213,28 @@ export function AssetInventoryPage({
open={detailOpen} open={detailOpen}
/> />
{editor && catalog.data && ( {editor && catalog.data && (
<FormDialog <AssetEditorDialog
cancelLabel={t("cancel")} asset={edited}
formActions companyId={companyId}
onOpenChange={(open) => { csrfToken={csrfToken}
if (!open) { items={catalog.data.items}
setEditor(undefined); lookups={lookups}
} onClose={() => setEditor(undefined)}
onSaved={(id) => {
setSelectedId(id);
setDetailOpen(true);
setEditor(undefined);
}} }}
open />
title={t(edited ? "editAsset" : "newAsset")}
wide
>
<AssetForm
asset={edited}
companyId={companyId}
csrfToken={csrfToken}
items={catalog.data.items}
lookups={lookups}
onSaved={(id) => {
setSelectedId(id);
setDetailOpen(true);
setEditor(undefined);
}}
/>
</FormDialog>
)} )}
{viewed && ( {viewed && (
<FormDialog <AssetDetailDialog
cancelLabel={t("close")} asset={viewed}
onOpenChange={(open) => { companyId={companyId}
if (!open) { csrfToken={csrfToken}
setViewId(undefined); lookups={lookups}
} onClose={() => setViewId(undefined)}
}} />
open
title={viewed.values.name}
wide
>
<AssetDetail asset={viewed} lookups={lookups}>
<AssetRelationships assetId={viewed.id} companyId={companyId} csrfToken={csrfToken} />
<AssetSupport
assetId={viewed.id}
companyId={companyId}
csrfToken={csrfToken}
retired={viewed.values.status === "retired"}
/>
</AssetDetail>
</FormDialog>
)} )}
</div> </div>
); );

View File

@ -0,0 +1,37 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useTranslation } from "react-i18next";
import { RelationshipForm } from "./relationship-form";
export function RelationshipEditorDialog({
companyId,
assetId,
csrfToken,
onSaved,
onOpenChange,
}: {
companyId: string;
assetId: string;
csrfToken: string;
onSaved: () => void;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={onOpenChange}
open
title={t("linkAsset")}
>
<RelationshipForm
assetId={assetId}
companyId={companyId}
csrfToken={csrfToken}
onSaved={onSaved}
/>
</FormDialog>
);
}

View File

@ -1,6 +1,6 @@
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { ContextHelp } from "@repo/shadcn-ui/components/context-help"; import { ContextHelp } from "@repo/shadcn-ui/components/context-help";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog"; import { FormActions } from "@repo/shadcn-ui/components/form-dialog";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action"; import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
import { Textarea } from "@repo/shadcn-ui/components/textarea"; import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
import type { RelationshipInputDTO } from "../common"; import type { RelationshipInputDTO } from "../common";
import { catalogApi, catalogKeys } from "./catalog-api"; import { catalogApi, catalogKeys } from "./catalog-api";
import { CatalogRelationForm } from "./catalog-relation-form"; import { CatalogRelationCreateDialog } from "./catalog-relation-create-dialog";
import { inventoryApi, inventoryKeys } from "./inventory-api"; import { inventoryApi, inventoryKeys } from "./inventory-api";
import { InventoryFeedback } from "./inventory-feedback"; import { InventoryFeedback } from "./inventory-feedback";
@ -112,23 +112,15 @@ export function RelationshipForm({
</FormActions> </FormActions>
</form> </form>
{quick && ( {quick && (
<FormDialog <CatalogRelationCreateDialog
cancelLabel={t("cancel")} companyId={companyId}
formActions csrfToken={csrfToken}
onOpenChange={setQuick} onOpenChange={setQuick}
open onSaved={(savedId) => {
title={t("newRelation")} setTypeId(savedId);
> setQuick(false);
<p className="text-sm text-muted-foreground">{t("quickRelationNotice")}</p> }}
<CatalogRelationForm />
csrfToken={csrfToken}
onSaved={(savedId) => {
setTypeId(savedId);
setQuick(false);
}}
scope={companyId}
/>
</FormDialog>
)} )}
</> </>
); );

View File

@ -1,10 +1,9 @@
import { z } from "zod/v4"; import { z } from "zod/v4";
import type { SupportInput, SupportToolInput } from "../common/support";
import { import {
type SupportInput,
SupportListSchema, SupportListSchema,
SupportResponseSchema, SupportResponseSchema,
type SupportToolInput,
SupportToolResponseSchema, SupportToolResponseSchema,
SupportToolsSchema, SupportToolsSchema,
} from "../common/support"; } from "../common/support";

View File

@ -0,0 +1,47 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { SupportForm } from "./support-form";
export function SupportConnectionEditorDialog({
open,
edit,
companyId,
assetId,
csrfToken,
tools,
onSaved,
onOpenChange,
}: {
open: boolean;
edit: ComponentProps<typeof SupportForm>["connection"];
companyId: string;
assetId: string;
csrfToken: string;
tools: ComponentProps<typeof SupportForm>["tools"];
onSaved: () => void;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={onOpenChange}
open={open}
title={t(edit ? "support.edit" : "support.new")}
>
<SupportForm
assetId={assetId}
companyId={companyId}
csrfToken={csrfToken}
key={edit?.id ?? "new"}
tools={tools}
{...(edit ? { connection: edit } : {})}
onSaved={onSaved}
/>
</FormDialog>
);
}

View File

@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog"; import { FormActions } from "@repo/shadcn-ui/components/form-dialog";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action"; import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
@ -9,17 +9,13 @@ import { useId, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import type { SupportConnectionDTO, SupportInput, SupportToolDTO } from "../common/support";
type SupportConnectionDTO, import { SupportValuesSchema } from "../common/support";
type SupportInput,
type SupportToolDTO,
SupportValuesSchema,
} from "../common/support";
import { supportApi, supportKeys } from "./support-api"; import { supportApi, supportKeys } from "./support-api";
import { SupportFeedback } from "./support-feedback"; import { SupportFeedback } from "./support-feedback";
import { SupportHelp } from "./support-help"; import { SupportHelp } from "./support-help";
import { SupportToolForm } from "./support-tool-form"; import { SupportToolEditorDialog } from "./support-tool-editor-dialog";
export function SupportForm({ export function SupportForm({
companyId, companyId,
assetId, assetId,
@ -135,21 +131,16 @@ export function SupportForm({
</Button> </Button>
</FormActions> </FormActions>
</form> </form>
<FormDialog <SupportToolEditorDialog
cancelLabel={t("cancel")} csrfToken={csrfToken}
formActions edit={undefined}
onOpenChange={setQuick} onOpenChange={setQuick}
onSaved={(id) => {
form.setValue("toolId", id);
setQuick(false);
}}
open={quick} open={quick}
title={t("support.newTool")} />
>
<SupportToolForm
csrfToken={csrfToken}
onSaved={(id) => {
form.setValue("toolId", id);
setQuick(false);
}}
/>
</FormDialog>
</> </>
); );
} }

View File

@ -0,0 +1,44 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useTranslation } from "react-i18next";
import type { SupportConnectionDTO, SupportToolDTO } from "../common/support";
import { SupportSummary } from "./support-summary";
export function SupportHistoryDialog({
history,
tools,
onOpenChange,
}: {
history: SupportConnectionDTO | undefined;
tools: readonly SupportToolDTO[];
onOpenChange: (open: boolean) => void;
}) {
const { t, i18n } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
onOpenChange={onOpenChange}
open={Boolean(history)}
title={t("support.history")}
>
{history && (
<ol className="space-y-3">
{[...history.history].reverse().map((revision) => (
<li className="space-y-2 rounded-md border p-3" key={revision.id}>
<p className="text-sm">
{new Intl.DateTimeFormat(i18n.language, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(revision.recordedAt))}{" "}
· {revision.actorName}
</p>
<SupportSummary tools={tools} values={revision.values} />
</li>
))}
</ol>
)}
</FormDialog>
);
}

View File

@ -1,31 +0,0 @@
import { useTranslation } from "react-i18next";
import type { SupportConnectionDTO, SupportToolDTO } from "../common/support";
import { SupportSummary } from "./support-summary";
export function SupportHistory({
connection,
tools,
}: {
connection: SupportConnectionDTO;
tools: readonly SupportToolDTO[];
}) {
const { i18n } = useTranslation("assets");
return (
<ol className="space-y-3">
{[...connection.history].reverse().map((revision) => (
<li className="space-y-2 rounded-md border p-3" key={revision.id}>
<p className="text-sm">
{new Intl.DateTimeFormat(i18n.language, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(revision.recordedAt))}{" "}
· {revision.actorName}
</p>
<SupportSummary tools={tools} values={revision.values} />
</li>
))}
</ol>
);
}

View File

@ -0,0 +1,38 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { SupportToolForm } from "./support-tool-form";
export function SupportToolEditorDialog({
open,
edit,
csrfToken,
onSaved,
onOpenChange,
}: {
open: boolean;
edit: ComponentProps<typeof SupportToolForm>["tool"];
csrfToken: string;
onSaved: ComponentProps<typeof SupportToolForm>["onSaved"];
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={onOpenChange}
open={open}
title={t(edit ? "support.editTool" : "support.newTool")}
>
<SupportToolForm
csrfToken={csrfToken}
key={edit?.id ?? "new"}
{...(edit ? { tool: edit } : {})}
onSaved={onSaved}
/>
</FormDialog>
);
}

View File

@ -8,11 +8,8 @@ import { useId } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import type { SupportToolDTO, SupportToolInput } from "../common/support";
type SupportToolDTO, import { SupportToolInputSchema } from "../common/support";
type SupportToolInput,
SupportToolInputSchema,
} from "../common/support";
import { supportApi, supportKeys } from "./support-api"; import { supportApi, supportKeys } from "./support-api";
import { SupportFeedback } from "./support-feedback"; import { SupportFeedback } from "./support-feedback";

View File

@ -0,0 +1,27 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useTranslation } from "react-i18next";
import { SupportTools } from "./support-tools";
export function SupportToolsDialog({
open,
csrfToken,
onOpenChange,
}: {
open: boolean;
csrfToken: string;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
onOpenChange={onOpenChange}
open={open}
title={t("support.tools")}
>
<SupportTools csrfToken={csrfToken} />
</FormDialog>
);
}

View File

@ -6,7 +6,6 @@ import {
EntityStatus, EntityStatus,
EntityTable, EntityTable,
} from "@repo/shadcn-ui/components/entity-list"; } from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -14,7 +13,7 @@ import { useTranslation } from "react-i18next";
import type { SupportToolDTO } from "../common/support"; import type { SupportToolDTO } from "../common/support";
import { supportApi, supportKeys } from "./support-api"; import { supportApi, supportKeys } from "./support-api";
import { SupportToolForm } from "./support-tool-form"; import { SupportToolEditorDialog } from "./support-tool-editor-dialog";
export function SupportTools({ csrfToken }: { csrfToken: string }) { export function SupportTools({ csrfToken }: { csrfToken: string }) {
const { t, i18n } = useTranslation("assets"); const { t, i18n } = useTranslation("assets");
const query = useQuery({ queryKey: supportKeys.tools, queryFn: () => supportApi.tools() }); const query = useQuery({ queryKey: supportKeys.tools, queryFn: () => supportApi.tools() });
@ -102,20 +101,13 @@ export function SupportTools({ csrfToken }: { csrfToken: string }) {
title={t("support.tools")} title={t("support.tools")}
unavailable={query.isError} unavailable={query.isError}
/> />
<FormDialog <SupportToolEditorDialog
cancelLabel={t("cancel")} csrfToken={csrfToken}
formActions edit={edit}
onOpenChange={setOpen} onOpenChange={setOpen}
onSaved={() => setOpen(false)}
open={open} open={open}
title={t(edit ? "support.editTool" : "support.newTool")} />
>
<SupportToolForm
csrfToken={csrfToken}
key={edit?.id ?? "new"}
{...(edit ? { tool: edit } : {})}
onSaved={() => setOpen(false)}
/>
</FormDialog>
</div> </div>
); );
} }

View File

@ -0,0 +1,44 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { AssetForm } from "./asset-form";
export function WorkstationAssetCreateDialog({
companyId,
csrfToken,
items,
lookups,
onSaved,
onOpenChange,
}: {
companyId: string;
csrfToken: string;
items: ComponentProps<typeof AssetForm>["items"];
lookups: ComponentProps<typeof AssetForm>["lookups"];
onSaved: (id: string) => void;
onOpenChange: (open: boolean) => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={onOpenChange}
open
title={t("newAsset")}
wide
>
<p className="text-sm text-muted-foreground">{t("relatedNotice")}</p>
<AssetForm
allowQuickCreate={false}
companyId={companyId}
csrfToken={csrfToken}
items={items}
lookups={lookups}
onSaved={onSaved}
/>
</FormDialog>
);
}

View File

@ -0,0 +1,44 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import type { ComponentProps } from "react";
import { useTranslation } from "react-i18next";
import { WorkstationForm } from "./workstation-form";
export function WorkstationEditorDialog({
workstation,
companyId,
csrfToken,
lookups,
onClose,
}: {
workstation: ComponentProps<typeof WorkstationForm>["workstation"];
companyId: string;
csrfToken: string;
lookups: ComponentProps<typeof WorkstationForm>["lookups"];
onClose: () => void;
}) {
const { t } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
open
title={t(workstation ? "editWorkstation" : "newWorkstation")}
wide
>
<WorkstationForm
companyId={companyId}
csrfToken={csrfToken}
lookups={lookups}
onSaved={() => onClose()}
workstation={workstation}
/>
</FormDialog>
);
}

View File

@ -1,7 +1,7 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@repo/shadcn-ui/components/button"; import { Button } from "@repo/shadcn-ui/components/button";
import { ContextHelp } from "@repo/shadcn-ui/components/context-help"; import { ContextHelp } from "@repo/shadcn-ui/components/context-help";
import { FormActions, FormDialog } from "@repo/shadcn-ui/components/form-dialog"; import { FormActions } from "@repo/shadcn-ui/components/form-dialog";
import { Input } from "@repo/shadcn-ui/components/input"; import { Input } from "@repo/shadcn-ui/components/input";
import { Label } from "@repo/shadcn-ui/components/label"; import { Label } from "@repo/shadcn-ui/components/label";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action"; import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
@ -10,13 +10,14 @@ import { useEffect, useId, useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { type WorkstationDTO, type WorkstationInput, WorkstationInputSchema } from "../common"; import type { WorkstationDTO, WorkstationInput } from "../common";
import { WorkstationInputSchema } from "../common";
import type { InventoryLookups } from "./asset-form"; import type { InventoryLookups } from "./asset-form";
import { AssetForm } from "./asset-form";
import { catalogApi, catalogKeys } from "./catalog-api"; import { catalogApi, catalogKeys } from "./catalog-api";
import { inventoryApi, inventoryKeys } from "./inventory-api"; import { inventoryApi, inventoryKeys } from "./inventory-api";
import { InventoryFeedback } from "./inventory-feedback"; import { InventoryFeedback } from "./inventory-feedback";
import { WorkstationAssetCreateDialog } from "./workstation-asset-create-dialog";
export function WorkstationForm({ export function WorkstationForm({
companyId, companyId,
csrfToken, csrfToken,
@ -257,27 +258,17 @@ export function WorkstationForm({
</FormActions> </FormActions>
</form> </form>
{quick && catalog.data && ( {quick && catalog.data && (
<FormDialog <WorkstationAssetCreateDialog
cancelLabel={t("cancel")} companyId={companyId}
formActions csrfToken={csrfToken}
items={catalog.data.items}
lookups={lookups}
onOpenChange={setQuick} onOpenChange={setQuick}
open onSaved={(id) => {
title={t("newAsset")} add(quickField, id);
wide setQuick(false);
> }}
<p className="text-sm text-muted-foreground">{t("relatedNotice")}</p> />
<AssetForm
allowQuickCreate={false}
companyId={companyId}
csrfToken={csrfToken}
items={catalog.data.items}
lookups={lookups}
onSaved={(id) => {
add(quickField, id);
setQuick(false);
}}
/>
</FormDialog>
)} )}
</> </>
); );

View File

@ -0,0 +1,61 @@
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useTranslation } from "react-i18next";
import type { WorkstationDTO } from "../common";
export function WorkstationHistoryDialog({
workstation,
personName,
siteName,
members,
onClose,
}: {
workstation: WorkstationDTO;
personName: (id: string | null) => string;
siteName: (id: string | null) => string;
members: (ids: readonly string[], empty?: string) => string;
onClose: () => void;
}) {
const { t, i18n } = useTranslation("assets");
return (
<FormDialog
cancelLabel={t("close")}
onOpenChange={(open) => {
if (!open) {
onClose();
}
}}
open
title={t("workstationHistory")}
wide
>
<ol className="space-y-4">
{[...workstation.history].reverse().map((revision) => (
<li className="space-y-2 rounded-md border p-3 text-sm" key={revision.id}>
<p className="font-medium">
{new Intl.DateTimeFormat(i18n.language, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(revision.recordedAt))}{" "}
· {t("recordedBy", { name: revision.actorName })}
</p>
<p>
{revision.values.name} · {t(revision.values.active ? "active" : "inactive")}
</p>
<p>
{siteName(revision.values.siteId)} · {personName(revision.values.personId)}
</p>
<p>
{t("exclusiveWorkstationAssets")}: {members(revision.values.assetIds)}
</p>
<p>
{t("sharedWorkstationAssets")}:{" "}
{members(revision.values.sharedAssetIds, "noSharedWorkstationAssets")}
</p>
</li>
))}
</ol>
</FormDialog>
);
}

View File

@ -6,7 +6,6 @@ import {
EntityStatus, EntityStatus,
EntityTable, EntityTable,
} from "@repo/shadcn-ui/components/entity-list"; } from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useQuery } from "@tanstack/react-query"; import { useQuery } from "@tanstack/react-query";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
@ -14,7 +13,8 @@ import { useTranslation } from "react-i18next";
import type { InventoryLookups } from "./asset-form"; import type { InventoryLookups } from "./asset-form";
import { inventoryApi, inventoryKeys } from "./inventory-api"; import { inventoryApi, inventoryKeys } from "./inventory-api";
import { InventoryFeedback } from "./inventory-feedback"; import { InventoryFeedback } from "./inventory-feedback";
import { WorkstationForm } from "./workstation-form"; import { WorkstationEditorDialog } from "./workstation-editor-dialog";
import { WorkstationHistoryDialog } from "./workstation-history-dialog";
export function WorkstationsPage({ export function WorkstationsPage({
companyId, companyId,
csrfToken, csrfToken,
@ -166,66 +166,22 @@ export function WorkstationsPage({
/> />
)} )}
{editor && ( {editor && (
<FormDialog <WorkstationEditorDialog
cancelLabel={t("cancel")} companyId={companyId}
formActions csrfToken={csrfToken}
onOpenChange={(open) => { lookups={lookups}
if (!open) { onClose={() => setEditor(undefined)}
setEditor(undefined); workstation={edited}
} />
}}
open
title={t(edited ? "editWorkstation" : "newWorkstation")}
wide
>
<WorkstationForm
companyId={companyId}
csrfToken={csrfToken}
lookups={lookups}
onSaved={() => setEditor(undefined)}
workstation={edited}
/>
</FormDialog>
)} )}
{viewed && ( {viewed && (
<FormDialog <WorkstationHistoryDialog
cancelLabel={t("close")} members={members}
onOpenChange={(open) => { onClose={() => setView(undefined)}
if (!open) { personName={personName}
setView(undefined); siteName={siteName}
} workstation={viewed}
}} />
open
title={t("workstationHistory")}
wide
>
<ol className="space-y-4">
{[...viewed.history].reverse().map((revision) => (
<li className="space-y-2 rounded-md border p-3 text-sm" key={revision.id}>
<p className="font-medium">
{new Intl.DateTimeFormat(i18n.language, {
dateStyle: "medium",
timeStyle: "short",
}).format(new Date(revision.recordedAt))}{" "}
· {t("recordedBy", { name: revision.actorName })}
</p>
<p>
{revision.values.name} · {t(revision.values.active ? "active" : "inactive")}
</p>
<p>
{siteName(revision.values.siteId)} · {personName(revision.values.personId)}
</p>
<p>
{t("exclusiveWorkstationAssets")}: {members(revision.values.assetIds)}
</p>
<p>
{t("sharedWorkstationAssets")}:{" "}
{members(revision.values.sharedAssetIds, "noSharedWorkstationAssets")}
</p>
</li>
))}
</ol>
</FormDialog>
)} )}
</div> </div>
); );

View File

@ -1,10 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { ApplicationConfiguration } from "../domain/application-configuration.aggregate"; import type { ApplicationConfiguration } from "../domain/application-configuration.aggregate";
import { import type { ApplicationConfigurationInput } from "../domain/application-configuration-values";
ApplicationConfigurationError, import { ApplicationConfigurationError } from "../domain/application-configuration-values";
type ApplicationConfigurationInput,
} from "../domain/application-configuration-values";
import { ApplicationConfigurationValues } from "../domain/value-objects/application-configuration-values"; import { ApplicationConfigurationValues } from "../domain/value-objects/application-configuration-values";
import type { ApplicationContext } from "./application-configuration.repository"; import type { ApplicationContext } from "./application-configuration.repository";

View File

@ -2,10 +2,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { ApplicationConfiguration } from "../domain/application-configuration.aggregate"; import { ApplicationConfiguration } from "../domain/application-configuration.aggregate";
import { import type { ApplicationConfigurationInput } from "../domain/application-configuration-values";
ApplicationConfigurationError, import { ApplicationConfigurationError } from "../domain/application-configuration-values";
type ApplicationConfigurationInput,
} from "../domain/application-configuration-values";
import type { IApplicationConfigurationRepository } from "./application-configuration.repository"; import type { IApplicationConfigurationRepository } from "./application-configuration.repository";
import { mapApplicationInput } from "./application-configuration-input.mapper"; import { mapApplicationInput } from "./application-configuration-input.mapper";

View File

@ -1,10 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { CertificateConfiguration } from "../domain/certificate-configuration.aggregate"; import type { CertificateConfiguration } from "../domain/certificate-configuration.aggregate";
import { import type { CertificateConfigurationInput } from "../domain/certificate-configuration-values";
CertificateConfigurationError, import { CertificateConfigurationError } from "../domain/certificate-configuration-values";
type CertificateConfigurationInput,
} from "../domain/certificate-configuration-values";
import { CertificateConfigurationValues } from "../domain/value-objects/certificate-configuration-values"; import { CertificateConfigurationValues } from "../domain/value-objects/certificate-configuration-values";
import type { CertificateContext } from "./certificate-configuration.repository"; import type { CertificateContext } from "./certificate-configuration.repository";

View File

@ -2,10 +2,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { CertificateConfiguration } from "../domain/certificate-configuration.aggregate"; import { CertificateConfiguration } from "../domain/certificate-configuration.aggregate";
import { import type { CertificateConfigurationInput } from "../domain/certificate-configuration-values";
CertificateConfigurationError, import { CertificateConfigurationError } from "../domain/certificate-configuration-values";
type CertificateConfigurationInput,
} from "../domain/certificate-configuration-values";
import type { ICertificateConfigurationRepository } from "./certificate-configuration.repository"; import type { ICertificateConfigurationRepository } from "./certificate-configuration.repository";
import { mapCertificateInput } from "./certificate-configuration-input.mapper"; import { mapCertificateInput } from "./certificate-configuration-input.mapper";

View File

@ -1,7 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { FolderMapping } from "../domain/folder-mapping.aggregate"; import type { FolderMapping } from "../domain/folder-mapping.aggregate";
import { FolderMappingError, type FolderMappingInput } from "../domain/folder-mapping-values"; import type { FolderMappingInput } from "../domain/folder-mapping-values";
import { FolderMappingError } from "../domain/folder-mapping-values";
import { FolderMappingValues } from "../domain/value-objects/folder-mapping-values"; import { FolderMappingValues } from "../domain/value-objects/folder-mapping-values";
import type { FolderContext } from "./folder-mapping.repository"; import type { FolderContext } from "./folder-mapping.repository";

View File

@ -2,7 +2,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { FolderMapping } from "../domain/folder-mapping.aggregate"; import { FolderMapping } from "../domain/folder-mapping.aggregate";
import { FolderMappingError, type FolderMappingInput } from "../domain/folder-mapping-values"; import type { FolderMappingInput } from "../domain/folder-mapping-values";
import { FolderMappingError } from "../domain/folder-mapping-values";
import type { IFolderMappingRepository } from "./folder-mapping.repository"; import type { IFolderMappingRepository } from "./folder-mapping.repository";
import { mapFolderInput } from "./folder-mapping-input.mapper"; import { mapFolderInput } from "./folder-mapping-input.mapper";

View File

@ -1,7 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Intervention } from "../domain/intervention.aggregate"; import type { Intervention } from "../domain/intervention.aggregate";
import { InterventionError, type InterventionInput } from "../domain/intervention-values"; import type { InterventionInput } from "../domain/intervention-values";
import { InterventionError } from "../domain/intervention-values";
import { InterventionValues } from "../domain/value-objects/intervention-values"; import { InterventionValues } from "../domain/value-objects/intervention-values";
import type { InterventionSaveContext } from "./intervention.repository"; import type { InterventionSaveContext } from "./intervention.repository";

View File

@ -1,10 +1,8 @@
import { UniqueID } from "@repo/rdx-ddd"; import { UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils"; import { Maybe, Result } from "@repo/rdx-utils";
import { import type { SaveInterventionTemplateInput } from "../../common/intervention-templates";
type SaveInterventionTemplateInput, import { SaveInterventionTemplateSchema } from "../../common/intervention-templates";
SaveInterventionTemplateSchema,
} from "../../common/intervention-templates";
import { InterventionTemplate } from "../domain/intervention-template.aggregate"; import { InterventionTemplate } from "../domain/intervention-template.aggregate";
import { InterventionTemplateError } from "../domain/intervention-template-values"; import { InterventionTemplateError } from "../domain/intervention-template-values";
import { InterventionTemplateValues } from "../domain/value-objects/intervention-template-values"; import { InterventionTemplateValues } from "../domain/value-objects/intervention-template-values";

View File

@ -2,10 +2,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { MailConfiguration } from "../domain/mail-configuration.aggregate"; import type { MailConfiguration } from "../domain/mail-configuration.aggregate";
import { import type { MailConfigurationInput } from "../domain/mail-configuration-values";
MailConfigurationError, import { MailConfigurationError } from "../domain/mail-configuration-values";
type MailConfigurationInput,
} from "../domain/mail-configuration-values";
import { MailConfigurationValues } from "../domain/value-objects/mail-configuration-values"; import { MailConfigurationValues } from "../domain/value-objects/mail-configuration-values";
import type { MailContext } from "./mail-configuration.repository"; import type { MailContext } from "./mail-configuration.repository";

View File

@ -2,10 +2,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { MailConfiguration } from "../domain/mail-configuration.aggregate"; import { MailConfiguration } from "../domain/mail-configuration.aggregate";
import { import type { MailConfigurationInput } from "../domain/mail-configuration-values";
MailConfigurationError, import { MailConfigurationError } from "../domain/mail-configuration-values";
type MailConfigurationInput,
} from "../domain/mail-configuration-values";
import type { IMailConfigurationRepository } from "./mail-configuration.repository"; import type { IMailConfigurationRepository } from "./mail-configuration.repository";
import { mapMailInput } from "./mail-configuration-input.mapper"; import { mapMailInput } from "./mail-configuration-input.mapper";

View File

@ -2,7 +2,8 @@ import { Result } from "@repo/rdx-utils";
import type { Mailbox } from "../domain/mailbox.aggregate"; import type { Mailbox } from "../domain/mailbox.aggregate";
import type { MailboxUse } from "../domain/mailbox-use.aggregate"; import type { MailboxUse } from "../domain/mailbox-use.aggregate";
import { MailboxError, type MailboxInput, type MailboxUseInput } from "../domain/mailbox-values"; import type { MailboxInput, MailboxUseInput } from "../domain/mailbox-values";
import { MailboxError } from "../domain/mailbox-values";
import { MailboxUseValues } from "../domain/value-objects/mailbox-use-values"; import { MailboxUseValues } from "../domain/value-objects/mailbox-use-values";
import { MailboxValues } from "../domain/value-objects/mailbox-values"; import { MailboxValues } from "../domain/value-objects/mailbox-values";

View File

@ -3,7 +3,8 @@ import { Result } from "@repo/rdx-utils";
import { Mailbox } from "../domain/mailbox.aggregate"; import { Mailbox } from "../domain/mailbox.aggregate";
import { MailboxUse } from "../domain/mailbox-use.aggregate"; import { MailboxUse } from "../domain/mailbox-use.aggregate";
import { MailboxError, type MailboxInput, type MailboxUseInput } from "../domain/mailbox-values"; import type { MailboxInput, MailboxUseInput } from "../domain/mailbox-values";
import { MailboxError } from "../domain/mailbox-values";
import type { IMailboxRepository, MailboxContext } from "./mailbox.repository"; import type { IMailboxRepository, MailboxContext } from "./mailbox.repository";
import { mapMailboxInput, mapMailboxUseInput } from "./mailbox-input.mapper"; import { mapMailboxInput, mapMailboxUseInput } from "./mailbox-input.mapper";

View File

@ -1,10 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { PrinterConfiguration } from "../domain/printer-configuration.aggregate"; import type { PrinterConfiguration } from "../domain/printer-configuration.aggregate";
import { import type { PrinterConfigurationInput } from "../domain/printer-configuration-values";
PrinterConfigurationError, import { PrinterConfigurationError } from "../domain/printer-configuration-values";
type PrinterConfigurationInput,
} from "../domain/printer-configuration-values";
import { PrinterConfigurationValues } from "../domain/value-objects/printer-configuration-values"; import { PrinterConfigurationValues } from "../domain/value-objects/printer-configuration-values";
import type { PrinterContext } from "./printer-configuration.repository"; import type { PrinterContext } from "./printer-configuration.repository";

View File

@ -2,10 +2,8 @@ import { UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { PrinterConfiguration } from "../domain/printer-configuration.aggregate"; import { PrinterConfiguration } from "../domain/printer-configuration.aggregate";
import { import type { PrinterConfigurationInput } from "../domain/printer-configuration-values";
PrinterConfigurationError, import { PrinterConfigurationError } from "../domain/printer-configuration-values";
type PrinterConfigurationInput,
} from "../domain/printer-configuration-values";
import type { IPrinterConfigurationRepository } from "./printer-configuration.repository"; import type { IPrinterConfigurationRepository } from "./printer-configuration.repository";
import { mapPrinterInput } from "./printer-configuration-input.mapper"; import { mapPrinterInput } from "./printer-configuration-input.mapper";

View File

@ -4,12 +4,12 @@ import { Maybe, Result } from "@repo/rdx-utils";
import { PersonProcess } from "../domain/process.aggregate"; import { PersonProcess } from "../domain/process.aggregate";
import { ProcessTask } from "../domain/process-task.entity"; import { ProcessTask } from "../domain/process-task.entity";
import { ProcessTemplate } from "../domain/process-template.aggregate"; import { ProcessTemplate } from "../domain/process-template.aggregate";
import { import type {
ProcessError, ProcessTemplateInput,
type ProcessTemplateInput, StartProcessInput,
type StartProcessInput, UpdateProcessInput,
type UpdateProcessInput,
} from "../domain/process-values"; } from "../domain/process-values";
import { ProcessError } from "../domain/process-values";
import { ProcessTemplateValues } from "../domain/value-objects/process-template-values"; import { ProcessTemplateValues } from "../domain/value-objects/process-template-values";
import { ProcessValues } from "../domain/value-objects/process-values"; import { ProcessValues } from "../domain/value-objects/process-values";

View File

@ -1,10 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { RemoteConnection } from "../domain/remote-connection.aggregate"; import type { RemoteConnection } from "../domain/remote-connection.aggregate";
import { import type { RemoteConnectionInput } from "../domain/remote-connection-values";
RemoteConnectionError, import { RemoteConnectionError } from "../domain/remote-connection-values";
type RemoteConnectionInput,
} from "../domain/remote-connection-values";
import { RemoteConnectionValues } from "../domain/value-objects/remote-connection-values"; import { RemoteConnectionValues } from "../domain/value-objects/remote-connection-values";
import type { RemoteContext } from "./remote-connection.repository"; import type { RemoteContext } from "./remote-connection.repository";

View File

@ -1,10 +1,8 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { TeleworkAuthorization } from "../domain/telework-authorization.aggregate"; import type { TeleworkAuthorization } from "../domain/telework-authorization.aggregate";
import { import type { TeleworkAuthorizationInput } from "../domain/telework-authorization-values";
TeleworkAuthorizationError, import { TeleworkAuthorizationError } from "../domain/telework-authorization-values";
type TeleworkAuthorizationInput,
} from "../domain/telework-authorization-values";
import { TeleworkAuthorizationValues } from "../domain/value-objects/telework-authorization-values"; import { TeleworkAuthorizationValues } from "../domain/value-objects/telework-authorization-values";
import type { TeleworkAuthorizationContext } from "./telework-authorization.repository"; import type { TeleworkAuthorizationContext } from "./telework-authorization.repository";

View File

@ -1,10 +1,12 @@
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import type { Ticket } from "../domain/ticket.aggregate"; import type { Ticket } from "../domain/ticket.aggregate";
import { TicketError, type TicketInput } from "../domain/ticket-values"; import type { TicketInput } from "../domain/ticket-values";
import { TicketError } from "../domain/ticket-values";
import { TicketValues } from "../domain/value-objects/ticket-values"; import { TicketValues } from "../domain/value-objects/ticket-values";
import { type TicketOption, type TicketSaveContext, targetOptions } from "./ticket.repository"; import type { TicketOption, TicketSaveContext } from "./ticket.repository";
import { targetOptions } from "./ticket.repository";
function available( function available(
options: readonly TicketOption[], options: readonly TicketOption[],

View File

@ -2,7 +2,8 @@ import { Result } from "@repo/rdx-utils";
import { WorkProfileValues } from "../domain/value-objects/work-profile-values"; import { WorkProfileValues } from "../domain/value-objects/work-profile-values";
import type { WorkProfile } from "../domain/work-profile.aggregate"; import type { WorkProfile } from "../domain/work-profile.aggregate";
import { WorkProfileError, type WorkProfileInput } from "../domain/work-profile-values"; import type { WorkProfileInput } from "../domain/work-profile-values";
import { WorkProfileError } from "../domain/work-profile-values";
import type { WorkProfileContext } from "./work-profile.repository"; import type { WorkProfileContext } from "./work-profile.repository";

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import type { Maybe } from "@repo/rdx-utils"; import type { Maybe } from "@repo/rdx-utils";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";

View File

@ -1,4 +1,5 @@
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { DomainEntity } from "@repo/rdx-ddd";
import type { ApplicationConfigurationValues } from "./value-objects/application-configuration-values"; import type { ApplicationConfigurationValues } from "./value-objects/application-configuration-values";
export type ApplicationConfigurationRevisionProps = Readonly<{ export type ApplicationConfigurationRevisionProps = Readonly<{

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";

View File

@ -9,10 +9,8 @@ import { buildApplicationResponse } from "../application/application-configurati
import { ApplicationConfigurationPersistenceMapper } from "../infrastructure/persistence/application-configuration-persistence.mapper"; import { ApplicationConfigurationPersistenceMapper } from "../infrastructure/persistence/application-configuration-persistence.mapper";
import { ApplicationConfiguration } from "./application-configuration.aggregate"; import { ApplicationConfiguration } from "./application-configuration.aggregate";
import { import type { ApplicationConfigurationInput } from "./application-configuration-values";
type ApplicationConfigurationInput, import { ApplicationConfigurationInputSchema } from "./application-configuration-values";
ApplicationConfigurationInputSchema,
} from "./application-configuration-values";
import { ApplicationConfigurationValues } from "./value-objects/application-configuration-values"; import { ApplicationConfigurationValues } from "./value-objects/application-configuration-values";
import { WorkProfileValues } from "./value-objects/work-profile-values"; import { WorkProfileValues } from "./value-objects/work-profile-values";
import { WorkProfile } from "./work-profile.aggregate"; import { WorkProfile } from "./work-profile.aggregate";

View File

@ -1,4 +1,5 @@
import { DomainEntity, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { DomainEntity } from "@repo/rdx-ddd";
import type { CertificateConfigurationValues } from "./value-objects/certificate-configuration-values"; import type { CertificateConfigurationValues } from "./value-objects/certificate-configuration-values";
export type CertificateConfigurationRevisionProps = Readonly<{ export type CertificateConfigurationRevisionProps = Readonly<{

View File

@ -1,4 +1,5 @@
import { AggregateRoot, type UniqueID } from "@repo/rdx-ddd"; import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils"; import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4"; import { z } from "zod/v4";

Some files were not shown because too many files have changed in this diff Show More