218 lines
8.5 KiB
TypeScript
218 lines
8.5 KiB
TypeScript
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
|
import { useAuth } from '../context/AuthContext.tsx';
|
|
import { useFarm } from '../context/FarmContext.tsx';
|
|
import type { UserStatus } from '../types/api.ts';
|
|
import { formatDateId, resolveCycleStatus } from '../utils/format.ts';
|
|
|
|
const ROLE_LABELS: Record<UserStatus, string> = {
|
|
inactive: 'Inactive',
|
|
gm: 'GM',
|
|
gm_admin: 'GM Admin',
|
|
buh: 'BUH',
|
|
bu_admin: 'BU Admin',
|
|
director: 'Director',
|
|
director_admin: 'Director Admin',
|
|
account_admin: 'Account Admin',
|
|
superuser: 'Superuser',
|
|
};
|
|
|
|
type HeaderProps = {
|
|
onToggleSidebar: () => void;
|
|
isTimelineVisible: boolean;
|
|
onToggleTimeline: () => void;
|
|
};
|
|
|
|
const Header: React.FC<HeaderProps> = ({
|
|
onToggleSidebar,
|
|
isTimelineVisible,
|
|
onToggleTimeline,
|
|
}) => {
|
|
const { user, logout } = useAuth();
|
|
const {
|
|
sites,
|
|
kandangs,
|
|
cycles,
|
|
selectedSiteId,
|
|
selectedKandangId,
|
|
selectedCycleId,
|
|
setSelectedSiteId,
|
|
setSelectedKandangId,
|
|
setSelectedCycleId,
|
|
} = useFarm();
|
|
const [open, setOpen] = useState(false);
|
|
const [gmFilter, setGmFilter] = useState<'all' | number>('all');
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
const isBuh = user?.status === 'buh' || user?.status === 'bu_admin';
|
|
|
|
const gmOptions = useMemo(() => {
|
|
const byId = new Map<number, string>();
|
|
for (const site of sites) {
|
|
if (!byId.has(site.user)) {
|
|
byId.set(site.user, site.owner_name ?? `GM #${site.user}`);
|
|
}
|
|
}
|
|
return [...byId.entries()]
|
|
.map(([id, name]) => ({ id, name }))
|
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
}, [sites]);
|
|
|
|
const visibleSites = useMemo(() => {
|
|
if (!isBuh || gmFilter === 'all') return sites;
|
|
return sites.filter((site) => site.user === gmFilter);
|
|
}, [gmFilter, isBuh, sites]);
|
|
|
|
useEffect(() => {
|
|
if (!isBuh || gmFilter === 'all') return;
|
|
if (selectedSiteId == null) return;
|
|
const stillVisible = visibleSites.some((site) => site.id === selectedSiteId);
|
|
if (!stillVisible) {
|
|
setSelectedSiteId(visibleSites[0]?.id ?? null);
|
|
}
|
|
}, [gmFilter, isBuh, selectedSiteId, setSelectedSiteId, visibleSites]);
|
|
|
|
useEffect(() => {
|
|
const onClick = (event: MouseEvent) => {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
setOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener('mousedown', onClick);
|
|
return () => document.removeEventListener('mousedown', onClick);
|
|
}, []);
|
|
|
|
if (!user) return null;
|
|
|
|
return (
|
|
<header className="bg-white shadow-sm h-16 flex-shrink-0 z-10 border-b border-gray-200">
|
|
<div className="flex items-center justify-between h-16 px-4 sm:px-6 lg:px-8">
|
|
<div className="flex items-center gap-3 flex-wrap">
|
|
<button
|
|
onClick={onToggleSidebar}
|
|
className="text-gray-500 hover:text-gray-800 focus:outline-none -ml-2"
|
|
aria-label="Toggle sidebar"
|
|
>
|
|
<i className="fa-solid fa-bars text-xl w-10 h-10 p-2 rounded-full hover:bg-gray-100" />
|
|
</button>
|
|
{isBuh && gmOptions.length > 0 && (
|
|
<div className="flex items-center gap-2">
|
|
<i className="fa-solid fa-user-tie text-gray-500" />
|
|
<select
|
|
value={gmFilter === 'all' ? 'all' : String(gmFilter)}
|
|
onChange={(e) => {
|
|
const value = e.target.value;
|
|
setGmFilter(value === 'all' ? 'all' : Number(value));
|
|
}}
|
|
className="px-3 py-1.5 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white text-sm text-gray-900"
|
|
aria-label="Filter GM"
|
|
>
|
|
<option value="all">Semua GM</option>
|
|
{gmOptions.map((gm) => (
|
|
<option key={gm.id} value={gm.id}>
|
|
{gm.name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
)}
|
|
<div className="flex items-center gap-2">
|
|
<i className="fa-solid fa-map-marker-alt text-gray-500" />
|
|
<select
|
|
value={selectedSiteId ?? ''}
|
|
onChange={(e) => setSelectedSiteId(e.target.value ? Number(e.target.value) : null)}
|
|
className="px-3 py-1.5 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white text-sm text-gray-900"
|
|
>
|
|
{visibleSites.length === 0 && <option value="">Tidak ada site</option>}
|
|
{visibleSites.map((site) => (
|
|
<option key={site.id} value={site.id}>
|
|
{isBuh && site.owner_name
|
|
? `${site.site_name} (${site.owner_name})`
|
|
: site.site_name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<i className="fa-solid fa-warehouse text-gray-500" />
|
|
<select
|
|
value={selectedKandangId ?? ''}
|
|
onChange={(e) => setSelectedKandangId(e.target.value ? Number(e.target.value) : null)}
|
|
className="px-3 py-1.5 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white text-sm text-gray-900"
|
|
>
|
|
{kandangs.length === 0 && <option value="">Tidak ada kandang</option>}
|
|
{kandangs.map((kandang) => (
|
|
<option key={kandang.id} value={kandang.id}>
|
|
{kandang.kandang_name}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<i className="fa-solid fa-rotate text-gray-500" />
|
|
<select
|
|
value={selectedCycleId ?? ''}
|
|
onChange={(e) => setSelectedCycleId(e.target.value ? Number(e.target.value) : null)}
|
|
className="px-3 py-1.5 border border-gray-300 rounded-md shadow-sm focus:ring-red-500 focus:border-red-500 bg-white text-sm text-gray-900"
|
|
>
|
|
{cycles.length === 0 && <option value="">Tidak ada siklus</option>}
|
|
{cycles.map((cycle) => (
|
|
<option key={cycle.id} value={cycle.id}>
|
|
{formatDateId(cycle.start_date)} (
|
|
{resolveCycleStatus(cycle) === 'active' ? 'Aktif' : 'Selesai'})
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center ml-auto">
|
|
<button
|
|
onClick={onToggleTimeline}
|
|
title="Toggle Linimasa"
|
|
className="hidden sm:flex items-center text-gray-500 hover:text-gray-800 focus:outline-none p-2 rounded-full hover:bg-gray-100"
|
|
>
|
|
<i
|
|
className={`fa-solid fa-calendar-week text-xl ${isTimelineVisible ? 'text-red-600' : 'text-gray-400'}`}
|
|
/>
|
|
</button>
|
|
|
|
<div className="relative ml-4" ref={dropdownRef}>
|
|
<button
|
|
onClick={() => setOpen(!open)}
|
|
className="flex items-center space-x-2 focus:outline-none"
|
|
>
|
|
<div className="w-10 h-10 rounded-full bg-red-100 text-red-700 flex items-center justify-center font-bold">
|
|
{user.display_name.slice(0, 1).toUpperCase()}
|
|
</div>
|
|
<div className="hidden lg:block text-left">
|
|
<p className="text-sm font-semibold text-gray-800">{user.display_name}</p>
|
|
<p className="text-xs text-gray-500">{ROLE_LABELS[user.status]}</p>
|
|
</div>
|
|
<i className="fa-solid fa-chevron-down text-xs text-gray-500" />
|
|
</button>
|
|
{open && (
|
|
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg py-1 bg-white ring-1 ring-black ring-opacity-5 z-50">
|
|
<div className="px-4 py-3 border-b">
|
|
<p className="text-sm font-semibold text-gray-800">{user.display_name}</p>
|
|
<p className="text-xs text-gray-500">{ROLE_LABELS[user.status]}</p>
|
|
</div>
|
|
<button
|
|
onClick={() => {
|
|
void logout();
|
|
setOpen(false);
|
|
}}
|
|
className="w-full text-left px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 flex items-center"
|
|
>
|
|
<i className="fa-solid fa-sign-out-alt w-4 mr-2 text-gray-500" />
|
|
Keluar
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
);
|
|
};
|
|
|
|
export default Header;
|