style: apply consistent code formatting across project files
This commit is contained in:
+110
-52
@@ -28,7 +28,7 @@ export default function BalancesPage() {
|
||||
setLoading(true)
|
||||
const [subRes, creRes] = await Promise.all([
|
||||
getBalancesAsSubmittor(id),
|
||||
getBalancesAsCreator(id)
|
||||
getBalancesAsCreator(id),
|
||||
])
|
||||
setSubmittorBalances(subRes)
|
||||
setCreatorBalances(creRes)
|
||||
@@ -36,29 +36,33 @@ export default function BalancesPage() {
|
||||
}
|
||||
|
||||
const formatRupiah = (n: number) =>
|
||||
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(n)
|
||||
new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
maximumFractionDigits: 0,
|
||||
}).format(n)
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in duration-500 pb-12">
|
||||
<div className="animate-in fade-in space-y-6 pb-12 duration-500">
|
||||
<div>
|
||||
<span className="text-xs font-semibold text-slate-400">Keuangan / Saldo</span>
|
||||
<h2 className="text-xl font-black text-slate-900 dark:text-white flex items-center gap-2 tracking-tight">
|
||||
<Wallet className="w-6 h-6 text-[#1B2CC1]" />
|
||||
<h2 className="flex items-center gap-2 text-xl font-black tracking-tight text-slate-900 dark:text-white">
|
||||
<Wallet className="h-6 w-6 text-[#1B2CC1]" />
|
||||
Buku Saldo
|
||||
</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
Pantau riwayat saldo lebih atau kurang dari transaksi pesanan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex bg-slate-100 dark:bg-slate-900 p-1 rounded-xl w-full max-w-sm border border-slate-200 dark:border-slate-800">
|
||||
<div className="flex w-full max-w-sm rounded-xl border border-slate-200 bg-slate-100 p-1 dark:border-slate-800 dark:bg-slate-900">
|
||||
<button
|
||||
onClick={() => setActiveTab('SUBMITTOR')}
|
||||
className={cn(
|
||||
"flex-1 text-xs font-bold py-2 rounded-lg transition-all",
|
||||
activeTab === 'SUBMITTOR'
|
||||
? "bg-white dark:bg-slate-800 shadow-sm text-slate-900 dark:text-white"
|
||||
: "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
||||
'flex-1 rounded-lg py-2 text-xs font-bold transition-all',
|
||||
activeTab === 'SUBMITTOR'
|
||||
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-white'
|
||||
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'
|
||||
)}
|
||||
>
|
||||
Saldo Saya (Penitip)
|
||||
@@ -66,10 +70,10 @@ export default function BalancesPage() {
|
||||
<button
|
||||
onClick={() => setActiveTab('CREATOR')}
|
||||
className={cn(
|
||||
"flex-1 text-xs font-bold py-2 rounded-lg transition-all",
|
||||
activeTab === 'CREATOR'
|
||||
? "bg-white dark:bg-slate-800 shadow-sm text-slate-900 dark:text-white"
|
||||
: "text-slate-500 hover:text-slate-700 dark:hover:text-slate-300"
|
||||
'flex-1 rounded-lg py-2 text-xs font-bold transition-all',
|
||||
activeTab === 'CREATOR'
|
||||
? 'bg-white text-slate-900 shadow-sm dark:bg-slate-800 dark:text-white'
|
||||
: 'text-slate-500 hover:text-slate-700 dark:hover:text-slate-300'
|
||||
)}
|
||||
>
|
||||
Saldo Orang (Kreator)
|
||||
@@ -77,45 +81,71 @@ export default function BalancesPage() {
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex flex-col items-center justify-center min-h-[30vh] gap-3">
|
||||
<Loader2 className="animate-spin text-[#1B2CC1] w-8 h-8" />
|
||||
<span className="text-xs text-slate-500 font-semibold">Memuat saldo...</span>
|
||||
<div className="flex min-h-[30vh] flex-col items-center justify-center gap-3">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-[#1B2CC1]" />
|
||||
<span className="text-xs font-semibold text-slate-500">Memuat saldo...</span>
|
||||
</div>
|
||||
) : activeTab === 'SUBMITTOR' ? (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 dark:bg-blue-950/30 p-4 rounded-xl border border-blue-100 dark:border-blue-900/50 mb-6">
|
||||
<h3 className="font-bold text-blue-900 dark:text-blue-100 text-sm mb-1">Saldo Anda di Kreator Lain</h3>
|
||||
<p className="text-xs text-blue-700 dark:text-blue-300 leading-relaxed">
|
||||
Jika saldo <strong>positif</strong> (hijau), Anda memiliki deposit yang bisa digunakan untuk pesanan berikutnya di kreator tersebut. Jika <strong>negatif</strong> (merah), Anda berhutang.
|
||||
<div className="mb-6 rounded-xl border border-blue-100 bg-blue-50 p-4 dark:border-blue-900/50 dark:bg-blue-950/30">
|
||||
<h3 className="mb-1 text-sm font-bold text-blue-900 dark:text-blue-100">
|
||||
Saldo Anda di Kreator Lain
|
||||
</h3>
|
||||
<p className="text-xs leading-relaxed text-blue-700 dark:text-blue-300">
|
||||
Jika saldo <strong>positif</strong> (hijau), Anda memiliki deposit yang bisa digunakan
|
||||
untuk pesanan berikutnya di kreator tersebut. Jika <strong>negatif</strong> (merah),
|
||||
Anda berhutang.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{submittorBalances.length === 0 ? (
|
||||
<div className="text-center p-12 rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm">
|
||||
<p className="text-xs text-slate-500 font-medium">Belum ada catatan saldo.</p>
|
||||
<div className="rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<p className="text-xs font-medium text-slate-500">Belum ada catatan saldo.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{submittorBalances.map((b, i) => (
|
||||
<Card key={i} className="rounded-2xl border-slate-200/90 dark:border-slate-800 overflow-hidden hover:border-[#1B2CC1]/30 transition-all shadow-sm">
|
||||
<div className="p-4 flex items-center gap-3 bg-slate-50/50 dark:bg-slate-800/40 border-b border-slate-100 dark:border-slate-800">
|
||||
<Card
|
||||
key={i}
|
||||
className="overflow-hidden rounded-2xl border-slate-200/90 shadow-sm transition-all hover:border-[#1B2CC1]/30 dark:border-slate-800"
|
||||
>
|
||||
<div className="flex items-center gap-3 border-b border-slate-100 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
|
||||
{b.creator.photo ? (
|
||||
<img src={b.creator.photo} alt={b.creator.name} className="w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-slate-900 shadow-sm" />
|
||||
<img
|
||||
src={b.creator.photo}
|
||||
alt={b.creator.name}
|
||||
className="h-10 w-10 rounded-full object-cover shadow-sm ring-2 ring-white dark:ring-slate-900"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center font-bold text-sm ring-2 ring-white dark:ring-slate-900 shadow-sm">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-[#1B2CC1]/10 text-sm font-bold text-[#1B2CC1] shadow-sm ring-2 ring-white dark:ring-slate-900">
|
||||
{b.creator.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-bold text-slate-900 dark:text-white text-sm">{b.creator.name}</p>
|
||||
<p className="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Kreator</p>
|
||||
<p className="text-sm font-bold text-slate-900 dark:text-white">
|
||||
{b.creator.name}
|
||||
</p>
|
||||
<p className="text-[10px] font-semibold tracking-wider text-slate-500 uppercase">
|
||||
Kreator
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="p-4 bg-white dark:bg-slate-900">
|
||||
<CardContent className="bg-white p-4 dark:bg-slate-900">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">Total Saldo</span>
|
||||
<div className={cn("text-xl font-black flex items-center gap-1.5", b.amount > 0 ? "text-emerald-600" : "text-rose-600")}>
|
||||
{b.amount > 0 ? <ArrowUpRight className="w-5 h-5" /> : <ArrowDownRight className="w-5 h-5" />}
|
||||
<span className="text-[11px] font-bold tracking-wider text-slate-400 uppercase">
|
||||
Total Saldo
|
||||
</span>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-xl font-black',
|
||||
b.amount > 0 ? 'text-emerald-600' : 'text-rose-600'
|
||||
)}
|
||||
>
|
||||
{b.amount > 0 ? (
|
||||
<ArrowUpRight className="h-5 w-5" />
|
||||
) : (
|
||||
<ArrowDownRight className="h-5 w-5" />
|
||||
)}
|
||||
{formatRupiah(b.amount)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -127,40 +157,68 @@ export default function BalancesPage() {
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-amber-50 dark:bg-amber-950/30 p-4 rounded-xl border border-amber-100 dark:border-amber-900/50 mb-6">
|
||||
<h3 className="font-bold text-amber-900 dark:text-amber-100 text-sm mb-1">Saldo Orang Lain di Anda</h3>
|
||||
<p className="text-xs text-amber-700 dark:text-amber-300 leading-relaxed">
|
||||
Jika saldo <strong>positif</strong> (merah bagi Anda), artinya Anda memegang uang lebih milik penitip (Hutang Anda ke mereka). Jika <strong>negatif</strong> (hijau), mereka berhutang ke Anda.
|
||||
<div className="mb-6 rounded-xl border border-amber-100 bg-amber-50 p-4 dark:border-amber-900/50 dark:bg-amber-950/30">
|
||||
<h3 className="mb-1 text-sm font-bold text-amber-900 dark:text-amber-100">
|
||||
Saldo Orang Lain di Anda
|
||||
</h3>
|
||||
<p className="text-xs leading-relaxed text-amber-700 dark:text-amber-300">
|
||||
Jika saldo <strong>positif</strong> (merah bagi Anda), artinya Anda memegang uang
|
||||
lebih milik penitip (Hutang Anda ke mereka). Jika <strong>negatif</strong> (hijau),
|
||||
mereka berhutang ke Anda.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{creatorBalances.length === 0 ? (
|
||||
<div className="text-center p-12 rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm">
|
||||
<p className="text-xs text-slate-500 font-medium">Belum ada penitip yang memiliki catatan saldo dengan Anda.</p>
|
||||
<div className="rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<p className="text-xs font-medium text-slate-500">
|
||||
Belum ada penitip yang memiliki catatan saldo dengan Anda.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
|
||||
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
|
||||
{creatorBalances.map((b, i) => (
|
||||
<Card key={i} className="rounded-2xl border-slate-200/90 dark:border-slate-800 overflow-hidden hover:border-[#1B2CC1]/30 transition-all shadow-sm">
|
||||
<div className="p-4 flex items-center gap-3 bg-slate-50/50 dark:bg-slate-800/40 border-b border-slate-100 dark:border-slate-800">
|
||||
<Card
|
||||
key={i}
|
||||
className="overflow-hidden rounded-2xl border-slate-200/90 shadow-sm transition-all hover:border-[#1B2CC1]/30 dark:border-slate-800"
|
||||
>
|
||||
<div className="flex items-center gap-3 border-b border-slate-100 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
|
||||
{b.user.photo ? (
|
||||
<img src={b.user.photo} alt={b.user.name} className="w-10 h-10 rounded-full object-cover ring-2 ring-white dark:ring-slate-900 shadow-sm" />
|
||||
<img
|
||||
src={b.user.photo}
|
||||
alt={b.user.name}
|
||||
className="h-10 w-10 rounded-full object-cover shadow-sm ring-2 ring-white dark:ring-slate-900"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-slate-200 dark:bg-slate-700 text-slate-600 dark:text-slate-300 flex items-center justify-center font-bold text-sm ring-2 ring-white dark:ring-slate-900 shadow-sm">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-slate-200 text-sm font-bold text-slate-600 shadow-sm ring-2 ring-white dark:bg-slate-700 dark:text-slate-300 dark:ring-slate-900">
|
||||
{b.user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<p className="font-bold text-slate-900 dark:text-white text-sm">{b.user.name}</p>
|
||||
<p className="text-[10px] font-semibold text-slate-500 uppercase tracking-wider">Penitip</p>
|
||||
<p className="text-sm font-bold text-slate-900 dark:text-white">
|
||||
{b.user.name}
|
||||
</p>
|
||||
<p className="text-[10px] font-semibold tracking-wider text-slate-500 uppercase">
|
||||
Penitip
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<CardContent className="p-4 bg-white dark:bg-slate-900">
|
||||
<CardContent className="bg-white p-4 dark:bg-slate-900">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-[11px] font-bold text-slate-400 uppercase tracking-wider">Saldo Penitip</span>
|
||||
<span className="text-[11px] font-bold tracking-wider text-slate-400 uppercase">
|
||||
Saldo Penitip
|
||||
</span>
|
||||
{/* From Creator perspective: positive balance of submittor is bad for creator (creator holds their money) */}
|
||||
<div className={cn("text-xl font-black flex items-center gap-1.5", b.amount > 0 ? "text-rose-600" : "text-emerald-600")}>
|
||||
{b.amount > 0 ? <ArrowDownRight className="w-5 h-5" /> : <ArrowUpRight className="w-5 h-5" />}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 text-xl font-black',
|
||||
b.amount > 0 ? 'text-rose-600' : 'text-emerald-600'
|
||||
)}
|
||||
>
|
||||
{b.amount > 0 ? (
|
||||
<ArrowDownRight className="h-5 w-5" />
|
||||
) : (
|
||||
<ArrowUpRight className="h-5 w-5" />
|
||||
)}
|
||||
{formatRupiah(b.amount)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
import { AppLayout } from "@/components/AppLayout";
|
||||
import { AppLayout } from '@/components/AppLayout'
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<AppLayout>
|
||||
{children}
|
||||
</AppLayout>
|
||||
);
|
||||
return <AppLayout>{children}</AppLayout>
|
||||
}
|
||||
|
||||
+563
-410
File diff suppressed because it is too large
Load Diff
+441
-320
File diff suppressed because it is too large
Load Diff
@@ -16,18 +16,18 @@ export function PublicOrderActions({ order }: { order: any }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-row justify-center items-center gap-2 mt-5">
|
||||
<div className="mt-5 flex flex-row items-center justify-center gap-2">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className="h-10 px-5 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold text-sm shadow-md shadow-[#1B2CC1]/20 gap-2 flex items-center justify-center transition-all cursor-pointer">
|
||||
<ShoppingBag className="w-4 h-4" />
|
||||
<DialogTrigger className="flex h-10 cursor-pointer items-center justify-center gap-2 rounded-xl bg-[#1B2CC1] px-5 text-sm font-bold text-white shadow-md shadow-[#1B2CC1]/20 transition-all hover:bg-[#15229E]">
|
||||
<ShoppingBag className="h-4 w-4" />
|
||||
Titip Sekarang
|
||||
</DialogTrigger>
|
||||
{open && <OrderFormModal order={order} onSuccess={() => setOpen(false)} />}
|
||||
</Dialog>
|
||||
|
||||
<ShareButton
|
||||
|
||||
<ShareButton
|
||||
orderId={order.id}
|
||||
className="h-10 px-4 rounded-xl border-slate-200 dark:border-slate-700 font-bold text-sm gap-2 hover:bg-slate-50 dark:hover:bg-slate-800 shadow-sm text-slate-700 dark:text-slate-200"
|
||||
className="h-10 gap-2 rounded-xl border-slate-200 px-4 text-sm font-bold text-slate-700 shadow-sm hover:bg-slate-50 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
||||
showText={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -10,10 +10,14 @@ import { cn } from '@/lib/utils'
|
||||
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
export default async function PublicOrderDetailPage({ params }: { params: Promise<{ id: string }> }) {
|
||||
export default async function PublicOrderDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>
|
||||
}) {
|
||||
const { id } = await params
|
||||
const session = await getSessionUser()
|
||||
|
||||
|
||||
if (!session) {
|
||||
redirect(`/login?callbackUrl=/order/${id}`)
|
||||
}
|
||||
@@ -25,48 +29,59 @@ export default async function PublicOrderDetailPage({ params }: { params: Promis
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 max-w-4xl mx-auto pb-20 animate-in fade-in duration-500">
|
||||
<div className="animate-in fade-in mx-auto max-w-4xl space-y-6 pb-20 duration-500">
|
||||
<div className="flex items-center gap-3">
|
||||
<Link href="/" className="p-2 -ml-2 rounded-xl hover:bg-slate-100 dark:hover:bg-slate-800 text-slate-500 transition-colors">
|
||||
<ChevronLeft className="w-5 h-5" />
|
||||
<Link
|
||||
href="/"
|
||||
className="-ml-2 rounded-xl p-2 text-slate-500 transition-colors hover:bg-slate-100 dark:hover:bg-slate-800"
|
||||
>
|
||||
<ChevronLeft className="h-5 w-5" />
|
||||
</Link>
|
||||
<h1 className="text-xl font-black text-slate-900 dark:text-white tracking-tight">Detail PO</h1>
|
||||
<h1 className="text-xl font-black tracking-tight text-slate-900 dark:text-white">
|
||||
Detail PO
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Hero Card */}
|
||||
<div className="relative overflow-hidden rounded-3xl bg-white dark:bg-slate-900 shadow-sm border border-slate-200/60 dark:border-slate-800">
|
||||
<div className="absolute top-0 left-0 w-full h-24 bg-gradient-to-r from-[#1B2CC1] to-[#121E85]" />
|
||||
|
||||
<div className="relative pt-10 px-5 pb-5 sm:px-6 sm:pb-6 flex flex-col items-center text-center">
|
||||
<div className="w-20 h-20 rounded-2xl bg-white dark:bg-slate-900 shadow-md border-4 border-white dark:border-slate-900 flex items-center justify-center mb-3">
|
||||
<div className="relative overflow-hidden rounded-3xl border border-slate-200/60 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="absolute top-0 left-0 h-24 w-full bg-gradient-to-r from-[#1B2CC1] to-[#121E85]" />
|
||||
|
||||
<div className="relative flex flex-col items-center px-5 pt-10 pb-5 text-center sm:px-6 sm:pb-6">
|
||||
<div className="mb-3 flex h-20 w-20 items-center justify-center rounded-2xl border-4 border-white bg-white shadow-md dark:border-slate-900 dark:bg-slate-900">
|
||||
{order.creator.photo ? (
|
||||
<img src={order.creator.photo} alt={order.creator.name} className="w-full h-full object-cover rounded-xl" />
|
||||
<img
|
||||
src={order.creator.photo}
|
||||
alt={order.creator.name}
|
||||
className="h-full w-full rounded-xl object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Store className="w-8 h-8 text-[#1B2CC1]" />
|
||||
<Store className="h-8 w-8 text-[#1B2CC1]" />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<h2 className="text-xl sm:text-2xl font-black text-slate-900 dark:text-white mb-2 leading-tight">
|
||||
|
||||
<h2 className="mb-2 text-xl leading-tight font-black text-slate-900 sm:text-2xl dark:text-white">
|
||||
{order.title}
|
||||
</h2>
|
||||
|
||||
<div className="flex flex-wrap justify-center items-center gap-3 text-sm font-semibold text-slate-600 dark:text-slate-400">
|
||||
<span className="flex items-center gap-1.5 bg-slate-100 dark:bg-slate-800 px-3 py-1 rounded-full">
|
||||
<User className="w-4 h-4 text-[#1B2CC1]" />
|
||||
|
||||
<div className="flex flex-wrap items-center justify-center gap-3 text-sm font-semibold text-slate-600 dark:text-slate-400">
|
||||
<span className="flex items-center gap-1.5 rounded-full bg-slate-100 px-3 py-1 dark:bg-slate-800">
|
||||
<User className="h-4 w-4 text-[#1B2CC1]" />
|
||||
{order.creator.name}
|
||||
</span>
|
||||
<span className={cn(
|
||||
"flex items-center gap-1.5 px-3 py-1 rounded-full border",
|
||||
order.status === 'OPEN'
|
||||
? "bg-emerald-50 text-emerald-700 dark:bg-emerald-950/30 dark:text-emerald-400 border-emerald-100 dark:border-emerald-900/50"
|
||||
: "bg-rose-50 text-rose-700 dark:bg-rose-950/30 dark:text-rose-400 border-rose-100 dark:border-rose-900/50"
|
||||
)}>
|
||||
<CheckCircle2 className="w-4 h-4" />
|
||||
<span
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-full border px-3 py-1',
|
||||
order.status === 'OPEN'
|
||||
? 'border-emerald-100 bg-emerald-50 text-emerald-700 dark:border-emerald-900/50 dark:bg-emerald-950/30 dark:text-emerald-400'
|
||||
: 'border-rose-100 bg-rose-50 text-rose-700 dark:border-rose-900/50 dark:bg-rose-950/30 dark:text-rose-400'
|
||||
)}
|
||||
>
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
{order.status}
|
||||
</span>
|
||||
{order.allow_custom && (
|
||||
<span className="flex items-center gap-1.5 bg-blue-50 text-blue-700 dark:bg-blue-950/30 dark:text-blue-400 px-3 py-1 rounded-full border border-blue-100 dark:border-blue-900/50">
|
||||
<Sparkles className="w-4 h-4" />
|
||||
<span className="flex items-center gap-1.5 rounded-full border border-blue-100 bg-blue-50 px-3 py-1 text-blue-700 dark:border-blue-900/50 dark:bg-blue-950/30 dark:text-blue-400">
|
||||
<Sparkles className="h-4 w-4" />
|
||||
Custom Item
|
||||
</span>
|
||||
)}
|
||||
@@ -74,46 +89,54 @@ export default async function PublicOrderDetailPage({ params }: { params: Promis
|
||||
|
||||
<PublicOrderActions order={order} />
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 dark:bg-slate-800/50 border-t border-slate-100 dark:border-slate-800 p-4 sm:p-5 flex flex-col sm:flex-row divide-y sm:divide-y-0 sm:divide-x divide-slate-200 dark:divide-slate-700">
|
||||
<div className="flex-1 py-3 sm:py-0 sm:px-4 text-center">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Tanggal PO</p>
|
||||
|
||||
<div className="flex flex-col divide-y divide-slate-200 border-t border-slate-100 bg-slate-50 p-4 sm:flex-row sm:divide-x sm:divide-y-0 sm:p-5 dark:divide-slate-700 dark:border-slate-800 dark:bg-slate-800/50">
|
||||
<div className="flex-1 py-3 text-center sm:px-4 sm:py-0">
|
||||
<p className="mb-0.5 text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
||||
Tanggal PO
|
||||
</p>
|
||||
<p className="text-base font-black text-slate-800 dark:text-slate-200">
|
||||
{format(new Date(order.date), 'dd MMM yyyy', { locale: idLocale })}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 py-3 sm:py-0 sm:px-4 text-center">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Total Orang</p>
|
||||
<div className="flex-1 py-3 text-center sm:px-4 sm:py-0">
|
||||
<p className="mb-0.5 text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
||||
Total Orang
|
||||
</p>
|
||||
<p className="text-base font-black text-slate-800 dark:text-slate-200">
|
||||
{order.submissions.length} <span className="text-xs font-semibold text-slate-500">menitip</span>
|
||||
{order.submissions.length}{' '}
|
||||
<span className="text-xs font-semibold text-slate-500">menitip</span>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex-1 py-3 sm:py-0 sm:px-4 text-center">
|
||||
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-0.5">Menu Tersedia</p>
|
||||
<div className="flex-1 py-3 text-center sm:px-4 sm:py-0">
|
||||
<p className="mb-0.5 text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
||||
Menu Tersedia
|
||||
</p>
|
||||
<p className="text-base font-black text-slate-800 dark:text-slate-200">
|
||||
{order.available_items.length} <span className="text-xs font-semibold text-slate-500">item</span>
|
||||
{order.available_items.length}{' '}
|
||||
<span className="text-xs font-semibold text-slate-500">item</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 items-start">
|
||||
<div className="grid grid-cols-1 items-start gap-6 lg:grid-cols-3">
|
||||
{/* Left 2 Cols: Daftar Penitip & Description */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{order.description && (
|
||||
<div className="bg-white dark:bg-slate-900 rounded-3xl border border-slate-200/60 dark:border-slate-800 shadow-sm overflow-hidden p-5 sm:p-6">
|
||||
<h3 className="font-bold text-slate-900 dark:text-white mb-3">Deskripsi</h3>
|
||||
<div className="text-slate-600 dark:text-slate-300 text-sm leading-relaxed whitespace-pre-wrap">
|
||||
<div className="overflow-hidden rounded-3xl border border-slate-200/60 bg-white p-5 shadow-sm sm:p-6 dark:border-slate-800 dark:bg-slate-900">
|
||||
<h3 className="mb-3 font-bold text-slate-900 dark:text-white">Deskripsi</h3>
|
||||
<div className="text-sm leading-relaxed whitespace-pre-wrap text-slate-600 dark:text-slate-300">
|
||||
{order.description}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white dark:bg-slate-900 rounded-3xl border border-slate-200/60 dark:border-slate-800 shadow-sm overflow-hidden">
|
||||
<div className="p-5 sm:p-6 border-b border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/30 flex items-center justify-between">
|
||||
<div className="overflow-hidden rounded-3xl border border-slate-200/60 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 bg-slate-50/50 p-5 sm:p-6 dark:border-slate-800 dark:bg-slate-800/30">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-8 h-8 rounded-xl bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center">
|
||||
<Users className="w-4 h-4" />
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-[#1B2CC1]/10 text-[#1B2CC1]">
|
||||
<Users className="h-4 w-4" />
|
||||
</div>
|
||||
<h3 className="font-bold text-slate-900 dark:text-white">Daftar Penitip</h3>
|
||||
</div>
|
||||
@@ -122,28 +145,40 @@ export default async function PublicOrderDetailPage({ params }: { params: Promis
|
||||
<div className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{order.submissions.length > 0 ? (
|
||||
order.submissions.map((sub: any) => (
|
||||
<div key={sub.id} className="p-4 sm:p-6 flex items-start gap-4 hover:bg-slate-50/50 dark:hover:bg-slate-800/20 transition-colors">
|
||||
<div
|
||||
key={sub.id}
|
||||
className="flex items-start gap-4 p-4 transition-colors hover:bg-slate-50/50 sm:p-6 dark:hover:bg-slate-800/20"
|
||||
>
|
||||
{sub.user.photo ? (
|
||||
<img src={sub.user.photo} alt={sub.user.name} className="w-10 h-10 rounded-full object-cover shrink-0" />
|
||||
<img
|
||||
src={sub.user.photo}
|
||||
alt={sub.user.name}
|
||||
className="h-10 w-10 shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-full bg-slate-100 dark:bg-slate-800 text-slate-500 flex items-center justify-center font-bold text-sm shrink-0">
|
||||
<div className="flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-slate-100 text-sm font-bold text-slate-500 dark:bg-slate-800">
|
||||
{sub.user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<h4 className="font-bold text-slate-800 dark:text-slate-200 text-sm mb-2">{sub.user.name}</h4>
|
||||
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="mb-2 text-sm font-bold text-slate-800 dark:text-slate-200">
|
||||
{sub.user.name}
|
||||
</h4>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{sub.items.map((item: any) => (
|
||||
<span
|
||||
key={item.id}
|
||||
className={`inline-flex items-center px-2 py-0.5 rounded-md text-[11px] font-medium border ${
|
||||
item.is_custom
|
||||
? 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-900/30 dark:text-purple-300 dark:border-purple-800'
|
||||
: 'bg-slate-100 text-slate-700 border-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700'
|
||||
<span
|
||||
key={item.id}
|
||||
className={`inline-flex items-center rounded-md border px-2 py-0.5 text-[11px] font-medium ${
|
||||
item.is_custom
|
||||
? 'border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-800 dark:bg-purple-900/30 dark:text-purple-300'
|
||||
: 'border-slate-200 bg-slate-100 text-slate-700 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300'
|
||||
}`}
|
||||
>
|
||||
{item.qty}x {item.name} {item.note && <span className="italic ml-0.5 opacity-70">({item.note})</span>}
|
||||
{item.qty}x {item.name}{' '}
|
||||
{item.note && (
|
||||
<span className="ml-0.5 italic opacity-70">({item.note})</span>
|
||||
)}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
@@ -152,8 +187,10 @@ export default async function PublicOrderDetailPage({ params }: { params: Promis
|
||||
))
|
||||
) : (
|
||||
<div className="p-12 text-center">
|
||||
<p className="text-slate-500 font-medium">Belum ada yang menitip.</p>
|
||||
<p className="text-xs text-slate-400 mt-1">Jadilah yang pertama untuk menitip pesanan!</p>
|
||||
<p className="font-medium text-slate-500">Belum ada yang menitip.</p>
|
||||
<p className="mt-1 text-xs text-slate-400">
|
||||
Jadilah yang pertama untuk menitip pesanan!
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+35
-31
@@ -11,37 +11,38 @@ export default async function Dashboard() {
|
||||
const orders = await getAvailableOrders()
|
||||
|
||||
return (
|
||||
<div className="space-y-8 animate-in fade-in duration-500">
|
||||
<div className="animate-in fade-in space-y-8 duration-500">
|
||||
{/* Banner / Hero Section */}
|
||||
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-[#1B2CC1] via-[#2135E0] to-[#121E85] p-6 sm:p-8 text-white shadow-xl shadow-[#1B2CC1]/15">
|
||||
<div className="absolute right-0 top-0 -mt-10 -mr-10 h-64 w-64 rounded-full bg-white/10 blur-3xl pointer-events-none" />
|
||||
<div className="relative overflow-hidden rounded-3xl bg-gradient-to-r from-[#1B2CC1] via-[#2135E0] to-[#121E85] p-6 text-white shadow-xl shadow-[#1B2CC1]/15 sm:p-8">
|
||||
<div className="pointer-events-none absolute top-0 right-0 -mt-10 -mr-10 h-64 w-64 rounded-full bg-white/10 blur-3xl" />
|
||||
<div className="relative z-10 max-w-2xl space-y-3">
|
||||
<div className="inline-flex items-center gap-2 rounded-full bg-white/15 px-3 py-1 text-xs font-bold text-blue-100 backdrop-blur-md border border-white/20">
|
||||
<div className="inline-flex items-center gap-2 rounded-full border border-white/20 bg-white/15 px-3 py-1 text-xs font-bold text-blue-100 backdrop-blur-md">
|
||||
<Sparkles className="h-3.5 w-3.5" />
|
||||
<span>Sistem Titip & Jastip Cepat</span>
|
||||
</div>
|
||||
<h2 className="text-2xl sm:text-3xl font-black tracking-tight leading-tight">
|
||||
<h2 className="text-2xl leading-tight font-black tracking-tight sm:text-3xl">
|
||||
Titip Makanan & Minuman Bareng Teman Kantor
|
||||
</h2>
|
||||
<p className="text-sm text-blue-100/90 leading-relaxed max-w-xl">
|
||||
Pilih PO yang sedang buka hari ini, tentukan menu favoritmu, dan biarkan pembuat PO mengurus tagihan secara praktis.
|
||||
<p className="max-w-xl text-sm leading-relaxed text-blue-100/90">
|
||||
Pilih PO yang sedang buka hari ini, tentukan menu favoritmu, dan biarkan pembuat PO
|
||||
mengurus tagihan secara praktis.
|
||||
</p>
|
||||
<div className="pt-2 flex flex-wrap gap-3">
|
||||
<Link
|
||||
<div className="flex flex-wrap gap-3 pt-2">
|
||||
<Link
|
||||
href="/my-orders"
|
||||
className={cn(
|
||||
buttonVariants(),
|
||||
"rounded-xl bg-white text-[#1B2CC1] hover:bg-blue-50 font-bold shadow-md h-10 px-5 gap-2"
|
||||
'h-10 gap-2 rounded-xl bg-white px-5 font-bold text-[#1B2CC1] shadow-md hover:bg-blue-50'
|
||||
)}
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
<Plus className="h-4 w-4" />
|
||||
Buka Jasa PO Baru
|
||||
</Link>
|
||||
<Link
|
||||
<Link
|
||||
href="/my-purchases"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "outline" }),
|
||||
"rounded-xl bg-white/10 hover:bg-white/20 text-white border-white/20 font-semibold h-10 px-4"
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'h-10 rounded-xl border-white/20 bg-white/10 px-4 font-semibold text-white hover:bg-white/20'
|
||||
)}
|
||||
>
|
||||
Lihat Titipan Saya
|
||||
@@ -51,46 +52,49 @@ export default async function Dashboard() {
|
||||
</div>
|
||||
|
||||
{/* Main Grid Header */}
|
||||
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div className="flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2.5 h-2.5 rounded-full bg-[#1B2CC1] animate-pulse" />
|
||||
<h2 className="text-xl font-black text-slate-900 dark:text-white tracking-tight">
|
||||
<div className="h-2.5 w-2.5 animate-pulse rounded-full bg-[#1B2CC1]" />
|
||||
<h2 className="text-xl font-black tracking-tight text-slate-900 dark:text-white">
|
||||
Daftar PO Terbuka Hari Ini
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5">
|
||||
<p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
|
||||
Pesanan dengan status OPEN yang bisa Anda ikuti hari ini.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<span className="text-xs font-bold text-[#1B2CC1] bg-[#1B2CC1]/10 px-3 py-1.5 rounded-xl">
|
||||
<span className="rounded-xl bg-[#1B2CC1]/10 px-3 py-1.5 text-xs font-bold text-[#1B2CC1]">
|
||||
Total: {orders.length} PO Aktif
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{orders.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center p-12 sm:p-16 text-center rounded-3xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm">
|
||||
<div className="w-16 h-16 bg-[#1B2CC1]/10 rounded-2xl flex items-center justify-center mb-4 text-[#1B2CC1]">
|
||||
<Store className="w-8 h-8" />
|
||||
<div className="flex flex-col items-center justify-center rounded-3xl border-2 border-dashed border-slate-200 bg-white p-12 text-center shadow-sm sm:p-16 dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="mb-4 flex h-16 w-16 items-center justify-center rounded-2xl bg-[#1B2CC1]/10 text-[#1B2CC1]">
|
||||
<Store className="h-8 w-8" />
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-slate-800 dark:text-slate-200 mb-1">Belum Ada PO yang Dibuka Hari Ini</h3>
|
||||
<p className="text-xs text-slate-500 max-w-md mb-6 leading-relaxed">
|
||||
Mau jajan atau beli sesuatu? Jadilah orang pertama yang membuka jasa titip pesanan untuk teman-teman Anda!
|
||||
<h3 className="mb-1 text-lg font-bold text-slate-800 dark:text-slate-200">
|
||||
Belum Ada PO yang Dibuka Hari Ini
|
||||
</h3>
|
||||
<p className="mb-6 max-w-md text-xs leading-relaxed text-slate-500">
|
||||
Mau jajan atau beli sesuatu? Jadilah orang pertama yang membuka jasa titip pesanan untuk
|
||||
teman-teman Anda!
|
||||
</p>
|
||||
<Link
|
||||
<Link
|
||||
href="/my-orders"
|
||||
className={cn(
|
||||
buttonVariants(),
|
||||
"bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold rounded-xl shadow-md shadow-[#1B2CC1]/25 h-10 px-5 gap-2"
|
||||
'h-10 gap-2 rounded-xl bg-[#1B2CC1] px-5 font-bold text-white shadow-md shadow-[#1B2CC1]/25 hover:bg-[#15229E]'
|
||||
)}
|
||||
>
|
||||
<Plus className="w-4 h-4" /> Buka PO Sekarang
|
||||
<Plus className="h-4 w-4" /> Buka PO Sekarang
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col h-full space-y-4">
|
||||
<div className="flex flex-col space-y-4 max-h-[calc(100vh-260px)] overflow-y-auto pr-2 scrollbar-thin">
|
||||
<div className="flex h-full flex-col space-y-4">
|
||||
<div className="flex max-h-[calc(100vh-260px)] scrollbar-thin flex-col space-y-4 overflow-y-auto pr-2">
|
||||
{orders.map((order: any) => (
|
||||
<OrderCard key={order.id} order={order} />
|
||||
))}
|
||||
|
||||
+180
-77
@@ -6,20 +6,34 @@ import { Card, CardContent, CardHeader, CardTitle, CardFooter } from '@/componen
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import { Loader2, Save, CheckCircle, ShieldCheck, UserCircle, KeyRound, LockKeyhole } from 'lucide-react'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import {
|
||||
Loader2,
|
||||
Save,
|
||||
CheckCircle,
|
||||
ShieldCheck,
|
||||
UserCircle,
|
||||
KeyRound,
|
||||
LockKeyhole,
|
||||
} from 'lucide-react'
|
||||
|
||||
export default function ProfilePage() {
|
||||
const [user, setUser] = useState<any>(null)
|
||||
const [name, setName] = useState('')
|
||||
const [photo, setPhoto] = useState('')
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
|
||||
// Profile state
|
||||
const [savingProfile, setSavingProfile] = useState(false)
|
||||
const [profileError, setProfileError] = useState('')
|
||||
const [profileSuccess, setProfileSuccess] = useState(false)
|
||||
|
||||
|
||||
// Password state
|
||||
const [oldPassword, setOldPassword] = useState('')
|
||||
const [newPassword, setNewPassword] = useState('')
|
||||
@@ -50,13 +64,13 @@ export default function ProfilePage() {
|
||||
setProfileError('Nama wajib diisi')
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
setProfileError('')
|
||||
setProfileSuccess(false)
|
||||
setSavingProfile(true)
|
||||
|
||||
|
||||
const res = await updateProfile(user.id, name.trim(), photo.trim() || null)
|
||||
|
||||
|
||||
if (res.success) {
|
||||
setUser(res.user)
|
||||
setProfileSuccess(true)
|
||||
@@ -104,167 +118,256 @@ export default function ProfilePage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-3">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-[#1B2CC1]" />
|
||||
<span className="text-xs text-slate-500 font-semibold">Memuat profil...</span>
|
||||
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-3">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-[#1B2CC1]" />
|
||||
<span className="text-xs font-semibold text-slate-500">Memuat profil...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[50vh] gap-3">
|
||||
<span className="text-xs text-slate-500 font-semibold">Anda belum login.</span>
|
||||
<div className="flex min-h-[50vh] flex-col items-center justify-center gap-3">
|
||||
<span className="text-xs font-semibold text-slate-500">Anda belum login.</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in duration-500 pb-12 max-w-5xl">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
|
||||
<div className="animate-in fade-in max-w-5xl space-y-6 pb-12 duration-500">
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
{/* Left 2 Cols: Forms */}
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
{/* PROFILE FORM */}
|
||||
<Card className="rounded-3xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
||||
<div className="p-6 border-b border-slate-100 dark:border-slate-800 flex items-center justify-between">
|
||||
<Card className="overflow-hidden rounded-3xl border border-slate-200/90 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex items-center justify-between border-b border-slate-100 p-6 dark:border-slate-800">
|
||||
<div>
|
||||
<h2 className="text-lg font-black text-slate-900 dark:text-white">Informasi Akun</h2>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Ubah nama tampilan dan foto profil Anda.</p>
|
||||
<h2 className="text-lg font-black text-slate-900 dark:text-white">
|
||||
Informasi Akun
|
||||
</h2>
|
||||
<p className="mt-0.5 text-xs text-slate-500">
|
||||
Ubah nama tampilan dan foto profil Anda.
|
||||
</p>
|
||||
</div>
|
||||
<span className="text-[11px] font-bold text-[#1B2CC1] bg-[#1B2CC1]/10 px-2.5 py-1 rounded-full uppercase tracking-wider">
|
||||
<span className="rounded-full bg-[#1B2CC1]/10 px-2.5 py-1 text-[11px] font-bold tracking-wider text-[#1B2CC1] uppercase">
|
||||
{user.role}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSaveProfile}>
|
||||
<CardContent className="p-6 space-y-6">
|
||||
<CardContent className="space-y-6 p-6">
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300">Foto Profil</Label>
|
||||
<div className="flex flex-col sm:flex-row items-center gap-5 p-5 rounded-2xl border-2 border-dashed border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-800/30">
|
||||
<Label className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300">
|
||||
Foto Profil
|
||||
</Label>
|
||||
<div className="flex flex-col items-center gap-5 rounded-2xl border-2 border-dashed border-slate-200 bg-slate-50/50 p-5 sm:flex-row dark:border-slate-800 dark:bg-slate-800/30">
|
||||
{photo ? (
|
||||
<img src={photo} alt={name} className="w-20 h-20 rounded-2xl object-cover ring-4 ring-white dark:ring-slate-700 shadow-md shrink-0" />
|
||||
<img
|
||||
src={photo}
|
||||
alt={name}
|
||||
className="h-20 w-20 shrink-0 rounded-2xl object-cover shadow-md ring-4 ring-white dark:ring-slate-700"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-20 h-20 rounded-2xl bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center font-bold text-2xl ring-4 ring-white dark:ring-slate-700 shadow-sm shrink-0">
|
||||
{name ? name.charAt(0).toUpperCase() : <UserCircle className="w-10 h-10" />}
|
||||
<div className="flex h-20 w-20 shrink-0 items-center justify-center rounded-2xl bg-[#1B2CC1]/10 text-2xl font-bold text-[#1B2CC1] shadow-sm ring-4 ring-white dark:ring-slate-700">
|
||||
{name ? name.charAt(0).toUpperCase() : <UserCircle className="h-10 w-10" />}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1.5 flex-1 w-full text-center sm:text-left">
|
||||
<p className="text-xs font-bold text-slate-800 dark:text-slate-200">Pratinjau Avatar</p>
|
||||
<p className="text-[11px] text-slate-400">Masukkan tautan URL foto gambar di bawah untuk memperbarui gambar profil.</p>
|
||||
<div className="w-full flex-1 space-y-1.5 text-center sm:text-left">
|
||||
<p className="text-xs font-bold text-slate-800 dark:text-slate-200">
|
||||
Pratinjau Avatar
|
||||
</p>
|
||||
<p className="text-[11px] text-slate-400">
|
||||
Masukkan tautan URL foto gambar di bawah untuk memperbarui gambar profil.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="prof-name" className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300">
|
||||
<Label
|
||||
htmlFor="prof-name"
|
||||
className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300"
|
||||
>
|
||||
Nama Tampilan <span className="text-red-500">*</span>
|
||||
</Label>
|
||||
<Input id="prof-name" value={name} onChange={(e) => setName(e.target.value)} placeholder="Contoh: Budi Santoso" className="h-11 rounded-xl" />
|
||||
<Input
|
||||
id="prof-name"
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
placeholder="Contoh: Budi Santoso"
|
||||
className="h-11 rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="prof-photo" className="text-xs font-bold uppercase tracking-wider text-slate-700 dark:text-slate-300">
|
||||
<Label
|
||||
htmlFor="prof-photo"
|
||||
className="text-xs font-bold tracking-wider text-slate-700 uppercase dark:text-slate-300"
|
||||
>
|
||||
URL Foto Profil
|
||||
</Label>
|
||||
<Input id="prof-photo" value={photo} onChange={(e) => setPhoto(e.target.value)} placeholder="https://..." className="h-11 rounded-xl" />
|
||||
<Input
|
||||
id="prof-photo"
|
||||
value={photo}
|
||||
onChange={(e) => setPhoto(e.target.value)}
|
||||
placeholder="https://..."
|
||||
className="h-11 rounded-xl"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
{profileError && (
|
||||
<div className="p-3 bg-red-50 text-red-600 rounded-xl text-xs font-semibold">{profileError}</div>
|
||||
<div className="rounded-xl bg-red-50 p-3 text-xs font-semibold text-red-600">
|
||||
{profileError}
|
||||
</div>
|
||||
)}
|
||||
{profileSuccess && (
|
||||
<div className="p-3 bg-emerald-50 text-emerald-700 rounded-xl text-xs font-bold flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" /><span>Profil berhasil diperbarui!</span>
|
||||
<div className="flex items-center gap-2 rounded-xl bg-emerald-50 p-3 text-xs font-bold text-emerald-700">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<span>Profil berhasil diperbarui!</span>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
<div className="p-6 bg-slate-50/60 border-t border-slate-100 flex justify-end">
|
||||
<Button type="submit" disabled={savingProfile} className="h-11 px-6 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold">
|
||||
{savingProfile ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Save className="w-4 h-4 mr-2" />}
|
||||
<div className="flex justify-end border-t border-slate-100 bg-slate-50/60 p-6">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={savingProfile}
|
||||
className="h-11 rounded-xl bg-[#1B2CC1] px-6 font-bold text-white hover:bg-[#15229E]"
|
||||
>
|
||||
{savingProfile ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Simpan Profil
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
{/* Right 1 Col: Account Details */}
|
||||
<div className="space-y-6">
|
||||
<Card className="rounded-3xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm p-6 space-y-4">
|
||||
<div className="flex items-center gap-2 text-slate-800 dark:text-slate-200 font-bold text-sm mb-0">
|
||||
<ShieldCheck className="w-4 h-4 text-[#1B2CC1]" />
|
||||
<Card className="space-y-4 rounded-3xl border border-slate-200/90 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="mb-0 flex items-center gap-2 text-sm font-bold text-slate-800 dark:text-slate-200">
|
||||
<ShieldCheck className="h-4 w-4 text-[#1B2CC1]" />
|
||||
<span>Detail Akun</span>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 pt-2 border-t border-slate-100 dark:border-slate-800">
|
||||
|
||||
<div className="space-y-2 border-t border-slate-100 pt-2 dark:border-slate-800">
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] uppercase font-bold text-slate-400 block">Username</span>
|
||||
<div className="font-semibold text-slate-800 text-sm">{user.username}</div>
|
||||
<span className="block text-[10px] font-bold text-slate-400 uppercase">
|
||||
Username
|
||||
</span>
|
||||
<div className="text-sm font-semibold text-slate-800">{user.username}</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<span className="text-[10px] uppercase font-bold text-slate-400 block">Role Akses</span>
|
||||
<div className="font-semibold text-[#1B2CC1] text-sm uppercase">{user.role}</div>
|
||||
<span className="block text-[10px] font-bold text-slate-400 uppercase">
|
||||
Role Akses
|
||||
</span>
|
||||
<div className="text-sm font-semibold text-[#1B2CC1] uppercase">{user.role}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* PASSWORD CHANGE MODAL BUTTON */}
|
||||
<div className="pt-2 border-t border-slate-100 dark:border-slate-800">
|
||||
<div className="border-t border-slate-100 pt-2 dark:border-slate-800">
|
||||
<Dialog open={isPasswordModalOpen} onOpenChange={setIsPasswordModalOpen}>
|
||||
<DialogTrigger className="cursor-pointer inline-flex items-center justify-center whitespace-nowrap text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[#1B2CC1] disabled:pointer-events-none disabled:opacity-50 h-11 px-4 py-2 rounded-xl gap-2 text-[#1B2CC1] hover:text-[#15229E] font-bold border border-[#1B2CC1]/20 bg-[#1B2CC1]/5 hover:bg-[#1B2CC1]/10 w-full">
|
||||
<KeyRound className="w-4 h-4" />
|
||||
<DialogTrigger className="inline-flex h-11 w-full cursor-pointer items-center justify-center gap-2 rounded-xl border border-[#1B2CC1]/20 bg-[#1B2CC1]/5 px-4 py-2 text-sm font-bold whitespace-nowrap text-[#1B2CC1] hover:bg-[#1B2CC1]/10 hover:text-[#15229E] focus-visible:ring-1 focus-visible:ring-[#1B2CC1] focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50">
|
||||
<KeyRound className="h-4 w-4" />
|
||||
Ganti Password
|
||||
</DialogTrigger>
|
||||
<DialogContent className="sm:max-w-[425px] rounded-3xl p-0 overflow-hidden border-slate-200/90 dark:border-slate-800 shadow-xl">
|
||||
<DialogHeader className="p-6 bg-slate-50 dark:bg-slate-900/50 border-b border-slate-100 dark:border-slate-800">
|
||||
<DialogTitle className="text-xl font-black text-slate-900 dark:text-white">Ganti Password</DialogTitle>
|
||||
<p className="text-xs text-slate-500 mt-1">Lindungi akun Anda dengan mengubah password secara berkala.</p>
|
||||
<DialogContent className="overflow-hidden rounded-3xl border-slate-200/90 p-0 shadow-xl sm:max-w-[425px] dark:border-slate-800">
|
||||
<DialogHeader className="border-b border-slate-100 bg-slate-50 p-6 dark:border-slate-800 dark:bg-slate-900/50">
|
||||
<DialogTitle className="text-xl font-black text-slate-900 dark:text-white">
|
||||
Ganti Password
|
||||
</DialogTitle>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Lindungi akun Anda dengan mengubah password secara berkala.
|
||||
</p>
|
||||
</DialogHeader>
|
||||
|
||||
<form onSubmit={handleChangePassword}>
|
||||
<div className="p-6 space-y-5 bg-white dark:bg-slate-900">
|
||||
<div className="space-y-5 bg-white p-6 dark:bg-slate-900">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700">Password Lama</Label>
|
||||
<Label className="text-xs font-bold tracking-wider text-slate-700 uppercase">
|
||||
Password Lama
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<LockKeyhole className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input type="password" value={oldPassword} onChange={(e) => setOldPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Masukkan password lama" />
|
||||
<LockKeyhole className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<Input
|
||||
type="password"
|
||||
value={oldPassword}
|
||||
onChange={(e) => setOldPassword(e.target.value)}
|
||||
className="h-11 rounded-xl pl-9"
|
||||
placeholder="Masukkan password lama"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700">Password Baru</Label>
|
||||
<Label className="text-xs font-bold tracking-wider text-slate-700 uppercase">
|
||||
Password Baru
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input type="password" value={newPassword} onChange={(e) => setNewPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Minimal 6 karakter" />
|
||||
<KeyRound className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<Input
|
||||
type="password"
|
||||
value={newPassword}
|
||||
onChange={(e) => setNewPassword(e.target.value)}
|
||||
className="h-11 rounded-xl pl-9"
|
||||
placeholder="Minimal 6 karakter"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase tracking-wider text-slate-700">Konfirmasi Password Baru</Label>
|
||||
<Label className="text-xs font-bold tracking-wider text-slate-700 uppercase">
|
||||
Konfirmasi Password Baru
|
||||
</Label>
|
||||
<div className="relative">
|
||||
<KeyRound className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<Input type="password" value={confirmPassword} onChange={(e) => setConfirmPassword(e.target.value)} className="pl-9 h-11 rounded-xl" placeholder="Ketik ulang password baru" />
|
||||
<KeyRound className="absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-slate-400" />
|
||||
<Input
|
||||
type="password"
|
||||
value={confirmPassword}
|
||||
onChange={(e) => setConfirmPassword(e.target.value)}
|
||||
className="h-11 rounded-xl pl-9"
|
||||
placeholder="Ketik ulang password baru"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{passwordError && (
|
||||
<div className="p-3 bg-red-50 text-red-600 rounded-xl text-xs font-semibold">{passwordError}</div>
|
||||
<div className="rounded-xl bg-red-50 p-3 text-xs font-semibold text-red-600">
|
||||
{passwordError}
|
||||
</div>
|
||||
)}
|
||||
{passwordSuccess && (
|
||||
<div className="p-3 bg-emerald-50 text-emerald-700 rounded-xl text-xs font-bold flex items-center gap-2">
|
||||
<CheckCircle className="w-4 h-4" /><span>Password berhasil diubah!</span>
|
||||
<div className="flex items-center gap-2 rounded-xl bg-emerald-50 p-3 text-xs font-bold text-emerald-700">
|
||||
<CheckCircle className="h-4 w-4" />
|
||||
<span>Password berhasil diubah!</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-slate-50/60 border-t border-slate-100 flex justify-end gap-3">
|
||||
<Button type="button" variant="ghost" onClick={() => setIsPasswordModalOpen(false)} className="h-11 rounded-xl font-bold">Batal</Button>
|
||||
<Button type="submit" disabled={savingPassword} className="h-11 px-6 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold">
|
||||
{savingPassword ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Save className="w-4 h-4 mr-2" />}
|
||||
<div className="flex justify-end gap-3 border-t border-slate-100 bg-slate-50/60 p-6">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
onClick={() => setIsPasswordModalOpen(false)}
|
||||
className="h-11 rounded-xl font-bold"
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={savingPassword}
|
||||
className="h-11 rounded-xl bg-[#1B2CC1] px-6 font-bold text-white hover:bg-[#15229E]"
|
||||
>
|
||||
{savingPassword ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Ubah Password
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+113
-66
@@ -1,18 +1,34 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { getCreatorReport, getSubmittorReport, getSessionUser, getBalancesAsCreator } from '@/app/actions'
|
||||
import {
|
||||
getCreatorReport,
|
||||
getSubmittorReport,
|
||||
getSessionUser,
|
||||
getBalancesAsCreator,
|
||||
} from '@/app/actions'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { Loader2, TrendingUp, TrendingDown, Wallet, ArrowRightLeft, Calendar as CalendarIcon } from 'lucide-react'
|
||||
import {
|
||||
Loader2,
|
||||
TrendingUp,
|
||||
TrendingDown,
|
||||
Wallet,
|
||||
ArrowRightLeft,
|
||||
Calendar as CalendarIcon,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import ReportCharts from '@/components/ReportCharts'
|
||||
import ReportDataGrid from '@/components/ReportDataGrid'
|
||||
import ReportByPersonGrid from '@/components/ReportByPersonGrid'
|
||||
import {
|
||||
getISOWeek, getYear,
|
||||
startOfISOWeek, endOfISOWeek,
|
||||
startOfMonth, endOfMonth,
|
||||
setISOWeek, setYear as dfSetYear
|
||||
getISOWeek,
|
||||
getYear,
|
||||
startOfISOWeek,
|
||||
endOfISOWeek,
|
||||
startOfMonth,
|
||||
endOfMonth,
|
||||
setISOWeek,
|
||||
setYear as dfSetYear,
|
||||
} from 'date-fns'
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────
|
||||
@@ -28,7 +44,10 @@ function getMonthValue(date: Date) {
|
||||
return `${y}-${m.toString().padStart(2, '0')}`
|
||||
}
|
||||
|
||||
function getIntervalFromFilter(type: 'WEEK' | 'MONTH', value: string): { start: Date; end: Date } | null {
|
||||
function getIntervalFromFilter(
|
||||
type: 'WEEK' | 'MONTH',
|
||||
value: string
|
||||
): { start: Date; end: Date } | null {
|
||||
if (!value) return null
|
||||
|
||||
if (type === 'WEEK') {
|
||||
@@ -83,7 +102,7 @@ export default function ReportsPage() {
|
||||
const [subRes, creRes, balRes] = await Promise.all([
|
||||
getSubmittorReport(id, interval?.start, interval?.end),
|
||||
getCreatorReport(id, interval?.start, interval?.end),
|
||||
getBalancesAsCreator(id)
|
||||
getBalancesAsCreator(id),
|
||||
])
|
||||
setSubmittorData(subRes)
|
||||
setCreatorData(creRes)
|
||||
@@ -97,10 +116,16 @@ export default function ReportsPage() {
|
||||
}
|
||||
|
||||
const formatRupiah = (n: number) =>
|
||||
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(n)
|
||||
new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
maximumFractionDigits: 0,
|
||||
}).format(n)
|
||||
|
||||
// ── Summary calcs ──────────────────────────────────────
|
||||
const getPaidAmount = (sub: any) => (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? (Number(sub.bill) || 0) : 0)) + (sub.saldo_used || 0)
|
||||
const getPaidAmount = (sub: any) =>
|
||||
(sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? Number(sub.bill) || 0 : 0)) +
|
||||
(sub.saldo_used || 0)
|
||||
const getUnpaidAmount = (sub: any) => Math.max(0, (Number(sub.bill) || 0) - getPaidAmount(sub))
|
||||
|
||||
const totalPengeluaran = submittorData.reduce((acc, s) => acc + getPaidAmount(s), 0)
|
||||
@@ -108,7 +133,7 @@ export default function ReportsPage() {
|
||||
|
||||
let totalOmzet = 0
|
||||
let totalPiutang = 0
|
||||
creatorData.forEach(order => {
|
||||
creatorData.forEach((order) => {
|
||||
order.submissions.forEach((sub: any) => {
|
||||
totalOmzet += Number(sub.bill) || 0
|
||||
totalPiutang += getUnpaidAmount(sub)
|
||||
@@ -118,49 +143,47 @@ export default function ReportsPage() {
|
||||
const filterLabel = filterType === 'WEEK' ? `Minggu ${filterValue}` : `Bulan ${filterValue}`
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in duration-500 pb-12">
|
||||
|
||||
<div className="animate-in fade-in space-y-6 pb-12 duration-500">
|
||||
{/* ── Header: Tab Switcher (left) + Filter Controls (right) ── */}
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4 bg-white dark:bg-slate-900 px-4 py-3 rounded-2xl border border-slate-200/80 dark:border-slate-800 shadow-sm">
|
||||
|
||||
<div className="flex flex-col justify-between gap-4 rounded-2xl border border-slate-200/80 bg-white px-4 py-3 shadow-sm md:flex-row md:items-center dark:border-slate-800 dark:bg-slate-900">
|
||||
{/* Tab Switcher — positioned where the title was */}
|
||||
<div className="flex items-center bg-slate-100 dark:bg-slate-800/80 p-1 rounded-xl text-xs font-bold">
|
||||
<div className="flex items-center rounded-xl bg-slate-100 p-1 text-xs font-bold dark:bg-slate-800/80">
|
||||
<button
|
||||
onClick={() => setActiveTab('SUBMITTOR')}
|
||||
className={cn(
|
||||
'px-5 py-2 rounded-lg transition-all duration-300 flex items-center justify-center gap-2',
|
||||
'flex items-center justify-center gap-2 rounded-lg px-5 py-2 transition-all duration-300',
|
||||
activeTab === 'SUBMITTOR'
|
||||
? 'bg-white dark:bg-slate-900 text-[#1B2CC1] dark:text-white shadow-sm font-black'
|
||||
? 'bg-white font-black text-[#1B2CC1] shadow-sm dark:bg-slate-900 dark:text-white'
|
||||
: 'text-slate-500 hover:text-slate-900 dark:hover:text-white'
|
||||
)}
|
||||
>
|
||||
<TrendingDown className="w-4 h-4" /> Pengeluaran Saya
|
||||
<TrendingDown className="h-4 w-4" /> Pengeluaran Saya
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('CREATOR')}
|
||||
className={cn(
|
||||
'px-5 py-2 rounded-lg transition-all duration-300 flex items-center justify-center gap-2',
|
||||
'flex items-center justify-center gap-2 rounded-lg px-5 py-2 transition-all duration-300',
|
||||
activeTab === 'CREATOR'
|
||||
? 'bg-white dark:bg-slate-900 text-[#1B2CC1] dark:text-white shadow-sm font-black'
|
||||
? 'bg-white font-black text-[#1B2CC1] shadow-sm dark:bg-slate-900 dark:text-white'
|
||||
: 'text-slate-500 hover:text-slate-900 dark:hover:text-white'
|
||||
)}
|
||||
>
|
||||
<TrendingUp className="w-4 h-4" /> Omzet (Kreator)
|
||||
<TrendingUp className="h-4 w-4" /> Omzet (Kreator)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filter Controls */}
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* Type toggle */}
|
||||
<div className="flex items-center bg-slate-100 dark:bg-slate-800 rounded-xl p-1 gap-1">
|
||||
{(['WEEK', 'MONTH'] as const).map(t => (
|
||||
<div className="flex items-center gap-1 rounded-xl bg-slate-100 p-1 dark:bg-slate-800">
|
||||
{(['WEEK', 'MONTH'] as const).map((t) => (
|
||||
<button
|
||||
key={t}
|
||||
onClick={() => handleFilterTypeChange(t)}
|
||||
className={cn(
|
||||
'px-3 py-1.5 rounded-lg text-xs font-bold transition-all',
|
||||
'rounded-lg px-3 py-1.5 text-xs font-bold transition-all',
|
||||
filterType === t
|
||||
? 'bg-white dark:bg-slate-700 text-[#1B2CC1] shadow-sm'
|
||||
? 'bg-white text-[#1B2CC1] shadow-sm dark:bg-slate-700'
|
||||
: 'text-slate-500 hover:text-slate-800 dark:hover:text-slate-200'
|
||||
)}
|
||||
>
|
||||
@@ -172,57 +195,67 @@ export default function ReportsPage() {
|
||||
{/* Date Picker */}
|
||||
<div
|
||||
onClick={() => pickerRef.current?.showPicker()}
|
||||
className="flex items-center gap-2 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-xl px-3 py-2 shadow-sm cursor-pointer hover:border-[#1B2CC1]/50 transition-colors select-none"
|
||||
className="flex cursor-pointer items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 shadow-sm transition-colors select-none hover:border-[#1B2CC1]/50 dark:border-slate-700 dark:bg-slate-900"
|
||||
>
|
||||
<input
|
||||
ref={pickerRef}
|
||||
type={filterType === 'WEEK' ? 'week' : 'month'}
|
||||
value={filterValue}
|
||||
onChange={e => setFilterValue(e.target.value)}
|
||||
className="text-sm font-bold text-slate-700 dark:text-slate-200 bg-transparent border-none outline-none cursor-pointer w-[140px] [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-inner-spin-button]:hidden"
|
||||
onChange={(e) => setFilterValue(e.target.value)}
|
||||
className="w-[140px] cursor-pointer border-none bg-transparent text-sm font-bold text-slate-700 outline-none dark:text-slate-200 [&::-webkit-calendar-picker-indicator]:hidden [&::-webkit-inner-spin-button]:hidden"
|
||||
/>
|
||||
<CalendarIcon className="w-4 h-4 text-slate-400 shrink-0" />
|
||||
<CalendarIcon className="h-4 w-4 shrink-0 text-slate-400" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Loading overlay */}
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-16 gap-3 text-slate-400">
|
||||
<Loader2 className="w-6 h-6 animate-spin text-[#1B2CC1]" />
|
||||
<div className="flex items-center justify-center gap-3 py-16 text-slate-400">
|
||||
<Loader2 className="h-6 w-6 animate-spin text-[#1B2CC1]" />
|
||||
<span className="text-sm font-semibold">Memuat data {filterLabel}...</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* ── SUBMITTOR TAB ── */}
|
||||
{!loading && activeTab === 'SUBMITTOR' && (
|
||||
<div className="space-y-6 animate-in slide-in-from-left-2 duration-300">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="animate-in slide-in-from-left-2 space-y-6 duration-300">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Card className="rounded-2xl border-none bg-gradient-to-br from-emerald-500 to-emerald-700 text-white shadow-lg shadow-emerald-500/20">
|
||||
<div className="p-4 flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm shrink-0">
|
||||
<Wallet className="w-6 h-6 text-white" />
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
|
||||
<Wallet className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-emerald-100 font-bold text-xs tracking-wide uppercase mb-1">Total Pengeluaran (Lunas)</span>
|
||||
<h2 className="text-2xl font-black leading-none mb-1">{formatRupiah(totalPengeluaran)}</h2>
|
||||
<span className="mb-1 text-xs font-bold tracking-wide text-emerald-100 uppercase">
|
||||
Total Pengeluaran (Lunas)
|
||||
</span>
|
||||
<h2 className="mb-1 text-2xl leading-none font-black">
|
||||
{formatRupiah(totalPengeluaran)}
|
||||
</h2>
|
||||
<p className="text-xs text-emerald-100">
|
||||
Dari {submittorData.filter(s => s.payment_status === 'LUNAS').length} pesanan lunas.
|
||||
Dari {submittorData.filter((s) => s.payment_status === 'LUNAS').length} pesanan
|
||||
lunas.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="rounded-2xl border-none bg-gradient-to-br from-rose-500 to-rose-700 text-white shadow-lg shadow-rose-500/20">
|
||||
<div className="p-4 flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm shrink-0">
|
||||
<ArrowRightLeft className="w-6 h-6 text-white" />
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
|
||||
<ArrowRightLeft className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-rose-100 font-bold text-xs tracking-wide uppercase mb-1">Hutang Pribadi (Belum Bayar)</span>
|
||||
<h2 className="text-2xl font-black leading-none mb-1">{formatRupiah(totalHutang)}</h2>
|
||||
<span className="mb-1 text-xs font-bold tracking-wide text-rose-100 uppercase">
|
||||
Hutang Pribadi (Belum Bayar)
|
||||
</span>
|
||||
<h2 className="mb-1 text-2xl leading-none font-black">
|
||||
{formatRupiah(totalHutang)}
|
||||
</h2>
|
||||
<p className="text-xs text-rose-100">
|
||||
{submittorData.filter(s => s.payment_status !== 'LUNAS').length} tagihan masih gantung.
|
||||
{submittorData.filter((s) => s.payment_status !== 'LUNAS').length} tagihan masih
|
||||
gantung.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@@ -236,30 +269,42 @@ export default function ReportsPage() {
|
||||
|
||||
{/* ── CREATOR TAB ── */}
|
||||
{!loading && activeTab === 'CREATOR' && (
|
||||
<div className="space-y-6 animate-in slide-in-from-right-2 duration-300">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="animate-in slide-in-from-right-2 space-y-6 duration-300">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<Card className="rounded-2xl border-none bg-gradient-to-br from-[#1B2CC1] to-[#121E85] text-white shadow-lg shadow-[#1B2CC1]/20">
|
||||
<div className="p-4 flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm shrink-0">
|
||||
<TrendingUp className="w-6 h-6 text-white" />
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
|
||||
<TrendingUp className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-blue-200 font-bold text-xs tracking-wide uppercase mb-1">Total Omzet Dikelola</span>
|
||||
<h2 className="text-2xl font-black leading-none mb-1">{formatRupiah(totalOmzet)}</h2>
|
||||
<p className="text-xs text-blue-200">Dari {creatorData.length} PO yang Anda buat.</p>
|
||||
<span className="mb-1 text-xs font-bold tracking-wide text-blue-200 uppercase">
|
||||
Total Omzet Dikelola
|
||||
</span>
|
||||
<h2 className="mb-1 text-2xl leading-none font-black">
|
||||
{formatRupiah(totalOmzet)}
|
||||
</h2>
|
||||
<p className="text-xs text-blue-200">
|
||||
Dari {creatorData.length} PO yang Anda buat.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card className="rounded-2xl border-none bg-gradient-to-br from-amber-500 to-amber-600 text-white shadow-lg shadow-amber-500/20">
|
||||
<div className="p-4 flex items-center gap-4">
|
||||
<div className="w-14 h-14 rounded-full bg-white/20 flex items-center justify-center backdrop-blur-sm shrink-0">
|
||||
<ArrowRightLeft className="w-6 h-6 text-white" />
|
||||
<div className="flex items-center gap-4 p-4">
|
||||
<div className="flex h-14 w-14 shrink-0 items-center justify-center rounded-full bg-white/20 backdrop-blur-sm">
|
||||
<ArrowRightLeft className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<span className="text-amber-100 font-bold text-xs tracking-wide uppercase mb-1">Total Piutang (Belum Dibayar)</span>
|
||||
<h2 className="text-2xl font-black leading-none mb-1">{formatRupiah(totalPiutang)}</h2>
|
||||
<p className="text-xs text-amber-100">Uang Anda yang masih tertahan di teman-teman.</p>
|
||||
<span className="mb-1 text-xs font-bold tracking-wide text-amber-100 uppercase">
|
||||
Total Piutang (Belum Dibayar)
|
||||
</span>
|
||||
<h2 className="mb-1 text-2xl leading-none font-black">
|
||||
{formatRupiah(totalPiutang)}
|
||||
</h2>
|
||||
<p className="text-xs text-amber-100">
|
||||
Uang Anda yang masih tertahan di teman-teman.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
@@ -268,15 +313,15 @@ export default function ReportsPage() {
|
||||
<ReportCharts type="CREATOR" data={creatorData} />
|
||||
|
||||
{/* Sub-Tabs */}
|
||||
<div className="flex items-center bg-slate-100 dark:bg-slate-800/80 p-1 rounded-xl text-xs font-bold w-full sm:w-auto">
|
||||
{([ 'BY_PERSON','BY_ORDER',] as const).map(tab => (
|
||||
<div className="flex w-full items-center rounded-xl bg-slate-100 p-1 text-xs font-bold sm:w-auto dark:bg-slate-800/80">
|
||||
{(['BY_PERSON', 'BY_ORDER'] as const).map((tab) => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setCreatorTab(tab)}
|
||||
className={cn(
|
||||
'flex-1 sm:flex-none px-5 py-2 rounded-lg transition-all duration-200 flex items-center justify-center gap-1.5',
|
||||
'flex flex-1 items-center justify-center gap-1.5 rounded-lg px-5 py-2 transition-all duration-200 sm:flex-none',
|
||||
creatorTab === tab
|
||||
? 'bg-white dark:bg-slate-900 text-[#1B2CC1] dark:text-white shadow-sm font-black'
|
||||
? 'bg-white font-black text-[#1B2CC1] shadow-sm dark:bg-slate-900 dark:text-white'
|
||||
: 'text-slate-500 hover:text-slate-900 dark:hover:text-white'
|
||||
)}
|
||||
>
|
||||
@@ -292,7 +337,9 @@ export default function ReportsPage() {
|
||||
data={creatorData}
|
||||
balancesData={creatorBalances}
|
||||
creatorId={userId!}
|
||||
onUpdate={() => { if (userId) loadData(userId) }}
|
||||
onUpdate={() => {
|
||||
if (userId) loadData(userId)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -15,7 +15,7 @@ export default function IntegrationsPage() {
|
||||
const [settings, setSettings] = useState({
|
||||
MATTERMOST_WEBHOOK_URL: '',
|
||||
MATTERMOST_TEMPLATE: '',
|
||||
WHATSAPP_TEMPLATE: ''
|
||||
WHATSAPP_TEMPLATE: '',
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
@@ -24,8 +24,12 @@ export default function IntegrationsPage() {
|
||||
if (data) {
|
||||
setSettings({
|
||||
MATTERMOST_WEBHOOK_URL: data.MATTERMOST_WEBHOOK_URL || '',
|
||||
MATTERMOST_TEMPLATE: data.MATTERMOST_TEMPLATE || 'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}',
|
||||
WHATSAPP_TEMPLATE: data.WHATSAPP_TEMPLATE || 'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}'
|
||||
MATTERMOST_TEMPLATE:
|
||||
data.MATTERMOST_TEMPLATE ||
|
||||
'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}',
|
||||
WHATSAPP_TEMPLATE:
|
||||
data.WHATSAPP_TEMPLATE ||
|
||||
'Halo tim! Lagi ada orderan open nih untuk {title}. Mumpung belum di-checkout, yang mau ikutan nitip bisa langsung cek ke sini ya: {url}',
|
||||
})
|
||||
}
|
||||
setLoading(false)
|
||||
@@ -34,7 +38,7 @@ export default function IntegrationsPage() {
|
||||
}, [])
|
||||
|
||||
const handleChange = (key: string, value: string) => {
|
||||
setSettings(prev => ({ ...prev, [key]: value }))
|
||||
setSettings((prev) => ({ ...prev, [key]: value }))
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
@@ -43,12 +47,12 @@ export default function IntegrationsPage() {
|
||||
if (res.success) {
|
||||
toast.success('Berhasil Disimpan', {
|
||||
description: 'Pengaturan integrasi berhasil disimpan!',
|
||||
duration: 3000
|
||||
duration: 3000,
|
||||
})
|
||||
} else {
|
||||
toast.error('Gagal', {
|
||||
description: 'Gagal menyimpan pengaturan',
|
||||
duration: 3000
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
setSaving(false)
|
||||
@@ -56,73 +60,92 @@ export default function IntegrationsPage() {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex justify-center items-center h-64">
|
||||
<Loader2 className="w-8 h-8 animate-spin text-[#1B2CC1]" />
|
||||
<div className="flex h-64 items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-[#1B2CC1]" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-8">
|
||||
<div className="space-y-8 p-6">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 dark:text-white">Integrasi & Sharing</h2>
|
||||
<p className="text-xs text-slate-500 mt-1">Konfigurasi endpoint API dan template pesan otomatis.</p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Konfigurasi endpoint API dan template pesan otomatis.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6 max-w-3xl">
|
||||
<div className="max-w-3xl space-y-6">
|
||||
{/* Mattermost Section */}
|
||||
<div className="p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 space-y-4">
|
||||
<div className="flex items-center gap-2 text-indigo-600 dark:text-indigo-400 font-black">
|
||||
<MessageSquare className="w-5 h-5" />
|
||||
<div className="space-y-4 rounded-2xl border border-slate-200 bg-slate-50/50 p-5 dark:border-slate-800 dark:bg-slate-900/50">
|
||||
<div className="flex items-center gap-2 font-black text-indigo-600 dark:text-indigo-400">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
<h3>Mattermost Webhook</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Gunakan URL Incoming Webhook dari Mattermost untuk mengirim broadcast PO baru secara otomatis.</p>
|
||||
|
||||
<p className="text-xs text-slate-500">
|
||||
Gunakan URL Incoming Webhook dari Mattermost untuk mengirim broadcast PO baru secara
|
||||
otomatis.
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase text-slate-700">Webhook URL</Label>
|
||||
<Input
|
||||
value={settings.MATTERMOST_WEBHOOK_URL}
|
||||
onChange={e => handleChange('MATTERMOST_WEBHOOK_URL', e.target.value)}
|
||||
className="h-11 rounded-xl bg-white dark:bg-slate-950"
|
||||
placeholder="https://mattermost.yourdomain.com/hooks/xxx"
|
||||
<Label className="text-xs font-bold text-slate-700 uppercase">Webhook URL</Label>
|
||||
<Input
|
||||
value={settings.MATTERMOST_WEBHOOK_URL}
|
||||
onChange={(e) => handleChange('MATTERMOST_WEBHOOK_URL', e.target.value)}
|
||||
className="h-11 rounded-xl bg-white dark:bg-slate-950"
|
||||
placeholder="https://mattermost.yourdomain.com/hooks/xxx"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase text-slate-700">Pesan Template</Label>
|
||||
<Textarea
|
||||
value={settings.MATTERMOST_TEMPLATE}
|
||||
onChange={e => handleChange('MATTERMOST_TEMPLATE', e.target.value)}
|
||||
className="rounded-xl min-h-[100px] bg-white dark:bg-slate-950"
|
||||
placeholder="Template pesan..."
|
||||
<Label className="text-xs font-bold text-slate-700 uppercase">Pesan Template</Label>
|
||||
<Textarea
|
||||
value={settings.MATTERMOST_TEMPLATE}
|
||||
onChange={(e) => handleChange('MATTERMOST_TEMPLATE', e.target.value)}
|
||||
className="min-h-[100px] rounded-xl bg-white dark:bg-slate-950"
|
||||
placeholder="Template pesan..."
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 flex items-center gap-1"><AlertCircle className="w-3 h-3"/> Gunakan variabel: {'{title}'}, {'{url}'}</p>
|
||||
<p className="flex items-center gap-1 text-[10px] text-slate-500">
|
||||
<AlertCircle className="h-3 w-3" /> Gunakan variabel: {'{title}'}, {'{url}'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* WhatsApp Section */}
|
||||
<div className="p-5 rounded-2xl border border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 space-y-4">
|
||||
<div className="flex items-center gap-2 text-emerald-600 dark:text-emerald-400 font-black">
|
||||
<MessageCircle className="w-5 h-5" />
|
||||
<div className="space-y-4 rounded-2xl border border-slate-200 bg-slate-50/50 p-5 dark:border-slate-800 dark:bg-slate-900/50">
|
||||
<div className="flex items-center gap-2 font-black text-emerald-600 dark:text-emerald-400">
|
||||
<MessageCircle className="h-5 w-5" />
|
||||
<h3>WhatsApp Share Template</h3>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500">Atur template teks default saat user menekan tombol share ke WhatsApp.</p>
|
||||
<p className="text-xs text-slate-500">
|
||||
Atur template teks default saat user menekan tombol share ke WhatsApp.
|
||||
</p>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase text-slate-700">Pesan Template</Label>
|
||||
<Textarea
|
||||
value={settings.WHATSAPP_TEMPLATE}
|
||||
onChange={e => handleChange('WHATSAPP_TEMPLATE', e.target.value)}
|
||||
className="rounded-xl min-h-[100px] bg-white dark:bg-slate-950"
|
||||
placeholder="Template pesan..."
|
||||
<Label className="text-xs font-bold text-slate-700 uppercase">Pesan Template</Label>
|
||||
<Textarea
|
||||
value={settings.WHATSAPP_TEMPLATE}
|
||||
onChange={(e) => handleChange('WHATSAPP_TEMPLATE', e.target.value)}
|
||||
className="min-h-[100px] rounded-xl bg-white dark:bg-slate-950"
|
||||
placeholder="Template pesan..."
|
||||
/>
|
||||
<p className="text-[10px] text-slate-500 flex items-center gap-1"><AlertCircle className="w-3 h-3"/> Gunakan variabel: {'{title}'}, {'{url}'}</p>
|
||||
<p className="flex items-center gap-1 text-[10px] text-slate-500">
|
||||
<AlertCircle className="h-3 w-3" /> Gunakan variabel: {'{title}'}, {'{url}'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button onClick={handleSave} disabled={saving} className="bg-[#1B2CC1] hover:bg-[#15229E] text-white rounded-xl h-11 px-8 font-bold shadow-md w-full sm:w-auto">
|
||||
{saving ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : <Save className="w-4 h-4 mr-2" />}
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="h-11 w-full rounded-xl bg-[#1B2CC1] px-8 font-bold text-white shadow-md hover:bg-[#15229E] sm:w-auto"
|
||||
>
|
||||
{saving ? (
|
||||
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<Save className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Simpan Konfigurasi
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -14,10 +14,10 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6 animate-in fade-in duration-500 pb-12">
|
||||
<div className="animate-in fade-in space-y-6 pb-12 duration-500">
|
||||
{/* Settings Navigation Tabs */}
|
||||
<div className="flex flex-col md:flex-row items-start md:items-center gap-4 bg-white dark:bg-slate-900 p-4 rounded-2xl border border-slate-200/80 dark:border-slate-800 shadow-sm overflow-x-auto">
|
||||
<div className="flex items-center bg-slate-100 dark:bg-slate-800/80 p-1 rounded-xl text-xs font-bold w-full md:w-auto">
|
||||
<div className="flex flex-col items-start gap-4 overflow-x-auto rounded-2xl border border-slate-200/80 bg-white p-4 shadow-sm md:flex-row md:items-center dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex w-full items-center rounded-xl bg-slate-100 p-1 text-xs font-bold md:w-auto dark:bg-slate-800/80">
|
||||
{tabs.map((tab) => {
|
||||
const isActive = pathname === tab.href
|
||||
const Icon = tab.icon
|
||||
@@ -26,13 +26,13 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
|
||||
key={tab.href}
|
||||
href={tab.href}
|
||||
className={cn(
|
||||
"flex items-center gap-2 px-4 py-2 rounded-lg transition-all duration-200 whitespace-nowrap",
|
||||
isActive
|
||||
? "bg-white dark:bg-slate-900 text-[#1B2CC1] dark:text-blue-400 shadow-sm font-black"
|
||||
: "text-slate-500 hover:text-slate-900 dark:hover:text-white"
|
||||
'flex items-center gap-2 rounded-lg px-4 py-2 whitespace-nowrap transition-all duration-200',
|
||||
isActive
|
||||
? 'bg-white font-black text-[#1B2CC1] shadow-sm dark:bg-slate-900 dark:text-blue-400'
|
||||
: 'text-slate-500 hover:text-slate-900 dark:hover:text-white'
|
||||
)}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
<Icon className="h-4 w-4" />
|
||||
{tab.name}
|
||||
</Link>
|
||||
)
|
||||
@@ -41,7 +41,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
|
||||
</div>
|
||||
|
||||
{/* Settings Content */}
|
||||
<div className="bg-white dark:bg-slate-900 rounded-2xl border border-slate-200/80 dark:border-slate-800 shadow-sm overflow-hidden">
|
||||
<div className="overflow-hidden rounded-2xl border border-slate-200/80 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { getUsers, createUserByAdmin, toggleUserActive, deleteUser, resetUserPassword } from '@/app/actions'
|
||||
import {
|
||||
getUsers,
|
||||
createUserByAdmin,
|
||||
toggleUserActive,
|
||||
deleteUser,
|
||||
resetUserPassword,
|
||||
} from '@/app/actions'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardHeader } from '@/components/ui/card'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Loader2, Plus, KeyRound, Ban, Trash2, CheckCircle2, ShieldCheck, User } from 'lucide-react'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select'
|
||||
|
||||
export default function UsersPage() {
|
||||
const [users, setUsers] = useState<any[]>([])
|
||||
@@ -28,7 +40,7 @@ export default function UsersPage() {
|
||||
const [password, setPassword] = useState('')
|
||||
const [role, setRole] = useState('user')
|
||||
const [newPass, setNewPass] = useState('')
|
||||
|
||||
|
||||
const [actionLoading, setActionLoading] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -47,7 +59,7 @@ export default function UsersPage() {
|
||||
const handleCreateUser = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!name || !username || !password) return alert('Lengkapi semua field')
|
||||
|
||||
|
||||
setActionLoading(true)
|
||||
const res = await createUserByAdmin(name, username, password, role)
|
||||
if (res.success) {
|
||||
@@ -66,7 +78,7 @@ export default function UsersPage() {
|
||||
const handleResetPassword = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!newPass || newPass.length < 6) return alert('Password minimal 6 karakter')
|
||||
|
||||
|
||||
setActionLoading(true)
|
||||
const res = await resetUserPassword(selectedUser.id, newPass)
|
||||
if (res.success) {
|
||||
@@ -80,7 +92,8 @@ export default function UsersPage() {
|
||||
}
|
||||
|
||||
const handleToggleActive = async (id: string, currentStatus: boolean) => {
|
||||
if (!confirm(`Yakin ingin ${currentStatus ? 'menonaktifkan' : 'mengaktifkan'} user ini?`)) return
|
||||
if (!confirm(`Yakin ingin ${currentStatus ? 'menonaktifkan' : 'mengaktifkan'} user ini?`))
|
||||
return
|
||||
const res = await toggleUserActive(id, !currentStatus)
|
||||
if (res.success) {
|
||||
loadUsers(page)
|
||||
@@ -101,187 +114,251 @@ export default function UsersPage() {
|
||||
|
||||
return (
|
||||
<div className="p-6">
|
||||
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4 mb-6">
|
||||
<div className="mb-6 flex flex-col items-start justify-between gap-4 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-900 dark:text-white">Manajemen Pengguna</h2>
|
||||
<p className="text-xs text-slate-500 mt-1">Kelola data seluruh pengguna TitipIn.</p>
|
||||
<p className="mt-1 text-xs text-slate-500">Kelola data seluruh pengguna TitipIn.</p>
|
||||
</div>
|
||||
<Button onClick={() => setIsCreateOpen(true)} className="bg-[#1B2CC1] hover:bg-[#15229E] text-white rounded-xl h-10 px-4 shadow-sm text-xs font-bold">
|
||||
<Plus className="w-4 h-4 mr-1.5" /> Tambah User
|
||||
<Button
|
||||
onClick={() => setIsCreateOpen(true)}
|
||||
className="h-10 rounded-xl bg-[#1B2CC1] px-4 text-xs font-bold text-white shadow-sm hover:bg-[#15229E]"
|
||||
>
|
||||
<Plus className="mr-1.5 h-4 w-4" /> Tambah User
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="bg-slate-50/50 dark:bg-slate-800/50 border-b border-slate-100 dark:border-slate-800 text-xs uppercase font-bold text-slate-500">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="border-b border-slate-100 bg-slate-50/50 text-xs font-bold text-slate-500 uppercase dark:border-slate-800 dark:bg-slate-800/50">
|
||||
<tr>
|
||||
<th className="px-6 py-4">Pengguna</th>
|
||||
<th className="px-6 py-4">Role</th>
|
||||
<th className="px-6 py-4">Status</th>
|
||||
<th className="px-6 py-4">Terdaftar</th>
|
||||
<th className="px-6 py-4 text-right">Aksi</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{loading ? (
|
||||
<tr>
|
||||
<th className="px-6 py-4">Pengguna</th>
|
||||
<th className="px-6 py-4">Role</th>
|
||||
<th className="px-6 py-4">Status</th>
|
||||
<th className="px-6 py-4">Terdaftar</th>
|
||||
<th className="px-6 py-4 text-right">Aksi</th>
|
||||
<td colSpan={5} className="px-6 py-12 text-center">
|
||||
<Loader2 className="mx-auto h-6 w-6 animate-spin text-[#1B2CC1]" />
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{loading ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-12 text-center">
|
||||
<Loader2 className="w-6 h-6 animate-spin mx-auto text-[#1B2CC1]" />
|
||||
) : users.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-12 text-center text-slate-500">
|
||||
Belum ada pengguna lain.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
users.map((u) => (
|
||||
<tr
|
||||
key={u.id}
|
||||
className="transition-colors hover:bg-slate-50/50 dark:hover:bg-slate-800/20"
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{u.photo ? (
|
||||
<img
|
||||
src={u.photo}
|
||||
alt={u.name}
|
||||
className="h-9 w-9 shrink-0 rounded-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-blue-100 text-xs font-bold text-blue-700 uppercase">
|
||||
{u.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="font-bold text-slate-900 dark:text-slate-100">{u.name}</div>
|
||||
<div className="text-xs text-slate-500">@{u.username}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{u.role === 'superadmin' ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-purple-100 px-2 py-1 text-[10px] font-bold text-purple-700 uppercase">
|
||||
<ShieldCheck className="h-3 w-3" /> Superadmin
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-slate-100 px-2 py-1 text-[10px] font-bold text-slate-700 uppercase">
|
||||
<User className="h-3 w-3" /> User
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{u.is_active ? (
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-emerald-100 px-2 py-1 text-[10px] font-bold text-emerald-700 uppercase">
|
||||
<CheckCircle2 className="h-3 w-3" /> Aktif
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 rounded-md bg-red-100 px-2 py-1 text-[10px] font-bold text-red-700 uppercase">
|
||||
<Ban className="h-3 w-3" /> Nonaktif
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-xs text-slate-500">
|
||||
{new Date(u.created_at).toLocaleDateString('id-ID')}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSelectedUser(u)
|
||||
setIsPassOpen(true)
|
||||
}}
|
||||
className="h-8 rounded-lg bg-slate-100 px-2 font-medium text-slate-700 hover:bg-slate-200 hover:text-slate-900"
|
||||
title="Ubah Password"
|
||||
>
|
||||
<KeyRound className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
{u.role !== 'superadmin' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleToggleActive(u.id, u.is_active)}
|
||||
className={`h-8 rounded-lg px-2 font-medium ${u.is_active ? 'bg-amber-100 text-amber-700 hover:bg-amber-200 hover:text-amber-900' : 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 hover:text-emerald-900'}`}
|
||||
title={u.is_active ? 'Nonaktifkan' : 'Aktifkan'}
|
||||
>
|
||||
{u.is_active ? (
|
||||
<Ban className="h-4 w-4" />
|
||||
) : (
|
||||
<CheckCircle2 className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDelete(u.id)}
|
||||
className="h-8 rounded-lg bg-rose-100 px-2 font-medium text-rose-700 hover:bg-rose-200 hover:text-rose-900"
|
||||
title="Hapus"
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : users.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="px-6 py-12 text-center text-slate-500">Belum ada pengguna lain.</td>
|
||||
</tr>
|
||||
) : (
|
||||
users.map(u => (
|
||||
<tr key={u.id} className="hover:bg-slate-50/50 dark:hover:bg-slate-800/20 transition-colors">
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{u.photo ? (
|
||||
<img src={u.photo} alt={u.name} className="w-9 h-9 rounded-full object-cover shrink-0" />
|
||||
) : (
|
||||
<div className="w-9 h-9 rounded-full bg-blue-100 text-blue-700 flex items-center justify-center font-bold text-xs uppercase shrink-0">
|
||||
{u.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<div className="font-bold text-slate-900 dark:text-slate-100">{u.name}</div>
|
||||
<div className="text-xs text-slate-500">@{u.username}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{u.role === 'superadmin' ? (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded-md bg-purple-100 text-purple-700 uppercase">
|
||||
<ShieldCheck className="w-3 h-3" /> Superadmin
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded-md bg-slate-100 text-slate-700 uppercase">
|
||||
<User className="w-3 h-3" /> User
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
{u.is_active ? (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded-md bg-emerald-100 text-emerald-700 uppercase">
|
||||
<CheckCircle2 className="w-3 h-3" /> Aktif
|
||||
</span>
|
||||
) : (
|
||||
<span className="inline-flex items-center gap-1 text-[10px] font-bold px-2 py-1 rounded-md bg-red-100 text-red-700 uppercase">
|
||||
<Ban className="w-3 h-3" /> Nonaktif
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-slate-500 text-xs">
|
||||
{new Date(u.created_at).toLocaleDateString('id-ID')}
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => {
|
||||
setSelectedUser(u)
|
||||
setIsPassOpen(true)
|
||||
}}
|
||||
className="h-8 px-2 rounded-lg bg-slate-100 text-slate-700 hover:bg-slate-200 hover:text-slate-900 font-medium"
|
||||
title="Ubah Password"
|
||||
>
|
||||
<KeyRound className="w-4 h-4" />
|
||||
</Button>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{u.role !== 'superadmin' && (
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleToggleActive(u.id, u.is_active)}
|
||||
className={`h-8 px-2 rounded-lg font-medium ${u.is_active ? 'bg-amber-100 text-amber-700 hover:bg-amber-200 hover:text-amber-900' : 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200 hover:text-emerald-900'}`}
|
||||
title={u.is_active ? 'Nonaktifkan' : 'Aktifkan'}
|
||||
>
|
||||
{u.is_active ? <Ban className="w-4 h-4" /> : <CheckCircle2 className="w-4 h-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => handleDelete(u.id)}
|
||||
className="h-8 px-2 rounded-lg bg-rose-100 text-rose-700 hover:bg-rose-200 hover:text-rose-900 font-medium"
|
||||
title="Hapus"
|
||||
>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{totalPages > 1 && (
|
||||
<div className="p-4 border-t border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50">
|
||||
<span className="text-xs text-slate-500">Halaman {page} dari {totalPages}</span>
|
||||
<div className="flex gap-2">
|
||||
<Button disabled={page === 1} onClick={() => setPage(p => p - 1)} variant="outline" size="sm" className="h-8 rounded-lg text-xs">Sebelumnya</Button>
|
||||
<Button disabled={page === totalPages} onClick={() => setPage(p => p + 1)} variant="outline" size="sm" className="h-8 rounded-lg text-xs">Selanjutnya</Button>
|
||||
</div>
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between border-t border-slate-100 bg-slate-50/50 p-4 dark:border-slate-800">
|
||||
<span className="text-xs text-slate-500">
|
||||
Halaman {page} dari {totalPages}
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
disabled={page === 1}
|
||||
onClick={() => setPage((p) => p - 1)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 rounded-lg text-xs"
|
||||
>
|
||||
Sebelumnya
|
||||
</Button>
|
||||
<Button
|
||||
disabled={page === totalPages}
|
||||
onClick={() => setPage((p) => p + 1)}
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="h-8 rounded-lg text-xs"
|
||||
>
|
||||
Selanjutnya
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* CREATE MODAL */}
|
||||
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
|
||||
<DialogContent className="sm:max-w-md rounded-3xl p-0 overflow-hidden border-0">
|
||||
<div className="px-6 pt-6 pb-4 border-b border-slate-100">
|
||||
<DialogContent className="overflow-hidden rounded-3xl border-0 p-0 sm:max-w-md">
|
||||
<div className="border-b border-slate-100 px-6 pt-6 pb-4">
|
||||
<DialogTitle className="text-xl font-black">Tambah User Baru</DialogTitle>
|
||||
</div>
|
||||
<form onSubmit={handleCreateUser} className="px-6 py-4 space-y-4">
|
||||
<form onSubmit={handleCreateUser} className="space-y-4 px-6 py-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase text-slate-700">Nama Lengkap</Label>
|
||||
<Input required value={name} onChange={e => setName(e.target.value)} className="h-11 rounded-xl" placeholder="Masukkan nama lengkap" />
|
||||
<Label className="text-xs font-bold text-slate-700 uppercase">Nama Lengkap</Label>
|
||||
<Input
|
||||
required
|
||||
value={name}
|
||||
onChange={(e) => setName(e.target.value)}
|
||||
className="h-11 rounded-xl"
|
||||
placeholder="Masukkan nama lengkap"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase text-slate-700">Username</Label>
|
||||
<Input required value={username} onChange={e => setUsername(e.target.value)} className="h-11 rounded-xl" placeholder="Masukkan username unik" />
|
||||
<Label className="text-xs font-bold text-slate-700 uppercase">Username</Label>
|
||||
<Input
|
||||
required
|
||||
value={username}
|
||||
onChange={(e) => setUsername(e.target.value)}
|
||||
className="h-11 rounded-xl"
|
||||
placeholder="Masukkan username unik"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase text-slate-700">Password</Label>
|
||||
<Input required type="password" value={password} onChange={e => setPassword(e.target.value)} className="h-11 rounded-xl" placeholder="Buat kata sandi (min. 6 karakter)" />
|
||||
<Label className="text-xs font-bold text-slate-700 uppercase">Password</Label>
|
||||
<Input
|
||||
required
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="h-11 rounded-xl"
|
||||
placeholder="Buat kata sandi (min. 6 karakter)"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label className="text-xs font-bold uppercase text-slate-700">Role Akses</Label>
|
||||
<Label className="text-xs font-bold text-slate-700 uppercase">Role Akses</Label>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRole('user')}
|
||||
className={`h-11 rounded-xl border flex items-center justify-center gap-2 text-sm font-bold transition-all ${
|
||||
className={`flex h-11 items-center justify-center gap-2 rounded-xl border text-sm font-bold transition-all ${
|
||||
role === 'user'
|
||||
? 'border-[#1B2CC1] bg-[#1B2CC1]/5 text-[#1B2CC1] ring-1 ring-[#1B2CC1]/20'
|
||||
: 'border-slate-200 text-slate-500 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<User className="w-4 h-4" /> User Biasa
|
||||
<User className="h-4 w-4" /> User Biasa
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setRole('superadmin')}
|
||||
className={`h-11 rounded-xl border flex items-center justify-center gap-2 text-sm font-bold transition-all ${
|
||||
className={`flex h-11 items-center justify-center gap-2 rounded-xl border text-sm font-bold transition-all ${
|
||||
role === 'superadmin'
|
||||
? 'border-purple-500 bg-purple-50 text-purple-700 ring-1 ring-purple-500/20'
|
||||
: 'border-slate-200 text-slate-500 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<ShieldCheck className="w-4 h-4" /> Superadmin
|
||||
<ShieldCheck className="h-4 w-4" /> Superadmin
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="pt-4 flex gap-3">
|
||||
<Button type="button" variant="outline" onClick={() => setIsCreateOpen(false)} className="flex-1 h-11 rounded-xl">Batal</Button>
|
||||
<Button type="submit" disabled={actionLoading} className="flex-1 h-11 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white">
|
||||
{actionLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Simpan User'}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsCreateOpen(false)}
|
||||
className="h-11 flex-1 rounded-xl"
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={actionLoading}
|
||||
className="h-11 flex-1 rounded-xl bg-[#1B2CC1] text-white hover:bg-[#15229E]"
|
||||
>
|
||||
{actionLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : 'Simpan User'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -290,20 +367,41 @@ export default function UsersPage() {
|
||||
|
||||
{/* RESET PASSWORD MODAL */}
|
||||
<Dialog open={isPassOpen} onOpenChange={setIsPassOpen}>
|
||||
<DialogContent className="sm:max-w-md rounded-3xl p-0 overflow-hidden border-0">
|
||||
<div className="px-6 pt-6 pb-4 border-b border-slate-100">
|
||||
<DialogContent className="overflow-hidden rounded-3xl border-0 p-0 sm:max-w-md">
|
||||
<div className="border-b border-slate-100 px-6 pt-6 pb-4">
|
||||
<DialogTitle className="text-xl font-black">Ubah Password</DialogTitle>
|
||||
<p className="text-xs text-slate-500 mt-1">Ubah password untuk user <span className="font-bold text-slate-900">@{selectedUser?.username}</span></p>
|
||||
<p className="mt-1 text-xs text-slate-500">
|
||||
Ubah password untuk user{' '}
|
||||
<span className="font-bold text-slate-900">@{selectedUser?.username}</span>
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleResetPassword} className="px-6 py-4 space-y-4">
|
||||
<form onSubmit={handleResetPassword} className="space-y-4 px-6 py-4">
|
||||
<div className="space-y-1.5">
|
||||
<Label className="text-xs font-bold uppercase text-slate-700">Password Baru</Label>
|
||||
<Input required type="password" value={newPass} onChange={e => setNewPass(e.target.value)} className="h-11 rounded-xl" placeholder="Minimal 6 karakter" />
|
||||
<Label className="text-xs font-bold text-slate-700 uppercase">Password Baru</Label>
|
||||
<Input
|
||||
required
|
||||
type="password"
|
||||
value={newPass}
|
||||
onChange={(e) => setNewPass(e.target.value)}
|
||||
className="h-11 rounded-xl"
|
||||
placeholder="Minimal 6 karakter"
|
||||
/>
|
||||
</div>
|
||||
<div className="pt-4 flex gap-3">
|
||||
<Button type="button" variant="outline" onClick={() => setIsPassOpen(false)} className="flex-1 h-11 rounded-xl">Batal</Button>
|
||||
<Button type="submit" disabled={actionLoading} className="flex-1 h-11 rounded-xl bg-slate-900 hover:bg-slate-800 text-white">
|
||||
{actionLoading ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Simpan Password'}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsPassOpen(false)}
|
||||
className="h-11 flex-1 rounded-xl"
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={actionLoading}
|
||||
className="h-11 flex-1 rounded-xl bg-slate-900 text-white hover:bg-slate-800"
|
||||
>
|
||||
{actionLoading ? <Loader2 className="h-5 w-5 animate-spin" /> : 'Simpan Password'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
Reference in New Issue
Block a user