Refactorización
This commit is contained in:
parent
6bb9845c8c
commit
2a9d810d31
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
});
|
||||
|
||||
@ -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.
|
||||
|
||||
182
docs/inventario-de-modales.md
Normal file
182
docs/inventario-de-modales.md
Normal 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.
|
||||
149
docs/revision-de-buenas-practicas.md
Normal file
149
docs/revision-de-buenas-practicas.md
Normal 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.
|
||||
@ -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) {
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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<{
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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<{
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<{
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { QueryTypes, type Sequelize } from "sequelize";
|
||||
import type { Sequelize } from "sequelize";
|
||||
import { QueryTypes } from "sequelize";
|
||||
|
||||
type Entry = {
|
||||
eventId?: string;
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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 }>(
|
||||
|
||||
47
modules/assets/src/web/asset-detail-dialog.tsx
Normal file
47
modules/assets/src/web/asset-detail-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
49
modules/assets/src/web/asset-editor-dialog.tsx
Normal file
49
modules/assets/src/web/asset-editor-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
30
modules/assets/src/web/brand-editor-dialog.tsx
Normal file
30
modules/assets/src/web/brand-editor-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
79
modules/assets/src/web/catalog-editor-dialog.tsx
Normal file
79
modules/assets/src/web/catalog-editor-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
31
modules/assets/src/web/catalog-field-create-dialog.tsx
Normal file
31
modules/assets/src/web/catalog-field-create-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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";
|
||||
}
|
||||
|
||||
31
modules/assets/src/web/catalog-relation-create-dialog.tsx
Normal file
31
modules/assets/src/web/catalog-relation-create-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
41
modules/assets/src/web/catalog-template-create-dialog.tsx
Normal file
41
modules/assets/src/web/catalog-template-create-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
60
modules/assets/src/web/end-relationship-dialog.tsx
Normal file
60
modules/assets/src/web/end-relationship-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
37
modules/assets/src/web/relationship-editor-dialog.tsx
Normal file
37
modules/assets/src/web/relationship-editor-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -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";
|
||||
|
||||
47
modules/assets/src/web/support-connection-editor-dialog.tsx
Normal file
47
modules/assets/src/web/support-connection-editor-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
44
modules/assets/src/web/support-history-dialog.tsx
Normal file
44
modules/assets/src/web/support-history-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
38
modules/assets/src/web/support-tool-editor-dialog.tsx
Normal file
38
modules/assets/src/web/support-tool-editor-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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";
|
||||
|
||||
27
modules/assets/src/web/support-tools-dialog.tsx
Normal file
27
modules/assets/src/web/support-tools-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
44
modules/assets/src/web/workstation-asset-create-dialog.tsx
Normal file
44
modules/assets/src/web/workstation-asset-create-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
44
modules/assets/src/web/workstation-editor-dialog.tsx
Normal file
44
modules/assets/src/web/workstation-editor-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
61
modules/assets/src/web/workstation-history-dialog.tsx
Normal file
61
modules/assets/src/web/workstation-history-dialog.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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[],
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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<{
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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<{
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user