first commit
This commit is contained in:
commit
b54624be96
226 files changed
+108840
No files matched your search
@@ -0,0 +1,217 @@
|
||||
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.user_name.slice(0, 1).toUpperCase()}
|
||||
</div>
|
||||
<div className="hidden lg:block text-left">
|
||||
<p className="text-sm font-semibold text-gray-800">{user.user_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.user_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;
|
||||
Reference in new issue
Block a user