feat(FIX-158): completed feature

This commit is contained in:
chattie
2026-08-22 17:26:02 +02:00
parent 49a5755eb8
commit 699dfc6fca
17 changed files with 4053 additions and 1491 deletions

View File

@@ -1,7 +1,7 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/types/routes.d.ts";
import "./.next/types/root-params.d.ts";
import "./.next/dev/types/routes.d.ts";
import "./.next/dev/types/root-params.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

View File

@@ -1,6 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
allowedDevOrigins: ['192.168.18.93', 'localhost'],
// Keep Turbopack rooted at this app. The repository also contains the
// legacy project/frontend/package-lock.json; without an explicit root,
// Next.js 16 may infer the wrong workspace during production builds.

View File

@@ -1,21 +1,41 @@
'use client';
import { useEffect, useState } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useEffect } from 'react';
import { useRouter } from 'next/navigation';
import Link from 'next/link';
import { AuthProvider, useAuth } from '@/features/auth/components/AuthProvider';
import { visibleNavItems, type NavItem } from '@/lib/permissions';
import { topNavItems, subNavItems, type NavItem } from '@/lib/permissions';
import type { Role } from '@/types';
function Sidebar({
navItems,
user,
onLogout,
}: {
navItems: NavItem[];
user: { email: string; role: Role };
onLogout: () => void;
}) {
const pathname = usePathname();
function NavItemRow({ item }: { item: NavItem }) {
const pathname = window?.location?.pathname ?? '';
const active = item.href === '/'
? pathname === '/'
: pathname.startsWith(item.href);
return (
<Link
key={item.href}
href={item.href}
className={`
flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all
${active
? 'bg-[#2D6A4F]/10 text-[#2D6A4F] border-l-[3px] border-[#2D6A4F]'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'}
`}
>
<span className="text-base">{item.icon}</span>
<span className="truncate">{item.label}</span>
{item.badge != null && item.badge > 0 && (
<span className="ml-auto bg-[#E76F51] text-white text-xs font-bold rounded-full px-1.5 py-0.5 min-w-[18px] text-center">
{item.badge}
</span>
)}
</Link>
);
}
function Sidebar({ role, email }: { role: Role; email: string }) {
const topItems = topNavItems(role);
return (
<div className="w-60 bg-white border-r border-gray-200 flex flex-col h-screen sticky top-0">
@@ -30,32 +50,20 @@ function Sidebar({
{/* Nav */}
<nav className="flex-1 px-3 py-4 space-y-0.5 overflow-y-auto">
{navItems.map((item) => {
const active =
item.href === '/'
? pathname === '/'
: pathname.startsWith(item.href);
{topItems.map((item) => {
const subs = subNavItems(item.href, role);
return (
<Link
key={item.href}
href={item.href}
className={`
flex items-center gap-3 px-3 py-2 rounded-lg text-sm font-medium transition-all
${
active
? 'bg-[#2D6A4F]/10 text-[#2D6A4F] border-l-[3px] border-[#2D6A4F]'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}
`}
>
<span className="text-base">{item.icon}</span>
<span className="truncate">{item.label}</span>
{item.badge != null && item.badge > 0 && (
<span className="ml-auto bg-[#E76F51] text-white text-xs font-bold rounded-full px-1.5 py-0.5 min-w-[18px] text-center">
{item.badge}
</span>
<div key={item.href}>
<NavItemRow item={item} />
{subs.length > 0 && (
<div className="ml-4 mt-0.5 space-y-0.5">
{subs.map((sub) => (
<NavItemRow key={sub.href} item={sub} />
))}
</div>
)}
</Link>
</div>
);
})}
</nav>
@@ -63,18 +71,9 @@ function Sidebar({
{/* User footer */}
<div className="px-3 py-4 border-t border-gray-100">
<div className="px-3 py-2 mb-2">
<p className="text-xs text-gray-400 truncate">{user.email}</p>
<p className="text-xs text-gray-500 capitalize">{user.role}</p>
<p className="text-xs text-gray-400 truncate">{email}</p>
<p className="text-xs text-gray-500 capitalize">{role}</p>
</div>
<button
onClick={onLogout}
className="w-full flex items-center gap-2 px-3 py-2 text-sm text-gray-500 hover:text-gray-700 hover:bg-gray-50 rounded-lg transition-colors"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2} aria-hidden="true">
<path strokeLinecap="round" strokeLinejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
</svg>
Cerrar sesión
</button>
</div>
</div>
);
@@ -100,11 +99,9 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
if (!user) return null;
const navItems = visibleNavItems(user.role);
return (
<div className="flex min-h-screen bg-gray-50">
<Sidebar navItems={navItems} user={user} onLogout={logout} />
<Sidebar role={user.role} email={user.email} />
<main className="flex-1 min-w-0">
<div className="w-full px-4 sm:px-6 lg:px-10 py-6 lg:py-8">
{children}
@@ -114,11 +111,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) {
);
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode;
}) {
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<AuthProvider>
<DashboardShell>{children}</DashboardShell>

View File

@@ -28,7 +28,6 @@ export type Permission =
export function can(role: Role, permission: Permission): boolean {
if (role === 'admin') return true;
// Future: granular permission checks when backend supports them
return false;
}
@@ -38,14 +37,15 @@ export interface NavItem {
icon: string;
permission: Permission;
badge?: number;
parentHref?: string;
}
export const NAV_ITEMS: NavItem[] = [
{ href: '/', label: 'Dashboard', icon: '📊', permission: 'dashboard' },
{ href: '/reporting', label: 'Reporting', icon: '📈', permission: 'reporting.read' },
{ href: '/reporting/dashboard', label: ' Dashboard', icon: '📊', permission: 'reporting.read' },
{ href: '/reporting/sales', label: ' Ventas', icon: '🧾', permission: 'reporting.read' },
{ href: '/reporting/products', label: ' Productos', icon: '📦', permission: 'reporting.read' },
{ href: '/reporting/dashboard', label: 'Dashboard', icon: '📊', permission: 'reporting.read', parentHref: '/reporting' },
{ href: '/reporting/sales', label: 'Ventas', icon: '🧾', permission: 'reporting.read', parentHref: '/reporting' },
{ href: '/reporting/products', label: 'Productos', icon: '📦', permission: 'reporting.read', parentHref: '/reporting' },
{ href: '/products', label: 'Productos', icon: '📦', permission: 'products.read' },
{ href: '/orders', label: 'Pedidos', icon: '🧾', permission: 'orders.read' },
{ href: '/payments', label: 'Pagos', icon: '💳', permission: 'orders.read' },
@@ -64,6 +64,10 @@ export const NAV_ITEMS: NavItem[] = [
{ href: '/settings', label: 'Ajustes', icon: '⚙️', permission: 'dashboard' },
];
export function visibleNavItems(role: Role): NavItem[] {
return NAV_ITEMS.filter((item) => can(role, item.permission));
export function topNavItems(role: Role): NavItem[] {
return NAV_ITEMS.filter((item) => !item.parentHref && can(role, item.permission));
}
export function subNavItems(parentHref: string, role: Role): NavItem[] {
return NAV_ITEMS.filter((item) => item.parentHref === parentHref && can(role, item.permission));
}