feat : add monthly
This commit is contained in:
@@ -68,26 +68,6 @@ export function AdminHeader() {
|
||||
<BookOpenIcon data-icon="inline-start" />
|
||||
<span className="hidden xl:inline">Glossary</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={pathname === "/admin" ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
nativeButton={false}
|
||||
render={<Link href="/admin" />}
|
||||
aria-label="Dashboard"
|
||||
>
|
||||
<LayoutDashboardIcon data-icon="inline-start" />
|
||||
<span className="hidden xl:inline">Dashboard</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={pathname === "/admin/create" ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
nativeButton={false}
|
||||
render={<Link href="/admin/create" />}
|
||||
aria-label="New Guide"
|
||||
>
|
||||
<PlusIcon data-icon="inline-start" />
|
||||
<span className="hidden xl:inline">New Guide</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
pathname.startsWith("/admin/image") ? "secondary" : "ghost"
|
||||
@@ -100,6 +80,17 @@ export function AdminHeader() {
|
||||
<ImageIcon data-icon="inline-start" />
|
||||
<span className="hidden xl:inline">Image Generator</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant={pathname === "/admin" ? "secondary" : "ghost"}
|
||||
size="sm"
|
||||
nativeButton={false}
|
||||
render={<Link href="/admin" />}
|
||||
aria-label="Dashboard"
|
||||
>
|
||||
<LayoutDashboardIcon data-icon="inline-start" />
|
||||
<span className="hidden xl:inline">Dashboard</span>
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
|
||||
@@ -32,7 +32,7 @@ export function AdminCommissionAvailabilityControl({ isOpen, closureMessage }: {
|
||||
|
||||
return <Card><CardHeader>
|
||||
<CardTitle>การรับ Commission</CardTitle>
|
||||
<CardDescription>การปิดรับจะหยุดคำขอใหม่ ลูกค้าที่มีคำขออยู่แล้วชำระเงินต่อได้</CardDescription>
|
||||
<CardDescription>การปิดรับจะหยุดคำขอใหม่ ลูกค้าที่มีคำขออยู่แล้วชำระเงินต่อได้ ทุก Ticket ยังคงคุยได้ตามปกติ</CardDescription>
|
||||
</CardHeader><CardContent className="flex flex-col gap-3">
|
||||
<label htmlFor="commission-closure-message" className="text-sm font-medium">ข้อความเมื่อปิดรับ</label>
|
||||
<Textarea id="commission-closure-message" value={message} maxLength={1000}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
||||
import { commissionIncomeDay, commissionIncomeShares } from "@/lib/commission/income";
|
||||
|
||||
type IncomeRow = { ticketId: string; title: string | null; creatorName: string; amountBaht: number; transferredAt: Date };
|
||||
type Period = { month: string; year: number; number: number; previous: string; next: string; days: number; leadingDays: number };
|
||||
const weekdays = ["จ.", "อ.", "พ.", "พฤ.", "ศ.", "ส.", "อา."];
|
||||
const money = (satang: number) => `฿${(satang / 100).toLocaleString("th-TH", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
|
||||
const time = new Intl.DateTimeFormat("th-TH", { timeZone: "Asia/Bangkok", hour: "2-digit", minute: "2-digit" });
|
||||
const date = new Intl.DateTimeFormat("th-TH", { timeZone: "Asia/Bangkok", day: "numeric", month: "long", year: "numeric" });
|
||||
|
||||
function Totals({ amount }: { amount: number }) {
|
||||
const shares = commissionIncomeShares(amount);
|
||||
return <span>รวม {money(amount * 100)} · Buzz {money(shares.buzzSatang)} · Gunshin {money(shares.gunshinSatang)}</span>;
|
||||
}
|
||||
|
||||
export function CommissionIncomeCalendar({ period, rows }: { period: Period; rows: IncomeRow[] }) {
|
||||
const byDay = new Map<number, IncomeRow[]>();
|
||||
for (const row of rows) {
|
||||
const day = commissionIncomeDay(row.transferredAt);
|
||||
const list = byDay.get(day) ?? [];
|
||||
list.push(row);
|
||||
byDay.set(day, list);
|
||||
}
|
||||
const total = rows.reduce((sum, row) => sum + row.amountBaht, 0);
|
||||
const monthLabel = new Intl.DateTimeFormat("th-TH", { month: "long", year: "numeric", timeZone: "UTC" })
|
||||
.format(new Date(Date.UTC(period.year, period.number - 1, 1)));
|
||||
|
||||
return <>
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<nav className="flex items-center gap-2" aria-label="เดือนของรายงาน">
|
||||
<Button variant="outline" size="icon" nativeButton={false} render={<Link href={`/admin/commission/income?month=${period.previous}`} prefetch={false} />} aria-label="เดือนก่อนหน้า"><ChevronLeft /></Button>
|
||||
<span className="min-w-36 text-center text-lg font-semibold">{monthLabel}</span>
|
||||
<Button variant="outline" size="icon" nativeButton={false} render={<Link href={`/admin/commission/income?month=${period.next}`} prefetch={false} />} aria-label="เดือนถัดไป"><ChevronRight /></Button>
|
||||
</nav>
|
||||
<p className="text-sm font-medium"><Totals amount={total} /></p>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<div className="grid min-w-[700px] grid-cols-7 gap-1">
|
||||
{weekdays.map((day) => <div key={day} className="p-2 text-center text-sm text-muted-foreground">{day}</div>)}
|
||||
{Array.from({ length: period.leadingDays }, (_, index) => <div key={`pad-${index}`} />)}
|
||||
{Array.from({ length: period.days }, (_, index) => {
|
||||
const day = index + 1;
|
||||
const tickets = byDay.get(day) ?? [];
|
||||
const dayTotal = tickets.reduce((sum, row) => sum + row.amountBaht, 0);
|
||||
const label = date.format(new Date(`${period.month}-${String(day).padStart(2, "0")}T12:00:00+07:00`));
|
||||
if (tickets.length === 0) return <div key={day} className="min-h-24 rounded-lg border p-2 font-medium">{day}</div>;
|
||||
return <Dialog key={day}>
|
||||
<DialogTrigger render={<button type="button" className="flex min-h-24 flex-col gap-1 rounded-lg border p-2 text-left hover:bg-muted/50" />}>
|
||||
<span className="font-medium">{day}</span>
|
||||
<span className="text-xs text-muted-foreground">{tickets.length} tickets</span>
|
||||
<span className="text-sm">{money(dayTotal * 100)}</span>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[85svh] overflow-y-auto sm:max-w-2xl">
|
||||
<DialogHeader><DialogTitle>{label}</DialogTitle></DialogHeader>
|
||||
<p className="text-sm"><Totals amount={dayTotal} /></p>
|
||||
<div className="space-y-2">
|
||||
{tickets.map((row) => <div key={row.ticketId} className="rounded-lg border p-3 text-sm">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<Link href={`/admin/commission/${row.ticketId}`} className="font-medium underline">{row.title || `Ticket #${row.ticketId.slice(0, 8)}`}</Link>
|
||||
<p className="text-muted-foreground">สร้างโดย {row.creatorName}</p>
|
||||
</div>
|
||||
<div className="shrink-0 text-right">
|
||||
<time className="block text-muted-foreground">{time.format(row.transferredAt)}</time>
|
||||
<span className="font-medium">{money(row.amountBaht * 100)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>;
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</>;
|
||||
}
|
||||
@@ -85,7 +85,6 @@ export function MaterialPoster({
|
||||
const dragStart = useRef<{ pointerId: number; x: number; y: number; offsetX: number; offsetY: number } | null>(null);
|
||||
const stats = character.baseStats.slice(0, 5);
|
||||
const statBoxCount = Math.max(4, stats.length);
|
||||
const hasFiveStats = statBoxCount === 5;
|
||||
const materials = mergePosterMaterials(
|
||||
[...character.ascension, ...character.leveling].flatMap((group) => group.items),
|
||||
);
|
||||
@@ -218,7 +217,7 @@ export function MaterialPoster({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={cn("absolute inset-x-[4%]", hasFiveStats ? "top-[57%]" : "top-[49%]")} style={{ fontFamily: "Kanit Poster", fontWeight: 800, fontStyle: "italic" }}>
|
||||
<div className="absolute inset-x-[4%] top-[49%]" style={{ fontFamily: "Kanit Poster", fontWeight: 800, fontStyle: "italic" }}>
|
||||
<h2 className="text-[7cqw] leading-none" style={{ textShadow: "-6px 14px 0 rgba(0,0,0,0.59)" }}>MATERIALS</h2>
|
||||
<p className="mt-[1%] text-[4.5cqw] leading-none">LV.90</p>
|
||||
<div className="mt-[5%] grid grid-cols-8 gap-[1.2%]">
|
||||
|
||||
Reference in New Issue
Block a user