style: apply consistent code formatting across project files
This commit is contained in:
@@ -8,16 +8,14 @@ export function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen bg-[#F4F6FB] dark:bg-[#0B0F19] text-slate-900 dark:text-slate-100 font-sans">
|
||||
<div className="flex min-h-screen bg-[#F4F6FB] font-sans text-slate-900 dark:bg-[#0B0F19] dark:text-slate-100">
|
||||
{/* Sidebar */}
|
||||
<Sidebar isOpen={sidebarOpen} onClose={() => setSidebarOpen(false)} />
|
||||
|
||||
{/* Main Content Area */}
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<Header onMenuClick={() => setSidebarOpen(true)} />
|
||||
<main className="flex-1 p-4 sm:p-6 lg:p-8 max-w-7xl w-full mx-auto">
|
||||
{children}
|
||||
</main>
|
||||
<main className="mx-auto w-full max-w-7xl flex-1 p-4 sm:p-6 lg:p-8">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
+35
-22
@@ -2,7 +2,17 @@
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { Menu, Calendar, Store, ClipboardList, Package, User, FileText, BarChart2, Settings } from 'lucide-react'
|
||||
import {
|
||||
Menu,
|
||||
Calendar,
|
||||
Store,
|
||||
ClipboardList,
|
||||
Package,
|
||||
User,
|
||||
FileText,
|
||||
BarChart2,
|
||||
Settings,
|
||||
} from 'lucide-react'
|
||||
import { buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
@@ -24,35 +34,35 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) {
|
||||
title: 'Open Order Hari Ini',
|
||||
subtitle: 'Daftar pesanan aktif yang siap kamu titip.',
|
||||
badge: 'Live PO',
|
||||
Icon: Store
|
||||
Icon: Store,
|
||||
}
|
||||
case '/my-orders':
|
||||
return {
|
||||
title: 'Jasa Order Saya',
|
||||
subtitle: 'Kelola PO yang Anda buka untuk teman-teman.',
|
||||
badge: 'Manajemen PO',
|
||||
Icon: ClipboardList
|
||||
Icon: ClipboardList,
|
||||
}
|
||||
case '/my-purchases':
|
||||
return {
|
||||
title: 'Pesanan Saya',
|
||||
subtitle: 'Pantau barang yang Anda titip beserta status tagihannya.',
|
||||
badge: 'Riwayat Titipan',
|
||||
Icon: Package
|
||||
Icon: Package,
|
||||
}
|
||||
case '/profile':
|
||||
return {
|
||||
title: 'Pengaturan Profil',
|
||||
subtitle: 'Kelola identitas dan preferensi akun Anda.',
|
||||
badge: 'Akun',
|
||||
Icon: User
|
||||
Icon: User,
|
||||
}
|
||||
case '/reports':
|
||||
return {
|
||||
title: 'Laporan Keuangan',
|
||||
subtitle: 'Pantau omzet, pengeluaran, dan hutang piutang Anda.',
|
||||
badge: 'Laporan',
|
||||
Icon: BarChart2
|
||||
Icon: BarChart2,
|
||||
}
|
||||
case '/settings/users':
|
||||
case '/settings/integrations':
|
||||
@@ -60,7 +70,7 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) {
|
||||
title: 'Pengaturan Aplikasi',
|
||||
subtitle: 'Konfigurasi integrasi, role, dan sistem.',
|
||||
badge: 'Superadmin',
|
||||
Icon: Settings
|
||||
Icon: Settings,
|
||||
}
|
||||
default:
|
||||
if (pathname.startsWith('/my-orders/')) {
|
||||
@@ -68,14 +78,14 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) {
|
||||
title: 'Detail & Rekap Order',
|
||||
subtitle: 'Rincian pesanan, tagihan pemesan, dan ringkasan belanja.',
|
||||
badge: 'Detail PO',
|
||||
Icon: FileText
|
||||
Icon: FileText,
|
||||
}
|
||||
}
|
||||
return {
|
||||
title: 'TitipIn',
|
||||
subtitle: 'Sistem Titip Pesanan Bersama',
|
||||
badge: 'App',
|
||||
Icon: Store
|
||||
Icon: Store,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -83,31 +93,34 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) {
|
||||
const { title, subtitle, badge, Icon } = getPageInfo()
|
||||
|
||||
return (
|
||||
<header className="sticky top-0 z-30 bg-white/90 dark:bg-slate-900/90 backdrop-blur-md border-b border-slate-200/80 dark:border-slate-800 px-6 py-4">
|
||||
<header className="sticky top-0 z-30 border-b border-slate-200/80 bg-white/90 px-6 py-4 backdrop-blur-md dark:border-slate-800 dark:bg-slate-900/90">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
{/* Left: Mobile hamburger & Page Title */}
|
||||
<div className="flex items-center gap-3">
|
||||
<button
|
||||
<button
|
||||
onClick={onMenuClick}
|
||||
className="lg:hidden p-2 rounded-xl border border-slate-200 dark:border-slate-800 text-slate-600 dark:text-slate-300 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
className="rounded-xl border border-slate-200 p-2 text-slate-600 transition-colors hover:bg-slate-100 lg:hidden dark:border-slate-800 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
>
|
||||
<Menu className="w-5 h-5" />
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
|
||||
<div className="flex items-center gap-3 sm:gap-4">
|
||||
<div className="hidden sm:flex items-center justify-center w-11 h-11 md:w-12 md:h-12 rounded-xl border border-blue-100 dark:border-blue-900/50 bg-gradient-to-br from-blue-50 to-[#1B2CC1]/10 dark:from-[#1B2CC1]/20 dark:to-[#121E85]/20 shadow-inner shrink-0">
|
||||
<Icon className="w-5 h-5 md:w-6 md:h-6 text-[#1B2CC1] dark:text-blue-400" strokeWidth={2.5} />
|
||||
<div className="hidden h-11 w-11 shrink-0 items-center justify-center rounded-xl border border-blue-100 bg-gradient-to-br from-blue-50 to-[#1B2CC1]/10 shadow-inner sm:flex md:h-12 md:w-12 dark:border-blue-900/50 dark:from-[#1B2CC1]/20 dark:to-[#121E85]/20">
|
||||
<Icon
|
||||
className="h-5 w-5 text-[#1B2CC1] md:h-6 md:w-6 dark:text-blue-400"
|
||||
strokeWidth={2.5}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h1 className="text-xl sm:text-2xl font-black tracking-tight text-slate-900 dark:text-white">
|
||||
<h1 className="text-xl font-black tracking-tight text-slate-900 sm:text-2xl dark:text-white">
|
||||
{title}
|
||||
</h1>
|
||||
<span className="hidden sm:inline-flex items-center px-2 py-0.5 rounded-full text-[11px] font-bold bg-[#1B2CC1]/10 text-[#1B2CC1] dark:bg-blue-900/40 dark:text-blue-300">
|
||||
<span className="hidden items-center rounded-full bg-[#1B2CC1]/10 px-2 py-0.5 text-[11px] font-bold text-[#1B2CC1] sm:inline-flex dark:bg-blue-900/40 dark:text-blue-300">
|
||||
{badge}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 dark:text-slate-400 mt-0.5 hidden sm:block">
|
||||
<p className="mt-0.5 hidden text-xs text-slate-500 sm:block dark:text-slate-400">
|
||||
{subtitle}
|
||||
</p>
|
||||
</div>
|
||||
@@ -117,9 +130,9 @@ export function Header({ onMenuClick }: { onMenuClick: () => void }) {
|
||||
{/* Right: Date info & Quick Action */}
|
||||
<div className="flex items-center gap-3">
|
||||
{todayStr && (
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 rounded-xl bg-slate-100/70 dark:bg-slate-800/60 border border-slate-200/60 dark:border-slate-800 text-[10px] sm:text-xs font-medium text-slate-600 dark:text-slate-300 animate-in fade-in font-bold">
|
||||
<Calendar className="w-3.5 h-3.5 text-[#1B2CC1] shrink-0" />
|
||||
<span className='font-bold whitespace-nowrap'>{todayStr}</span>
|
||||
<div className="animate-in fade-in flex items-center gap-2 rounded-xl border border-slate-200/60 bg-slate-100/70 px-3 py-1.5 text-[10px] font-bold font-medium text-slate-600 sm:text-xs dark:border-slate-800 dark:bg-slate-800/60 dark:text-slate-300">
|
||||
<Calendar className="h-3.5 w-3.5 shrink-0 text-[#1B2CC1]" />
|
||||
<span className="font-bold whitespace-nowrap">{todayStr}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+22
-22
@@ -18,23 +18,23 @@ export function Navbar() {
|
||||
const pathname = usePathname()
|
||||
|
||||
return (
|
||||
<nav className="sticky top-0 z-50 w-full border-b border-border/40 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-16 items-center px-4 mx-auto max-w-5xl">
|
||||
<nav className="border-border/40 bg-background/95 supports-[backdrop-filter]:bg-background/60 sticky top-0 z-50 w-full border-b backdrop-blur">
|
||||
<div className="container mx-auto flex h-16 max-w-5xl items-center px-4">
|
||||
<div className="mr-8 flex items-center gap-2">
|
||||
<PackageOpen className="h-6 w-6 text-primary" />
|
||||
<Link href="/" className="font-bold text-xl tracking-tight text-primary">
|
||||
<PackageOpen className="text-primary h-6 w-6" />
|
||||
<Link href="/" className="text-primary text-xl font-bold tracking-tight">
|
||||
TitipIn
|
||||
</Link>
|
||||
</div>
|
||||
<div className="hidden md:flex flex-1 items-center justify-between text-sm font-medium">
|
||||
<div className="hidden flex-1 items-center justify-between text-sm font-medium md:flex">
|
||||
<div className="flex gap-6">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"transition-colors hover:text-foreground/80",
|
||||
pathname === item.href ? "text-foreground" : "text-foreground/60"
|
||||
'hover:text-foreground/80 transition-colors',
|
||||
pathname === item.href ? 'text-foreground' : 'text-foreground/60'
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
@@ -43,21 +43,21 @@ export function Navbar() {
|
||||
</div>
|
||||
</div>
|
||||
{/* Mobile Navigation */}
|
||||
<div className="flex flex-1 items-center justify-end md:hidden overflow-hidden">
|
||||
<div className="flex gap-4 overflow-x-auto text-sm font-medium pb-1 no-scrollbar w-full">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
"whitespace-nowrap transition-colors",
|
||||
pathname === item.href ? "text-foreground" : "text-foreground/60"
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex flex-1 items-center justify-end overflow-hidden md:hidden">
|
||||
<div className="no-scrollbar flex w-full gap-4 overflow-x-auto pb-1 text-sm font-medium">
|
||||
{navItems.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={cn(
|
||||
'whitespace-nowrap transition-colors',
|
||||
pathname === item.href ? 'text-foreground' : 'text-foreground/60'
|
||||
)}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -3,12 +3,27 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Checkbox } from '@/components/ui/checkbox'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { submitOrder, getUserSubmission, getSessionUser } from '@/app/actions'
|
||||
import { PlusCircle, MinusCircle, User, CheckCircle2, ShoppingBag, Sparkles, Users, Eye } from 'lucide-react'
|
||||
import {
|
||||
PlusCircle,
|
||||
MinusCircle,
|
||||
User,
|
||||
CheckCircle2,
|
||||
ShoppingBag,
|
||||
Sparkles,
|
||||
Users,
|
||||
Eye,
|
||||
} from 'lucide-react'
|
||||
import { OrderFormModal } from '@/components/OrderFormModal'
|
||||
import { ShareButton } from '@/components/ShareButton'
|
||||
import Link from 'next/link'
|
||||
@@ -18,51 +33,56 @@ import { id as idLocale } from 'date-fns/locale'
|
||||
|
||||
export function OrderCard({ order }: { order: any }) {
|
||||
const [open, setOpen] = useState(false)
|
||||
|
||||
|
||||
const activeItems = order.available_items.filter((i: any) => !i.is_sold_out)
|
||||
|
||||
return (
|
||||
<div className="flex flex-col md:flex-row h-full md:h-auto rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm hover:shadow-xl hover:border-[#1B2CC1]/40 dark:hover:border-blue-500/40 transition-all duration-300 group overflow-hidden items-start md:items-stretch">
|
||||
|
||||
<div className="group flex h-full flex-col items-start overflow-hidden rounded-2xl border border-slate-200/90 bg-white shadow-sm transition-all duration-300 hover:border-[#1B2CC1]/40 hover:shadow-xl md:h-auto md:flex-row md:items-stretch dark:border-slate-800 dark:bg-slate-900 dark:hover:border-blue-500/40">
|
||||
{/* Left: Info */}
|
||||
<div className="flex-[1.2] p-5 md:border-r border-slate-100 dark:border-slate-800/80 flex flex-col justify-center min-w-0 w-full">
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-lg font-bold text-slate-900 dark:text-white line-clamp-2 leading-snug group-hover:text-[#1B2CC1] dark:group-hover:text-blue-400 transition-colors">
|
||||
<div className="flex w-full min-w-0 flex-[1.2] flex-col justify-center border-slate-100 p-5 md:border-r dark:border-slate-800/80">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="line-clamp-2 text-lg leading-snug font-bold text-slate-900 transition-colors group-hover:text-[#1B2CC1] dark:text-white dark:group-hover:text-blue-400">
|
||||
{order.title}
|
||||
</h3>
|
||||
{order.description && (
|
||||
<p className="text-xs text-slate-500 mt-1 line-clamp-2 font-medium">
|
||||
<p className="mt-1 line-clamp-2 text-xs font-medium text-slate-500">
|
||||
{order.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row items-end sm:items-center gap-2 shrink-0">
|
||||
<span className="text-[10px] sm:text-[11px] text-slate-500 font-semibold bg-slate-100/80 dark:bg-slate-800 px-2.5 py-1 rounded-full whitespace-nowrap">
|
||||
<div className="flex shrink-0 flex-col items-end gap-2 sm:flex-row sm:items-center">
|
||||
<span className="rounded-full bg-slate-100/80 px-2.5 py-1 text-[10px] font-semibold whitespace-nowrap text-slate-500 sm:text-[11px] dark:bg-slate-800">
|
||||
{format(new Date(order.date), 'EEEE, dd MMM yyyy', { locale: idLocale })}
|
||||
</span>
|
||||
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-[10px] sm:text-[11px] font-black tracking-wide bg-emerald-50 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-400 border border-emerald-200/60 dark:border-emerald-900/50">
|
||||
<span className="inline-flex items-center rounded-full border border-emerald-200/60 bg-emerald-50 px-2.5 py-1 text-[10px] font-black tracking-wide text-emerald-700 sm:text-[11px] dark:border-emerald-900/50 dark:bg-emerald-950/50 dark:text-emerald-400">
|
||||
● OPEN
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap items-center gap-3 mt-3 pt-3 border-t border-slate-100 dark:border-slate-800/60">
|
||||
<div className="mt-3 flex flex-wrap items-center gap-3 border-t border-slate-100 pt-3 dark:border-slate-800/60">
|
||||
<div className="flex items-center gap-2">
|
||||
{order.creator.photo ? (
|
||||
<img src={order.creator.photo} alt={order.creator.name} className="w-5 h-5 rounded-full object-cover ring-2 ring-slate-100 dark:ring-slate-800" />
|
||||
<img
|
||||
src={order.creator.photo}
|
||||
alt={order.creator.name}
|
||||
className="h-5 w-5 rounded-full object-cover ring-2 ring-slate-100 dark:ring-slate-800"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-5 h-5 rounded-full bg-[#1B2CC1]/10 dark:bg-blue-900/40 text-[#1B2CC1] dark:text-blue-300 flex items-center justify-center font-bold text-[10px]">
|
||||
<div className="flex h-5 w-5 items-center justify-center rounded-full bg-[#1B2CC1]/10 text-[10px] font-bold text-[#1B2CC1] dark:bg-blue-900/40 dark:text-blue-300">
|
||||
{order.creator.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="text-xs font-bold text-slate-700 dark:text-slate-300">{order.creator.name}</span>
|
||||
<span className="text-xs font-bold text-slate-700 dark:text-slate-300">
|
||||
{order.creator.name}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="w-1 h-1 rounded-full bg-slate-300 dark:bg-slate-600 hidden sm:block"></div>
|
||||
|
||||
<div className="hidden h-1 w-1 rounded-full bg-slate-300 sm:block dark:bg-slate-600"></div>
|
||||
|
||||
<div className="flex items-center gap-1.5">
|
||||
<Users className="w-3.5 h-3.5 text-[#1B2CC1]" />
|
||||
<Users className="h-3.5 w-3.5 text-[#1B2CC1]" />
|
||||
<span className="text-xs font-bold text-slate-600 dark:text-slate-400">
|
||||
{order.submissions.length} Orang Menitip
|
||||
</span>
|
||||
@@ -71,31 +91,34 @@ export function OrderCard({ order }: { order: any }) {
|
||||
</div>
|
||||
|
||||
{/* Middle: Items List */}
|
||||
<div className="w-full md:w-64 p-5 md:border-r border-slate-100 dark:border-slate-800/80 flex flex-col justify-center border-t md:border-t-0">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[10px] font-bold uppercase tracking-wider text-slate-400 block">
|
||||
<div className="flex w-full flex-col justify-center border-t border-slate-100 p-5 md:w-64 md:border-t-0 md:border-r dark:border-slate-800/80">
|
||||
<div className="mb-2 flex items-center justify-between">
|
||||
<span className="block text-[10px] font-bold tracking-wider text-slate-400 uppercase">
|
||||
Item Tersedia ({activeItems.length})
|
||||
</span>
|
||||
{order.allow_custom && (
|
||||
<div className="inline-flex items-center gap-1 text-[9px] font-semibold text-[#1B2CC1] bg-blue-50 dark:bg-blue-950/40 px-1.5 py-0.5 rounded-md">
|
||||
<Sparkles className="w-3 h-3" /> Kustom
|
||||
<div className="inline-flex items-center gap-1 rounded-md bg-blue-50 px-1.5 py-0.5 text-[9px] font-semibold text-[#1B2CC1] dark:bg-blue-950/40">
|
||||
<Sparkles className="h-3 w-3" /> Kustom
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-50 dark:bg-slate-800/40 p-3 rounded-xl border border-slate-200/70 dark:border-slate-800 max-h-24 overflow-y-auto scrollbar-thin">
|
||||
<div className="max-h-24 scrollbar-thin overflow-y-auto rounded-xl border border-slate-200/70 bg-slate-50 p-3 dark:border-slate-800 dark:bg-slate-800/40">
|
||||
<ul className="space-y-1.5">
|
||||
{activeItems.map((item: any) => (
|
||||
<li key={item.id} className="text-[11px] flex items-start gap-2 text-slate-700 dark:text-slate-300 font-medium">
|
||||
<div className="w-3.5 h-3.5 rounded-full bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center shrink-0 mt-0.5">
|
||||
<CheckCircle2 className="w-2.5 h-2.5" />
|
||||
<li
|
||||
key={item.id}
|
||||
className="flex items-start gap-2 text-[11px] font-medium text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
<div className="mt-0.5 flex h-3.5 w-3.5 shrink-0 items-center justify-center rounded-full bg-[#1B2CC1]/10 text-[#1B2CC1]">
|
||||
<CheckCircle2 className="h-2.5 w-2.5" />
|
||||
</div>
|
||||
<span className="truncate leading-snug">{item.name}</span>
|
||||
</li>
|
||||
))}
|
||||
{activeItems.length === 0 && (
|
||||
<li className="text-[10px] text-slate-400 italic">
|
||||
{order.allow_custom ? "Hanya menerima kustom." : "Semua item habis."}
|
||||
{order.allow_custom ? 'Hanya menerima kustom.' : 'Semua item habis.'}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
@@ -103,28 +126,28 @@ export function OrderCard({ order }: { order: any }) {
|
||||
</div>
|
||||
|
||||
{/* Right: Actions */}
|
||||
<div className="w-full md:w-56 p-5 bg-slate-50/60 dark:bg-slate-800/40 flex flex-col items-center justify-center gap-2.5 shrink-0">
|
||||
<div className="flex w-full shrink-0 flex-col items-center justify-center gap-2.5 bg-slate-50/60 p-5 md:w-56 dark:bg-slate-800/40">
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger className="w-full flex items-center justify-center h-10 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold text-xs shadow-md shadow-[#1B2CC1]/20 gap-1.5 transition-all cursor-pointer px-2">
|
||||
<ShoppingBag className="w-4 h-4 shrink-0" />
|
||||
<DialogTrigger className="flex h-10 w-full cursor-pointer items-center justify-center gap-1.5 rounded-xl bg-[#1B2CC1] px-2 text-xs font-bold text-white shadow-md shadow-[#1B2CC1]/20 transition-all hover:bg-[#15229E]">
|
||||
<ShoppingBag className="h-4 w-4 shrink-0" />
|
||||
<span className="truncate">Titip Sekarang</span>
|
||||
</DialogTrigger>
|
||||
{open && <OrderFormModal order={order} onSuccess={() => setOpen(false)} />}
|
||||
</Dialog>
|
||||
|
||||
<div className="flex w-full gap-2">
|
||||
<Link
|
||||
<Link
|
||||
href={`/order/${order.id}`}
|
||||
className="flex-1 flex items-center justify-center h-9 rounded-xl bg-white dark:bg-slate-900 hover:bg-slate-50 dark:hover:bg-slate-800 text-slate-700 dark:text-slate-300 font-bold text-xs shadow-sm gap-1.5 transition-all cursor-pointer px-1 border border-slate-200 dark:border-slate-700"
|
||||
className="flex h-9 flex-1 cursor-pointer items-center justify-center gap-1.5 rounded-xl border border-slate-200 bg-white px-1 text-xs font-bold text-slate-700 shadow-sm transition-all hover:bg-slate-50 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
>
|
||||
<Eye className="w-3.5 h-3.5 shrink-0" />
|
||||
<Eye className="h-3.5 w-3.5 shrink-0" />
|
||||
<span className="truncate">Detail</span>
|
||||
</Link>
|
||||
|
||||
<ShareButton
|
||||
orderId={order.id}
|
||||
|
||||
<ShareButton
|
||||
orderId={order.id}
|
||||
orderTitle={order.title}
|
||||
className="flex-1 h-9 rounded-xl font-bold text-xs gap-1.5 shadow-sm bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-800 hover:text-slate-900 transition-all text-slate-700 dark:text-slate-300 px-1"
|
||||
className="h-9 flex-1 gap-1.5 rounded-xl border border-slate-200 bg-white px-1 text-xs font-bold text-slate-700 shadow-sm transition-all hover:bg-slate-50 hover:text-slate-900 dark:border-slate-700 dark:bg-slate-900 dark:text-slate-300 dark:hover:bg-slate-800"
|
||||
showText={true}
|
||||
/>
|
||||
</div>
|
||||
|
||||
+143
-102
@@ -10,11 +10,15 @@ import { Input } from '@/components/ui/input'
|
||||
import { submitOrder, getUserSubmission, getSessionUser, getOrderDetail } from '@/app/actions'
|
||||
import { PlusCircle, MinusCircle, CheckCircle2 } from 'lucide-react'
|
||||
|
||||
export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: () => void }) {
|
||||
export function OrderFormModal({ order, onSuccess }: { order: any; onSuccess: () => void }) {
|
||||
const [liveOrder, setLiveOrder] = useState<any>(order)
|
||||
const [userId, setUserId] = useState<string>('')
|
||||
const [items, setItems] = useState<Record<string, { selected: boolean, qty: number, note?: string }>>({})
|
||||
const [customItems, setCustomItems] = useState<Array<{ id: string, name: string, qty: number, note?: string }>>([])
|
||||
const [items, setItems] = useState<
|
||||
Record<string, { selected: boolean; qty: number; note?: string }>
|
||||
>({})
|
||||
const [customItems, setCustomItems] = useState<
|
||||
Array<{ id: string; name: string; qty: number; note?: string }>
|
||||
>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [error, setError] = useState('')
|
||||
|
||||
@@ -22,7 +26,7 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
const init = async () => {
|
||||
const freshOrder = await getOrderDetail(order.id)
|
||||
if (freshOrder) setLiveOrder(freshOrder)
|
||||
|
||||
|
||||
const user = await getSessionUser()
|
||||
if (user?.id) {
|
||||
setUserId(user.id)
|
||||
@@ -37,7 +41,7 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
if (sub) {
|
||||
const freshItems: any = {}
|
||||
const newCustoms: any[] = []
|
||||
|
||||
|
||||
sub.items.forEach((item: any) => {
|
||||
if (!item.is_custom) {
|
||||
const stdItem = currentOrder.available_items.find((ai: any) => ai.name === item.name)
|
||||
@@ -49,7 +53,7 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
id: Math.random().toString(36).substr(2, 9),
|
||||
name: item.name,
|
||||
qty: item.qty,
|
||||
note: item.note || ''
|
||||
note: item.note || '',
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -59,9 +63,9 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
}
|
||||
|
||||
const handleStandardItemToggle = (id: string, checked: boolean) => {
|
||||
setItems(prev => ({
|
||||
setItems((prev) => ({
|
||||
...prev,
|
||||
[id]: { selected: checked, qty: checked ? 1 : 0, note: prev[id]?.note || '' }
|
||||
[id]: { selected: checked, qty: checked ? 1 : 0, note: prev[id]?.note || '' },
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -70,30 +74,39 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
handleStandardItemToggle(id, false)
|
||||
return
|
||||
}
|
||||
setItems(prev => ({
|
||||
setItems((prev) => ({
|
||||
...prev,
|
||||
[id]: { ...prev[id], qty }
|
||||
[id]: { ...prev[id], qty },
|
||||
}))
|
||||
}
|
||||
|
||||
const handleStandardItemNote = (id: string, note: string) => {
|
||||
setItems(prev => ({
|
||||
setItems((prev) => ({
|
||||
...prev,
|
||||
[id]: { ...prev[id], note }
|
||||
[id]: { ...prev[id], note },
|
||||
}))
|
||||
}
|
||||
|
||||
const addCustomItem = () => {
|
||||
setCustomItems(prev => [...prev, { id: Math.random().toString(36).substr(2, 9), name: '', qty: 1, note: '' }])
|
||||
setCustomItems((prev) => [
|
||||
...prev,
|
||||
{ id: Math.random().toString(36).substr(2, 9), name: '', qty: 1, note: '' },
|
||||
])
|
||||
}
|
||||
|
||||
const handleCustomItemChange = (id: string, field: 'name' | 'qty' | 'note', value: string | number) => {
|
||||
setCustomItems(prev => prev.map(item => {
|
||||
if (item.id === id) {
|
||||
return { ...item, [field]: value }
|
||||
}
|
||||
return item
|
||||
}))
|
||||
const handleCustomItemChange = (
|
||||
id: string,
|
||||
field: 'name' | 'qty' | 'note',
|
||||
value: string | number
|
||||
) => {
|
||||
setCustomItems((prev) =>
|
||||
prev.map((item) => {
|
||||
if (item.id === id) {
|
||||
return { ...item, [field]: value }
|
||||
}
|
||||
return item
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
const handleCustomItemQty = (id: string, qty: number) => {
|
||||
@@ -102,16 +115,16 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
}
|
||||
|
||||
const removeCustomItem = (id: string) => {
|
||||
setCustomItems(customItems.filter(c => c.id !== id))
|
||||
setCustomItems(customItems.filter((c) => c.id !== id))
|
||||
}
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
setError('')
|
||||
setLoading(true)
|
||||
|
||||
|
||||
const payloadItems: any[] = []
|
||||
|
||||
|
||||
liveOrder.available_items.forEach((ai: any) => {
|
||||
const selectedItem = items[ai.id]
|
||||
if (selectedItem?.selected && selectedItem.qty > 0) {
|
||||
@@ -119,22 +132,22 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
name: ai.name,
|
||||
qty: selectedItem.qty,
|
||||
is_custom: false,
|
||||
note: selectedItem.note?.trim() || undefined
|
||||
note: selectedItem.note?.trim() || undefined,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
customItems.forEach(ci => {
|
||||
|
||||
customItems.forEach((ci) => {
|
||||
if (ci.name.trim() && ci.qty > 0) {
|
||||
payloadItems.push({
|
||||
name: ci.name.trim(),
|
||||
qty: ci.qty,
|
||||
is_custom: true,
|
||||
note: ci.note?.trim() || undefined
|
||||
note: ci.note?.trim() || undefined,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
if (payloadItems.length === 0) {
|
||||
setError('Harap pilih minimal 1 item atau tambahkan item lainnya.')
|
||||
setLoading(false)
|
||||
@@ -144,7 +157,7 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
const res = await submitOrder({
|
||||
order_id: liveOrder.id,
|
||||
user_id: userId,
|
||||
items: payloadItems
|
||||
items: payloadItems,
|
||||
})
|
||||
|
||||
if (res.success) {
|
||||
@@ -156,21 +169,26 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
}
|
||||
|
||||
return (
|
||||
<DialogContent className="sm:max-w-[520px] p-0 overflow-hidden rounded-3xl border-slate-200/90 dark:border-slate-800 shadow-2xl max-h-[90vh] flex flex-col">
|
||||
<DialogContent className="flex max-h-[90vh] flex-col overflow-hidden rounded-3xl border-slate-200/90 p-0 shadow-2xl sm:max-w-[520px] dark:border-slate-800">
|
||||
<div className="bg-gradient-to-br from-[#1B2CC1] to-[#121E85] p-6 text-white">
|
||||
<span className="text-[11px] font-bold uppercase tracking-wider text-blue-200">Form Titipan</span>
|
||||
<DialogTitle className="text-2xl font-black tracking-tight text-white mt-1">
|
||||
<span className="text-[11px] font-bold tracking-wider text-blue-200 uppercase">
|
||||
Form Titipan
|
||||
</span>
|
||||
<DialogTitle className="mt-1 text-2xl font-black tracking-tight text-white">
|
||||
{liveOrder.title}
|
||||
</DialogTitle>
|
||||
<p className="text-xs text-blue-100 mt-1">
|
||||
<p className="mt-1 text-xs text-blue-100">
|
||||
Pilih menu yang tersedia di bawah atau tambahkan item khusus jika diizinkan.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="p-6 overflow-y-auto space-y-6 flex-1 bg-white dark:bg-slate-900">
|
||||
<form
|
||||
onSubmit={handleSubmit}
|
||||
className="flex-1 space-y-6 overflow-y-auto bg-white p-6 dark:bg-slate-900"
|
||||
>
|
||||
{/* Standard Items */}
|
||||
<div className="space-y-3">
|
||||
<Label className="text-xs font-bold uppercase tracking-wider text-slate-400">
|
||||
<Label className="text-xs font-bold tracking-wider text-slate-400 uppercase">
|
||||
Daftar Menu Tersedia
|
||||
</Label>
|
||||
<div className="space-y-2">
|
||||
@@ -178,50 +196,64 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
const isSelected = items[item.id]?.selected || false
|
||||
const qty = items[item.id]?.qty || 0
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
<div
|
||||
key={item.id}
|
||||
className={cn(
|
||||
"flex flex-col gap-2 p-3.5 rounded-xl border transition-all",
|
||||
isSelected ? "border-[#1B2CC1]/40 bg-blue-50/20 dark:bg-blue-950/10 shadow-sm" : "border-slate-200/90 dark:border-slate-800 bg-slate-50/70 dark:bg-slate-800/40"
|
||||
'flex flex-col gap-2 rounded-xl border p-3.5 transition-all',
|
||||
isSelected
|
||||
? 'border-[#1B2CC1]/40 bg-blue-50/20 shadow-sm dark:bg-blue-950/10'
|
||||
: 'border-slate-200/90 bg-slate-50/70 dark:border-slate-800 dark:bg-slate-800/40'
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-3 flex-1 min-w-0">
|
||||
<Checkbox
|
||||
id={`item-${item.id}`}
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<Checkbox
|
||||
id={`item-${item.id}`}
|
||||
checked={isSelected && !item.is_sold_out}
|
||||
disabled={item.is_sold_out || loading}
|
||||
onCheckedChange={(c) => !item.is_sold_out && handleStandardItemToggle(item.id, c as boolean)}
|
||||
className="rounded-lg data-[state=checked]:bg-[#1B2CC1] data-[state=checked]:border-[#1B2CC1] disabled:opacity-50"
|
||||
onCheckedChange={(c) =>
|
||||
!item.is_sold_out && handleStandardItemToggle(item.id, c as boolean)
|
||||
}
|
||||
className="rounded-lg disabled:opacity-50 data-[state=checked]:border-[#1B2CC1] data-[state=checked]:bg-[#1B2CC1]"
|
||||
/>
|
||||
<Label htmlFor={`item-${item.id}`} className={cn("text-sm font-bold truncate flex items-center gap-2", item.is_sold_out ? "text-slate-400 dark:text-slate-500 cursor-not-allowed" : "text-slate-800 dark:text-slate-200 cursor-pointer")}>
|
||||
<span className={item.is_sold_out ? "line-through" : ""}>{item.name}</span>
|
||||
<Label
|
||||
htmlFor={`item-${item.id}`}
|
||||
className={cn(
|
||||
'flex items-center gap-2 truncate text-sm font-bold',
|
||||
item.is_sold_out
|
||||
? 'cursor-not-allowed text-slate-400 dark:text-slate-500'
|
||||
: 'cursor-pointer text-slate-800 dark:text-slate-200'
|
||||
)}
|
||||
>
|
||||
<span className={item.is_sold_out ? 'line-through' : ''}>{item.name}</span>
|
||||
{item.is_sold_out && (
|
||||
<span className="text-[9px] bg-rose-100 dark:bg-rose-950/30 text-rose-600 dark:text-rose-400 px-1.5 py-0.5 rounded font-black uppercase tracking-wider border border-rose-200 dark:border-rose-900/50">
|
||||
<span className="rounded border border-rose-200 bg-rose-100 px-1.5 py-0.5 text-[9px] font-black tracking-wider text-rose-600 uppercase dark:border-rose-900/50 dark:bg-rose-950/30 dark:text-rose-400">
|
||||
Sold Out
|
||||
</span>
|
||||
)}
|
||||
</Label>
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="flex items-center gap-1 bg-white dark:bg-slate-800 border border-slate-200 dark:border-slate-700 rounded-lg p-0.5 shadow-sm shrink-0">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<div className="flex shrink-0 items-center gap-1 rounded-lg border border-slate-200 bg-white p-0.5 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={loading}
|
||||
className="h-7 w-7 text-slate-500 hover:text-slate-800"
|
||||
className="h-7 w-7 text-slate-500 hover:text-slate-800"
|
||||
onClick={() => handleStandardItemQty(item.id, qty - 1)}
|
||||
>
|
||||
<MinusCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
<span className="w-7 text-center text-xs font-extrabold text-[#1B2CC1] dark:text-blue-400">{qty}</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<span className="w-7 text-center text-xs font-extrabold text-[#1B2CC1] dark:text-blue-400">
|
||||
{qty}
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={loading}
|
||||
className="h-7 w-7 text-slate-500 hover:text-slate-800"
|
||||
className="h-7 w-7 text-slate-500 hover:text-slate-800"
|
||||
onClick={() => handleStandardItemQty(item.id, qty + 1)}
|
||||
>
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
@@ -230,10 +262,10 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
)}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="pl-7 mt-1">
|
||||
<Input
|
||||
placeholder="Catatan (opsional), misal: 1/2 porsi"
|
||||
className="h-8 text-xs bg-slate-50/50 dark:bg-slate-900/50 border-slate-200/60 dark:border-slate-700/60"
|
||||
<div className="mt-1 pl-7">
|
||||
<Input
|
||||
placeholder="Catatan (opsional), misal: 1/2 porsi"
|
||||
className="h-8 border-slate-200/60 bg-slate-50/50 text-xs dark:border-slate-700/60 dark:bg-slate-900/50"
|
||||
value={items[item.id]?.note || ''}
|
||||
disabled={loading}
|
||||
onChange={(e) => handleStandardItemNote(item.id, e.target.value)}
|
||||
@@ -244,7 +276,7 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
)
|
||||
})}
|
||||
{liveOrder.available_items.length === 0 && (
|
||||
<p className="text-xs text-slate-400 text-center py-3 bg-slate-50 dark:bg-slate-800/40 rounded-xl border border-dashed border-slate-200 dark:border-slate-800">
|
||||
<p className="rounded-xl border border-dashed border-slate-200 bg-slate-50 py-3 text-center text-xs text-slate-400 dark:border-slate-800 dark:bg-slate-800/40">
|
||||
Tidak ada menu standar yang ditentukan.
|
||||
</p>
|
||||
)}
|
||||
@@ -255,48 +287,53 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
{liveOrder.allow_custom && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label className="text-xs font-bold uppercase tracking-wider text-slate-400">
|
||||
<Label className="text-xs font-bold tracking-wider text-slate-400 uppercase">
|
||||
Item Tambahan (Kustom)
|
||||
</Label>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
disabled={loading}
|
||||
onClick={addCustomItem}
|
||||
className="h-8 text-xs font-bold rounded-lg border-dashed border-[#1B2CC1]/40 text-[#1B2CC1] hover:bg-[#1B2CC1]/10 gap-1.5"
|
||||
onClick={addCustomItem}
|
||||
className="h-8 gap-1.5 rounded-lg border-dashed border-[#1B2CC1]/40 text-xs font-bold text-[#1B2CC1] hover:bg-[#1B2CC1]/10"
|
||||
>
|
||||
<PlusCircle className="h-3.5 w-3.5" /> Tambah Kustom
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
|
||||
{customItems.length > 0 ? (
|
||||
<div className="space-y-2.5">
|
||||
{customItems.map((ci, idx) => (
|
||||
<div key={ci.id} className="flex gap-2 items-center p-3 rounded-xl border border-slate-200/90 dark:border-slate-800 bg-slate-50/70 dark:bg-slate-800/40">
|
||||
<span className="text-xs font-bold text-slate-400 w-4">{idx + 1}.</span>
|
||||
<Input
|
||||
placeholder="Nama Menu Kustom"
|
||||
value={ci.name}
|
||||
<div
|
||||
key={ci.id}
|
||||
className="flex items-center gap-2 rounded-xl border border-slate-200/90 bg-slate-50/70 p-3 dark:border-slate-800 dark:bg-slate-800/40"
|
||||
>
|
||||
<span className="w-4 text-xs font-bold text-slate-400">{idx + 1}.</span>
|
||||
<Input
|
||||
placeholder="Nama Menu Kustom"
|
||||
value={ci.name}
|
||||
disabled={loading}
|
||||
onChange={(e) => handleCustomItemChange(ci.id, 'name', e.target.value)}
|
||||
className="flex-1 h-9 rounded-lg bg-white dark:bg-slate-900 text-xs font-medium"
|
||||
className="h-9 flex-1 rounded-lg bg-white text-xs font-medium dark:bg-slate-900"
|
||||
/>
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={ci.qty}
|
||||
<Input
|
||||
type="number"
|
||||
min="1"
|
||||
value={ci.qty}
|
||||
disabled={loading}
|
||||
onChange={(e) => handleCustomItemChange(ci.id, 'qty', parseInt(e.target.value) || 1)}
|
||||
className="w-16 h-9 rounded-lg bg-white dark:bg-slate-900 text-center font-bold text-xs"
|
||||
onChange={(e) =>
|
||||
handleCustomItemChange(ci.id, 'qty', parseInt(e.target.value) || 1)
|
||||
}
|
||||
className="h-9 w-16 rounded-lg bg-white text-center text-xs font-bold dark:bg-slate-900"
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
<Button
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
disabled={loading}
|
||||
onClick={() => removeCustomItem(ci.id)}
|
||||
className="h-8 w-8 text-red-500 hover:text-red-700 hover:bg-red-50 dark:hover:bg-red-950/30 rounded-lg shrink-0"
|
||||
onClick={() => removeCustomItem(ci.id)}
|
||||
className="h-8 w-8 shrink-0 rounded-lg text-red-500 hover:bg-red-50 hover:text-red-700 dark:hover:bg-red-950/30"
|
||||
>
|
||||
<MinusCircle className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -304,29 +341,33 @@ export function OrderFormModal({ order, onSuccess }: { order: any, onSuccess: ()
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
<div
|
||||
onClick={addCustomItem}
|
||||
className="p-4 border-2 border-dashed border-slate-200 dark:border-slate-800 hover:border-[#1B2CC1]/50 rounded-2xl text-center bg-slate-50/50 dark:bg-slate-800/30 cursor-pointer transition-all group"
|
||||
className="group cursor-pointer rounded-2xl border-2 border-dashed border-slate-200 bg-slate-50/50 p-4 text-center transition-all hover:border-[#1B2CC1]/50 dark:border-slate-800 dark:bg-slate-800/30"
|
||||
>
|
||||
<PlusCircle className="w-5 h-5 text-slate-400 group-hover:text-[#1B2CC1] mx-auto mb-1 transition-colors" />
|
||||
<p className="text-xs font-bold text-slate-600 dark:text-slate-300">Klik untuk Tambah Item Custom</p>
|
||||
<p className="text-[11px] text-slate-400 mt-0.5">Ingin titip menu lain? Masukkan nama dan kuantitasnya di sini.</p>
|
||||
<PlusCircle className="mx-auto mb-1 h-5 w-5 text-slate-400 transition-colors group-hover:text-[#1B2CC1]" />
|
||||
<p className="text-xs font-bold text-slate-600 dark:text-slate-300">
|
||||
Klik untuk Tambah Item Custom
|
||||
</p>
|
||||
<p className="mt-0.5 text-[11px] text-slate-400">
|
||||
Ingin titip menu lain? Masukkan nama dan kuantitasnya di sini.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="p-3 bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-900 rounded-xl text-xs text-red-600 dark:text-red-400 font-semibold">
|
||||
<div className="rounded-xl border border-red-200 bg-red-50 p-3 text-xs font-semibold text-red-600 dark:border-red-900 dark:bg-red-950/30 dark:text-red-400">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end gap-3 pt-2 border-t border-slate-100 dark:border-slate-800">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="w-full h-11 rounded-xl bg-[#1B2CC1] hover:bg-[#15229E] text-white font-bold shadow-md shadow-[#1B2CC1]/25"
|
||||
|
||||
<div className="flex justify-end gap-3 border-t border-slate-100 pt-2 dark:border-slate-800">
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={loading}
|
||||
className="h-11 w-full rounded-xl bg-[#1B2CC1] font-bold text-white shadow-md shadow-[#1B2CC1]/25 hover:bg-[#15229E]"
|
||||
>
|
||||
{loading ? 'Menyimpan Titipan...' : 'Kirim Titip Pesanan'}
|
||||
</Button>
|
||||
|
||||
@@ -2,25 +2,61 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { ChevronDown, ChevronUp, Users, CheckCircle2, Package, InboxIcon, ChevronLeft, ChevronRight, Search } from 'lucide-react'
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Users,
|
||||
CheckCircle2,
|
||||
Package,
|
||||
InboxIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Search,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, DialogDescription } from '@/components/ui/dialog'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter,
|
||||
DialogDescription,
|
||||
} from '@/components/ui/dialog'
|
||||
import { processBulkPayment } from '@/app/actions'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
const ITEMS_PER_PAGE = 5
|
||||
|
||||
const formatRupiah = (value: number) =>
|
||||
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(value)
|
||||
new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value)
|
||||
|
||||
export default function ReportByPersonGrid({ data, balancesData = [], creatorId, onUpdate }: { data: any[]; balancesData?: any[]; creatorId: string; onUpdate: () => void }) {
|
||||
export default function ReportByPersonGrid({
|
||||
data,
|
||||
balancesData = [],
|
||||
creatorId,
|
||||
onUpdate,
|
||||
}: {
|
||||
data: any[]
|
||||
balancesData?: any[]
|
||||
creatorId: string
|
||||
onUpdate: () => void
|
||||
}) {
|
||||
const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({})
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
const [modalOpen, setModalOpen] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
const [target, setTarget] = useState<{ ids: string[]; label: string; amount: number; userId: string }>({ ids: [], label: '', amount: 0, userId: '' })
|
||||
const [target, setTarget] = useState<{
|
||||
ids: string[]
|
||||
label: string
|
||||
amount: number
|
||||
userId: string
|
||||
}>({ ids: [], label: '', amount: 0, userId: '' })
|
||||
const [cashInput, setCashInput] = useState<string>('')
|
||||
|
||||
// Reset page + search when data changes (e.g. filter changed)
|
||||
@@ -35,24 +71,24 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
setCurrentPage(1)
|
||||
}, [searchQuery])
|
||||
|
||||
const toggleRow = (id: string) =>
|
||||
setExpandedRows(prev => ({ ...prev, [id]: !prev[id] }))
|
||||
const toggleRow = (id: string) => setExpandedRows((prev) => ({ ...prev, [id]: !prev[id] }))
|
||||
|
||||
// Group by user
|
||||
const usersMap = new Map<string, any>()
|
||||
data.forEach(order => {
|
||||
data.forEach((order) => {
|
||||
order.submissions.forEach((sub: any) => {
|
||||
if (!usersMap.has(sub.user.id)) {
|
||||
usersMap.set(sub.user.id, {
|
||||
user: sub.user,
|
||||
totalPiutang: 0,
|
||||
totalPaid: 0,
|
||||
submissions: []
|
||||
submissions: [],
|
||||
})
|
||||
}
|
||||
const obj = usersMap.get(sub.user.id)
|
||||
const bill = Number(sub.bill) || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const paid =
|
||||
(sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const unpaid = Math.max(0, bill - paid)
|
||||
obj.totalPiutang += unpaid
|
||||
obj.totalPaid += paid
|
||||
@@ -61,24 +97,33 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
})
|
||||
|
||||
const userList = Array.from(usersMap.values())
|
||||
.map(obj => ({
|
||||
.map((obj) => ({
|
||||
...obj,
|
||||
// Sort each person's PO list by order date, newest first
|
||||
submissions: [...obj.submissions].sort(
|
||||
(a: any, b: any) => new Date(b.orderDate).getTime() - new Date(a.orderDate).getTime()
|
||||
)
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => b.totalPiutang - a.totalPiutang)
|
||||
|
||||
const q = searchQuery.toLowerCase().trim()
|
||||
const filteredUserList = q
|
||||
? userList.filter(u => u.user.name?.toLowerCase().includes(q))
|
||||
? userList.filter((u) => u.user.name?.toLowerCase().includes(q))
|
||||
: userList
|
||||
|
||||
const totalPages = Math.ceil(filteredUserList.length / ITEMS_PER_PAGE)
|
||||
const paginatedList = filteredUserList.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE)
|
||||
const paginatedList = filteredUserList.slice(
|
||||
(currentPage - 1) * ITEMS_PER_PAGE,
|
||||
currentPage * ITEMS_PER_PAGE
|
||||
)
|
||||
|
||||
const openModal = (e: React.MouseEvent, ids: string[], label: string, amount: number, userId: string) => {
|
||||
const openModal = (
|
||||
e: React.MouseEvent,
|
||||
ids: string[],
|
||||
label: string,
|
||||
amount: number,
|
||||
userId: string
|
||||
) => {
|
||||
e.stopPropagation()
|
||||
setTarget({ ids, label, amount, userId })
|
||||
setCashInput('')
|
||||
@@ -94,7 +139,7 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
cash_amount: cash,
|
||||
use_balance: true,
|
||||
creator_id: creatorId,
|
||||
user_id: target.userId
|
||||
user_id: target.userId,
|
||||
})
|
||||
setSaving(false)
|
||||
if (res.success) {
|
||||
@@ -103,49 +148,53 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
}
|
||||
}
|
||||
|
||||
const targetUserBalanceObj = balancesData.find(b => b.user.id === target.userId)
|
||||
const targetUserBalanceObj = balancesData.find((b) => b.user.id === target.userId)
|
||||
const targetUserBalance = targetUserBalanceObj ? targetUserBalanceObj.amount : 0
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
||||
<div className="bg-slate-50/50 dark:bg-slate-800/40 px-4 py-3 border-b border-slate-100 dark:border-slate-800 flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Users className="w-4 h-4 text-[#1B2CC1] shrink-0" />
|
||||
<h3 className="text-sm font-black text-slate-800 dark:text-slate-200">Detail Piutang Berdasarkan Orang</h3>
|
||||
<Card className="overflow-hidden rounded-2xl border border-slate-200/90 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex flex-col gap-3 border-b border-slate-100 bg-slate-50/50 px-4 py-3 sm:flex-row sm:items-center dark:border-slate-800 dark:bg-slate-800/40">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<Users className="h-4 w-4 shrink-0 text-[#1B2CC1]" />
|
||||
<h3 className="text-sm font-black text-slate-800 dark:text-slate-200">
|
||||
Detail Piutang Berdasarkan Orang
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-1.5 w-full sm:w-48">
|
||||
<Search className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
||||
<div className="flex w-full items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-1.5 sm:w-48 dark:border-slate-700 dark:bg-slate-900">
|
||||
<Search className="h-3.5 w-3.5 shrink-0 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari nama pemesan..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="text-xs font-medium bg-transparent border-none outline-none w-full text-slate-700 dark:text-slate-200 placeholder:text-slate-400"
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full border-none bg-transparent text-xs font-medium text-slate-700 outline-none placeholder:text-slate-400 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{userList.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 gap-3 text-slate-400">
|
||||
<InboxIcon className="w-10 h-10 opacity-40" />
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-16 text-slate-400">
|
||||
<InboxIcon className="h-10 w-10 opacity-40" />
|
||||
<p className="text-sm font-semibold">Tidak ada data pemesan di periode ini.</p>
|
||||
</div>
|
||||
) : filteredUserList.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-16 gap-3 text-slate-400">
|
||||
<Search className="w-10 h-10 opacity-40" />
|
||||
<p className="text-sm font-semibold">Tidak ada hasil untuk "{searchQuery}".</p>
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-16 text-slate-400">
|
||||
<Search className="h-10 w-10 opacity-40" />
|
||||
<p className="text-sm font-semibold">
|
||||
Tidak ada hasil untuk "{searchQuery}".
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-500 uppercase bg-slate-50/30 dark:bg-slate-900/50">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-slate-50/30 text-xs text-slate-500 uppercase dark:bg-slate-900/50">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-bold w-8"></th>
|
||||
<th className="w-8 px-6 py-4 font-bold"></th>
|
||||
<th className="px-6 py-4 font-bold">Nama Pemesan</th>
|
||||
<th className="px-6 py-4 font-bold text-center">Total PO</th>
|
||||
<th className="px-6 py-4 font-bold text-right">Total Piutang</th>
|
||||
<th className="px-6 py-4 text-center font-bold">Total PO</th>
|
||||
<th className="px-6 py-4 text-right font-bold">Total Piutang</th>
|
||||
{/* <th className="px-6 py-4 font-bold text-center">Aksi</th> */}
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -159,25 +208,35 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
<tr
|
||||
onClick={() => toggleRow(userObj.user.id)}
|
||||
className={cn(
|
||||
'hover:bg-slate-50/50 dark:hover:bg-slate-800/40 cursor-pointer transition-colors group',
|
||||
'group cursor-pointer transition-colors hover:bg-slate-50/50 dark:hover:bg-slate-800/40',
|
||||
isExpanded && 'bg-slate-50/30 dark:bg-slate-800/20'
|
||||
)}
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<button className="text-slate-400 group-hover:text-[#1B2CC1] transition-colors p-1 rounded-full hover:bg-blue-50 dark:hover:bg-blue-900/30">
|
||||
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
<button className="rounded-full p-1 text-slate-400 transition-colors group-hover:text-[#1B2CC1] hover:bg-blue-50 dark:hover:bg-blue-900/30">
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{userObj.user.photo ? (
|
||||
<img src={userObj.user.photo} alt={userObj.user.name} className="w-8 h-8 rounded-full object-cover ring-2 ring-slate-100 dark:ring-slate-800" />
|
||||
<img
|
||||
src={userObj.user.photo}
|
||||
alt={userObj.user.name}
|
||||
className="h-8 w-8 rounded-full object-cover ring-2 ring-slate-100 dark:ring-slate-800"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-8 h-8 rounded-full bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center font-bold text-xs ring-2 ring-slate-100 dark:ring-slate-800">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-[#1B2CC1]/10 text-xs font-bold text-[#1B2CC1] ring-2 ring-slate-100 dark:ring-slate-800">
|
||||
{userObj.user.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<p className="font-bold text-slate-800 dark:text-slate-200">{userObj.user.name}</p>
|
||||
<p className="font-bold text-slate-800 dark:text-slate-200">
|
||||
{userObj.user.name}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-center font-semibold text-slate-600 dark:text-slate-400">
|
||||
@@ -185,12 +244,18 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
{userObj.totalPiutang > 0 ? (
|
||||
<p className="font-black text-rose-600 dark:text-rose-400">{formatRupiah(userObj.totalPiutang)}</p>
|
||||
<p className="font-black text-rose-600 dark:text-rose-400">
|
||||
{formatRupiah(userObj.totalPiutang)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="font-bold text-emerald-600 dark:text-emerald-400">Lunas Semua</p>
|
||||
<p className="font-bold text-emerald-600 dark:text-emerald-400">
|
||||
Lunas Semua
|
||||
</p>
|
||||
)}
|
||||
{userObj.totalPaid > 0 && (
|
||||
<p className="text-[10px] text-slate-500 font-semibold mt-0.5">Lunas: {formatRupiah(userObj.totalPaid)}</p>
|
||||
<p className="mt-0.5 text-[10px] font-semibold text-slate-500">
|
||||
Lunas: {formatRupiah(userObj.totalPaid)}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
{/* <td className="px-6 py-4 text-center">
|
||||
@@ -208,14 +273,14 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
|
||||
{isExpanded && (
|
||||
<tr className="bg-slate-50 dark:bg-slate-900/60">
|
||||
<td colSpan={5} className="px-6 py-5 border-l-4 border-l-[#1B2CC1]">
|
||||
<div className="pl-8 space-y-3">
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-wider text-slate-500 flex items-center gap-1.5">
|
||||
<Package className="w-3.5 h-3.5" /> Rincian Hutang per PO
|
||||
<td colSpan={5} className="border-l-4 border-l-[#1B2CC1] px-6 py-5">
|
||||
<div className="space-y-3 pl-8">
|
||||
<h4 className="flex items-center gap-1.5 text-[11px] font-bold tracking-wider text-slate-500 uppercase">
|
||||
<Package className="h-3.5 w-3.5" /> Rincian Hutang per PO
|
||||
</h4>
|
||||
<div className="overflow-hidden border border-slate-200/80 dark:border-slate-700 rounded-xl bg-white dark:bg-slate-800">
|
||||
<div className="overflow-hidden rounded-xl border border-slate-200/80 bg-white dark:border-slate-700 dark:bg-slate-800">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-slate-100/50 dark:bg-slate-700/30 text-slate-500 uppercase font-semibold">
|
||||
<thead className="bg-slate-100/50 font-semibold text-slate-500 uppercase dark:bg-slate-700/30">
|
||||
<tr>
|
||||
<th className="px-4 py-2.5 text-left">Judul PO</th>
|
||||
<th className="px-4 py-2.5 text-left">Tgl Order</th>
|
||||
@@ -229,25 +294,39 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
const isUnpaid = sub.unpaid > 0
|
||||
return (
|
||||
<tr key={sub.id}>
|
||||
<td className="px-4 py-2.5 font-bold text-slate-800 dark:text-slate-200">{sub.orderTitle}</td>
|
||||
<td className="px-4 py-2.5 text-slate-500 font-medium">
|
||||
<td className="px-4 py-2.5 font-bold text-slate-800 dark:text-slate-200">
|
||||
{sub.orderTitle}
|
||||
</td>
|
||||
<td className="px-4 py-2.5 font-medium text-slate-500">
|
||||
{format(new Date(sub.orderDate), 'dd MMM yyyy')}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span className={cn(
|
||||
'text-[9px] font-bold px-1.5 py-0.5 rounded uppercase',
|
||||
!isUnpaid
|
||||
? 'text-emerald-700 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-950/40'
|
||||
: 'text-rose-700 bg-rose-100 dark:text-rose-400 dark:bg-rose-950/40'
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded px-1.5 py-0.5 text-[9px] font-bold uppercase',
|
||||
!isUnpaid
|
||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-400'
|
||||
: 'bg-rose-100 text-rose-700 dark:bg-rose-950/40 dark:text-rose-400'
|
||||
)}
|
||||
>
|
||||
{isUnpaid ? 'Belum' : 'Lunas'}
|
||||
</span>
|
||||
</td>
|
||||
<td className={cn('px-4 py-2.5 text-right flex flex-col', isUnpaid ? 'text-rose-600 dark:text-rose-400' : 'text-emerald-600 dark:text-emerald-400')}>
|
||||
<span className="font-black">{sub.bill ? formatRupiah(sub.bill) : 'Rp0'}</span>
|
||||
<td
|
||||
className={cn(
|
||||
'flex flex-col px-4 py-2.5 text-right',
|
||||
isUnpaid
|
||||
? 'text-rose-600 dark:text-rose-400'
|
||||
: 'text-emerald-600 dark:text-emerald-400'
|
||||
)}
|
||||
>
|
||||
<span className="font-black">
|
||||
{sub.bill ? formatRupiah(sub.bill) : 'Rp0'}
|
||||
</span>
|
||||
{sub.unpaid > 0 && sub.paid > 0 && (
|
||||
<div className="text-[10px] text-slate-500 mt-0.5 font-medium leading-tight">
|
||||
Masuk: {formatRupiah(sub.paid)}<br/>
|
||||
<div className="mt-0.5 text-[10px] leading-tight font-medium text-slate-500">
|
||||
Masuk: {formatRupiah(sub.paid)}
|
||||
<br />
|
||||
Sisa: {formatRupiah(sub.unpaid)}
|
||||
</div>
|
||||
)}
|
||||
@@ -285,17 +364,29 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-slate-50/50 dark:bg-slate-900/50 border-t border-slate-100 dark:border-slate-800">
|
||||
<div className="flex items-center justify-between border-t border-slate-100 bg-slate-50/50 px-6 py-4 dark:border-slate-800 dark:bg-slate-900/50">
|
||||
<p className="text-xs font-medium text-slate-500">
|
||||
Halaman <span className="font-bold text-slate-700 dark:text-slate-300">{currentPage}</span> dari{' '}
|
||||
Halaman{' '}
|
||||
<span className="font-bold text-slate-700 dark:text-slate-300">
|
||||
{currentPage}
|
||||
</span>{' '}
|
||||
dari{' '}
|
||||
<span className="font-bold text-slate-700 dark:text-slate-300">{totalPages}</span>
|
||||
</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<button onClick={() => setCurrentPage(p => Math.max(1, p - 1))} disabled={currentPage === 1} className="p-1.5 rounded-lg border border-slate-200 dark:border-slate-700 disabled:opacity-40 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="rounded-lg border border-slate-200 p-1.5 transition-colors hover:bg-slate-100 disabled:opacity-40 dark:border-slate-700 dark:hover:bg-slate-800"
|
||||
>
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button onClick={() => setCurrentPage(p => Math.min(totalPages, p + 1))} disabled={currentPage === totalPages} className="p-1.5 rounded-lg border border-slate-200 dark:border-slate-700 disabled:opacity-40 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors">
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
<button
|
||||
onClick={() => setCurrentPage((p) => Math.min(totalPages, p + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="rounded-lg border border-slate-200 p-1.5 transition-colors hover:bg-slate-100 disabled:opacity-40 dark:border-slate-700 dark:hover:bg-slate-800"
|
||||
>
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -306,72 +397,93 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
|
||||
{/* Confirmation Modal */}
|
||||
<Dialog open={modalOpen} onOpenChange={setModalOpen}>
|
||||
<DialogContent className="sm:max-w-md rounded-2xl">
|
||||
<DialogContent className="rounded-2xl sm:max-w-md">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="text-xl font-black text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<CheckCircle2 className="w-6 h-6 text-emerald-500" />
|
||||
<DialogTitle className="flex items-center gap-2 text-xl font-black text-slate-900 dark:text-white">
|
||||
<CheckCircle2 className="h-6 w-6 text-emerald-500" />
|
||||
Konfirmasi Pelunasan
|
||||
</DialogTitle>
|
||||
<DialogDescription className="text-slate-500 pt-2">
|
||||
Tandai <strong className="text-slate-800 dark:text-slate-200">{target.label}</strong> sebagai <strong>LUNAS</strong>?
|
||||
<DialogDescription className="pt-2 text-slate-500">
|
||||
Tandai <strong className="text-slate-800 dark:text-slate-200">{target.label}</strong>{' '}
|
||||
sebagai <strong>LUNAS</strong>?
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="bg-slate-50 dark:bg-slate-900/50 p-4 rounded-xl border border-slate-100 dark:border-slate-800 my-2">
|
||||
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">Total Tagihan</p>
|
||||
<p className="text-2xl font-black text-rose-600 dark:text-rose-400">{formatRupiah(target.amount)}</p>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-slate-200 dark:border-slate-700 flex flex-col gap-3">
|
||||
<div className="my-2 rounded-xl border border-slate-100 bg-slate-50 p-4 dark:border-slate-800 dark:bg-slate-900/50">
|
||||
<p className="mb-1 text-xs font-bold tracking-wider text-slate-500 uppercase">
|
||||
Total Tagihan
|
||||
</p>
|
||||
<p className="text-2xl font-black text-rose-600 dark:text-rose-400">
|
||||
{formatRupiah(target.amount)}
|
||||
</p>
|
||||
|
||||
<div className="mt-4 flex flex-col gap-3 border-t border-slate-200 pt-4 dark:border-slate-700">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<label className="text-xs font-bold text-slate-500">Nominal Dibayar (Cash/Transfer)</label>
|
||||
<label className="text-xs font-bold text-slate-500">
|
||||
Nominal Dibayar (Cash/Transfer)
|
||||
</label>
|
||||
<div className="relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm font-bold text-slate-500">Rp</span>
|
||||
<input
|
||||
type="text"
|
||||
<span className="absolute top-1/2 left-3 -translate-y-1/2 text-sm font-bold text-slate-500">
|
||||
Rp
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
value={cashInput}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value.replace(/\D/g, '')
|
||||
setCashInput(val ? new Intl.NumberFormat('id-ID').format(Number(val)) : '')
|
||||
}}
|
||||
placeholder="0"
|
||||
className="w-full bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg h-10 pl-9 pr-3 text-sm font-bold focus:outline-none focus:ring-2 focus:ring-[#1B2CC1]"
|
||||
className="h-10 w-full rounded-lg border border-slate-200 bg-white pr-3 pl-9 text-sm font-bold focus:ring-2 focus:ring-[#1B2CC1] focus:outline-none dark:border-slate-700 dark:bg-slate-900"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{targetUserBalance > 0 && (
|
||||
<div className="flex justify-between items-center p-2 rounded-lg border border-slate-200 dark:border-slate-700 bg-emerald-50/50 dark:bg-emerald-900/20">
|
||||
<span className="text-sm font-bold text-slate-600 dark:text-slate-300">Dipotong dari Saldo (Otomatis)</span>
|
||||
<span className="text-sm font-black text-emerald-600 dark:text-emerald-400">{formatRupiah(targetUserBalance)}</span>
|
||||
<div className="flex items-center justify-between rounded-lg border border-slate-200 bg-emerald-50/50 p-2 dark:border-slate-700 dark:bg-emerald-900/20">
|
||||
<span className="text-sm font-bold text-slate-600 dark:text-slate-300">
|
||||
Dipotong dari Saldo (Otomatis)
|
||||
</span>
|
||||
<span className="text-sm font-black text-emerald-600 dark:text-emerald-400">
|
||||
{formatRupiah(targetUserBalance)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="bg-white dark:bg-slate-800 p-3 rounded-lg border border-slate-200 dark:border-slate-700 mt-1 flex justify-between items-center shadow-sm">
|
||||
<div className="mt-1 flex items-center justify-between rounded-lg border border-slate-200 bg-white p-3 shadow-sm dark:border-slate-700 dark:bg-slate-800">
|
||||
<div className="flex flex-col">
|
||||
<span className="text-xs font-bold text-slate-500">Total Pembayaran</span>
|
||||
{targetUserBalance > 0 && (
|
||||
<span className="text-[10px] font-medium text-slate-400 leading-none mt-0.5">(Nominal Dibayar + Saldo)</span>
|
||||
<span className="mt-0.5 text-[10px] leading-none font-medium text-slate-400">
|
||||
(Nominal Dibayar + Saldo)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-lg font-black text-[#1B2CC1] dark:text-blue-400">
|
||||
{formatRupiah((Number(cashInput.replace(/\D/g, '')) || 0) + targetUserBalance)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{(() => {
|
||||
const totalBayar = (Number(cashInput.replace(/\D/g, '')) || 0) + targetUserBalance
|
||||
const kurang = target.amount - totalBayar
|
||||
if (kurang > 0) {
|
||||
return (
|
||||
<div className="flex justify-between items-center px-2 py-1">
|
||||
<span className="text-xs font-bold text-rose-500">Masih Kurang (Sisa Hutang)</span>
|
||||
<span className="text-xs font-black text-rose-500">{formatRupiah(kurang)}</span>
|
||||
<div className="flex items-center justify-between px-2 py-1">
|
||||
<span className="text-xs font-bold text-rose-500">
|
||||
Masih Kurang (Sisa Hutang)
|
||||
</span>
|
||||
<span className="text-xs font-black text-rose-500">
|
||||
{formatRupiah(kurang)}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
} else if (target.amount > 0) {
|
||||
return (
|
||||
<div className="flex justify-between items-center px-2 py-1">
|
||||
<div className="flex items-center justify-between px-2 py-1">
|
||||
<span className="text-xs font-bold text-emerald-500">Status</span>
|
||||
<span className="text-xs font-black text-emerald-500">Akan Lunas Sepenuhnya</span>
|
||||
<span className="text-xs font-black text-emerald-500">
|
||||
Akan Lunas Sepenuhnya
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -379,11 +491,20 @@ export default function ReportByPersonGrid({ data, balancesData = [], creatorId,
|
||||
})()}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter className="gap-2 sm:gap-0 mt-2">
|
||||
<Button variant="outline" onClick={() => setModalOpen(false)} disabled={saving} className="rounded-xl font-bold">
|
||||
<DialogFooter className="mt-2 gap-2 sm:gap-0">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setModalOpen(false)}
|
||||
disabled={saving}
|
||||
className="rounded-xl font-bold"
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button onClick={handleConfirm} disabled={saving} className="rounded-xl font-bold bg-[#1B2CC1] hover:bg-[#121E85] text-white shadow-md shadow-[#1B2CC1]/20">
|
||||
<Button
|
||||
onClick={handleConfirm}
|
||||
disabled={saving}
|
||||
className="rounded-xl bg-[#1B2CC1] font-bold text-white shadow-md shadow-[#1B2CC1]/20 hover:bg-[#121E85]"
|
||||
>
|
||||
{saving ? 'Memproses...' : 'Proses Pelunasan'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -1,12 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Cell } from 'recharts'
|
||||
import {
|
||||
BarChart,
|
||||
Bar,
|
||||
XAxis,
|
||||
YAxis,
|
||||
Tooltip,
|
||||
ResponsiveContainer,
|
||||
CartesianGrid,
|
||||
Cell,
|
||||
} from 'recharts'
|
||||
|
||||
export default function ReportCharts({ type, data }: { type: 'SUBMITTOR' | 'CREATOR', data: any[] }) {
|
||||
|
||||
export default function ReportCharts({
|
||||
type,
|
||||
data,
|
||||
}: {
|
||||
type: 'SUBMITTOR' | 'CREATOR'
|
||||
data: any[]
|
||||
}) {
|
||||
const formatRupiah = (value: number) => {
|
||||
return new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(value)
|
||||
return new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value)
|
||||
}
|
||||
|
||||
// Prepare chart data based on type
|
||||
@@ -14,32 +32,36 @@ export default function ReportCharts({ type, data }: { type: 'SUBMITTOR' | 'CREA
|
||||
|
||||
if (type === 'SUBMITTOR') {
|
||||
chartData = [...data]
|
||||
.filter(sub => sub.order.status === 'CLOSE')
|
||||
.filter((sub) => sub.order.status === 'CLOSE')
|
||||
.sort((a, b) => new Date(a.order.date).getTime() - new Date(b.order.date).getTime()) // oldest → newest
|
||||
.map(sub => {
|
||||
.map((sub) => {
|
||||
const bill = sub.bill || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const paid =
|
||||
(sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const unpaid = Math.max(0, bill - paid)
|
||||
return {
|
||||
name: sub.order.title.length > 15 ? sub.order.title.substring(0, 15) + '...' : sub.order.title,
|
||||
name:
|
||||
sub.order.title.length > 15
|
||||
? sub.order.title.substring(0, 15) + '...'
|
||||
: sub.order.title,
|
||||
fullTitle: sub.order.title,
|
||||
Total: bill,
|
||||
Paid: paid,
|
||||
Unpaid: unpaid,
|
||||
status: unpaid > 0 ? (paid > 0 ? 'SEBAGIAN' : 'BELUM BAYAR') : 'LUNAS'
|
||||
status: unpaid > 0 ? (paid > 0 ? 'SEBAGIAN' : 'BELUM BAYAR') : 'LUNAS',
|
||||
}
|
||||
})
|
||||
} else {
|
||||
chartData = [...data]
|
||||
.filter(order => order.status === 'CLOSE')
|
||||
.filter((order) => order.status === 'CLOSE')
|
||||
.sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()) // oldest → newest
|
||||
.map(order => {
|
||||
.map((order) => {
|
||||
const total = order.submissions.reduce((acc: number, sub: any) => acc + (sub.bill || 0), 0)
|
||||
return {
|
||||
name: order.title.length > 15 ? order.title.substring(0, 15) + '...' : order.title,
|
||||
fullTitle: order.title,
|
||||
Total: total,
|
||||
status: order.status
|
||||
status: order.status,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -47,9 +69,11 @@ export default function ReportCharts({ type, data }: { type: 'SUBMITTOR' | 'CREA
|
||||
// If no data to show
|
||||
if (chartData.length === 0) {
|
||||
return (
|
||||
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm">
|
||||
<Card className="rounded-2xl border border-slate-200/90 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<CardContent className="p-12 text-center">
|
||||
<p className="text-slate-500 text-xs font-medium">Belum ada data transaksi yang cukup untuk ditampilkan di grafik.</p>
|
||||
<p className="text-xs font-medium text-slate-500">
|
||||
Belum ada data transaksi yang cukup untuk ditampilkan di grafik.
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
@@ -59,18 +83,24 @@ export default function ReportCharts({ type, data }: { type: 'SUBMITTOR' | 'CREA
|
||||
if (active && payload && payload.length) {
|
||||
const data = payload[0].payload
|
||||
return (
|
||||
<div className="bg-white dark:bg-slate-900 p-3 border border-slate-200 dark:border-slate-800 rounded-xl shadow-lg">
|
||||
<p className="font-bold text-xs text-slate-800 dark:text-slate-200 mb-1">{data.fullTitle}</p>
|
||||
<p className="text-sm font-black text-[#1B2CC1]">
|
||||
Total: {formatRupiah(data.Total)}
|
||||
<div className="rounded-xl border border-slate-200 bg-white p-3 shadow-lg dark:border-slate-800 dark:bg-slate-900">
|
||||
<p className="mb-1 text-xs font-bold text-slate-800 dark:text-slate-200">
|
||||
{data.fullTitle}
|
||||
</p>
|
||||
<p className="text-sm font-black text-[#1B2CC1]">Total: {formatRupiah(data.Total)}</p>
|
||||
{type === 'SUBMITTOR' && (
|
||||
<>
|
||||
<p className="text-[11px] text-emerald-600 font-bold mt-1">Dibayar: {formatRupiah(data.Paid)}</p>
|
||||
{data.Unpaid > 0 && <p className="text-[11px] text-rose-600 font-bold">Kurang: {formatRupiah(data.Unpaid)}</p>}
|
||||
<p className="mt-1 text-[11px] font-bold text-emerald-600">
|
||||
Dibayar: {formatRupiah(data.Paid)}
|
||||
</p>
|
||||
{data.Unpaid > 0 && (
|
||||
<p className="text-[11px] font-bold text-rose-600">
|
||||
Kurang: {formatRupiah(data.Unpaid)}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<p className="text-[10px] text-slate-500 mt-1 uppercase font-bold tracking-wider">
|
||||
<p className="mt-1 text-[10px] font-bold tracking-wider text-slate-500 uppercase">
|
||||
Status: {data.status}
|
||||
</p>
|
||||
</div>
|
||||
@@ -80,24 +110,24 @@ export default function ReportCharts({ type, data }: { type: 'SUBMITTOR' | 'CREA
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
||||
<CardHeader className="bg-slate-50/50 dark:bg-slate-800/40 border-b border-slate-100 dark:border-slate-800 p-4">
|
||||
<Card className="overflow-hidden rounded-2xl border border-slate-200/90 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<CardHeader className="border-b border-slate-100 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
|
||||
<CardTitle className="text-sm font-black text-slate-800 dark:text-slate-200">
|
||||
{type === 'SUBMITTOR' ? 'Grafik Pengeluaran per PO' : 'Grafik Omzet per PO'}
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 sm:p-6 h-[300px] w-full">
|
||||
<CardContent className="h-[300px] w-full p-4 sm:p-6">
|
||||
<ResponsiveContainer width="100%" height="100%">
|
||||
<BarChart data={chartData} margin={{ top: 10, right: 10, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
<XAxis
|
||||
dataKey="name"
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 10, fill: '#64748b' }}
|
||||
dy={10}
|
||||
/>
|
||||
<YAxis
|
||||
<YAxis
|
||||
axisLine={false}
|
||||
tickLine={false}
|
||||
tick={{ fontSize: 10, fill: '#64748b' }}
|
||||
|
||||
+191
-100
@@ -2,34 +2,55 @@
|
||||
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { Card } from '@/components/ui/card'
|
||||
import { ChevronDown, ChevronUp, Package, Users, Receipt, Calendar, Wallet, TrendingUp, InboxIcon, ChevronLeft, ChevronRight, Search } from 'lucide-react'
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Package,
|
||||
Users,
|
||||
Receipt,
|
||||
Calendar,
|
||||
Wallet,
|
||||
TrendingUp,
|
||||
InboxIcon,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Search,
|
||||
} from 'lucide-react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { format } from 'date-fns'
|
||||
|
||||
const ITEMS_PER_PAGE = 5
|
||||
|
||||
function PaginationBar({ currentPage, totalPages, onPage }: { currentPage: number; totalPages: number; onPage: (p: number) => void }) {
|
||||
function PaginationBar({
|
||||
currentPage,
|
||||
totalPages,
|
||||
onPage,
|
||||
}: {
|
||||
currentPage: number
|
||||
totalPages: number
|
||||
onPage: (p: number) => void
|
||||
}) {
|
||||
if (totalPages <= 1) return null
|
||||
return (
|
||||
<div className="flex items-center justify-between px-6 py-4 bg-slate-50/50 dark:bg-slate-900/50 border-t border-slate-100 dark:border-slate-800">
|
||||
<div className="flex items-center justify-between border-t border-slate-100 bg-slate-50/50 px-6 py-4 dark:border-slate-800 dark:bg-slate-900/50">
|
||||
<p className="text-xs font-medium text-slate-500">
|
||||
Halaman <span className="font-bold text-slate-700 dark:text-slate-300">{currentPage}</span> dari{' '}
|
||||
<span className="font-bold text-slate-700 dark:text-slate-300">{totalPages}</span>
|
||||
Halaman <span className="font-bold text-slate-700 dark:text-slate-300">{currentPage}</span>{' '}
|
||||
dari <span className="font-bold text-slate-700 dark:text-slate-300">{totalPages}</span>
|
||||
</p>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => onPage(Math.max(1, currentPage - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="p-1.5 rounded-lg border border-slate-200 dark:border-slate-700 disabled:opacity-40 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
className="rounded-lg border border-slate-200 p-1.5 transition-colors hover:bg-slate-100 disabled:opacity-40 dark:border-slate-700 dark:hover:bg-slate-800"
|
||||
>
|
||||
<ChevronLeft className="w-3.5 h-3.5" />
|
||||
<ChevronLeft className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onPage(Math.min(totalPages, currentPage + 1))}
|
||||
disabled={currentPage === totalPages}
|
||||
className="p-1.5 rounded-lg border border-slate-200 dark:border-slate-700 disabled:opacity-40 hover:bg-slate-100 dark:hover:bg-slate-800 transition-colors"
|
||||
className="rounded-lg border border-slate-200 p-1.5 transition-colors hover:bg-slate-100 disabled:opacity-40 dark:border-slate-700 dark:hover:bg-slate-800"
|
||||
>
|
||||
<ChevronRight className="w-3.5 h-3.5" />
|
||||
<ChevronRight className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -38,17 +59,27 @@ function PaginationBar({ currentPage, totalPages, onPage }: { currentPage: numbe
|
||||
|
||||
function EmptyState({ label }: { label: string }) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center py-16 gap-3 text-slate-400">
|
||||
<InboxIcon className="w-10 h-10 opacity-40" />
|
||||
<div className="flex flex-col items-center justify-center gap-3 py-16 text-slate-400">
|
||||
<InboxIcon className="h-10 w-10 opacity-40" />
|
||||
<p className="text-sm font-semibold">{label}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const formatRupiah = (value: number) =>
|
||||
new Intl.NumberFormat('id-ID', { style: 'currency', currency: 'IDR', maximumFractionDigits: 0 }).format(value)
|
||||
new Intl.NumberFormat('id-ID', {
|
||||
style: 'currency',
|
||||
currency: 'IDR',
|
||||
maximumFractionDigits: 0,
|
||||
}).format(value)
|
||||
|
||||
export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CREATOR'; data: any[] }) {
|
||||
export default function ReportDataGrid({
|
||||
type,
|
||||
data,
|
||||
}: {
|
||||
type: 'SUBMITTOR' | 'CREATOR'
|
||||
data: any[]
|
||||
}) {
|
||||
const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({})
|
||||
const [currentPage, setCurrentPage] = useState(1)
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
@@ -68,7 +99,7 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
const q = searchQuery.toLowerCase().trim()
|
||||
|
||||
const filteredData = q
|
||||
? data.filter(item => {
|
||||
? data.filter((item) => {
|
||||
if (type === 'SUBMITTOR') {
|
||||
return (
|
||||
item.order?.title?.toLowerCase().includes(q) ||
|
||||
@@ -81,28 +112,32 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
: data
|
||||
|
||||
const totalPages = Math.ceil(filteredData.length / ITEMS_PER_PAGE)
|
||||
const paginatedData = filteredData.slice((currentPage - 1) * ITEMS_PER_PAGE, currentPage * ITEMS_PER_PAGE)
|
||||
const paginatedData = filteredData.slice(
|
||||
(currentPage - 1) * ITEMS_PER_PAGE,
|
||||
currentPage * ITEMS_PER_PAGE
|
||||
)
|
||||
|
||||
const toggleRow = (id: string) =>
|
||||
setExpandedRows(prev => ({ ...prev, [id]: !prev[id] }))
|
||||
const toggleRow = (id: string) => setExpandedRows((prev) => ({ ...prev, [id]: !prev[id] }))
|
||||
|
||||
/* ─────────────── SUBMITTOR TABLE ─────────────── */
|
||||
if (type === 'SUBMITTOR') {
|
||||
return (
|
||||
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
||||
<div className="bg-slate-50/50 dark:bg-slate-800/40 px-4 py-3 border-b border-slate-100 dark:border-slate-800 flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<Wallet className="w-4 h-4 text-[#1B2CC1] shrink-0" />
|
||||
<h3 className="text-sm font-black text-slate-800 dark:text-slate-200">Detail Pengeluaran Anda</h3>
|
||||
<Card className="overflow-hidden rounded-2xl border border-slate-200/90 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex flex-col gap-3 border-b border-slate-100 bg-slate-50/50 px-4 py-3 sm:flex-row sm:items-center dark:border-slate-800 dark:bg-slate-800/40">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<Wallet className="h-4 w-4 shrink-0 text-[#1B2CC1]" />
|
||||
<h3 className="text-sm font-black text-slate-800 dark:text-slate-200">
|
||||
Detail Pengeluaran Anda
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-1.5 w-full sm:w-52">
|
||||
<Search className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
||||
<div className="flex w-full items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-1.5 sm:w-52 dark:border-slate-700 dark:bg-slate-900">
|
||||
<Search className="h-3.5 w-3.5 shrink-0 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari judul PO atau kreator..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="text-xs font-medium bg-transparent border-none outline-none w-full text-slate-700 dark:text-slate-200 placeholder:text-slate-400"
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full border-none bg-transparent text-xs font-medium text-slate-700 outline-none placeholder:text-slate-400 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -114,14 +149,14 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-500 uppercase bg-slate-50/30 dark:bg-slate-900/50">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-slate-50/30 text-xs text-slate-500 uppercase dark:bg-slate-900/50">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-bold w-8"></th>
|
||||
<th className="w-8 px-6 py-4 font-bold"></th>
|
||||
<th className="px-6 py-4 font-bold">Judul PO & Kreator</th>
|
||||
<th className="px-6 py-4 font-bold">Tanggal</th>
|
||||
<th className="px-6 py-4 font-bold">Status Bayar</th>
|
||||
<th className="px-6 py-4 font-bold text-right">Total Tagihan</th>
|
||||
<th className="px-6 py-4 text-right font-bold">Total Tagihan</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
@@ -132,44 +167,55 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
<tr
|
||||
onClick={() => toggleRow(sub.id)}
|
||||
className={cn(
|
||||
'hover:bg-slate-50/50 dark:hover:bg-slate-800/40 cursor-pointer transition-colors group',
|
||||
'group cursor-pointer transition-colors hover:bg-slate-50/50 dark:hover:bg-slate-800/40',
|
||||
isExpanded && 'bg-slate-50/30 dark:bg-slate-800/20'
|
||||
)}
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<button className="text-slate-400 group-hover:text-[#1B2CC1] transition-colors p-1 rounded-full hover:bg-blue-50 dark:hover:bg-blue-900/30">
|
||||
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
<button className="rounded-full p-1 text-slate-400 transition-colors group-hover:text-[#1B2CC1] hover:bg-blue-50 dark:hover:bg-blue-900/30">
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="font-bold text-slate-800 dark:text-slate-200">{sub.order.title}</p>
|
||||
<p className="text-[11px] text-slate-500 flex items-center gap-1 mt-0.5">
|
||||
<Users className="w-3 h-3" /> {sub.order.creator.name}
|
||||
<p className="font-bold text-slate-800 dark:text-slate-200">
|
||||
{sub.order.title}
|
||||
</p>
|
||||
<p className="mt-0.5 flex items-center gap-1 text-[11px] text-slate-500">
|
||||
<Users className="h-3 w-3" /> {sub.order.creator.name}
|
||||
</p>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-xs text-slate-600 dark:text-slate-400">
|
||||
{format(new Date(sub.order.date), 'dd MMM yyyy')}
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={cn(
|
||||
'inline-flex px-2 py-0.5 rounded-full text-[10px] font-black tracking-wide',
|
||||
sub.payment_status === 'LUNAS'
|
||||
? 'bg-emerald-50 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-400 border border-emerald-200/80'
|
||||
: 'bg-rose-50 text-rose-700 dark:bg-rose-950/50 dark:text-rose-400 border border-rose-200/80'
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex rounded-full px-2 py-0.5 text-[10px] font-black tracking-wide',
|
||||
sub.payment_status === 'LUNAS'
|
||||
? 'border border-emerald-200/80 bg-emerald-50 text-emerald-700 dark:bg-emerald-950/50 dark:text-emerald-400'
|
||||
: 'border border-rose-200/80 bg-rose-50 text-rose-700 dark:bg-rose-950/50 dark:text-rose-400'
|
||||
)}
|
||||
>
|
||||
{sub.payment_status === 'LUNAS' ? 'LUNAS' : 'BELUM BAYAR'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right font-black text-slate-900 dark:text-white flex flex-col">
|
||||
<td className="flex flex-col px-6 py-4 text-right font-black text-slate-900 dark:text-white">
|
||||
<span>{sub.bill ? formatRupiah(sub.bill) : '-'}</span>
|
||||
{(() => {
|
||||
const bill = Number(sub.bill) || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const paid =
|
||||
(sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) +
|
||||
(sub.saldo_used || 0)
|
||||
const unpaid = Math.max(0, bill - paid)
|
||||
if (unpaid > 0 && paid > 0) {
|
||||
return (
|
||||
<div className="text-[10px] text-slate-500 font-medium mt-1 leading-tight">
|
||||
Dibayar: {formatRupiah(paid)}<br/>
|
||||
<div className="mt-1 text-[10px] leading-tight font-medium text-slate-500">
|
||||
Dibayar: {formatRupiah(paid)}
|
||||
<br />
|
||||
Sisa: {formatRupiah(unpaid)}
|
||||
</div>
|
||||
)
|
||||
@@ -181,16 +227,23 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
|
||||
{isExpanded && (
|
||||
<tr className="bg-slate-50 dark:bg-slate-900/60">
|
||||
<td colSpan={5} className="px-6 py-5 border-l-4 border-l-[#1B2CC1]">
|
||||
<td colSpan={5} className="border-l-4 border-l-[#1B2CC1] px-6 py-5">
|
||||
<div className="pl-8">
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-wider text-slate-500 mb-3 flex items-center gap-1.5">
|
||||
<Package className="w-3.5 h-3.5" /> Item yang Anda Pesan
|
||||
<h4 className="mb-3 flex items-center gap-1.5 text-[11px] font-bold tracking-wider text-slate-500 uppercase">
|
||||
<Package className="h-3.5 w-3.5" /> Item yang Anda Pesan
|
||||
</h4>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-2">
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3">
|
||||
{sub.items.map((item: any) => (
|
||||
<div key={item.id} className="bg-white dark:bg-slate-800 border border-slate-200/60 dark:border-slate-700 px-3 py-2 rounded-xl flex items-center justify-between shadow-sm">
|
||||
<span className="text-xs font-semibold text-slate-700 dark:text-slate-300 pr-2">{item.name}</span>
|
||||
<span className="text-xs font-black text-[#1B2CC1] bg-blue-50 dark:bg-blue-900/30 px-2 py-0.5 rounded-md">{item.qty}x</span>
|
||||
<div
|
||||
key={item.id}
|
||||
className="flex items-center justify-between rounded-xl border border-slate-200/60 bg-white px-3 py-2 shadow-sm dark:border-slate-700 dark:bg-slate-800"
|
||||
>
|
||||
<span className="pr-2 text-xs font-semibold text-slate-700 dark:text-slate-300">
|
||||
{item.name}
|
||||
</span>
|
||||
<span className="rounded-md bg-blue-50 px-2 py-0.5 text-xs font-black text-[#1B2CC1] dark:bg-blue-900/30">
|
||||
{item.qty}x
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -204,7 +257,11 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<PaginationBar currentPage={currentPage} totalPages={totalPages} onPage={setCurrentPage} />
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPage={setCurrentPage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
@@ -213,20 +270,22 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
|
||||
/* ─────────────── CREATOR TABLE ─────────────── */
|
||||
return (
|
||||
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
||||
<div className="bg-slate-50/50 dark:bg-slate-800/40 px-4 py-3 border-b border-slate-100 dark:border-slate-800 flex flex-col sm:flex-row sm:items-center gap-3">
|
||||
<div className="flex items-center gap-2 flex-1">
|
||||
<TrendingUp className="w-4 h-4 text-[#1B2CC1] shrink-0" />
|
||||
<h3 className="text-sm font-black text-slate-800 dark:text-slate-200">Riwayat Omzet per PO</h3>
|
||||
<Card className="overflow-hidden rounded-2xl border border-slate-200/90 bg-white shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
||||
<div className="flex flex-col gap-3 border-b border-slate-100 bg-slate-50/50 px-4 py-3 sm:flex-row sm:items-center dark:border-slate-800 dark:bg-slate-800/40">
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4 shrink-0 text-[#1B2CC1]" />
|
||||
<h3 className="text-sm font-black text-slate-800 dark:text-slate-200">
|
||||
Riwayat Omzet per PO
|
||||
</h3>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-700 rounded-lg px-3 py-1.5 w-full sm:w-52">
|
||||
<Search className="w-3.5 h-3.5 text-slate-400 shrink-0" />
|
||||
<div className="flex w-full items-center gap-2 rounded-lg border border-slate-200 bg-white px-3 py-1.5 sm:w-52 dark:border-slate-700 dark:bg-slate-900">
|
||||
<Search className="h-3.5 w-3.5 shrink-0 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Cari judul PO..."
|
||||
value={searchQuery}
|
||||
onChange={e => setSearchQuery(e.target.value)}
|
||||
className="text-xs font-medium bg-transparent border-none outline-none w-full text-slate-700 dark:text-slate-200 placeholder:text-slate-400"
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full border-none bg-transparent text-xs font-medium text-slate-700 outline-none placeholder:text-slate-400 dark:text-slate-200"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -238,23 +297,28 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
) : (
|
||||
<>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm text-left">
|
||||
<thead className="text-xs text-slate-500 uppercase bg-slate-50/30 dark:bg-slate-900/50">
|
||||
<table className="w-full text-left text-sm">
|
||||
<thead className="bg-slate-50/30 text-xs text-slate-500 uppercase dark:bg-slate-900/50">
|
||||
<tr>
|
||||
<th className="px-6 py-4 font-bold w-8"></th>
|
||||
<th className="w-8 px-6 py-4 font-bold"></th>
|
||||
<th className="px-6 py-4 font-bold">Judul PO</th>
|
||||
<th className="px-6 py-4 font-bold">Status</th>
|
||||
<th className="px-6 py-4 font-bold text-center">Peserta</th>
|
||||
<th className="px-6 py-4 font-bold text-right">Total Omzet PO</th>
|
||||
<th className="px-6 py-4 text-center font-bold">Peserta</th>
|
||||
<th className="px-6 py-4 text-right font-bold">Total Omzet PO</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-800">
|
||||
{paginatedData.map((order: any) => {
|
||||
const isExpanded = !!expandedRows[order.id]
|
||||
const totalOmzet = order.submissions.reduce((acc: number, sub: any) => acc + (Number(sub.bill) || 0), 0)
|
||||
const totalOmzet = order.submissions.reduce(
|
||||
(acc: number, sub: any) => acc + (Number(sub.bill) || 0),
|
||||
0
|
||||
)
|
||||
const totalPiutang = order.submissions.reduce((acc: number, sub: any) => {
|
||||
const bill = Number(sub.bill) || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const paid =
|
||||
(sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) +
|
||||
(sub.saldo_used || 0)
|
||||
return acc + Math.max(0, bill - paid)
|
||||
}, 0)
|
||||
|
||||
@@ -263,26 +327,37 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
<tr
|
||||
onClick={() => toggleRow(order.id)}
|
||||
className={cn(
|
||||
'hover:bg-slate-50/50 dark:hover:bg-slate-800/40 cursor-pointer transition-colors group',
|
||||
'group cursor-pointer transition-colors hover:bg-slate-50/50 dark:hover:bg-slate-800/40',
|
||||
isExpanded && 'bg-slate-50/30 dark:bg-slate-800/20'
|
||||
)}
|
||||
>
|
||||
<td className="px-6 py-4">
|
||||
<button className="text-slate-400 group-hover:text-[#1B2CC1] transition-colors p-1 rounded-full hover:bg-blue-50 dark:hover:bg-blue-900/30">
|
||||
{isExpanded ? <ChevronUp className="w-4 h-4" /> : <ChevronDown className="w-4 h-4" />}
|
||||
<button className="rounded-full p-1 text-slate-400 transition-colors group-hover:text-[#1B2CC1] hover:bg-blue-50 dark:hover:bg-blue-900/30">
|
||||
{isExpanded ? (
|
||||
<ChevronUp className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<p className="font-bold text-slate-800 dark:text-slate-200">{order.title}</p>
|
||||
<p className="text-[11px] text-slate-500 flex items-center gap-1 mt-0.5">
|
||||
<Calendar className="w-3 h-3" /> {format(new Date(order.date), 'dd MMM yyyy')}
|
||||
<p className="font-bold text-slate-800 dark:text-slate-200">
|
||||
{order.title}
|
||||
</p>
|
||||
<p className="mt-0.5 flex items-center gap-1 text-[11px] text-slate-500">
|
||||
<Calendar className="h-3 w-3" />{' '}
|
||||
{format(new Date(order.date), 'dd MMM yyyy')}
|
||||
</p>
|
||||
</td>
|
||||
<td className="px-6 py-4">
|
||||
<span className={cn(
|
||||
'inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-black tracking-wide',
|
||||
order.status === 'CLOSE' ? 'bg-rose-50 text-rose-700 dark:bg-rose-950/50 dark:text-rose-400 border border-rose-200/80' : 'bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300 border border-slate-200/90'
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2 py-0.5 text-[10px] font-black tracking-wide',
|
||||
order.status === 'CLOSE'
|
||||
? 'border border-rose-200/80 bg-rose-50 text-rose-700 dark:bg-rose-950/50 dark:text-rose-400'
|
||||
: 'border border-slate-200/90 bg-slate-100 text-slate-700 dark:bg-slate-800 dark:text-slate-300'
|
||||
)}
|
||||
>
|
||||
● {order.status}
|
||||
</span>
|
||||
</td>
|
||||
@@ -290,26 +365,30 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
{order.submissions.length} Orang
|
||||
</td>
|
||||
<td className="px-6 py-4 text-right">
|
||||
<p className="font-black text-slate-900 dark:text-white">{formatRupiah(totalOmzet)}</p>
|
||||
<p className="font-black text-slate-900 dark:text-white">
|
||||
{formatRupiah(totalOmzet)}
|
||||
</p>
|
||||
{totalPiutang > 0 && (
|
||||
<p className="text-[10px] text-rose-500 font-bold mt-0.5">Piutang: {formatRupiah(totalPiutang)}</p>
|
||||
<p className="mt-0.5 text-[10px] font-bold text-rose-500">
|
||||
Piutang: {formatRupiah(totalPiutang)}
|
||||
</p>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
{isExpanded && (
|
||||
<tr className="bg-slate-50 dark:bg-slate-900/60">
|
||||
<td colSpan={5} className="px-6 py-5 border-l-4 border-l-[#1B2CC1]">
|
||||
<div className="pl-8 space-y-3">
|
||||
<h4 className="text-[11px] font-bold uppercase tracking-wider text-slate-500 flex items-center gap-1.5">
|
||||
<Receipt className="w-3.5 h-3.5" /> Breakdown Pemesan
|
||||
<td colSpan={5} className="border-l-4 border-l-[#1B2CC1] px-6 py-5">
|
||||
<div className="space-y-3 pl-8">
|
||||
<h4 className="flex items-center gap-1.5 text-[11px] font-bold tracking-wider text-slate-500 uppercase">
|
||||
<Receipt className="h-3.5 w-3.5" /> Breakdown Pemesan
|
||||
</h4>
|
||||
{order.submissions.length === 0 ? (
|
||||
<p className="text-xs text-slate-400 italic">Belum ada pemesan.</p>
|
||||
) : (
|
||||
<div className="overflow-hidden border border-slate-200/80 dark:border-slate-700 rounded-xl bg-white dark:bg-slate-800">
|
||||
<div className="overflow-hidden rounded-xl border border-slate-200/80 bg-white dark:border-slate-700 dark:bg-slate-800">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="bg-slate-100/50 dark:bg-slate-700/30 text-slate-500 uppercase font-semibold">
|
||||
<thead className="bg-slate-100/50 font-semibold text-slate-500 uppercase dark:bg-slate-700/30">
|
||||
<tr>
|
||||
<th className="px-4 py-2.5 text-left">Nama</th>
|
||||
<th className="px-4 py-2.5 text-left">Status</th>
|
||||
@@ -319,27 +398,35 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
<tbody className="divide-y divide-slate-100 dark:divide-slate-700/50">
|
||||
{order.submissions.map((sub: any) => (
|
||||
<tr key={sub.id}>
|
||||
<td className="px-4 py-2.5 font-semibold text-slate-800 dark:text-slate-200">{sub.user.name}</td>
|
||||
<td className="px-4 py-2.5 font-semibold text-slate-800 dark:text-slate-200">
|
||||
{sub.user.name}
|
||||
</td>
|
||||
<td className="px-4 py-2.5">
|
||||
<span className={cn(
|
||||
'text-[9px] font-bold px-1.5 py-0.5 rounded uppercase',
|
||||
sub.payment_status === 'LUNAS'
|
||||
? 'text-emerald-700 bg-emerald-100 dark:text-emerald-400 dark:bg-emerald-950/40'
|
||||
: 'text-rose-700 bg-rose-100 dark:text-rose-400 dark:bg-rose-950/40'
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
'rounded px-1.5 py-0.5 text-[9px] font-bold uppercase',
|
||||
sub.payment_status === 'LUNAS'
|
||||
? 'bg-emerald-100 text-emerald-700 dark:bg-emerald-950/40 dark:text-emerald-400'
|
||||
: 'bg-rose-100 text-rose-700 dark:bg-rose-950/40 dark:text-rose-400'
|
||||
)}
|
||||
>
|
||||
{sub.payment_status === 'LUNAS' ? 'Lunas' : 'Belum'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-right font-black text-slate-700 dark:text-slate-300 flex flex-col">
|
||||
<td className="flex flex-col px-4 py-2.5 text-right font-black text-slate-700 dark:text-slate-300">
|
||||
<span>{sub.bill ? formatRupiah(sub.bill) : '-'}</span>
|
||||
{(() => {
|
||||
const bill = Number(sub.bill) || 0
|
||||
const paid = (sub.paid_amount ?? (sub.payment_status === 'LUNAS' ? bill : 0)) + (sub.saldo_used || 0)
|
||||
const paid =
|
||||
(sub.paid_amount ??
|
||||
(sub.payment_status === 'LUNAS' ? bill : 0)) +
|
||||
(sub.saldo_used || 0)
|
||||
const unpaid = Math.max(0, bill - paid)
|
||||
if (unpaid > 0 && paid > 0) {
|
||||
return (
|
||||
<div className="text-[10px] text-slate-500 font-medium mt-0.5 leading-tight">
|
||||
Masuk: {formatRupiah(paid)}<br/>
|
||||
<div className="mt-0.5 text-[10px] leading-tight font-medium text-slate-500">
|
||||
Masuk: {formatRupiah(paid)}
|
||||
<br />
|
||||
Sisa: {formatRupiah(unpaid)}
|
||||
</div>
|
||||
)
|
||||
@@ -363,7 +450,11 @@ export default function ReportDataGrid({ type, data }: { type: 'SUBMITTOR' | 'CR
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<PaginationBar currentPage={currentPage} totalPages={totalPages} onPage={setCurrentPage} />
|
||||
<PaginationBar
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPage={setCurrentPage}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -12,12 +12,19 @@ interface ShareButtonProps {
|
||||
orderId: string
|
||||
orderTitle?: string
|
||||
className?: string
|
||||
variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost"
|
||||
size?: "default" | "sm" | "lg" | "icon"
|
||||
variant?: 'link' | 'default' | 'destructive' | 'outline' | 'secondary' | 'ghost'
|
||||
size?: 'default' | 'sm' | 'lg' | 'icon'
|
||||
showText?: boolean
|
||||
}
|
||||
|
||||
export function ShareButton({ orderId, orderTitle = 'Pesanan', className, variant = "outline", size = "sm", showText = true }: ShareButtonProps) {
|
||||
export function ShareButton({
|
||||
orderId,
|
||||
orderTitle = 'Pesanan',
|
||||
className,
|
||||
variant = 'outline',
|
||||
size = 'sm',
|
||||
showText = true,
|
||||
}: ShareButtonProps) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [broadcasting, setBroadcasting] = useState(false)
|
||||
@@ -31,11 +38,13 @@ export function ShareButton({ orderId, orderTitle = 'Pesanan', className, varian
|
||||
|
||||
const handleShareWA = async () => {
|
||||
const settings = await getSettings()
|
||||
let template = settings.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}'
|
||||
|
||||
let template =
|
||||
settings.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}'
|
||||
|
||||
const url = `${window.location.origin}/order/${orderId}`
|
||||
const text = template.replace('{title}', orderTitle).replace('{url}', url)
|
||||
|
||||
|
||||
window.open(`https://api.whatsapp.com/send?text=${encodeURIComponent(text)}`, '_blank')
|
||||
}
|
||||
|
||||
@@ -46,13 +55,13 @@ export function ShareButton({ orderId, orderTitle = 'Pesanan', className, varian
|
||||
if (res.success) {
|
||||
toast.success('Broadcast Terkirim!', {
|
||||
description: 'Berhasil mengirim pesan ke Mattermost.',
|
||||
duration: 3000
|
||||
duration: 3000,
|
||||
})
|
||||
setOpen(false)
|
||||
} else {
|
||||
toast.error('Gagal Broadcast', {
|
||||
description: res.error || 'Terjadi kesalahan saat mengirim.',
|
||||
duration: 3000
|
||||
duration: 3000,
|
||||
})
|
||||
}
|
||||
setBroadcasting(false)
|
||||
@@ -60,48 +69,58 @@ export function ShareButton({ orderId, orderTitle = 'Pesanan', className, varian
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
<Button
|
||||
variant={variant}
|
||||
size={size}
|
||||
className={cn("transition-all", className)}
|
||||
className={cn('transition-all', className)}
|
||||
title="Bagikan PO"
|
||||
onClick={() => setOpen(true)}
|
||||
>
|
||||
<Share2 className="w-3.5 h-3.5 shrink-0" />
|
||||
{showText && <span className="truncate hidden sm:inline">Bagikan</span>}
|
||||
<Share2 className="h-3.5 w-3.5 shrink-0" />
|
||||
{showText && <span className="hidden truncate sm:inline">Bagikan</span>}
|
||||
</Button>
|
||||
|
||||
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent className="sm:max-w-xs p-6 rounded-3xl border-slate-200/90 dark:border-slate-800">
|
||||
<DialogTitle className="text-xl font-black text-center text-slate-900 dark:text-white mb-2">Bagikan Pesanan</DialogTitle>
|
||||
<p className="text-sm text-slate-500 text-center mb-4">
|
||||
<DialogContent className="rounded-3xl border-slate-200/90 p-6 sm:max-w-xs dark:border-slate-800">
|
||||
<DialogTitle className="mb-2 text-center text-xl font-black text-slate-900 dark:text-white">
|
||||
Bagikan Pesanan
|
||||
</DialogTitle>
|
||||
<p className="mb-4 text-center text-sm text-slate-500">
|
||||
Pilih metode untuk membagikan PO ini ke teman atau tim Anda.
|
||||
</p>
|
||||
|
||||
<div className="space-y-3">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopyLink}
|
||||
className="w-full h-11 rounded-xl justify-start font-bold gap-3 text-slate-700 dark:text-slate-300"
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopyLink}
|
||||
className="h-11 w-full justify-start gap-3 rounded-xl font-bold text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
{copied ? <Check className="w-4 h-4 text-emerald-500" /> : <Copy className="w-4 h-4" />}
|
||||
{copied ? (
|
||||
<Check className="h-4 w-4 text-emerald-500" />
|
||||
) : (
|
||||
<Copy className="h-4 w-4" />
|
||||
)}
|
||||
{copied ? 'Tautan Disalin!' : 'Salin Tautan'}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleShareWA}
|
||||
className="w-full h-11 rounded-xl justify-start font-bold gap-3 bg-[#25D366] hover:bg-[#20bd5a] text-white"
|
||||
<Button
|
||||
onClick={handleShareWA}
|
||||
className="h-11 w-full justify-start gap-3 rounded-xl bg-[#25D366] font-bold text-white hover:bg-[#20bd5a]"
|
||||
>
|
||||
<MessageCircle className="w-4 h-4" />
|
||||
<MessageCircle className="h-4 w-4" />
|
||||
Kirim ke WhatsApp
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
onClick={handleBroadcastMattermost}
|
||||
<Button
|
||||
onClick={handleBroadcastMattermost}
|
||||
disabled={broadcasting}
|
||||
className="w-full h-11 rounded-xl justify-start font-bold gap-3 bg-[#0668E1] hover:bg-[#0557bc] text-white"
|
||||
className="h-11 w-full justify-start gap-3 rounded-xl bg-[#0668E1] font-bold text-white hover:bg-[#0557bc]"
|
||||
>
|
||||
{broadcasting ? <Loader2 className="w-4 h-4 animate-spin" /> : <MessageSquare className="w-4 h-4" />}
|
||||
{broadcasting ? (
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<MessageSquare className="h-4 w-4" />
|
||||
)}
|
||||
{broadcasting ? 'Mengirim...' : 'Broadcast ke Mattermost'}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
+107
-71
@@ -4,29 +4,30 @@ import { useEffect, useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import { cn } from '@/lib/utils'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Dialog, DialogContent, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import {
|
||||
Store,
|
||||
ClipboardList,
|
||||
ShoppingBag,
|
||||
UserCircle,
|
||||
Sparkles,
|
||||
import {
|
||||
Store,
|
||||
ClipboardList,
|
||||
ShoppingBag,
|
||||
UserCircle,
|
||||
Sparkles,
|
||||
X,
|
||||
ArrowUpRight,
|
||||
Info,
|
||||
BarChart2,
|
||||
Wallet,
|
||||
Settings
|
||||
Settings,
|
||||
} from 'lucide-react'
|
||||
|
||||
const menuItems = [
|
||||
{ name: 'Open Order', href: '/', icon: Store, desc: 'Daftar PO live hari ini' },
|
||||
{ name: 'Jasa Order Saya', href: '/my-orders', icon: ClipboardList, desc: 'Kelola PO buatan Anda' },
|
||||
{
|
||||
name: 'Jasa Order Saya',
|
||||
href: '/my-orders',
|
||||
icon: ClipboardList,
|
||||
desc: 'Kelola PO buatan Anda',
|
||||
},
|
||||
{ name: 'Pesanan Saya', href: '/my-purchases', icon: ShoppingBag, desc: 'Riwayat titipan Anda' },
|
||||
{ name: 'Buku Saldo', href: '/balances', icon: Wallet, desc: 'Pantau riwayat saldo' },
|
||||
{ name: 'Laporan', href: '/reports', icon: BarChart2, desc: 'Ringkasan transaksi' },
|
||||
@@ -64,79 +65,95 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
|
||||
// Dynamic Menu Items based on role
|
||||
const finalMenuItems = [...menuItems]
|
||||
if (userRole === 'superadmin') {
|
||||
finalMenuItems.push({ name: 'Pengaturan', href: '/settings/users', icon: Settings, desc: 'Konfigurasi aplikasi' })
|
||||
finalMenuItems.push({
|
||||
name: 'Pengaturan',
|
||||
href: '/settings/users',
|
||||
icon: Settings,
|
||||
desc: 'Konfigurasi aplikasi',
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Mobile Backdrop */}
|
||||
{isOpen && (
|
||||
<div
|
||||
<div
|
||||
onClick={onClose}
|
||||
className="fixed inset-0 bg-slate-900/40 backdrop-blur-sm z-40 lg:hidden transition-opacity"
|
||||
className="fixed inset-0 z-40 bg-slate-900/40 backdrop-blur-sm transition-opacity lg:hidden"
|
||||
/>
|
||||
)}
|
||||
|
||||
<aside className={cn(
|
||||
"fixed lg:sticky top-0 left-0 z-50 h-screen w-72 bg-white dark:bg-slate-900 border-r border-slate-200/80 dark:border-slate-800 flex flex-col justify-between transition-transform duration-300 ease-in-out p-5",
|
||||
isOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"
|
||||
)}>
|
||||
<aside
|
||||
className={cn(
|
||||
'fixed top-0 left-0 z-50 flex h-screen w-72 flex-col justify-between border-r border-slate-200/80 bg-white p-5 transition-transform duration-300 ease-in-out lg:sticky dark:border-slate-800 dark:bg-slate-900',
|
||||
isOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'
|
||||
)}
|
||||
>
|
||||
<div className="space-y-6">
|
||||
{/* Brand Header */}
|
||||
<div className="flex items-center justify-between px-1">
|
||||
<Link href="/" className="flex items-center gap-3 group">
|
||||
<div className="w-11 h-11 rounded-2xl bg-gradient-to-br from-[#1B2CC1] to-[#121E85] flex items-center justify-center text-white shadow-lg shadow-[#1B2CC1]/25 transition-all duration-300 group-hover:scale-105 group-hover:shadow-[#1B2CC1]/40">
|
||||
<Sparkles className="w-5 h-5" />
|
||||
<Link href="/" className="group flex items-center gap-3">
|
||||
<div className="flex h-11 w-11 items-center justify-center rounded-2xl bg-gradient-to-br from-[#1B2CC1] to-[#121E85] text-white shadow-lg shadow-[#1B2CC1]/25 transition-all duration-300 group-hover:scale-105 group-hover:shadow-[#1B2CC1]/40">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-black text-xl tracking-tight text-slate-900 dark:text-white leading-none">
|
||||
<span className="text-xl leading-none font-black tracking-tight text-slate-900 dark:text-white">
|
||||
TitipIn
|
||||
</span>
|
||||
<span className="text-[9px] font-extrabold uppercase px-1.5 py-0.5 rounded-md bg-[#1B2CC1]/10 text-[#1B2CC1] dark:bg-blue-900/40 dark:text-blue-300">
|
||||
<span className="rounded-md bg-[#1B2CC1]/10 px-1.5 py-0.5 text-[9px] font-extrabold text-[#1B2CC1] uppercase dark:bg-blue-900/40 dark:text-blue-300">
|
||||
Pro
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-[11px] font-medium text-slate-400 mt-1">Sistem Titip Pesanan</span>
|
||||
<span className="mt-1 text-[11px] font-medium text-slate-400">
|
||||
Sistem Titip Pesanan
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
{onClose && (
|
||||
<button onClick={onClose} className="lg:hidden p-1.5 rounded-lg text-slate-400 hover:text-slate-600 hover:bg-slate-100">
|
||||
<X className="w-5 h-5" />
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-lg p-1.5 text-slate-400 hover:bg-slate-100 hover:text-slate-600 lg:hidden"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* User Profile Card */}
|
||||
<Link
|
||||
<Link
|
||||
href="/profile"
|
||||
onClick={onClose}
|
||||
className="flex items-center gap-3 p-3 rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-slate-50/70 dark:bg-slate-800/40 hover:bg-slate-100/80 dark:hover:bg-slate-800 transition-all duration-200 group shadow-xs"
|
||||
className="group flex items-center gap-3 rounded-2xl border border-slate-200/90 bg-slate-50/70 p-3 shadow-xs transition-all duration-200 hover:bg-slate-100/80 dark:border-slate-800 dark:bg-slate-800/40 dark:hover:bg-slate-800"
|
||||
>
|
||||
<div className="relative">
|
||||
{userPhoto ? (
|
||||
<img src={userPhoto} alt={userName} className="w-10 h-10 rounded-xl object-cover ring-2 ring-white dark:ring-slate-700 shadow-sm" />
|
||||
<img
|
||||
src={userPhoto}
|
||||
alt={userName}
|
||||
className="h-10 w-10 rounded-xl object-cover shadow-sm ring-2 ring-white dark:ring-slate-700"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-10 h-10 rounded-xl bg-[#1B2CC1]/10 text-[#1B2CC1] flex items-center justify-center font-black text-sm ring-2 ring-white dark:ring-slate-700 shadow-xs">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-xl bg-[#1B2CC1]/10 text-sm font-black text-[#1B2CC1] shadow-xs ring-2 ring-white dark:ring-slate-700">
|
||||
{userName.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
<span className="absolute -bottom-0.5 -right-0.5 w-3 h-3 bg-emerald-500 rounded-full ring-2 ring-white dark:ring-slate-900 shadow-xs" />
|
||||
<span className="absolute -right-0.5 -bottom-0.5 h-3 w-3 rounded-full bg-emerald-500 shadow-xs ring-2 ring-white dark:ring-slate-900" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0 flex-1">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-xs font-bold text-slate-800 dark:text-slate-200 truncate group-hover:text-[#1B2CC1] transition-colors">
|
||||
<span className="truncate text-xs font-bold text-slate-800 transition-colors group-hover:text-[#1B2CC1] dark:text-slate-200">
|
||||
{userName}
|
||||
</span>
|
||||
<ArrowUpRight className="w-3.5 h-3.5 text-slate-400 opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<ArrowUpRight className="h-3.5 w-3.5 text-slate-400 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<div className="mt-0.5 flex items-center gap-1.5">
|
||||
{userRole === 'superadmin' && (
|
||||
<span className="px-1.5 py-[2px] rounded-md bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 text-[8px] font-black uppercase tracking-wider">
|
||||
<span className="rounded-md bg-purple-100 px-1.5 py-[2px] text-[8px] font-black tracking-wider text-purple-700 uppercase dark:bg-purple-900/30 dark:text-purple-400">
|
||||
Admin
|
||||
</span>
|
||||
)}
|
||||
<span className="text-[10px] font-medium text-slate-500 dark:text-slate-400 truncate">
|
||||
<span className="truncate text-[10px] font-medium text-slate-500 dark:text-slate-400">
|
||||
{userNameAccount ? `@${userNameAccount}` : 'Aktif'}
|
||||
</span>
|
||||
</div>
|
||||
@@ -145,7 +162,7 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
|
||||
|
||||
{/* Main Navigation */}
|
||||
<div className="space-y-1.5">
|
||||
<div className="px-3 pb-1.5 text-[10px] font-black uppercase tracking-widest text-slate-400 dark:text-slate-500">
|
||||
<div className="px-3 pb-1.5 text-[10px] font-black tracking-widest text-slate-400 uppercase dark:text-slate-500">
|
||||
Navigasi Utama
|
||||
</div>
|
||||
<nav className="space-y-1">
|
||||
@@ -158,26 +175,30 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
|
||||
href={item.href}
|
||||
onClick={onClose}
|
||||
className={cn(
|
||||
"flex items-center gap-3 px-3.5 py-3 rounded-2xl text-sm font-semibold transition-all duration-200 group relative",
|
||||
isActive
|
||||
? "bg-[#1B2CC1] text-white shadow-md shadow-[#1B2CC1]/25 font-bold"
|
||||
: "text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-white hover:bg-slate-100/80 dark:hover:bg-slate-800/60"
|
||||
'group relative flex items-center gap-3 rounded-2xl px-3.5 py-3 text-sm font-semibold transition-all duration-200',
|
||||
isActive
|
||||
? 'bg-[#1B2CC1] font-bold text-white shadow-md shadow-[#1B2CC1]/25'
|
||||
: 'text-slate-600 hover:bg-slate-100/80 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800/60 dark:hover:text-white'
|
||||
)}
|
||||
>
|
||||
<div className={cn(
|
||||
"w-8 h-8 rounded-xl flex items-center justify-center transition-colors",
|
||||
isActive
|
||||
? "bg-white/15 text-white"
|
||||
: "bg-slate-100 dark:bg-slate-800 text-slate-500 group-hover:text-[#1B2CC1] group-hover:bg-[#1B2CC1]/10"
|
||||
)}>
|
||||
<Icon className="w-4 h-4" />
|
||||
<div
|
||||
className={cn(
|
||||
'flex h-8 w-8 items-center justify-center rounded-xl transition-colors',
|
||||
isActive
|
||||
? 'bg-white/15 text-white'
|
||||
: 'bg-slate-100 text-slate-500 group-hover:bg-[#1B2CC1]/10 group-hover:text-[#1B2CC1] dark:bg-slate-800'
|
||||
)}
|
||||
>
|
||||
<Icon className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="leading-tight">{item.name}</span>
|
||||
<span className={cn(
|
||||
"text-[10px] font-normal truncate mt-0.5",
|
||||
isActive ? "text-blue-100" : "text-slate-400"
|
||||
)}>
|
||||
<span
|
||||
className={cn(
|
||||
'mt-0.5 truncate text-[10px] font-normal',
|
||||
isActive ? 'text-blue-100' : 'text-slate-400'
|
||||
)}
|
||||
>
|
||||
{item.desc}
|
||||
</span>
|
||||
</div>
|
||||
@@ -190,31 +211,46 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
|
||||
<>
|
||||
<button
|
||||
onClick={() => setIsLogoutOpen(true)}
|
||||
className="w-full flex items-center gap-3 px-3.5 py-3 rounded-2xl text-sm font-semibold transition-all duration-200 group relative text-rose-600 dark:text-rose-400 hover:bg-rose-50 dark:hover:bg-rose-950/30 cursor-pointer"
|
||||
className="group relative flex w-full cursor-pointer items-center gap-3 rounded-2xl px-3.5 py-3 text-sm font-semibold text-rose-600 transition-all duration-200 hover:bg-rose-50 dark:text-rose-400 dark:hover:bg-rose-950/30"
|
||||
>
|
||||
<div className="w-8 h-8 rounded-xl flex items-center justify-center transition-colors bg-rose-100 dark:bg-rose-900/40 text-rose-500 group-hover:bg-rose-200 dark:group-hover:bg-rose-800/60">
|
||||
<X className="w-4 h-4" />
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-xl bg-rose-100 text-rose-500 transition-colors group-hover:bg-rose-200 dark:bg-rose-900/40 dark:group-hover:bg-rose-800/60">
|
||||
<X className="h-4 w-4" />
|
||||
</div>
|
||||
<div className="flex flex-col min-w-0 text-left">
|
||||
<div className="flex min-w-0 flex-col text-left">
|
||||
<span className="leading-tight">Logout</span>
|
||||
<span className="text-[10px] font-normal truncate mt-0.5 text-rose-400/80">Keluar dari akun</span>
|
||||
<span className="mt-0.5 truncate text-[10px] font-normal text-rose-400/80">
|
||||
Keluar dari akun
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<Dialog open={isLogoutOpen} onOpenChange={setIsLogoutOpen}>
|
||||
<DialogContent className="sm:max-w-xs rounded-3xl p-0 overflow-hidden border-0">
|
||||
<DialogContent className="overflow-hidden rounded-3xl border-0 p-0 sm:max-w-xs">
|
||||
<div className="px-6 pt-6 pb-2 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-rose-100 text-rose-600 flex items-center justify-center mx-auto mb-3">
|
||||
<X className="w-6 h-6" />
|
||||
<div className="mx-auto mb-3 flex h-12 w-12 items-center justify-center rounded-full bg-rose-100 text-rose-600">
|
||||
<X className="h-6 w-6" />
|
||||
</div>
|
||||
<DialogTitle className="text-xl font-black text-slate-800">Konfirmasi Logout</DialogTitle>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
<DialogTitle className="text-xl font-black text-slate-800">
|
||||
Konfirmasi Logout
|
||||
</DialogTitle>
|
||||
<p className="mt-2 text-xs text-slate-500">
|
||||
Apakah Anda yakin ingin keluar dari akun ini?
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-4 flex gap-3 bg-slate-50">
|
||||
<Button type="button" variant="outline" onClick={() => setIsLogoutOpen(false)} className="flex-1 h-11 rounded-xl">Batal</Button>
|
||||
<Button type="button" onClick={handleLogout} className="flex-1 h-11 rounded-xl bg-rose-600 hover:bg-rose-700 text-white font-bold cursor-pointer">
|
||||
<div className="flex gap-3 bg-slate-50 p-4">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => setIsLogoutOpen(false)}
|
||||
className="h-11 flex-1 rounded-xl"
|
||||
>
|
||||
Batal
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
className="h-11 flex-1 cursor-pointer rounded-xl bg-rose-600 font-bold text-white hover:bg-rose-700"
|
||||
>
|
||||
Ya, Logout
|
||||
</Button>
|
||||
</div>
|
||||
@@ -239,8 +275,8 @@ export function Sidebar({ isOpen, onClose }: { isOpen?: boolean; onClose?: () =>
|
||||
</p>
|
||||
</div> */}
|
||||
{/* Copyright */}
|
||||
<div className="mt-4 pt-4 border-t border-slate-100 dark:border-slate-800 text-center">
|
||||
<p className="text-[10px] text-slate-400 font-medium">
|
||||
<div className="mt-4 border-t border-slate-100 pt-4 text-center dark:border-slate-800">
|
||||
<p className="text-[10px] font-medium text-slate-400">
|
||||
© {new Date().getFullYear()} TitipIn - Sistem Titip Pesanan
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -14,9 +14,11 @@ export function SummaryGenerator({ order }: { order: any }) {
|
||||
const itemCounts: Record<string, number> = {}
|
||||
|
||||
order.submissions?.forEach((sub: any) => {
|
||||
const itemStrings = sub.items.map((i: any) => `${i.name} ${i.qty}x${i.note ? ` (${i.note})` : ''}`)
|
||||
const itemStrings = sub.items.map(
|
||||
(i: any) => `${i.name} ${i.qty}x${i.note ? ` (${i.note})` : ''}`
|
||||
)
|
||||
summaryByPerson += `- ${sub.user?.name || 'Unknown'} : ${itemStrings.join(', ')}\n`
|
||||
|
||||
|
||||
sub.items.forEach((i: any) => {
|
||||
const key = i.note ? `${i.name} (${i.note})` : i.name
|
||||
itemCounts[key] = (itemCounts[key] || 0) + i.qty
|
||||
@@ -43,54 +45,64 @@ export function SummaryGenerator({ order }: { order: any }) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="px-1">
|
||||
<h3 className="text-lg font-black text-slate-900 dark:text-white flex items-center gap-2">
|
||||
<Receipt className="w-5 h-5 text-[#1B2CC1]" />
|
||||
<h3 className="flex items-center gap-2 text-lg font-black text-slate-900 dark:text-white">
|
||||
<Receipt className="h-5 w-5 text-[#1B2CC1]" />
|
||||
<span>Generator Rekap</span>
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500 mt-0.5">Salin format teks siap kirim ke WhatsApp / grup.</p>
|
||||
<p className="mt-0.5 text-xs text-slate-500">
|
||||
Salin format teks siap kirim ke WhatsApp / grup.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Rekap Per Orang */}
|
||||
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
||||
<div className="p-4 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50 dark:bg-slate-800/40">
|
||||
<Card className="overflow-hidden rounded-2xl 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 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
|
||||
<span className="text-xs font-bold text-slate-800 dark:text-slate-200">
|
||||
Rekap per Orang
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyPerson}
|
||||
className="h-8 text-xs font-bold rounded-lg border-slate-200 gap-1.5 hover:bg-[#1B2CC1] hover:text-white transition-all"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyPerson}
|
||||
className="h-8 gap-1.5 rounded-lg border-slate-200 text-xs font-bold transition-all hover:bg-[#1B2CC1] hover:text-white"
|
||||
>
|
||||
{copiedPerson ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
{copiedPerson ? (
|
||||
<Check className="h-3.5 w-3.5 text-emerald-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span>{copiedPerson ? 'Tersalin!' : 'Copy Text'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<CardContent className="p-4">
|
||||
<pre className="text-xs font-mono bg-slate-50 dark:bg-slate-800/80 p-3.5 rounded-xl overflow-x-auto whitespace-pre-wrap border border-slate-200/80 dark:border-slate-700 text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
<pre className="overflow-x-auto rounded-xl border border-slate-200/80 bg-slate-50 p-3.5 font-mono text-xs leading-relaxed whitespace-pre-wrap text-slate-700 dark:border-slate-700 dark:bg-slate-800/80 dark:text-slate-300">
|
||||
{summaryByPerson}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
{/* Rekap Per Item */}
|
||||
<Card className="rounded-2xl border border-slate-200/90 dark:border-slate-800 bg-white dark:bg-slate-900 shadow-sm overflow-hidden">
|
||||
<div className="p-4 border-b border-slate-100 dark:border-slate-800 flex justify-between items-center bg-slate-50/50 dark:bg-slate-800/40">
|
||||
<Card className="overflow-hidden rounded-2xl 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 bg-slate-50/50 p-4 dark:border-slate-800 dark:bg-slate-800/40">
|
||||
<span className="text-xs font-bold text-slate-800 dark:text-slate-200">
|
||||
Rekap per Item (Akumulasi)
|
||||
</span>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyItem}
|
||||
className="h-8 text-xs font-bold rounded-lg border-slate-200 gap-1.5 hover:bg-[#1B2CC1] hover:text-white transition-all"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={handleCopyItem}
|
||||
className="h-8 gap-1.5 rounded-lg border-slate-200 text-xs font-bold transition-all hover:bg-[#1B2CC1] hover:text-white"
|
||||
>
|
||||
{copiedItem ? <Check className="w-3.5 h-3.5 text-emerald-500" /> : <Copy className="w-3.5 h-3.5" />}
|
||||
{copiedItem ? (
|
||||
<Check className="h-3.5 w-3.5 text-emerald-500" />
|
||||
) : (
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
)}
|
||||
<span>{copiedItem ? 'Tersalin!' : 'Copy Text'}</span>
|
||||
</Button>
|
||||
</div>
|
||||
<CardContent className="p-4">
|
||||
<pre className="text-xs font-mono bg-slate-50 dark:bg-slate-800/80 p-3.5 rounded-xl overflow-x-auto whitespace-pre-wrap border border-slate-200/80 dark:border-slate-700 text-slate-700 dark:text-slate-300 leading-relaxed">
|
||||
<pre className="overflow-x-auto rounded-xl border border-slate-200/80 bg-slate-50 p-3.5 font-mono text-xs leading-relaxed whitespace-pre-wrap text-slate-700 dark:border-slate-700 dark:bg-slate-800/80 dark:text-slate-300">
|
||||
{summaryByItem}
|
||||
</pre>
|
||||
</CardContent>
|
||||
|
||||
+17
-20
@@ -1,41 +1,38 @@
|
||||
import { mergeProps } from "@base-ui/react/merge-props"
|
||||
import { useRender } from "@base-ui/react/use-render"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { mergeProps } from '@base-ui/react/merge-props'
|
||||
import { useRender } from '@base-ui/react/use-render'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const badgeVariants = cva(
|
||||
"group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!",
|
||||
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-4xl border border-transparent px-2 py-0.5 text-xs font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-3!',
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80",
|
||||
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
||||
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20",
|
||||
outline:
|
||||
"border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground",
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
|
||||
outline: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
||||
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
variant: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Badge({
|
||||
className,
|
||||
variant = "default",
|
||||
variant = 'default',
|
||||
render,
|
||||
...props
|
||||
}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) {
|
||||
}: useRender.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) {
|
||||
return useRender({
|
||||
defaultTagName: "span",
|
||||
props: mergeProps<"span">(
|
||||
defaultTagName: 'span',
|
||||
props: mergeProps<'span'>(
|
||||
{
|
||||
className: cn(badgeVariants({ variant }), className),
|
||||
},
|
||||
@@ -43,7 +40,7 @@ function Badge({
|
||||
),
|
||||
render,
|
||||
state: {
|
||||
slot: "badge",
|
||||
slot: 'badge',
|
||||
variant,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -1,49 +1,49 @@
|
||||
import { Button as ButtonPrimitive } from "@base-ui/react/button"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
import { Button as ButtonPrimitive } from '@base-ui/react/button'
|
||||
import { cva, type VariantProps } from 'class-variance-authority'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
const buttonVariants = cva(
|
||||
"cursor-pointer group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/80",
|
||||
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
|
||||
outline:
|
||||
"border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
|
||||
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
|
||||
'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
||||
ghost:
|
||||
"hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
|
||||
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
||||
destructive:
|
||||
"bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
||||
link: 'text-primary underline-offset-4 hover:underline',
|
||||
},
|
||||
size: {
|
||||
default:
|
||||
"h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
||||
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
||||
lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
|
||||
icon: "size-8",
|
||||
"icon-xs":
|
||||
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
||||
icon: 'size-8',
|
||||
'icon-xs':
|
||||
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
||||
"icon-sm":
|
||||
"size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
|
||||
"icon-lg": "size-9",
|
||||
'icon-sm':
|
||||
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
||||
'icon-lg': 'size-9',
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
variant: 'default',
|
||||
size: 'default',
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Button({
|
||||
className,
|
||||
variant = "default",
|
||||
size = "default",
|
||||
variant = 'default',
|
||||
size = 'default',
|
||||
...props
|
||||
}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) {
|
||||
return (
|
||||
|
||||
@@ -1,29 +1,24 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
DayPicker,
|
||||
getDefaultClassNames,
|
||||
type DayButton,
|
||||
type Locale,
|
||||
} from "react-day-picker"
|
||||
import * as React from 'react'
|
||||
import { DayPicker, getDefaultClassNames, type DayButton, type Locale } from 'react-day-picker'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button, buttonVariants } from "@/components/ui/button"
|
||||
import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from "lucide-react"
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button, buttonVariants } from '@/components/ui/button'
|
||||
import { ChevronLeftIcon, ChevronRightIcon, ChevronDownIcon } from 'lucide-react'
|
||||
|
||||
function Calendar({
|
||||
className,
|
||||
classNames,
|
||||
showOutsideDays = true,
|
||||
captionLayout = "label",
|
||||
buttonVariant = "ghost",
|
||||
captionLayout = 'label',
|
||||
buttonVariant = 'ghost',
|
||||
locale,
|
||||
formatters,
|
||||
components,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DayPicker> & {
|
||||
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
|
||||
buttonVariant?: React.ComponentProps<typeof Button>['variant']
|
||||
}) {
|
||||
const defaultClassNames = getDefaultClassNames()
|
||||
|
||||
@@ -31,7 +26,7 @@ function Calendar({
|
||||
<DayPicker
|
||||
showOutsideDays={showOutsideDays}
|
||||
className={cn(
|
||||
"group/calendar bg-background p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent",
|
||||
'group/calendar bg-background p-2 [--cell-radius:var(--radius-md)] [--cell-size:--spacing(7)] in-data-[slot=card-content]:bg-transparent in-data-[slot=popover-content]:bg-transparent',
|
||||
String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
|
||||
String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
|
||||
className
|
||||
@@ -39,131 +34,100 @@ function Calendar({
|
||||
captionLayout={captionLayout}
|
||||
locale={locale}
|
||||
formatters={{
|
||||
formatMonthDropdown: (date) =>
|
||||
date.toLocaleString(locale?.code, { month: "short" }),
|
||||
formatMonthDropdown: (date) => date.toLocaleString(locale?.code, { month: 'short' }),
|
||||
...formatters,
|
||||
}}
|
||||
classNames={{
|
||||
root: cn("w-fit", defaultClassNames.root),
|
||||
months: cn(
|
||||
"relative flex flex-col gap-4 md:flex-row",
|
||||
defaultClassNames.months
|
||||
),
|
||||
month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
|
||||
root: cn('w-fit', defaultClassNames.root),
|
||||
months: cn('relative flex flex-col gap-4 md:flex-row', defaultClassNames.months),
|
||||
month: cn('flex w-full flex-col gap-4', defaultClassNames.month),
|
||||
nav: cn(
|
||||
"absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
|
||||
'absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1',
|
||||
defaultClassNames.nav
|
||||
),
|
||||
button_previous: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
||||
'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
|
||||
defaultClassNames.button_previous
|
||||
),
|
||||
button_next: cn(
|
||||
buttonVariants({ variant: buttonVariant }),
|
||||
"size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
|
||||
'size-(--cell-size) p-0 select-none aria-disabled:opacity-50',
|
||||
defaultClassNames.button_next
|
||||
),
|
||||
month_caption: cn(
|
||||
"flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
|
||||
'flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)',
|
||||
defaultClassNames.month_caption
|
||||
),
|
||||
dropdowns: cn(
|
||||
"flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
|
||||
'flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium',
|
||||
defaultClassNames.dropdowns
|
||||
),
|
||||
dropdown_root: cn(
|
||||
"relative rounded-(--cell-radius)",
|
||||
defaultClassNames.dropdown_root
|
||||
),
|
||||
dropdown: cn(
|
||||
"absolute inset-0 bg-popover opacity-0",
|
||||
defaultClassNames.dropdown
|
||||
),
|
||||
dropdown_root: cn('relative rounded-(--cell-radius)', defaultClassNames.dropdown_root),
|
||||
dropdown: cn('absolute inset-0 bg-popover opacity-0', defaultClassNames.dropdown),
|
||||
caption_label: cn(
|
||||
"font-medium select-none",
|
||||
captionLayout === "label"
|
||||
? "text-sm"
|
||||
: "flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
|
||||
'font-medium select-none',
|
||||
captionLayout === 'label'
|
||||
? 'text-sm'
|
||||
: 'flex items-center gap-1 rounded-(--cell-radius) text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground',
|
||||
defaultClassNames.caption_label
|
||||
),
|
||||
month_grid: cn("w-full border-collapse", defaultClassNames.month_grid),
|
||||
weekdays: cn("flex", defaultClassNames.weekdays),
|
||||
month_grid: cn('w-full border-collapse', defaultClassNames.month_grid),
|
||||
weekdays: cn('flex', defaultClassNames.weekdays),
|
||||
weekday: cn(
|
||||
"flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none",
|
||||
'flex-1 rounded-(--cell-radius) text-[0.8rem] font-normal text-muted-foreground select-none',
|
||||
defaultClassNames.weekday
|
||||
),
|
||||
week: cn("mt-2 flex w-full", defaultClassNames.week),
|
||||
week_number_header: cn(
|
||||
"w-(--cell-size) select-none",
|
||||
defaultClassNames.week_number_header
|
||||
),
|
||||
week: cn('mt-2 flex w-full', defaultClassNames.week),
|
||||
week_number_header: cn('w-(--cell-size) select-none', defaultClassNames.week_number_header),
|
||||
week_number: cn(
|
||||
"text-[0.8rem] text-muted-foreground select-none",
|
||||
'text-[0.8rem] text-muted-foreground select-none',
|
||||
defaultClassNames.week_number
|
||||
),
|
||||
day: cn(
|
||||
"group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)",
|
||||
'group/day relative aspect-square h-full w-full rounded-(--cell-radius) p-0 text-center select-none [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)',
|
||||
props.showWeekNumber
|
||||
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)"
|
||||
: "[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)",
|
||||
? '[&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)'
|
||||
: '[&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)',
|
||||
defaultClassNames.day
|
||||
),
|
||||
range_start: cn(
|
||||
"relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted",
|
||||
'relative isolate z-0 rounded-l-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:right-0 after:w-4 after:bg-muted',
|
||||
defaultClassNames.range_start
|
||||
),
|
||||
range_middle: cn("rounded-none", defaultClassNames.range_middle),
|
||||
range_middle: cn('rounded-none', defaultClassNames.range_middle),
|
||||
range_end: cn(
|
||||
"relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted",
|
||||
'relative isolate z-0 rounded-r-(--cell-radius) bg-muted after:absolute after:inset-y-0 after:left-0 after:w-4 after:bg-muted',
|
||||
defaultClassNames.range_end
|
||||
),
|
||||
today: cn(
|
||||
"rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none",
|
||||
'rounded-(--cell-radius) bg-muted text-foreground data-[selected=true]:rounded-none',
|
||||
defaultClassNames.today
|
||||
),
|
||||
outside: cn(
|
||||
"text-muted-foreground aria-selected:text-muted-foreground",
|
||||
'text-muted-foreground aria-selected:text-muted-foreground',
|
||||
defaultClassNames.outside
|
||||
),
|
||||
disabled: cn(
|
||||
"text-muted-foreground opacity-50",
|
||||
defaultClassNames.disabled
|
||||
),
|
||||
hidden: cn("invisible", defaultClassNames.hidden),
|
||||
disabled: cn('text-muted-foreground opacity-50', defaultClassNames.disabled),
|
||||
hidden: cn('invisible', defaultClassNames.hidden),
|
||||
...classNames,
|
||||
}}
|
||||
components={{
|
||||
Root: ({ className, rootRef, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
data-slot="calendar"
|
||||
ref={rootRef}
|
||||
className={cn(className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
return <div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />
|
||||
},
|
||||
Chevron: ({ className, orientation, ...props }) => {
|
||||
if (orientation === "left") {
|
||||
return (
|
||||
<ChevronLeftIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
if (orientation === 'left') {
|
||||
return <ChevronLeftIcon className={cn('size-4', className)} {...props} />
|
||||
}
|
||||
|
||||
if (orientation === "right") {
|
||||
return (
|
||||
<ChevronRightIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
if (orientation === 'right') {
|
||||
return <ChevronRightIcon className={cn('size-4', className)} {...props} />
|
||||
}
|
||||
|
||||
return (
|
||||
<ChevronDownIcon className={cn("size-4", className)} {...props} />
|
||||
)
|
||||
return <ChevronDownIcon className={cn('size-4', className)} {...props} />
|
||||
},
|
||||
DayButton: ({ ...props }) => (
|
||||
<CalendarDayButton locale={locale} {...props} />
|
||||
),
|
||||
DayButton: ({ ...props }) => <CalendarDayButton locale={locale} {...props} />,
|
||||
WeekNumber: ({ children, ...props }) => {
|
||||
return (
|
||||
<td {...props}>
|
||||
@@ -209,7 +173,7 @@ function CalendarDayButton({
|
||||
data-range-end={modifiers.range_end}
|
||||
data-range-middle={modifiers.range_middle}
|
||||
className={cn(
|
||||
"relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground [&>span]:text-xs [&>span]:opacity-70",
|
||||
'group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:bg-muted data-[range-middle=true]:text-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-foreground relative isolate z-10 flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 border-0 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-(--cell-radius) data-[range-end=true]:rounded-r-(--cell-radius) data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-(--cell-radius) data-[range-start=true]:rounded-l-(--cell-radius) [&>span]:text-xs [&>span]:opacity-70',
|
||||
defaultClassNames.day,
|
||||
className
|
||||
)}
|
||||
|
||||
+18
-33
@@ -1,18 +1,18 @@
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Card({
|
||||
className,
|
||||
size = "default",
|
||||
size = 'default',
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) {
|
||||
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"group/card flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl bg-card py-(--card-spacing) text-sm text-card-foreground ring-1 ring-foreground/10 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl",
|
||||
'group/card bg-card text-card-foreground ring-foreground/10 flex flex-col gap-(--card-spacing) overflow-hidden rounded-xl py-(--card-spacing) text-sm ring-1 [--card-spacing:--spacing(4)] has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:[--card-spacing:--spacing(3)] data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -20,12 +20,12 @@ function Card({
|
||||
)
|
||||
}
|
||||
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-header"
|
||||
className={cn(
|
||||
"group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)",
|
||||
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-(--card-spacing) has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-(--card-spacing)',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -33,12 +33,12 @@ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
)
|
||||
}
|
||||
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm",
|
||||
'font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -46,45 +46,38 @@ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
)
|
||||
}
|
||||
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-description"
|
||||
className={cn("text-sm text-muted-foreground", className)}
|
||||
className={cn('text-muted-foreground text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardAction({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-action"
|
||||
className={cn(
|
||||
"col-start-2 row-span-2 row-start-1 self-start justify-self-end",
|
||||
className
|
||||
)}
|
||||
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function CardContent({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-content"
|
||||
className={cn("px-(--card-spacing)", className)}
|
||||
{...props}
|
||||
/>
|
||||
<div data-slot="card-content" className={cn('px-(--card-spacing)', className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="card-footer"
|
||||
className={cn(
|
||||
"flex items-center rounded-b-xl border-t bg-muted/50 p-(--card-spacing)",
|
||||
'bg-muted/50 flex items-center rounded-b-xl border-t p-(--card-spacing)',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -92,12 +85,4 @@ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardFooter,
|
||||
CardTitle,
|
||||
CardAction,
|
||||
CardDescription,
|
||||
CardContent,
|
||||
}
|
||||
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent }
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"
|
||||
import { Checkbox as CheckboxPrimitive } from '@base-ui/react/checkbox'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CheckIcon } from "lucide-react"
|
||||
import { cn } from '@/lib/utils'
|
||||
import { CheckIcon } from 'lucide-react'
|
||||
|
||||
function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
||||
return (
|
||||
<CheckboxPrimitive.Root
|
||||
data-slot="checkbox"
|
||||
className={cn(
|
||||
"peer relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border border-input transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 group-has-[:focus-visible]/field-label:not-data-checked:border-input after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary",
|
||||
'peer border-input group-has-[:focus-visible]/field-label:not-data-checked:border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground group-has-[:focus-visible]/field-label:data-checked:border-primary dark:data-checked:bg-primary relative flex size-4 shrink-0 items-center justify-center rounded-[4px] border transition-colors outline-none group-has-disabled/field:opacity-50 group-has-[:focus-visible]/field-label:ring-0 after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -19,8 +19,7 @@ function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) {
|
||||
data-slot="checkbox-indicator"
|
||||
className="grid place-content-center text-current transition-none [&>svg]:size-3.5"
|
||||
>
|
||||
<CheckIcon
|
||||
/>
|
||||
<CheckIcon />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"
|
||||
import * as React from 'react'
|
||||
import { Dialog as DialogPrimitive } from '@base-ui/react/dialog'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { XIcon } from "lucide-react"
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { XIcon } from 'lucide-react'
|
||||
|
||||
function Dialog({ ...props }: DialogPrimitive.Root.Props) {
|
||||
return <DialogPrimitive.Root data-slot="dialog" {...props} />
|
||||
@@ -23,15 +23,12 @@ function DialogClose({ ...props }: DialogPrimitive.Close.Props) {
|
||||
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
|
||||
}
|
||||
|
||||
function DialogOverlay({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Backdrop.Props) {
|
||||
function DialogOverlay({ className, ...props }: DialogPrimitive.Backdrop.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Backdrop
|
||||
data-slot="dialog-overlay"
|
||||
className={cn(
|
||||
"fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0",
|
||||
'data-open:animate-in data-open:fade-in-0 data-closed:animate-out data-closed:fade-out-0 fixed inset-0 isolate z-50 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -55,7 +52,7 @@ function DialogContent({
|
||||
<DialogPrimitive.Popup
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 duration-100 outline-none sm:max-w-sm data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 fixed top-1/2 left-1/2 z-50 grid w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl p-4 text-sm ring-1 duration-100 outline-none sm:max-w-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -68,14 +65,15 @@ function DialogContent({
|
||||
<Button
|
||||
variant="ghost"
|
||||
className={cn(
|
||||
"absolute top-3 right-3 rounded-full transition-all z-10",
|
||||
closeClassName || "text-slate-500 hover:text-slate-900 hover:bg-slate-100 dark:text-slate-400 dark:hover:text-slate-100 dark:hover:bg-slate-800"
|
||||
'absolute top-3 right-3 z-10 rounded-full transition-all',
|
||||
closeClassName ||
|
||||
'text-slate-500 hover:bg-slate-100 hover:text-slate-900 dark:text-slate-400 dark:hover:bg-slate-800 dark:hover:text-slate-100'
|
||||
)}
|
||||
size="icon-sm"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<XIcon className="w-4 h-4" />
|
||||
<XIcon className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
@@ -84,13 +82,9 @@ function DialogContent({
|
||||
)
|
||||
}
|
||||
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-header"
|
||||
className={cn("flex flex-col gap-2", className)}
|
||||
{...props}
|
||||
/>
|
||||
<div data-slot="dialog-header" className={cn('flex flex-col gap-2', className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -99,23 +93,21 @@ function DialogFooter({
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
}: React.ComponentProps<'div'> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
className={cn(
|
||||
"-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end",
|
||||
'bg-muted/50 -mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t p-4 sm:flex-row sm:justify-end',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>
|
||||
Close
|
||||
</DialogPrimitive.Close>
|
||||
<DialogPrimitive.Close render={<Button variant="outline" />}>Close</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
@@ -125,24 +117,18 @@ function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Title
|
||||
data-slot="dialog-title"
|
||||
className={cn(
|
||||
"font-heading text-base leading-none font-medium",
|
||||
className
|
||||
)}
|
||||
className={cn('font-heading text-base leading-none font-medium', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogDescription({
|
||||
className,
|
||||
...props
|
||||
}: DialogPrimitive.Description.Props) {
|
||||
function DialogDescription({ className, ...props }: DialogPrimitive.Description.Props) {
|
||||
return (
|
||||
<DialogPrimitive.Description
|
||||
data-slot="dialog-description"
|
||||
className={cn(
|
||||
"text-sm text-muted-foreground *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground",
|
||||
'text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import * as React from "react"
|
||||
import { Input as InputPrimitive } from "@base-ui/react/input"
|
||||
import * as React from 'react'
|
||||
import { Input as InputPrimitive } from '@base-ui/react/input'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
||||
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
||||
return (
|
||||
<InputPrimitive
|
||||
type={type}
|
||||
data-slot="input"
|
||||
className={cn(
|
||||
"h-8 w-full min-w-0 rounded-lg border border-input bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
'border-input file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 h-8 w-full min-w-0 rounded-lg border bg-transparent px-2.5 py-1 text-base transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium focus-visible:ring-3 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
function Label({ className, ...props }: React.ComponentProps<'label'>) {
|
||||
return (
|
||||
<label
|
||||
data-slot="label"
|
||||
className={cn(
|
||||
"flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
|
||||
'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import { Popover as PopoverPrimitive } from "@base-ui/react/popover"
|
||||
import * as React from 'react'
|
||||
import { Popover as PopoverPrimitive } from '@base-ui/react/popover'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
||||
return <PopoverPrimitive.Root data-slot="popover" {...props} />
|
||||
@@ -15,16 +15,13 @@ function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
||||
|
||||
function PopoverContent({
|
||||
className,
|
||||
align = "center",
|
||||
align = 'center',
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
side = 'bottom',
|
||||
sideOffset = 4,
|
||||
...props
|
||||
}: PopoverPrimitive.Popup.Props &
|
||||
Pick<
|
||||
PopoverPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset"
|
||||
>) {
|
||||
Pick<PopoverPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) {
|
||||
return (
|
||||
<PopoverPrimitive.Portal>
|
||||
<PopoverPrimitive.Positioner
|
||||
@@ -37,7 +34,7 @@ function PopoverContent({
|
||||
<PopoverPrimitive.Popup
|
||||
data-slot="popover-content"
|
||||
className={cn(
|
||||
"z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg bg-popover p-2.5 text-sm text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 z-50 flex w-72 origin-(--transform-origin) flex-col gap-2.5 rounded-lg p-2.5 text-sm shadow-md ring-1 outline-hidden duration-100',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -47,11 +44,11 @@ function PopoverContent({
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="popover-header"
|
||||
className={cn("flex flex-col gap-0.5 text-sm", className)}
|
||||
className={cn('flex flex-col gap-0.5 text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -61,30 +58,20 @@ function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
|
||||
return (
|
||||
<PopoverPrimitive.Title
|
||||
data-slot="popover-title"
|
||||
className={cn("font-medium", className)}
|
||||
className={cn('font-medium', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function PopoverDescription({
|
||||
className,
|
||||
...props
|
||||
}: PopoverPrimitive.Description.Props) {
|
||||
function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
|
||||
return (
|
||||
<PopoverPrimitive.Description
|
||||
data-slot="popover-description"
|
||||
className={cn("text-muted-foreground", className)}
|
||||
className={cn('text-muted-foreground', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverHeader,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
}
|
||||
export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger }
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import { Select as SelectPrimitive } from "@base-ui/react/select"
|
||||
import * as React from 'react'
|
||||
import { Select as SelectPrimitive } from '@base-ui/react/select'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from "lucide-react"
|
||||
import { cn } from '@/lib/utils'
|
||||
import { ChevronDownIcon, CheckIcon, ChevronUpIcon } from 'lucide-react'
|
||||
|
||||
const Select = SelectPrimitive.Root
|
||||
|
||||
@@ -12,7 +12,7 @@ function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Group
|
||||
data-slot="select-group"
|
||||
className={cn("scroll-my-1 p-1", className)}
|
||||
className={cn('scroll-my-1 p-1', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -22,7 +22,7 @@ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Value
|
||||
data-slot="select-value"
|
||||
className={cn("flex flex-1 text-left", className)}
|
||||
className={cn('flex flex-1 text-left', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -30,27 +30,25 @@ function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) {
|
||||
|
||||
function SelectTrigger({
|
||||
className,
|
||||
size = "default",
|
||||
size = 'default',
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Trigger.Props & {
|
||||
size?: "sm" | "default"
|
||||
size?: 'sm' | 'default'
|
||||
}) {
|
||||
return (
|
||||
<SelectPrimitive.Trigger
|
||||
data-slot="select-trigger"
|
||||
data-size={size}
|
||||
className={cn(
|
||||
"flex w-fit items-center justify-between gap-1.5 rounded-lg border border-input bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 flex w-fit items-center justify-between gap-1.5 rounded-lg border bg-transparent py-2 pr-2 pl-2.5 text-sm whitespace-nowrap transition-colors outline-none select-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-[min(var(--radius-md),10px)] *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-1.5 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
<SelectPrimitive.Icon
|
||||
render={
|
||||
<ChevronDownIcon className="pointer-events-none size-4 text-muted-foreground" />
|
||||
}
|
||||
render={<ChevronDownIcon className="text-muted-foreground pointer-events-none size-4" />}
|
||||
/>
|
||||
</SelectPrimitive.Trigger>
|
||||
)
|
||||
@@ -59,16 +57,16 @@ function SelectTrigger({
|
||||
function SelectContent({
|
||||
className,
|
||||
children,
|
||||
side = "bottom",
|
||||
side = 'bottom',
|
||||
sideOffset = 4,
|
||||
align = "center",
|
||||
align = 'center',
|
||||
alignOffset = 0,
|
||||
alignItemWithTrigger = true,
|
||||
...props
|
||||
}: SelectPrimitive.Popup.Props &
|
||||
Pick<
|
||||
SelectPrimitive.Positioner.Props,
|
||||
"align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger"
|
||||
'align' | 'alignOffset' | 'side' | 'sideOffset' | 'alignItemWithTrigger'
|
||||
>) {
|
||||
return (
|
||||
<SelectPrimitive.Portal>
|
||||
@@ -83,7 +81,10 @@ function SelectContent({
|
||||
<SelectPrimitive.Popup
|
||||
data-slot="select-content"
|
||||
data-align-trigger={alignItemWithTrigger}
|
||||
className={cn("relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg bg-popover text-popover-foreground shadow-md ring-1 ring-foreground/10 duration-100 data-[align-trigger=true]:animate-none data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95", className )}
|
||||
className={cn(
|
||||
'bg-popover text-popover-foreground ring-foreground/10 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) min-w-36 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-lg shadow-md ring-1 duration-100 data-[align-trigger=true]:animate-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
@@ -95,29 +96,22 @@ function SelectContent({
|
||||
)
|
||||
}
|
||||
|
||||
function SelectLabel({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.GroupLabel.Props) {
|
||||
function SelectLabel({ className, ...props }: SelectPrimitive.GroupLabel.Props) {
|
||||
return (
|
||||
<SelectPrimitive.GroupLabel
|
||||
data-slot="select-label"
|
||||
className={cn("px-1.5 py-1 text-xs text-muted-foreground", className)}
|
||||
className={cn('text-muted-foreground px-1.5 py-1 text-xs', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function SelectItem({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: SelectPrimitive.Item.Props) {
|
||||
function SelectItem({ className, children, ...props }: SelectPrimitive.Item.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Item
|
||||
data-slot="select-item"
|
||||
className={cn(
|
||||
"relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
"focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground relative flex w-full cursor-default items-center gap-1.5 rounded-md py-1 pr-8 pl-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -136,14 +130,11 @@ function SelectItem({
|
||||
)
|
||||
}
|
||||
|
||||
function SelectSeparator({
|
||||
className,
|
||||
...props
|
||||
}: SelectPrimitive.Separator.Props) {
|
||||
function SelectSeparator({ className, ...props }: SelectPrimitive.Separator.Props) {
|
||||
return (
|
||||
<SelectPrimitive.Separator
|
||||
data-slot="select-separator"
|
||||
className={cn("pointer-events-none -mx-1 my-1 h-px bg-border", className)}
|
||||
className={cn('bg-border pointer-events-none -mx-1 my-1 h-px', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -157,13 +148,12 @@ function SelectScrollUpButton({
|
||||
<SelectPrimitive.ScrollUpArrow
|
||||
data-slot="select-scroll-up-button"
|
||||
className={cn(
|
||||
"top-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
"bg-popover top-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronUpIcon
|
||||
/>
|
||||
<ChevronUpIcon />
|
||||
</SelectPrimitive.ScrollUpArrow>
|
||||
)
|
||||
}
|
||||
@@ -176,13 +166,12 @@ function SelectScrollDownButton({
|
||||
<SelectPrimitive.ScrollDownArrow
|
||||
data-slot="select-scroll-down-button"
|
||||
className={cn(
|
||||
"bottom-0 z-10 flex w-full cursor-default items-center justify-center bg-popover py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
"bg-popover bottom-0 z-10 flex w-full cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ChevronDownIcon
|
||||
/>
|
||||
<ChevronDownIcon />
|
||||
</SelectPrimitive.ScrollDownArrow>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,48 +1,47 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
import { CircleCheckIcon, InfoIcon, TriangleAlertIcon, OctagonXIcon, Loader2Icon } from "lucide-react"
|
||||
import { useTheme } from 'next-themes'
|
||||
import { Toaster as Sonner, type ToasterProps } from 'sonner'
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
TriangleAlertIcon,
|
||||
OctagonXIcon,
|
||||
Loader2Icon,
|
||||
} from 'lucide-react'
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
const { theme = 'system' } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
theme={theme as ToasterProps['theme']}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: (
|
||||
<CircleCheckIcon className="size-4" />
|
||||
),
|
||||
info: (
|
||||
<InfoIcon className="size-4" />
|
||||
),
|
||||
warning: (
|
||||
<TriangleAlertIcon className="size-4" />
|
||||
),
|
||||
error: (
|
||||
<OctagonXIcon className="size-4" />
|
||||
),
|
||||
loading: (
|
||||
<Loader2Icon className="size-4 animate-spin" />
|
||||
),
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
'--border-radius': 'var(--radius)',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
toastOptions={{
|
||||
classNames: {
|
||||
toast: "cn-toast group-[.toaster]:bg-white dark:group-[.toaster]:bg-slate-950 group-[.toaster]:text-slate-950 dark:group-[.toaster]:text-slate-50 group-[.toaster]:border-slate-200 dark:group-[.toaster]:border-slate-800",
|
||||
title: "font-bold",
|
||||
description: "!text-slate-700 dark:!text-slate-300",
|
||||
success: "group-[.toaster]:border-emerald-500 group-[.toaster]:bg-emerald-50 dark:group-[.toaster]:bg-emerald-950/50 group-[.toaster]:text-emerald-900 dark:group-[.toaster]:text-emerald-100 [&_svg]:text-emerald-600 dark:[&_svg]:text-emerald-400 [&_[data-description]]:!text-emerald-700 dark:[&_[data-description]]:!text-emerald-300",
|
||||
error: "group-[.toaster]:border-red-500 group-[.toaster]:bg-red-50 dark:group-[.toaster]:bg-red-950/50 group-[.toaster]:text-red-900 dark:group-[.toaster]:text-red-100 [&_svg]:text-red-600 dark:[&_svg]:text-red-400 [&_[data-description]]:!text-red-700 dark:[&_[data-description]]:!text-red-300",
|
||||
toast:
|
||||
'cn-toast group-[.toaster]:bg-white dark:group-[.toaster]:bg-slate-950 group-[.toaster]:text-slate-950 dark:group-[.toaster]:text-slate-50 group-[.toaster]:border-slate-200 dark:group-[.toaster]:border-slate-800',
|
||||
title: 'font-bold',
|
||||
description: '!text-slate-700 dark:!text-slate-300',
|
||||
success:
|
||||
'group-[.toaster]:border-emerald-500 group-[.toaster]:bg-emerald-50 dark:group-[.toaster]:bg-emerald-950/50 group-[.toaster]:text-emerald-900 dark:group-[.toaster]:text-emerald-100 [&_svg]:text-emerald-600 dark:[&_svg]:text-emerald-400 [&_[data-description]]:!text-emerald-700 dark:[&_[data-description]]:!text-emerald-300',
|
||||
error:
|
||||
'group-[.toaster]:border-red-500 group-[.toaster]:bg-red-50 dark:group-[.toaster]:bg-red-950/50 group-[.toaster]:text-red-900 dark:group-[.toaster]:text-red-100 [&_svg]:text-red-600 dark:[&_svg]:text-red-400 [&_[data-description]]:!text-red-700 dark:[&_[data-description]]:!text-red-300',
|
||||
},
|
||||
}}
|
||||
{...props}
|
||||
|
||||
+21
-48
@@ -1,63 +1,51 @@
|
||||
"use client"
|
||||
'use client'
|
||||
|
||||
import * as React from "react"
|
||||
import * as React from 'react'
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
function Table({ className, ...props }: React.ComponentProps<'table'>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<div data-slot="table-container" className="relative w-full overflow-x-auto">
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
className={cn('w-full caption-bottom text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
|
||||
return <thead data-slot="table-header" className={cn('[&_tr]:border-b', className)} {...props} />
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
className={cn('[&_tr:last-child]:border-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
className={cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"border-b transition-colors hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted",
|
||||
'hover:bg-muted/50 has-aria-expanded:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -65,12 +53,12 @@ function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"h-10 px-2 text-left align-middle font-medium whitespace-nowrap text-foreground [&:has([role=checkbox])]:pr-0",
|
||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -78,39 +66,24 @@ function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0",
|
||||
className
|
||||
)}
|
||||
className={cn('p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
function TableCaption({ className, ...props }: React.ComponentProps<'caption'>) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("mt-4 text-sm text-muted-foreground", className)}
|
||||
className={cn('text-muted-foreground mt-4 text-sm', className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import * as React from "react"
|
||||
import { cn } from "cn"
|
||||
import * as React from 'react'
|
||||
import { cn } from 'cn'
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"flex field-sizing-content min-h-16 w-full rounded-lg border border-input bg-transparent px-2.5 py-2 text-base transition-colors outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:bg-input/50 disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40",
|
||||
'border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 disabled:bg-input/50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:bg-input/30 dark:disabled:bg-input/80 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 flex field-sizing-content min-h-16 w-full rounded-lg border bg-transparent px-2.5 py-2 text-base transition-colors outline-none focus-visible:ring-3 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:ring-3 md:text-sm',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
Reference in New Issue
Block a user