Restructure company nav: 8 primary tabs + HR/Ops/Admin dropdowns with active highlight
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,58 +1,95 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { page } from '$app/stores';
|
||||||
|
import { afterNavigate } from '$app/navigation';
|
||||||
|
import type { Snippet } from 'svelte';
|
||||||
import type { LayoutData } from './$types';
|
import type { LayoutData } from './$types';
|
||||||
|
|
||||||
let { data, children } = $props();
|
let { data, children }: { data: LayoutData; children: Snippet } = $props();
|
||||||
|
|
||||||
const tabs = $derived([
|
type MenuKey = 'hr' | 'ops' | 'admin';
|
||||||
{ href: `/companies/${data.company.id}`, label: 'Overview' },
|
let openMenu = $state<MenuKey | null>(null);
|
||||||
{ href: `/companies/${data.company.id}/links`, label: 'Links' },
|
|
||||||
{ href: `/companies/${data.company.id}/projects`, label: 'Projects' },
|
afterNavigate(() => {
|
||||||
{ href: `/companies/${data.company.id}/expenses`, label: 'Expenses' },
|
openMenu = null;
|
||||||
{ href: `/companies/${data.company.id}/budget`, label: 'Budget' },
|
});
|
||||||
{ href: `/companies/${data.company.id}/categories`, label: 'Categories' },
|
|
||||||
{ href: `/companies/${data.company.id}/reports`, label: 'Reports' },
|
const baseUrl = $derived(`/companies/${data.company.id}`);
|
||||||
...(data.companyRoles.some(r => r === 'admin' || r === 'manager' || r === 'hr')
|
const currentPath = $derived($page.url.pathname);
|
||||||
|
|
||||||
|
function isActive(href: string): boolean {
|
||||||
|
if (href === baseUrl) return currentPath === baseUrl;
|
||||||
|
return currentPath === href || currentPath.startsWith(href + '/');
|
||||||
|
}
|
||||||
|
|
||||||
|
function has(roles: string[]): boolean {
|
||||||
|
return data.companyRoles.some((r) => roles.includes(r));
|
||||||
|
}
|
||||||
|
|
||||||
|
const primaryTabs = $derived(
|
||||||
|
[
|
||||||
|
{ href: baseUrl, label: 'Overview', show: true },
|
||||||
|
{ href: `${baseUrl}/accounts`, label: 'Accounts', show: has(['admin', 'manager', 'accountant']) },
|
||||||
|
{ href: `${baseUrl}/projects`, label: 'Projects', show: true },
|
||||||
|
{ href: `${baseUrl}/expenses`, label: 'Expenses', show: true },
|
||||||
|
{ href: `${baseUrl}/bills`, label: 'Bills', show: has(['admin', 'manager', 'accountant']) },
|
||||||
|
{ href: `${baseUrl}/invoices`, label: 'Invoices', show: has(['admin', 'manager']) },
|
||||||
|
{ href: `${baseUrl}/budget`, label: 'Budget', show: true },
|
||||||
|
{ href: `${baseUrl}/reports`, label: 'Reports', show: true }
|
||||||
|
].filter((t) => t.show)
|
||||||
|
);
|
||||||
|
|
||||||
|
const hrItems = $derived(
|
||||||
|
has(['admin', 'manager', 'hr'])
|
||||||
? [
|
? [
|
||||||
{ href: `/companies/${data.company.id}/employees`, label: 'Employees' },
|
{ href: `${baseUrl}/employees`, label: 'Employees' },
|
||||||
{ href: `/companies/${data.company.id}/hr/leave-requests`, label: 'Leave' },
|
{ href: `${baseUrl}/hr/leave-requests`, label: 'Leave' },
|
||||||
{ href: `/companies/${data.company.id}/hr/payroll`, label: 'Payroll' },
|
{ href: `${baseUrl}/hr/payroll`, label: 'Payroll' },
|
||||||
{ href: `/companies/${data.company.id}/hr/holidays`, label: 'Holidays' }
|
{ href: `${baseUrl}/hr/holidays`, label: 'Holidays' }
|
||||||
]
|
]
|
||||||
: []),
|
: []
|
||||||
...(data.companyRoles.some(r => r === 'admin' || r === 'manager')
|
);
|
||||||
? [
|
|
||||||
{ href: `/companies/${data.company.id}/parties`, label: 'Contacts' },
|
const opsItems = $derived(
|
||||||
{ href: `/companies/${data.company.id}/invoices`, label: 'Invoices' }
|
[
|
||||||
]
|
{ href: `${baseUrl}/parties`, label: 'Contacts', show: has(['admin', 'manager']) },
|
||||||
: []),
|
{ href: `${baseUrl}/categories`, label: 'Categories', show: true },
|
||||||
...(data.companyRoles.some(r => r === 'admin' || r === 'manager' || r === 'user' || r === 'hr')
|
{ href: `${baseUrl}/packages`, label: 'Packages', show: has(['admin', 'manager', 'user', 'hr']) },
|
||||||
? [{ href: `/companies/${data.company.id}/packages`, label: 'Packages' }]
|
{ href: `${baseUrl}/links`, label: 'Links', show: true },
|
||||||
: []),
|
{ href: `${baseUrl}/documents`, label: 'Documents', show: has(['admin', 'manager', 'accountant']) }
|
||||||
...(data.companyRoles.includes('admin')
|
].filter((t) => t.show)
|
||||||
? [
|
);
|
||||||
{ href: `/companies/${data.company.id}/integrations`, label: 'Integrations' }
|
|
||||||
]
|
const adminItems = $derived(
|
||||||
: []),
|
[
|
||||||
...(data.companyRoles.some((r) => r === 'admin' || r === 'manager' || r === 'accountant')
|
{ href: `${baseUrl}/integrations`, label: 'Integrations', show: has(['admin']) },
|
||||||
? [
|
{ href: `${baseUrl}/import`, label: 'Import', show: has(['admin', 'manager']) },
|
||||||
{ href: `/companies/${data.company.id}/accounts`, label: 'Accounts' },
|
{ href: `${baseUrl}/export`, label: 'Export', show: has(['admin', 'accountant']) },
|
||||||
{ href: `/companies/${data.company.id}/bills`, label: 'Bills' },
|
{ href: `${baseUrl}/profile`, label: 'Profile', show: has(['admin', 'manager', 'accountant']) },
|
||||||
{ href: `/companies/${data.company.id}/profile`, label: 'Profile' },
|
{ href: `${baseUrl}/settings`, label: 'Settings', show: has(['admin', 'manager']) }
|
||||||
{ href: `/companies/${data.company.id}/documents`, label: 'Documents' }
|
].filter((t) => t.show)
|
||||||
]
|
);
|
||||||
: []),
|
|
||||||
...(data.companyRoles.includes('admin') || data.companyRoles.includes('accountant')
|
function menuActive(items: Array<{ href: string }>): boolean {
|
||||||
? [{ href: `/companies/${data.company.id}/export`, label: 'Export' }]
|
return items.some((i) => isActive(i.href));
|
||||||
: []),
|
}
|
||||||
...(data.companyRoles.includes('admin') || data.companyRoles.includes('manager')
|
|
||||||
? [
|
function toggleMenu(menu: MenuKey) {
|
||||||
{ href: `/companies/${data.company.id}/import`, label: 'Import' },
|
openMenu = openMenu === menu ? null : menu;
|
||||||
{ href: `/companies/${data.company.id}/settings`, label: 'Settings' }
|
}
|
||||||
]
|
|
||||||
: [])
|
function handleWindowClick(e: MouseEvent) {
|
||||||
]);
|
const target = e.target as HTMLElement | null;
|
||||||
|
if (target?.closest('[data-nav-dropdown]')) return;
|
||||||
|
openMenu = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleKeydown(e: KeyboardEvent) {
|
||||||
|
if (e.key === 'Escape') openMenu = null;
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<svelte:window onclick={handleWindowClick} onkeydown={handleKeydown} />
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-6">
|
<div class="mb-6">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{data.company.name}</h1>
|
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">{data.company.name}</h1>
|
||||||
@@ -61,16 +98,82 @@
|
|||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Tabs -->
|
<nav
|
||||||
<nav class="mb-6 flex gap-1 overflow-x-auto border-b border-gray-200 dark:border-gray-700">
|
class="mb-6 flex flex-wrap items-center gap-1 border-b border-gray-200 dark:border-gray-700"
|
||||||
{#each tabs as tab}
|
aria-label="Company navigation"
|
||||||
|
>
|
||||||
|
{#each primaryTabs as tab (tab.href)}
|
||||||
|
{@const active = isActive(tab.href)}
|
||||||
<a
|
<a
|
||||||
href={tab.href}
|
href={tab.href}
|
||||||
class="whitespace-nowrap border-b-2 px-4 py-2 text-sm font-medium transition-colors border-transparent text-gray-500 dark:text-gray-400 hover:border-gray-300 dark:hover:border-gray-600 hover:text-gray-700 dark:hover:text-gray-300"
|
class="whitespace-nowrap border-b-2 px-4 py-2 text-sm font-medium transition-colors {active
|
||||||
|
? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||||
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300'}"
|
||||||
|
aria-current={active ? 'page' : undefined}
|
||||||
>
|
>
|
||||||
{tab.label}
|
{tab.label}
|
||||||
</a>
|
</a>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
{#snippet dropdown(key: MenuKey, label: string, items: Array<{ href: string; label: string }>)}
|
||||||
|
{#if items.length > 0}
|
||||||
|
{@const active = menuActive(items)}
|
||||||
|
{@const open = openMenu === key}
|
||||||
|
<div class="relative" data-nav-dropdown>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onclick={() => toggleMenu(key)}
|
||||||
|
class="flex items-center gap-1 whitespace-nowrap border-b-2 px-4 py-2 text-sm font-medium transition-colors {active
|
||||||
|
? 'border-blue-500 text-blue-600 dark:border-blue-400 dark:text-blue-400'
|
||||||
|
: open
|
||||||
|
? 'border-gray-300 text-gray-700 dark:border-gray-600 dark:text-gray-300'
|
||||||
|
: 'border-transparent text-gray-500 hover:border-gray-300 hover:text-gray-700 dark:text-gray-400 dark:hover:border-gray-600 dark:hover:text-gray-300'}"
|
||||||
|
aria-haspopup="menu"
|
||||||
|
aria-expanded={open}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
<svg
|
||||||
|
class="h-3 w-3 transition-transform {open ? 'rotate-180' : ''}"
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
fill="none"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3 4.5L6 7.5L9 4.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
{#if open}
|
||||||
|
<div
|
||||||
|
role="menu"
|
||||||
|
class="absolute left-0 top-full z-20 mt-1 min-w-[180px] rounded-md border border-gray-200 bg-white py-1 shadow-lg dark:border-gray-700 dark:bg-gray-800"
|
||||||
|
>
|
||||||
|
{#each items as item (item.href)}
|
||||||
|
{@const itemActive = isActive(item.href)}
|
||||||
|
<a
|
||||||
|
href={item.href}
|
||||||
|
role="menuitem"
|
||||||
|
class="block px-4 py-2 text-sm transition-colors {itemActive
|
||||||
|
? 'bg-blue-50 font-medium text-blue-600 dark:bg-blue-900/30 dark:text-blue-400'
|
||||||
|
: 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-700/60'}"
|
||||||
|
aria-current={itemActive ? 'page' : undefined}
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
|
{@render dropdown('hr', 'HR', hrItems)}
|
||||||
|
{@render dropdown('ops', 'Ops', opsItems)}
|
||||||
|
{@render dropdown('admin', 'Admin', adminItems)}
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
{@render children()}
|
{@render children()}
|
||||||
|
|||||||
Reference in New Issue
Block a user