Mejora UX post-refactor (PR #61): las 4 secciones del sidebar expandido son
ahora colapsables individualmente, y el modo colapsado reemplaza la lista
larga de iconos por un icono por grupo con fly-out panel on hover.
Expandido (240px):
- Click en header de sección (Seguridad/Maestros/Catálogo/Tasación) toggle
collapse con chevron que rota.
- Default: todas colapsadas EXCEPTO la que contiene la ruta activa
(auto-expand override).
- Sección activa tiene el header disabled + sin chevron (no se puede colapsar
mientras estás ahí — evita esconder items de la ruta actual).
- Preferencia per-sección persistida en localStorage.
Colapsado (68px):
- Un icono por grupo en lugar de listar TODOS los items (evitando scroll
largo en usuarios con muchos permisos).
- Hover sobre el grupo despliega un fly-out panel al lado derecho con el
título del grupo + sus items clickeables.
- Grupo que contiene la ruta activa tiene un dot indicator.
- Icons de grupo: ShieldCheck (Seguridad), Building2 (Maestros),
Package (Catálogo), Calculator (Tasación).
Accessibility:
- Headers expandidos: aria-expanded refleja estado.
- Fly-out: aria-haspopup='menu' + role='menu' + keyboard focus.
z-index management (pedido explícito del user):
- aside wrapper en ProtectedLayout: z-10 -> z-30 (sobre contenido).
- HoverCardContent del fly-out: z-[60] (sobre cualquier overlay app-level,
excepto modal dialogs que siguen siendo z-50 por convención Radix).
- hover-card.tsx: envuelto en HoverCardPrimitive.Portal (faltaba en el
shadcn generated) — previene que el fly-out quede cortado por overflow
del aside.
Dependencies:
- shadcn hover-card agregado via 'npx shadcn@latest add' (+ @radix-ui/react-hover-card).
Tests:
- 16 tests (antes 10) — agregados 6 casos: default collapsed except active,
click toggle expand/collapse, aria-expanded reflection, disabled header
when active, root route collapses all, localStorage persistence.
Problem: sidebar was growing unwieldy — 4 top-level disabled items marked
'Próx.' acted as visual noise, and 12 admin items sat in a flat list with
no grouping (hard to scan).
Changes:
- Remove the 4 disabled top-level items (Ventas, Tasación, Integraciones,
Administración-as-link). Those features will surface via the admin
subsections when actually implemented, not as placeholder ghosts.
- Group the 12 admin items into 4 domain-aligned sections:
- Seguridad: Usuarios, Crear Usuario, Roles, Permisos, Auditoría
- Maestros: Medios, Secciones, Puntos de Venta
- Catálogo: Rubros, Tipos de Producto, Productos
- Tasación: Caracteres Tasables
- Sections auto-hide when no item passes the permission filter, preventing
empty headers for users with limited roles.
- Dashboard remains as the single top-level nav item (always visible).
TDD: new AppSidebar.test.tsx covers 10 scenarios — section rendering,
permission filtering, section auto-hide, role gating, active-route marking,
and section ordering.
Resuelve 4 de los followups creados post-archive de PRC-001:
#55 — Decisión de negocio (2026-04-21): emojis NO se permiten en Symbol config.
- WordCounterService.ContainsEmoji(string): helper publico que reutiliza los
rangos Unicode de IsEmojiRune (Emoticons, Pictographs, Dingbats, VS-16, ZWJ).
- CreateChargeableCharConfigCommandValidator: regla .Must que rechaza emoji
en Symbol con mensaje claro. Defensiva: cubre clientes directos al API
(Postman, adversariales) mas alla del SymbolInput blocker del frontend.
- Tests: 5 emojis positivos (smile/car/fire/heart VS-16/sun) + 8 plain symbols
($, %, !, ¡, @, €, ##, ABCD) + actualizacion del Api test E2E (Post_WithEmojiSymbol).
#57 — Alineacion FluentValidation con opt-in billing (CK_Price_NonNegative >= 0).
- CreateChargeableCharConfigCommandValidator.PricePerUnit: GreaterThan(0)
-> GreaterThanOrEqualTo(0). Mensaje explica el significado: 0 = no cobra.
- Tests actualizados: PricePerUnit_Zero ahora Passes (era Fails). Negative
sigue fallando. Api e2e usa -1 para el caso de rechazo.
#58 — tsconfig ignoreDeprecations + MSW handler (parte a).
- src/web/tsconfig.json: agrega "ignoreDeprecations": "6.0" para silenciar
el warning TS5101 del baseUrl deprecated en TS 6.x.
- (El MSW handler de /api/v1/admin/product-types no aplica — los tests ya
mockean ProductTypeSelect directamente; warning residual no existe.)
#54 — Seeder demo de overrides ficticios per-ProductType (V025).
- database/migrations/V025__seed_chargeable_char_overrides_demo.sql:
MERGE idempotente que crea overrides de ChargeableCharConfig para
ProductTypes Clasificado/Notables/Fúnebres si existen en la DB.
Precios ficticios ($ 5-8, % 3-5, ! 2-4, ¡ 2-4). No-op si los tipos no
estan seedados (sera cuando PRD-008 haga seed de los 12 legacy).
- V025_ROLLBACK.sql: elimina overrides demo preservando globales.
- Aplicado en SIGCM2, SIGCM2_Test_App, SIGCM2_Test_Api.
- database/README.md: V025 agregada al indice.
Tests: 1659 .NET (1310 Application + 349 Api) + 510 vitest — todos GREEN.
Closes#54, #55, #57, #58.
User feedback from smoke test: the FAC-001 reference is future-coupled — when
the invoicing module lands we would need to remember to update the dialog text.
Switched to a timeless message that describes the current behavior: 'La
eliminación es posible porque este carácter no está en uso.' It stays accurate
before and after the FAC-001 usage guard ships.
Three bugs surfaced while user smoke-testing Reactivate:
1. ReactivateAsync opened a SECOND connection for GetByIdAsync after the SP
call, inside the ambient TransactionScope. This promoted the tx to DTC
(distributed) which requires MSDTC — typically not enabled on dev/prod
servers. The API returned an opaque 500. Fix: run the post-SP SELECT on
the SAME connection (local tx stays lightweight / LTM).
2. Agent 1's V023 test refactor wrote 'INSERT INTO dbo.ProductType (Nombre,
Codigo, Activo)' in 2 test files — but dbo.ProductType has no 'Codigo' or
'Activo' columns (schema is Nombre + IsActive + flags + multimedia limits).
Fix: use '(Nombre, HasDuration, RequiresText, RequiresCategory, IsBundle,
AllowImages)' matching the other test files (ProductQueryRepositoryTests,
ProductRepositoryTests, ProductPriceRepositoryIntegrationTests).
3. SqlTestFixture.EnsureV021SchemaAsync unconditionally ALTERed the V021-era
SPs with '@MedioId' body. On second fixture run after V023 had already
refactored the table, ALTER PROCEDURE body referenced a MedioId column
that no longer existed — 'Invalid column name MedioId'. Fix: guard the
V021 SP ALTERs + seedV022 behind 'MedioId column exists' check. If V023
already dropped MedioId, skip V021 re-install; EnsureV023SchemaAsync
still recreates SPs with @ProductTypeId.
4. PricingExceptionTests still used 'medioId:' named-arg + '.MedioId' — Agent 2
renamed the exception property but not these 6 test references.
Tests: 1297/1297 Application.Tests green.
Part A — MedioId → ProductTypeId rename across all C# layers:
Domain, Application, Infrastructure, API, all test projects.
Solution was non-compilable after BD refactor (5c1675e); now compiles clean (0 errors).
Part B — PATCH /api/v1/admin/chargeable-chars/{id}/reactivate:
ReactivateChargeableCharConfigCommand/Handler, SP guard maps 50410/50411/50412
→ ChargeableCharConfigReactivationNotAllowedException(Reason) → HTTP 409.
Part C — DELETE /api/v1/admin/chargeable-chars/{id}:
DeleteChargeableCharConfigCommand/Handler, physical DELETE on SYSTEM_VERSIONED table.
KeyNotFoundException → 404 via ExceptionFilter.
Tests: +30 unit tests (TDD RED→GREEN). All 1266 unit tests pass.
- GET /api/v1/products/{id}/prices now returns PagedResult<ProductPriceDto>
with OFFSET/FETCH + COUNT via Dapper (two queries on same connection)
- Query params: ?page (default 1) and ?pageSize (default 20, max 100)
- Clamping: Math.Max(1, page) + Math.Clamp(pageSize, 1, 100) in handler
- Auth upgraded from [Authorize] to [RequirePermission("catalogo:productos:gestionar")]
- IProductPriceRepository.GetByProductIdAsync signature updated to paginated form
- AddProductPriceCommandHandler adapted to read back via page=1, pageSize=2
- TDD cycle: RED (tests updated to PagedResult shape) -> GREEN (implementation) -> REFACTOR
- Tests: 1418 total (1106 Application + 312 Api), 0 failures
closes#47
- Create src/web/src/lib/formatters.ts with all exports from both modules
- Migrate all 14 import sites to @/lib/formatters (Opción A — immediate migration)
- Replace dateFormat.test.ts with formatters.test.ts including 10 smoke tests + full suite
- Delete src/web/src/lib/dateFormat.ts and numberFormat.ts
- 464 tests green, tsc clean (TS5101 warning is pre-existing)
Mapea RubroConProductosActivosException → HTTP 409 con error code
rubro_con_productos_activos. Test e2e usa DI override (patrón issue #36)
para stub IProductQueryRepository sin sembrar Products reales en DB.
Implementa SELECT COUNT(1) FROM dbo.Product WHERE RubroId = @RubroId AND IsActive = 1.
Tests de integración verifican: 0 sin productos, count correcto con mix
activos/inactivos/otro rubro, y solo inactivos retorna 0.
Extiende IProductQueryRepository con CountActiveByRubroAsync, inyecta
el repositorio en el handler e intercala el chequeo después del guard
de hijos activos. Tests de unidad cubren: throw, success con 0 productos,
y estabilidad del orden de guardas (hijos primero).
Reemplaza el stub con nulls por queryClient.fetchQuery con getProductTypeById,
deshabilitando el botón durante la carga y mostrando toast.error si falla.
Implements full frontend for PRD-002: 5 API fns, 5 hooks (useProducts,
useCreateProduct, useUpdateProduct, useDeactivateProduct), ProductForm,
ProductFormDialog, DeactivateProductDialog, ProductsPage with CanPerform
gating. Router entry at /admin/products and sidebar link added. 19 Vitest
tests GREEN (api, hooks, page).
Conecta ProductTypeFormDialog (create/edit) y DeactivateProductTypeDialog
en ProductTypesPage: botón "Nuevo Tipo", acción Editar por fila, acción
Desactivar por fila, empty state CTA "Crear primer tipo".
9 tests nuevos de page integration. Total: 390.