306 lines
14 KiB
TypeScript
306 lines
14 KiB
TypeScript
import { Head, Link, usePage } from '@inertiajs/react';
|
|
import {
|
|
ArrowUpRight,
|
|
Building2,
|
|
FolderKanban,
|
|
Inbox,
|
|
Layers,
|
|
Users,
|
|
} from 'lucide-react';
|
|
import type { LucideIcon } from 'lucide-react';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { portfolio, yearRange } from '@/data/projects';
|
|
import {
|
|
formatDate,
|
|
sectorLabels,
|
|
statusBadgeClasses,
|
|
statusLabels,
|
|
} from '@/lib/admin-labels';
|
|
import { about, clients, contact, dashboard, home, services } from '@/routes';
|
|
import { index as consultationsIndex } from '@/routes/admin/consultations';
|
|
import { index as projectsIndex } from '@/routes/admin/projects';
|
|
import type { Consultation, Project, ProjectSector } from '@/types';
|
|
|
|
type DashboardProps = {
|
|
projects: Project[];
|
|
newConsultations: number;
|
|
latestConsultations: Pick<
|
|
Consultation,
|
|
'id' | 'name' | 'company' | 'service' | 'status' | 'created_at'
|
|
>[];
|
|
};
|
|
|
|
const sitePages = [
|
|
{ title: 'Beranda', href: home() },
|
|
{ title: 'Tentang Kami', href: about() },
|
|
{ title: 'Jasa', href: services() },
|
|
{ title: 'Klien & Portofolio', href: clients() },
|
|
{ title: 'Kontak', href: contact() },
|
|
];
|
|
|
|
function StatCard({
|
|
icon: Icon,
|
|
label,
|
|
value,
|
|
note,
|
|
}: {
|
|
icon: LucideIcon;
|
|
label: string;
|
|
value: string | number;
|
|
note: string;
|
|
}) {
|
|
return (
|
|
<div className="bg-card border-sidebar-border/70 flex flex-col gap-3 rounded-xl border p-5">
|
|
<div className="flex items-center justify-between">
|
|
<span className="text-muted-foreground font-mono text-xs font-semibold tracking-wider uppercase">
|
|
{label}
|
|
</span>
|
|
<span className="bg-accent text-accent-foreground flex size-9 items-center justify-center rounded-lg">
|
|
<Icon className="size-4" />
|
|
</span>
|
|
</div>
|
|
<span className="font-display text-3xl font-extrabold">
|
|
{value}
|
|
</span>
|
|
<span className="text-muted-foreground text-xs">{note}</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default function Dashboard({
|
|
projects,
|
|
newConsultations,
|
|
latestConsultations,
|
|
}: DashboardProps) {
|
|
const { auth } = usePage().props;
|
|
const {
|
|
clientCount,
|
|
clientsInSector,
|
|
projectsBySector,
|
|
projectsInProgram,
|
|
} = portfolio(projects);
|
|
const sectors: ProjectSector[] = ['government', 'private', 'soe'];
|
|
const erpProjects = projectsInProgram('erp');
|
|
const latest = projects.slice(0, 5);
|
|
|
|
return (
|
|
<>
|
|
<Head title="Dashboard" />
|
|
<div className="flex h-full flex-1 flex-col gap-6 overflow-x-auto p-4 md:p-6">
|
|
<div className="from-primary relative overflow-hidden rounded-xl bg-gradient-to-r to-[#c81e2a] p-6 text-white md:p-8">
|
|
<div className="pointer-events-none absolute -top-16 -right-16 size-56 rounded-full bg-white/10 blur-2xl" />
|
|
<div className="relative flex flex-col gap-2">
|
|
<span className="font-mono text-xs font-semibold tracking-wider text-white/80 uppercase">
|
|
Admin Panel · PT Probindo Artika Jaya
|
|
</span>
|
|
<h1 className="font-display text-2xl font-extrabold md:text-3xl">
|
|
Selamat datang, {auth.user?.name}
|
|
</h1>
|
|
<p className="max-w-2xl text-sm text-white/85">
|
|
Ringkasan permintaan konsultasi dan rekam jejak
|
|
proyek yang ditampilkan di website.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2 xl:grid-cols-4">
|
|
<StatCard
|
|
icon={FolderKanban}
|
|
label="Total Proyek"
|
|
value={projects.length}
|
|
note={`Periode ${yearRange(projects)}`}
|
|
/>
|
|
<StatCard
|
|
icon={Users}
|
|
label="Total Klien"
|
|
value={clientCount}
|
|
note="Pemerintahan, BUMN & swasta"
|
|
/>
|
|
<StatCard
|
|
icon={Layers}
|
|
label="Pengalaman ERP"
|
|
value={erpProjects.length}
|
|
note={`Periode ${yearRange(erpProjects)}`}
|
|
/>
|
|
<StatCard
|
|
icon={Inbox}
|
|
label="Konsultasi Baru"
|
|
value={newConsultations}
|
|
note="Belum ditindaklanjuti"
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-4 lg:grid-cols-3">
|
|
<div className="flex flex-col gap-4 lg:col-span-2">
|
|
<div className="bg-card border-sidebar-border/70 rounded-xl border p-5">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="font-display text-base font-bold">
|
|
Konsultasi Terbaru
|
|
</h2>
|
|
<Link
|
|
href={consultationsIndex()}
|
|
className="text-primary flex items-center gap-1 text-xs font-semibold hover:underline"
|
|
>
|
|
Buka inbox
|
|
<ArrowUpRight className="size-3.5" />
|
|
</Link>
|
|
</div>
|
|
{latestConsultations.length === 0 ? (
|
|
<p className="text-muted-foreground py-6 text-center text-sm">
|
|
Belum ada permintaan konsultasi.
|
|
</p>
|
|
) : (
|
|
<ul className="divide-border divide-y">
|
|
{latestConsultations.map((item) => (
|
|
<li
|
|
key={item.id}
|
|
className="flex items-center gap-4 py-3"
|
|
>
|
|
<div className="flex min-w-0 flex-1 flex-col">
|
|
<span className="truncate text-sm font-semibold">
|
|
{item.name}
|
|
{item.company &&
|
|
` · ${item.company}`}
|
|
</span>
|
|
<span className="text-muted-foreground truncate text-xs">
|
|
{formatDate(
|
|
item.created_at,
|
|
)}
|
|
{item.service &&
|
|
` · ${item.service}`}
|
|
</span>
|
|
</div>
|
|
<Badge
|
|
className={
|
|
statusBadgeClasses[
|
|
item.status
|
|
]
|
|
}
|
|
>
|
|
{statusLabels[item.status]}
|
|
</Badge>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</div>
|
|
<div className="bg-card border-sidebar-border/70 rounded-xl border p-5">
|
|
<div className="mb-4 flex items-center justify-between">
|
|
<h2 className="font-display text-base font-bold">
|
|
Proyek Terbaru
|
|
</h2>
|
|
<Link
|
|
href={projectsIndex()}
|
|
className="text-primary flex items-center gap-1 text-xs font-semibold hover:underline"
|
|
>
|
|
Kelola portofolio
|
|
<ArrowUpRight className="size-3.5" />
|
|
</Link>
|
|
</div>
|
|
<ul className="divide-border divide-y">
|
|
{latest.map((project) => (
|
|
<li
|
|
key={project.id}
|
|
className="flex items-start gap-4 py-3"
|
|
>
|
|
<span className="bg-accent text-accent-foreground shrink-0 rounded-md px-2 py-0.5 font-mono text-xs font-bold">
|
|
{project.year}
|
|
</span>
|
|
<div className="flex min-w-0 flex-col">
|
|
<span className="truncate text-sm font-semibold">
|
|
{project.title}
|
|
</span>
|
|
<span className="text-muted-foreground truncate text-xs">
|
|
{project.client} ·{' '}
|
|
{sectorLabels[project.sector]}
|
|
</span>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex flex-col gap-4">
|
|
<div className="bg-card border-sidebar-border/70 rounded-xl border p-5">
|
|
<h2 className="font-display mb-4 text-base font-bold">
|
|
Proyek per Sektor
|
|
</h2>
|
|
<div className="flex flex-col gap-4">
|
|
{sectors.map((sector) => {
|
|
const count =
|
|
projectsBySector(sector).length;
|
|
const percent = projects.length
|
|
? Math.round(
|
|
(count / projects.length) * 100,
|
|
)
|
|
: 0;
|
|
|
|
return (
|
|
<div
|
|
key={sector}
|
|
className="flex flex-col gap-1.5"
|
|
>
|
|
<div className="flex items-center justify-between text-sm">
|
|
<span className="flex items-center gap-2 font-medium">
|
|
<Building2 className="text-primary size-4" />
|
|
{sectorLabels[sector]}
|
|
</span>
|
|
<span className="text-muted-foreground font-mono text-xs">
|
|
{count} proyek ·{' '}
|
|
{
|
|
clientsInSector(sector)
|
|
.length
|
|
}{' '}
|
|
klien
|
|
</span>
|
|
</div>
|
|
<div className="bg-muted h-2 overflow-hidden rounded-full">
|
|
<div
|
|
className="bg-primary h-full rounded-full"
|
|
style={{
|
|
width: `${percent}%`,
|
|
}}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="bg-card border-sidebar-border/70 rounded-xl border p-5">
|
|
<h2 className="font-display mb-3 text-base font-bold">
|
|
Halaman Website
|
|
</h2>
|
|
<div className="flex flex-col">
|
|
{sitePages.map((page) => (
|
|
<a
|
|
key={page.title}
|
|
href={page.href.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="hover:bg-accent hover:text-accent-foreground flex items-center justify-between rounded-lg px-3 py-2 text-sm transition-colors"
|
|
>
|
|
{page.title}
|
|
<ArrowUpRight className="size-4 opacity-60" />
|
|
</a>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
}
|
|
|
|
Dashboard.layout = {
|
|
breadcrumbs: [
|
|
{
|
|
title: 'Dashboard',
|
|
href: dashboard(),
|
|
},
|
|
],
|
|
};
|