AppLayout.tsx 5.55 KB
// vibe_erp main app shell with i18n support.

import { NavLink, Outlet } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { useAuth } from '@/auth/AuthContext'
import { useLocale, useT, AVAILABLE_LOCALES } from '@/i18n/LocaleContext'
import type { MessageKey } from '@/i18n/messages'
import { meta } from '@/api/client'
import type { MetaInfo } from '@/types/api'

interface NavItem {
  to: string
  labelKey: MessageKey
}
interface NavGroup {
  headingKey: MessageKey
  items: NavItem[]
}

const NAV: NavGroup[] = [
  {
    headingKey: 'nav.overview',
    items: [{ to: '/', labelKey: 'nav.dashboard' }],
  },
  {
    headingKey: 'nav.catalog',
    items: [
      { to: '/items', labelKey: 'nav.items' },
      { to: '/uoms', labelKey: 'nav.uoms' },
      { to: '/partners', labelKey: 'nav.partners' },
    ],
  },
  {
    headingKey: 'nav.inventory',
    items: [
      { to: '/locations', labelKey: 'nav.locations' },
      { to: '/balances', labelKey: 'nav.balances' },
      { to: '/movements', labelKey: 'nav.movements' },
    ],
  },
  {
    headingKey: 'nav.orders',
    items: [
      { to: '/sales-orders', labelKey: 'nav.salesOrders' },
      { to: '/purchase-orders', labelKey: 'nav.purchaseOrders' },
    ],
  },
  {
    headingKey: 'nav.production',
    items: [
      { to: '/work-orders', labelKey: 'nav.workOrders' },
      { to: '/shop-floor', labelKey: 'nav.shopFloor' },
    ],
  },
  {
    headingKey: 'nav.finance',
    items: [
      { to: '/accounts', labelKey: 'nav.accounts' },
      { to: '/journal-entries', labelKey: 'nav.journalEntries' },
    ],
  },
  {
    headingKey: 'nav.system',
    items: [
      { to: '/users', labelKey: 'nav.users' },
      { to: '/roles', labelKey: 'nav.roles' },
    ],
  },
]

export function AppLayout() {
  const { username, logout } = useAuth()
  const { locale, setLocale } = useLocale()
  const t = useT()
  const [info, setInfo] = useState<MetaInfo | null>(null)

  useEffect(() => {
    meta.info().then(setInfo).catch(() => setInfo(null))
  }, [])

  return (
    <div className="flex min-h-screen bg-slate-50 text-slate-900">
      {/* ─── Sidebar ──────────────────────────────────────────── */}
      <aside className="hidden w-60 flex-shrink-0 border-r border-slate-200 bg-white sm:flex sm:flex-col">
        <div className="flex h-14 items-center px-4 border-b border-slate-200">
          <span className="text-lg font-bold text-brand-600">vibe_erp</span>
          {info && (
            <span className="ml-2 text-xs text-slate-400">
              {info.implementationVersion}
            </span>
          )}
        </div>
        <nav className="flex-1 overflow-y-auto px-3 py-4">
          {NAV.map((group) => (
            <div key={group.headingKey} className="mb-5">
              <div className="px-2 text-xs font-semibold uppercase tracking-wider text-slate-400">
                {t(group.headingKey)}
              </div>
              <ul className="mt-1 space-y-0.5">
                {group.items.map((item) => (
                  <li key={item.to}>
                    <NavLink
                      to={item.to}
                      end={item.to === '/'}
                      className={({ isActive }) =>
                        [
                          'block rounded-md px-2 py-1.5 text-sm transition',
                          isActive
                            ? 'bg-brand-50 text-brand-700 font-semibold'
                            : 'text-slate-600 hover:bg-slate-100 hover:text-slate-900',
                        ].join(' ')
                      }
                    >
                      {t(item.labelKey)}
                    </NavLink>
                  </li>
                ))}
              </ul>
            </div>
          ))}
        </nav>
        <div className="border-t border-slate-200 px-3 py-3 text-xs text-slate-400">
          {info?.activeProfiles?.length ? (
            <div>profiles: {info.activeProfiles.join(', ')}</div>
          ) : null}
          {info?.buildTime ? (
            <div>built: {new Date(info.buildTime).toLocaleString()}</div>
          ) : null}
        </div>
      </aside>

      {/* ─── Main column ─────────────────────────────────────── */}
      <div className="flex flex-1 flex-col">
        <header className="flex h-14 items-center justify-between border-b border-slate-200 bg-white px-6 sm:justify-end">
          <span className="text-lg font-bold text-brand-600 sm:hidden">vibe_erp</span>
          <div className="flex items-center gap-3">
            <select
              value={locale}
              onChange={(e) => setLocale(e.target.value as typeof locale)}
              className="rounded-md border border-slate-200 bg-slate-50 px-2 py-1 text-xs text-slate-600"
            >
              {AVAILABLE_LOCALES.map((l) => (
                <option key={l.code} value={l.code}>{l.label}</option>
              ))}
            </select>
            <span className="text-sm text-slate-500">
              {username ? (
                <>
                  {t('label.signedInAs')}{' '}
                  <span className="font-medium text-slate-700">{username}</span>
                </>
              ) : null}
            </span>
            <button className="btn-secondary" onClick={logout}>
              {t('action.logout')}
            </button>
          </div>
        </header>
        <main className="flex-1 overflow-y-auto p-6">
          <Outlet />
        </main>
      </div>
    </div>
  )
}