@@ -1,25 +1,294 @@
|
||||
import { Head } from '@inertiajs/react';
|
||||
import { PlaceholderPattern } from '@/components/ui/placeholder-pattern';
|
||||
import { dashboard } from '@/routes';
|
||||
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);
|
||||
|
||||
export default function Dashboard() {
|
||||
return (
|
||||
<>
|
||||
<Head title="Dashboard" />
|
||||
<div className="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
|
||||
<div className="grid auto-rows-min gap-4 md:grid-cols-3">
|
||||
<div className="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border">
|
||||
<PlaceholderPattern className="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
<div className="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border">
|
||||
<PlaceholderPattern className="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" />
|
||||
</div>
|
||||
<div className="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border">
|
||||
<PlaceholderPattern className="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" />
|
||||
<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="border-sidebar-border/70 dark:border-sidebar-border relative min-h-[100vh] flex-1 overflow-hidden rounded-xl border md:min-h-min">
|
||||
<PlaceholderPattern className="absolute inset-0 size-full stroke-neutral-900/20 dark:stroke-neutral-100/20" />
|
||||
|
||||
<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>
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user