feat(F-194): completed feature
This commit is contained in:
@@ -7322,6 +7322,23 @@
|
|||||||
"security": false,
|
"security": false,
|
||||||
"qa": false
|
"qa": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "F-194",
|
||||||
|
"type": "fix",
|
||||||
|
"title": "Fix crypto.randomUUID runtime error and move IVA/audit/logs into settings",
|
||||||
|
"description": "1) Replace bare crypto.randomUUID() calls in POS PaymentModal and cashier page with the safe generateIdempotencyKey fallback. 2) Move tax-rates, audit and logs navigation and routes into the settings section.",
|
||||||
|
"priority": "high",
|
||||||
|
"risk": "low",
|
||||||
|
"status": "done",
|
||||||
|
"created_at": "2026-08-22",
|
||||||
|
"gates": {
|
||||||
|
"reviewer": true,
|
||||||
|
"security": true,
|
||||||
|
"qa": true,
|
||||||
|
"close": true
|
||||||
|
},
|
||||||
|
"completed_at": "2026-08-22T20:29:26Z"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useCallback, useRef } from 'react';
|
import { useState, useEffect, useCallback, useRef } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
import { auditApi, type AuditEntry } from '@/lib/api-client';
|
import { auditApi, type AuditEntry } from '@/lib/api-client';
|
||||||
|
|
||||||
const PAGE_SIZE = 50;
|
const PAGE_SIZE = 50;
|
||||||
@@ -79,6 +80,7 @@ export default function AuditLogPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
|
<Link href="/settings" className="text-sm text-[#2D6A4F] hover:underline">← Ajustes</Link>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Log de auditoría
|
<h1 className="text-2xl font-bold text-gray-900">Log de auditoría
|
||||||
{pollingActive && (
|
{pollingActive && (
|
||||||
<span className="ml-3 inline-flex items-center gap-1.5 text-xs text-green-600 font-medium align-middle">
|
<span className="ml-3 inline-flex items-center gap-1.5 text-xs text-green-600 font-medium align-middle">
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
import Link from 'next/link';
|
||||||
import { ServerLogViewer } from '@/components/ServerLogViewer';
|
import { ServerLogViewer } from '@/components/ServerLogViewer';
|
||||||
|
|
||||||
export default function ServerLogsPage() {
|
export default function ServerLogsPage() {
|
||||||
@@ -10,6 +11,7 @@ export default function ServerLogsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
|
<Link href="/settings" className="text-sm text-[#2D6A4F] hover:underline">← Ajustes</Link>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Logs del servidor</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Logs del servidor</h1>
|
||||||
<p className="text-sm text-gray-500 mt-0.5">
|
<p className="text-sm text-gray-500 mt-0.5">
|
||||||
Stream en tiempo real via SSE. Las líneas erróneas se resaltan en rojo. Máximo 200 líneas.
|
Stream en tiempo real via SSE. Las líneas erróneas se resaltan en rojo. Máximo 200 líneas.
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from 'react';
|
||||||
|
import Link from 'next/link';
|
||||||
import { settingsApi, type StoreSettings } from '@/lib/api-client';
|
import { settingsApi, type StoreSettings } from '@/lib/api-client';
|
||||||
|
|
||||||
type FormData = StoreSettings;
|
type FormData = StoreSettings;
|
||||||
@@ -105,6 +106,17 @@ export default function SettingsPage() {
|
|||||||
{t.label}
|
{t.label}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
<div className="my-3 border-t border-gray-200" />
|
||||||
|
<p className="px-4 text-xs font-semibold uppercase tracking-wide text-gray-400">Sistema</p>
|
||||||
|
<Link href="/settings/tax-rates" className="flex w-full items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-600 hover:bg-gray-100 transition-colors">
|
||||||
|
<span>💰</span> IVA
|
||||||
|
</Link>
|
||||||
|
<Link href="/settings/audit" className="flex w-full items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-600 hover:bg-gray-100 transition-colors">
|
||||||
|
<span>📋</span> Auditoría
|
||||||
|
</Link>
|
||||||
|
<Link href="/settings/logs" className="flex w-full items-center gap-2 px-4 py-2.5 rounded-xl text-sm font-medium text-gray-600 hover:bg-gray-100 transition-colors">
|
||||||
|
<span>🖥️</span> Logs
|
||||||
|
</Link>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{/* Form area */}
|
{/* Form area */}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useState, useEffect, useCallback } from 'react';
|
import { useState, useEffect, useCallback } from 'react';
|
||||||
import { taxApi, type TaxRate } from '@/lib/api-client';
|
import { taxApi, type TaxRate } from '@/lib/api-client';
|
||||||
|
import Link from 'next/link';
|
||||||
import { RowActions } from '@/components/ui/RowActions';
|
import { RowActions } from '@/components/ui/RowActions';
|
||||||
|
|
||||||
export default function TaxRatesPage() {
|
export default function TaxRatesPage() {
|
||||||
@@ -69,6 +70,7 @@ export default function TaxRatesPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div>
|
<div>
|
||||||
|
<Link href="/settings" className="text-sm text-[#2D6A4F] hover:underline">← Ajustes</Link>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Tipos impositivos (IVA)</h1>
|
<h1 className="text-2xl font-bold text-gray-900">Tipos impositivos (IVA)</h1>
|
||||||
<p className="text-sm text-gray-500 mt-0.5">Configura los tipos de IVA aplicables a los productos.</p>
|
<p className="text-sm text-gray-500 mt-0.5">Configura los tipos de IVA aplicables a los productos.</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -55,10 +55,10 @@ export const NAV_ITEMS: NavItem[] = [
|
|||||||
{ href: '/reviews', label: 'Reseñas', icon: '⭐', permission: 'reviews.read' },
|
{ href: '/reviews', label: 'Reseñas', icon: '⭐', permission: 'reviews.read' },
|
||||||
{ href: '/cms', label: 'CMS', icon: '📄', permission: 'cms.read' },
|
{ href: '/cms', label: 'CMS', icon: '📄', permission: 'cms.read' },
|
||||||
{ href: '/users', label: 'Usuarios', icon: '🔐', permission: 'admin-users.read' },
|
{ href: '/users', label: 'Usuarios', icon: '🔐', permission: 'admin-users.read' },
|
||||||
{ href: '/tax-rates', label: 'IVA', icon: '📊', permission: 'orders.read' },
|
|
||||||
{ href: '/audit', label: 'Auditoría', icon: '📋', permission: 'audit.read' },
|
|
||||||
{ href: '/logs', label: 'Logs', icon: '🖥️', permission: 'audit.read' },
|
|
||||||
{ href: '/settings', label: 'Ajustes', icon: '⚙️', permission: 'dashboard' },
|
{ href: '/settings', label: 'Ajustes', icon: '⚙️', permission: 'dashboard' },
|
||||||
|
{ href: '/settings/tax-rates', label: 'IVA', icon: '💰', permission: 'orders.read' },
|
||||||
|
{ href: '/settings/audit', label: 'Auditoría', icon: '📋', permission: 'audit.read' },
|
||||||
|
{ href: '/settings/logs', label: 'Logs', icon: '🖥️', permission: 'audit.read' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function visibleNavItems(role: Role): NavItem[] {
|
export function visibleNavItems(role: Role): NavItem[] {
|
||||||
|
|||||||
@@ -274,7 +274,7 @@ export default function RegisterPage() {
|
|||||||
setCart((current) => [
|
setCart((current) => [
|
||||||
...current,
|
...current,
|
||||||
{
|
{
|
||||||
lineId: `free-${crypto.randomUUID()}`,
|
lineId: `free-${generateIdempotencyKey()}`,
|
||||||
kind: 'free',
|
kind: 'free',
|
||||||
variantId: null,
|
variantId: null,
|
||||||
productId: null,
|
productId: null,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useMemo, useState } from 'react';
|
import { useMemo, useState } from 'react';
|
||||||
import { formatPrice } from '@/lib/money';
|
import { formatPrice } from '@/lib/money';
|
||||||
import type { PaymentAllocation, PaymentMethod } from '@/types/checkout';
|
import type { PaymentAllocation, PaymentMethod } from '@/types/checkout';
|
||||||
|
import { generateIdempotencyKey } from '@/lib/idempotency';
|
||||||
|
|
||||||
interface PaymentModalProps {
|
interface PaymentModalProps {
|
||||||
method: PaymentMethod;
|
method: PaymentMethod;
|
||||||
@@ -48,7 +49,7 @@ export default function PaymentModal({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
onAdd({
|
onAdd({
|
||||||
id: crypto.randomUUID(),
|
id: generateIdempotencyKey(),
|
||||||
methodCode: method.code,
|
methodCode: method.code,
|
||||||
methodLabel: method.label,
|
methodLabel: method.label,
|
||||||
kind: method.kind,
|
kind: method.kind,
|
||||||
|
|||||||
21
work/artifacts/F-194/implementer.md
Normal file
21
work/artifacts/F-194/implementer.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# F-194 — Fix evidence
|
||||||
|
|
||||||
|
## Changes
|
||||||
|
|
||||||
|
### Fix 1: `crypto.randomUUID()` runtime TypeError
|
||||||
|
- `project/apps/pos/src/components/PaymentModal.tsx`: replaced `crypto.randomUUID()` with `generateIdempotencyKey()` (already imported pattern from `@/lib/idempotency` with safe fallback).
|
||||||
|
- `project/apps/pos/src/app/(terminal)/page.tsx`: same fix for the free-item lineId.
|
||||||
|
|
||||||
|
### Fix 2: IVA, Auditoría, Logs → inside Settings
|
||||||
|
- Moved routes: `app/(dashboard)/{tax-rates,audit,logs}/page.tsx` → `app/(dashboard)/settings/{tax-rates,audit,logs}/page.tsx`
|
||||||
|
- Updated `permissions.ts` NAV_ITEMS: `/tax-rates` → `/settings/tax-rates`, `/audit` → `/settings/audit`, `/logs` → `/settings/logs`
|
||||||
|
- Added "Sistema" sub-navigation in settings sidebar with links to each sub-page.
|
||||||
|
- Added "← Ajustes" breadcrumb link at the top of each moved page.
|
||||||
|
|
||||||
|
## Validation
|
||||||
|
- Admin typecheck: PASS
|
||||||
|
- POS typecheck: PASS
|
||||||
|
- Admin build: PASS (routes show `/settings/{audit,logs,tax-rates}`)
|
||||||
|
- POS build: PASS
|
||||||
|
- Backend unit tests: 268/268 PASS
|
||||||
|
- `./scripts/verify.sh`: PASS
|
||||||
1
work/artifacts/F-194/leader-close.json
Normal file
1
work/artifacts/F-194/leader-close.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"feature_id":"F-194","agent":"leader","stage":"close","verdict":"APPROVED","checks":[{"item":"All gates APPROVED","ok":true},{"item":"Builds and tests pass","ok":true}],"issues":[]}
|
||||||
1
work/artifacts/F-194/qa.json
Normal file
1
work/artifacts/F-194/qa.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"feature_id":"F-194","agent":"qa","stage":"qa_gate","verdict":"APPROVED","acceptance":[{"id":1,"criterion":"PaymentModal no longer calls bare crypto.randomUUID()","ok":true},{"id":2,"criterion":"Terminal free-item uses safe ID generation","ok":true},{"id":3,"criterion":"IVA/Audit/Logs are accessible under /settings/...","ok":true},{"id":4,"criterion":"Old /tax-rates, /audit, /logs routes removed","ok":true},{"id":5,"criterion":"Typecheck/builds/regression green","ok":true}],"issues":[]}
|
||||||
1
work/artifacts/F-194/reviewer.json
Normal file
1
work/artifacts/F-194/reviewer.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"feature_id":"F-194","agent":"reviewer","stage":"review_gate","verdict":"APPROVED","checks":[{"item":"crypto.randomUUID replaced with safe fallback","ok":true},{"item":"Routes moved from top-level to settings sub-paths","ok":true},{"item":"Navigation items updated to new paths","ok":true},{"item":"Settings sidebar links to sub-pages","ok":true},{"item":"Back-to-settings breadcrumbs on each page","ok":true},{"item":"Typecheck and builds pass","ok":true}],"issues":[]}
|
||||||
1
work/artifacts/F-194/security.json
Normal file
1
work/artifacts/F-194/security.json
Normal file
@@ -0,0 +1 @@
|
|||||||
|
{"feature_id":"F-194","agent":"security","stage":"security_gate","verdict":"APPROVED","checks":[{"item":"No new auth/authz changes","ok":true},{"item":"No secrets or sensitive data introduced","ok":true},{"item":"Moved pages preserve existing RBAC","ok":true}],"issues":[]}
|
||||||
@@ -502,3 +502,9 @@
|
|||||||
- Seguridad: login y sesiones bloquean cuentas inactivas/eliminadas; revocación y auditoría atómicas; sesión de caja abierta impide la baja.
|
- Seguridad: login y sesiones bloquean cuentas inactivas/eliminadas; revocación y auditoría atómicas; sesión de caja abierta impide la baja.
|
||||||
- Integridad: la fila/UUID se conserva para atribución histórica y la apertura de caja se serializa con la baja mediante bloqueo de fila.
|
- Integridad: la fila/UUID se conserva para atribución histórica y la apertura de caja se serializa con la baja mediante bloqueo de fila.
|
||||||
- Evidencia: 354/354 tests con PostgreSQL real en secuencia, migración up/no-op/down/up y builds backend/admin verdes; `work/artifacts/F-187/`.
|
- Evidencia: 354/354 tests con PostgreSQL real en secuencia, migración up/no-op/down/up y builds backend/admin verdes; `work/artifacts/F-187/`.
|
||||||
|
|
||||||
|
## F-194 cerrada (2026-08-22) — Fix crypto.randomUUID runtime error and move admin modules to settings
|
||||||
|
- Gates: reviewer APPROVED, security APPROVED, qa APPROVED, verify.sh exit 0.
|
||||||
|
- Fix 1: replaced bare crypto.randomUUID() with generateIdempotencyKey() in PaymentModal and terminal page.
|
||||||
|
- Fix 2: moved tax-rates, audit and logs routes from top-level dashboard into /settings/... with breadcrumb navigation.
|
||||||
|
- Evidencia: admin/POS typecheck and builds green; 268/268 unit tests pass; verify.sh green.
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"state": "waiting",
|
"state": "waiting",
|
||||||
"next_agent": "leader",
|
"next_agent": "leader",
|
||||||
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
"waiting_for": "Seleccionar una feature pending y actualizar este estado",
|
||||||
"updated_at": "2026-08-22T20:23:50Z",
|
"updated_at": "2026-08-22T20:29:26Z",
|
||||||
"timeline": []
|
"timeline": []
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user