Admin Panel
tests / ci (push) Canceled after 0s

This commit is contained in:
2026-09-29 14:57:37 +07:00
parent 04fbddba13
commit 8c6dad2a31
94 changed files with 4864 additions and 1151 deletions
+285 -16
View File
@@ -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>
</>