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 { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import type { ApplicationConfigurationInput } from "@metis/companies/common";
import {
type ApplicationConfigurationInput,
ApplicationConfigurationResponseSchema,
ApplicationConfigurationsResponseSchema,
CompanyResponseSchema,

View File

@ -7,7 +7,8 @@ import {
BrandsResponseSchema,
CatalogResponseSchema,
} 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 {

View File

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

View File

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

View File

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

View File

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

View File

@ -2,17 +2,16 @@ import assert from "node:assert/strict";
import { randomUUID } from "node:crypto";
import { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import type { ProcessTemplateInput, UpdateProcessInput } from "@metis/companies/common";
import {
ApplicationConfigurationResponseSchema,
CompanyResponseSchema,
ProcessResponseSchema,
ProcessReviewResponseSchema,
type ProcessTemplateInput,
ProcessTemplateResponseSchema,
ProcessTemplatesResponseSchema,
ProcessesResponseSchema,
RemoteConnectionResponseSchema,
type UpdateProcessInput,
WorkProfileResponseSchema,
} from "@metis/companies/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 { InventoryResponseSchema as AssetsSchema } from "@metis/assets/common";
import type { RemoteConnectionInput } from "@metis/companies/common";
import {
CompanyResponseSchema,
type RemoteConnectionInput,
RemoteConnectionResponseSchema,
RemoteConnectionsResponseSchema,
TeleworkAuthorizationResponseSchema,

View File

@ -4,7 +4,8 @@ import { AccessResponseSchema, MailboxesResponseSchema } from "@metis/companies/
import type { ResourceDTO } from "@metis/resources/common";
import { InventoryResponseSchema, ResourceResponseSchema } from "@metis/resources/common";
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";
type Request = (path: string, body?: unknown) => Promise<Response>;

View File

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

View File

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

View File

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

View File

@ -203,3 +203,66 @@ test("los enlaces del dashboard mantienen la compañía y la ficha exacta", () =
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
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).
## 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
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 { Result } from "@repo/rdx-utils";
import {
BrandError,
type BrandInput,
BrandInputSchema,
type BrandProps,
} from "../domain/brand.aggregate";
import type { BrandInput, BrandProps } from "../domain/brand.aggregate";
import { BrandError, BrandInputSchema } from "../domain/brand.aggregate";
export function mapBrandInput(input: BrandInput, builtin = false): Result<BrandProps, BrandError> {
const parsed = BrandInputSchema.safeParse(input);
if (!parsed.success) {

View File

@ -1,7 +1,8 @@
import { UniqueID } from "@repo/rdx-ddd";
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 { mapBrandInput } from "./brand-input.mapper";

View File

@ -1,7 +1,8 @@
import { Result } from "@repo/rdx-utils";
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 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 { 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 { 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 { 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 { 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 { z } from "zod/v4";

View File

@ -8,10 +8,8 @@ import { AssetCatalog } from "./asset-catalog.aggregate";
import { seedAssetCatalog } from "./asset-catalog-seed";
import { AssetRelationship } from "./asset-relationship.aggregate";
import { AssetDefinitionValues } from "./value-objects/asset-definition-values";
import {
AssetRelationshipValues,
type RelationshipValuesData,
} from "./value-objects/asset-relationship-values";
import type { RelationshipValuesData } from "./value-objects/asset-relationship-values";
import { AssetRelationshipValues } from "./value-objects/asset-relationship-values";
const sourceId = 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";
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 { 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 { 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";
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 { 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 { 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 }>;
export class SupportTool extends AggregateRoot<Props> {

View File

@ -1,7 +1,9 @@
import { UniqueID } from "@repo/rdx-ddd";
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 {
private constructor(
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 { 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 {
private constructor(
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";
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 { 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 = {
eventId?: string;

View File

@ -1,5 +1,6 @@
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 { mapBrandInput } from "../../application/brand-input.mapper";

View File

@ -1,5 +1,6 @@
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";

View File

@ -1,6 +1,7 @@
import type { UniqueID } from "@repo/rdx-ddd";
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 { AssetInputSchema } from "../../domain/asset-values";

View File

@ -1,5 +1,6 @@
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) {
const query = database.getQueryInterface();
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 { Button } from "@repo/shadcn-ui/components/button";
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 { Label } from "@repo/shadcn-ui/components/label";
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 { BrandSelect } from "./brand-select";
import { CatalogTemplateForm } from "./catalog-template-form";
import { CatalogTemplateCreateDialog } from "./catalog-template-create-dialog";
import { inventoryApi, inventoryKeys } from "./inventory-api";
import { InventoryFeedback } from "./inventory-feedback";
@ -294,32 +294,22 @@ export function AssetForm({
</FormActions>
</form>
{creatingTemplate && (
<FormDialog
cancelLabel={t("cancel")}
formActions
<CatalogTemplateCreateDialog
csrfToken={csrfToken}
items={items}
onOpenChange={(open) => {
if (!open) {
setCreatingTemplate(false);
}
}}
open
title={t("newTemplate")}
wide
>
<p className="text-sm text-muted-foreground">{t("relatedNotice")}</p>
<CatalogTemplateForm
allowQuickCreate={false}
csrfToken={csrfToken}
items={items}
onSaved={(id) => {
form.setValue("templateId", id);
form.setValue("characteristics", {});
form.setValue("brandId", null);
setCreatingTemplate(false);
}}
scope={companyId}
/>
</FormDialog>
onSaved={(id) => {
form.setValue("templateId", id);
form.setValue("characteristics", {});
form.setValue("brandId", null);
setCreatingTemplate(false);
}}
scope={companyId}
/>
)}
</>
);

View File

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

View File

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

View File

@ -1,6 +1,7 @@
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 class BrandApiError extends Error {
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 { 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";

View File

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

View File

@ -6,7 +6,6 @@ import {
EntityStatus,
EntityTable,
} 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 { useQuery } from "@tanstack/react-query";
import { useId, useState } from "react";
@ -15,7 +14,7 @@ import { useTranslation } from "react-i18next";
import type { BrandDTO } from "../common/brands";
import { brandApi, brandKeys } from "./brand-api";
import { BrandForm } from "./brand-form";
import { BrandEditorDialog } from "./brand-editor-dialog";
export function BrandCatalogPage({ csrfToken }: { csrfToken: string }) {
const { t, i18n } = useTranslation("assets");
@ -125,20 +124,13 @@ export function BrandCatalogPage({ csrfToken }: { csrfToken: string }) {
title={t("brands.title")}
unavailable={query.isError}
/>
<FormDialog
cancelLabel={t("cancel")}
formActions
<BrandEditorDialog
csrfToken={csrfToken}
onOpenChange={setOpen}
open={open}
title={t(edit ? "brands.edit" : "brands.new")}
>
<BrandForm
csrfToken={csrfToken}
key={edit?.id ?? "new"}
{...(edit ? { brand: edit } : {})}
onSaved={() => setOpen(false)}
/>
</FormDialog>
{...(edit ? { brand: edit } : {})}
onSaved={() => setOpen(false)}
/>
</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 { 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 { useState } from "react";
import { useTranslation } from "react-i18next";
@ -10,16 +9,9 @@ import type { CatalogItemDTO } from "../common";
import { catalogApi, catalogKeys } from "./catalog-api";
import { CatalogCell } from "./catalog-cell";
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 { 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 }) {
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 [search, setSearch] = useState("");
const [viewId, setViewId] = useState<string>();
const [editor, setEditor] = useState<Editor>();
const [editor, setEditor] = useState<CatalogEditor>();
const query = useQuery({
queryKey: catalogKeys.scope(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)} />
{editor && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setEditor(undefined);
}
}}
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>
<CatalogEditorDialog
csrfToken={csrfToken}
editor={editor}
items={items}
onClose={() => setEditor(undefined)}
scope={scope}
/>
)}
</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 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 { 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 { Button } from "@repo/shadcn-ui/components/button";
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 { Label } from "@repo/shadcn-ui/components/label";
import { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
@ -15,7 +15,7 @@ import type { CatalogItemDTO } from "../common";
import { AssetDefinitionSchema } from "../common";
import { CatalogError } from "./catalog-error";
import { CatalogFieldForm } from "./catalog-form";
import { CatalogFieldCreateDialog } from "./catalog-field-create-dialog";
import { CatalogHelp } from "./catalog-help";
import type { CatalogSaved } from "./use-catalog-save";
import { useCatalogSave } from "./use-catalog-save";
@ -173,27 +173,19 @@ export function CatalogTemplateForm({
</FormActions>
</form>
{creatingField && (
<FormDialog
cancelLabel={t("cancel")}
formActions
<CatalogFieldCreateDialog
csrfToken={csrfToken}
onOpenChange={(open) => {
if (!open) {
setCreatingField(false);
}
}}
open
title={t("newField")}
>
<p className="text-sm text-muted-foreground">{t("quickNotice")}</p>
<CatalogFieldForm
csrfToken={csrfToken}
onSaved={(fieldId) => {
add(fieldId);
setCreatingField(false);
}}
scope={scope}
/>
</FormDialog>
onSaved={(fieldId) => {
add(fieldId);
setCreatingField(false);
}}
scope={scope}
/>
)}
</>
);

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 { AssetResponseSchema, InventoryResponseSchema } from "../common/inventory";
import { type RelationshipInputDTO, RelationshipResponseSchema } from "../common/relationships";
import { type WorkstationInput, WorkstationResponseSchema } from "../common/workstations";
import type { RelationshipInputDTO } from "../common/relationships";
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 class InventoryApiError extends Error {
constructor(public readonly code: string) {

View File

@ -6,17 +6,14 @@ import {
EntityStatus,
EntityTable,
} from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
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 { AssetForm } from "./asset-form";
import { AssetRelationships } from "./asset-relationships";
import { AssetSelectionPanel } from "./asset-selection-panel";
import { AssetSupport } from "./asset-support";
import { catalogApi, catalogKeys } from "./catalog-api";
import { CatalogError } from "./catalog-error";
import { inventoryApi, inventoryKeys } from "./inventory-api";
@ -216,54 +213,28 @@ export function AssetInventoryPage({
open={detailOpen}
/>
{editor && catalog.data && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setEditor(undefined);
}
<AssetEditorDialog
asset={edited}
companyId={companyId}
csrfToken={csrfToken}
items={catalog.data.items}
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 && (
<FormDialog
cancelLabel={t("close")}
onOpenChange={(open) => {
if (!open) {
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>
<AssetDetailDialog
asset={viewed}
companyId={companyId}
csrfToken={csrfToken}
lookups={lookups}
onClose={() => setViewId(undefined)}
/>
)}
</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 { 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 { SelectWithAction } from "@repo/shadcn-ui/components/select-with-action";
import { Textarea } from "@repo/shadcn-ui/components/textarea";
@ -11,7 +11,7 @@ import { useTranslation } from "react-i18next";
import type { RelationshipInputDTO } from "../common";
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 { InventoryFeedback } from "./inventory-feedback";
@ -112,23 +112,15 @@ export function RelationshipForm({
</FormActions>
</form>
{quick && (
<FormDialog
cancelLabel={t("cancel")}
formActions
<CatalogRelationCreateDialog
companyId={companyId}
csrfToken={csrfToken}
onOpenChange={setQuick}
open
title={t("newRelation")}
>
<p className="text-sm text-muted-foreground">{t("quickRelationNotice")}</p>
<CatalogRelationForm
csrfToken={csrfToken}
onSaved={(savedId) => {
setTypeId(savedId);
setQuick(false);
}}
scope={companyId}
/>
</FormDialog>
onSaved={(savedId) => {
setTypeId(savedId);
setQuick(false);
}}
/>
)}
</>
);

View File

@ -1,10 +1,9 @@
import { z } from "zod/v4";
import type { SupportInput, SupportToolInput } from "../common/support";
import {
type SupportInput,
SupportListSchema,
SupportResponseSchema,
type SupportToolInput,
SupportToolResponseSchema,
SupportToolsSchema,
} 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 { 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 { Label } from "@repo/shadcn-ui/components/label";
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 { useTranslation } from "react-i18next";
import {
type SupportConnectionDTO,
type SupportInput,
type SupportToolDTO,
SupportValuesSchema,
} from "../common/support";
import type { SupportConnectionDTO, SupportInput, SupportToolDTO } from "../common/support";
import { SupportValuesSchema } from "../common/support";
import { supportApi, supportKeys } from "./support-api";
import { SupportFeedback } from "./support-feedback";
import { SupportHelp } from "./support-help";
import { SupportToolForm } from "./support-tool-form";
import { SupportToolEditorDialog } from "./support-tool-editor-dialog";
export function SupportForm({
companyId,
assetId,
@ -135,21 +131,16 @@ export function SupportForm({
</Button>
</FormActions>
</form>
<FormDialog
cancelLabel={t("cancel")}
formActions
<SupportToolEditorDialog
csrfToken={csrfToken}
edit={undefined}
onOpenChange={setQuick}
onSaved={(id) => {
form.setValue("toolId", id);
setQuick(false);
}}
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 { useTranslation } from "react-i18next";
import {
type SupportToolDTO,
type SupportToolInput,
SupportToolInputSchema,
} from "../common/support";
import type { SupportToolDTO, SupportToolInput } from "../common/support";
import { SupportToolInputSchema } from "../common/support";
import { supportApi, supportKeys } from "./support-api";
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,
EntityTable,
} from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@ -14,7 +13,7 @@ import { useTranslation } from "react-i18next";
import type { SupportToolDTO } from "../common/support";
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 }) {
const { t, i18n } = useTranslation("assets");
const query = useQuery({ queryKey: supportKeys.tools, queryFn: () => supportApi.tools() });
@ -102,20 +101,13 @@ export function SupportTools({ csrfToken }: { csrfToken: string }) {
title={t("support.tools")}
unavailable={query.isError}
/>
<FormDialog
cancelLabel={t("cancel")}
formActions
<SupportToolEditorDialog
csrfToken={csrfToken}
edit={edit}
onOpenChange={setOpen}
onSaved={() => setOpen(false)}
open={open}
title={t(edit ? "support.editTool" : "support.newTool")}
>
<SupportToolForm
csrfToken={csrfToken}
key={edit?.id ?? "new"}
{...(edit ? { tool: edit } : {})}
onSaved={() => setOpen(false)}
/>
</FormDialog>
/>
</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 { Button } from "@repo/shadcn-ui/components/button";
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 { Label } from "@repo/shadcn-ui/components/label";
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 { 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 { AssetForm } from "./asset-form";
import { catalogApi, catalogKeys } from "./catalog-api";
import { inventoryApi, inventoryKeys } from "./inventory-api";
import { InventoryFeedback } from "./inventory-feedback";
import { WorkstationAssetCreateDialog } from "./workstation-asset-create-dialog";
export function WorkstationForm({
companyId,
csrfToken,
@ -257,27 +258,17 @@ export function WorkstationForm({
</FormActions>
</form>
{quick && catalog.data && (
<FormDialog
cancelLabel={t("cancel")}
formActions
<WorkstationAssetCreateDialog
companyId={companyId}
csrfToken={csrfToken}
items={catalog.data.items}
lookups={lookups}
onOpenChange={setQuick}
open
title={t("newAsset")}
wide
>
<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>
onSaved={(id) => {
add(quickField, id);
setQuick(false);
}}
/>
)}
</>
);

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,
EntityTable,
} from "@repo/shadcn-ui/components/entity-list";
import { FormDialog } from "@repo/shadcn-ui/components/form-dialog";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@ -14,7 +13,8 @@ import { useTranslation } from "react-i18next";
import type { InventoryLookups } from "./asset-form";
import { inventoryApi, inventoryKeys } from "./inventory-api";
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({
companyId,
csrfToken,
@ -166,66 +166,22 @@ export function WorkstationsPage({
/>
)}
{editor && (
<FormDialog
cancelLabel={t("cancel")}
formActions
onOpenChange={(open) => {
if (!open) {
setEditor(undefined);
}
}}
open
title={t(edited ? "editWorkstation" : "newWorkstation")}
wide
>
<WorkstationForm
companyId={companyId}
csrfToken={csrfToken}
lookups={lookups}
onSaved={() => setEditor(undefined)}
workstation={edited}
/>
</FormDialog>
<WorkstationEditorDialog
companyId={companyId}
csrfToken={csrfToken}
lookups={lookups}
onClose={() => setEditor(undefined)}
workstation={edited}
/>
)}
{viewed && (
<FormDialog
cancelLabel={t("close")}
onOpenChange={(open) => {
if (!open) {
setView(undefined);
}
}}
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>
<WorkstationHistoryDialog
members={members}
onClose={() => setView(undefined)}
personName={personName}
siteName={siteName}
workstation={viewed}
/>
)}
</div>
);

View File

@ -1,10 +1,8 @@
import { Result } from "@repo/rdx-utils";
import type { ApplicationConfiguration } from "../domain/application-configuration.aggregate";
import {
ApplicationConfigurationError,
type ApplicationConfigurationInput,
} from "../domain/application-configuration-values";
import type { ApplicationConfigurationInput } from "../domain/application-configuration-values";
import { ApplicationConfigurationError } from "../domain/application-configuration-values";
import { ApplicationConfigurationValues } from "../domain/value-objects/application-configuration-values";
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 { ApplicationConfiguration } from "../domain/application-configuration.aggregate";
import {
ApplicationConfigurationError,
type ApplicationConfigurationInput,
} from "../domain/application-configuration-values";
import type { ApplicationConfigurationInput } from "../domain/application-configuration-values";
import { ApplicationConfigurationError } from "../domain/application-configuration-values";
import type { IApplicationConfigurationRepository } from "./application-configuration.repository";
import { mapApplicationInput } from "./application-configuration-input.mapper";

View File

@ -1,10 +1,8 @@
import { Result } from "@repo/rdx-utils";
import type { CertificateConfiguration } from "../domain/certificate-configuration.aggregate";
import {
CertificateConfigurationError,
type CertificateConfigurationInput,
} from "../domain/certificate-configuration-values";
import type { CertificateConfigurationInput } from "../domain/certificate-configuration-values";
import { CertificateConfigurationError } from "../domain/certificate-configuration-values";
import { CertificateConfigurationValues } from "../domain/value-objects/certificate-configuration-values";
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 { CertificateConfiguration } from "../domain/certificate-configuration.aggregate";
import {
CertificateConfigurationError,
type CertificateConfigurationInput,
} from "../domain/certificate-configuration-values";
import type { CertificateConfigurationInput } from "../domain/certificate-configuration-values";
import { CertificateConfigurationError } from "../domain/certificate-configuration-values";
import type { ICertificateConfigurationRepository } from "./certificate-configuration.repository";
import { mapCertificateInput } from "./certificate-configuration-input.mapper";

View File

@ -1,7 +1,8 @@
import { Result } from "@repo/rdx-utils";
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 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 { 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 { mapFolderInput } from "./folder-mapping-input.mapper";

View File

@ -1,7 +1,8 @@
import { Result } from "@repo/rdx-utils";
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 type { InterventionSaveContext } from "./intervention.repository";

View File

@ -1,10 +1,8 @@
import { UniqueID } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import {
type SaveInterventionTemplateInput,
SaveInterventionTemplateSchema,
} from "../../common/intervention-templates";
import type { SaveInterventionTemplateInput } from "../../common/intervention-templates";
import { SaveInterventionTemplateSchema } from "../../common/intervention-templates";
import { InterventionTemplate } from "../domain/intervention-template.aggregate";
import { InterventionTemplateError } from "../domain/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 type { MailConfiguration } from "../domain/mail-configuration.aggregate";
import {
MailConfigurationError,
type MailConfigurationInput,
} from "../domain/mail-configuration-values";
import type { MailConfigurationInput } from "../domain/mail-configuration-values";
import { MailConfigurationError } from "../domain/mail-configuration-values";
import { MailConfigurationValues } from "../domain/value-objects/mail-configuration-values";
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 { MailConfiguration } from "../domain/mail-configuration.aggregate";
import {
MailConfigurationError,
type MailConfigurationInput,
} from "../domain/mail-configuration-values";
import type { MailConfigurationInput } from "../domain/mail-configuration-values";
import { MailConfigurationError } from "../domain/mail-configuration-values";
import type { IMailConfigurationRepository } from "./mail-configuration.repository";
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 { 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 { 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 { 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 { mapMailboxInput, mapMailboxUseInput } from "./mailbox-input.mapper";

View File

@ -1,10 +1,8 @@
import { Result } from "@repo/rdx-utils";
import type { PrinterConfiguration } from "../domain/printer-configuration.aggregate";
import {
PrinterConfigurationError,
type PrinterConfigurationInput,
} from "../domain/printer-configuration-values";
import type { PrinterConfigurationInput } from "../domain/printer-configuration-values";
import { PrinterConfigurationError } from "../domain/printer-configuration-values";
import { PrinterConfigurationValues } from "../domain/value-objects/printer-configuration-values";
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 { PrinterConfiguration } from "../domain/printer-configuration.aggregate";
import {
PrinterConfigurationError,
type PrinterConfigurationInput,
} from "../domain/printer-configuration-values";
import type { PrinterConfigurationInput } from "../domain/printer-configuration-values";
import { PrinterConfigurationError } from "../domain/printer-configuration-values";
import type { IPrinterConfigurationRepository } from "./printer-configuration.repository";
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 { ProcessTask } from "../domain/process-task.entity";
import { ProcessTemplate } from "../domain/process-template.aggregate";
import {
ProcessError,
type ProcessTemplateInput,
type StartProcessInput,
type UpdateProcessInput,
import type {
ProcessTemplateInput,
StartProcessInput,
UpdateProcessInput,
} from "../domain/process-values";
import { ProcessError } from "../domain/process-values";
import { ProcessTemplateValues } from "../domain/value-objects/process-template-values";
import { ProcessValues } from "../domain/value-objects/process-values";

View File

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

View File

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

View File

@ -1,10 +1,12 @@
import { Result } from "@repo/rdx-utils";
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 { type TicketOption, type TicketSaveContext, targetOptions } from "./ticket.repository";
import type { TicketOption, TicketSaveContext } from "./ticket.repository";
import { targetOptions } from "./ticket.repository";
function available(
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 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";

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 { 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";
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 { 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 { ApplicationConfiguration } from "./application-configuration.aggregate";
import {
type ApplicationConfigurationInput,
ApplicationConfigurationInputSchema,
} from "./application-configuration-values";
import type { ApplicationConfigurationInput } from "./application-configuration-values";
import { ApplicationConfigurationInputSchema } from "./application-configuration-values";
import { ApplicationConfigurationValues } from "./value-objects/application-configuration-values";
import { WorkProfileValues } from "./value-objects/work-profile-values";
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";
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 { z } from "zod/v4";

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