feat : add monthly
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import { and, asc, eq, gte, lt } from "drizzle-orm";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { connection } from "next/server";
|
||||||
|
import { AdminHeader } from "@/components/admin/admin-header";
|
||||||
|
import { CommissionIncomeCalendar } from "@/components/commission/income-calendar";
|
||||||
|
import { getDb } from "@/db";
|
||||||
|
import { commissionPayments, commissionTickets, users } from "@/db/schema";
|
||||||
|
import { getAdminSession } from "@/lib/auth/server";
|
||||||
|
import { commissionIncomeMonth } from "@/lib/commission/income";
|
||||||
|
|
||||||
|
export const instant = false;
|
||||||
|
|
||||||
|
export default async function CommissionIncomePage({ searchParams }: PageProps<"/admin/commission/income">) {
|
||||||
|
await connection();
|
||||||
|
if (!(await getAdminSession())) redirect("/auth/login?next=%2Fadmin%2Fcommission%2Fincome");
|
||||||
|
const { month: rawMonth } = await searchParams;
|
||||||
|
const period = commissionIncomeMonth(rawMonth);
|
||||||
|
const rows = await getDb().select({
|
||||||
|
ticketId: commissionTickets.id,
|
||||||
|
title: commissionTickets.title,
|
||||||
|
creatorName: users.name,
|
||||||
|
amountBaht: commissionPayments.amountBaht,
|
||||||
|
transferredAt: commissionPayments.transferredAt,
|
||||||
|
}).from(commissionPayments)
|
||||||
|
.innerJoin(commissionTickets, eq(commissionTickets.id, commissionPayments.ticketId))
|
||||||
|
.innerJoin(users, eq(users.id, commissionTickets.userId))
|
||||||
|
.where(and(gte(commissionPayments.transferredAt, period.from), lt(commissionPayments.transferredAt, period.to)))
|
||||||
|
.orderBy(asc(commissionPayments.transferredAt), asc(commissionPayments.id));
|
||||||
|
|
||||||
|
return <div className="min-h-svh"><AdminHeader />
|
||||||
|
<main className="mx-auto flex max-w-6xl flex-col gap-5 p-4 py-10 sm:p-8">
|
||||||
|
<Link href="/admin/commission" className="w-fit text-sm underline">กลับไป Commission tickets</Link>
|
||||||
|
<header><h1 className="font-heading text-3xl font-semibold">Commission income</h1>
|
||||||
|
<p className="text-muted-foreground">รายได้ประจำเดือนจาก Commision</p></header>
|
||||||
|
<CommissionIncomeCalendar period={{ month: period.month, year: period.year, number: period.number,
|
||||||
|
previous: period.previous, next: period.next, days: period.days, leadingDays: period.leadingDays }} rows={rows} />
|
||||||
|
</main>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
@@ -31,6 +31,7 @@ export default async function AdminCommissionPage({ searchParams }: PageProps<"/
|
|||||||
<main className="mx-auto flex max-w-5xl flex-col gap-5 p-4 py-10 sm:p-8">
|
<main className="mx-auto flex max-w-5xl flex-col gap-5 p-4 py-10 sm:p-8">
|
||||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||||
<h1 className="font-heading text-3xl font-semibold">Commission tickets</h1>
|
<h1 className="font-heading text-3xl font-semibold">Commission tickets</h1>
|
||||||
|
<Button variant="outline" nativeButton={false} render={<Link href="/admin/commission/income" />}>ดูรายได้ประจำเดือน</Button>
|
||||||
</div>
|
</div>
|
||||||
<AdminCommissionAvailabilityControl key={`${availability.isOpen}:${availability.closureMessage}`}
|
<AdminCommissionAvailabilityControl key={`${availability.isOpen}:${availability.closureMessage}`}
|
||||||
{...availability} />
|
{...availability} />
|
||||||
|
|||||||
@@ -12,17 +12,35 @@ import { getActiveCatalog, listGuideCards } from "@/lib/guides/queries";
|
|||||||
export default async function CreateGuidePage() {
|
export default async function CreateGuidePage() {
|
||||||
await connection();
|
await connection();
|
||||||
if (!(await getAdminSession())) redirect("/auth/login?next=%2Fadmin");
|
if (!(await getAdminSession())) redirect("/auth/login?next=%2Fadmin");
|
||||||
const [catalog, guides] = await Promise.all([getActiveCatalog(), listGuideCards()]);
|
const [catalog, guides] = await Promise.all([
|
||||||
|
getActiveCatalog(),
|
||||||
|
listGuideCards(),
|
||||||
|
]);
|
||||||
if (!catalog) redirect("/admin?sync=required");
|
if (!catalog) redirect("/admin?sync=required");
|
||||||
return (
|
return (
|
||||||
<div className="min-h-svh">
|
<div className="min-h-svh">
|
||||||
<AdminHeader />
|
<AdminHeader />
|
||||||
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 p-4 sm:p-6 lg:p-8">
|
<main className="mx-auto flex w-full max-w-7xl flex-col gap-8 p-4 sm:p-6 lg:p-8">
|
||||||
<header className="flex items-center gap-4">
|
<header className="flex items-center gap-4">
|
||||||
<Button variant="ghost" size="icon" render={<Link href="/admin" />} nativeButton={false} aria-label="กลับ Dashboard"><ArrowLeftIcon /></Button>
|
<Button
|
||||||
<div><p className="text-sm text-muted-foreground">Catalog ล่าสุดจาก Lunaris</p><h1 className="font-heading text-3xl font-semibold">สร้าง Guide ใหม่</h1></div>
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
render={<Link href="/admin" />}
|
||||||
|
nativeButton={false}
|
||||||
|
aria-label="กลับ Dashboard"
|
||||||
|
>
|
||||||
|
<ArrowLeftIcon />
|
||||||
|
</Button>
|
||||||
|
<div>
|
||||||
|
<h1 className="font-heading text-3xl font-semibold">
|
||||||
|
สร้าง Guide ใหม่
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<CreateGuideFlow characters={catalog.characters} existingKeys={guides.map((guide) => guide.characterKey)} />
|
<CreateGuideFlow
|
||||||
|
characters={catalog.characters}
|
||||||
|
existingKeys={guides.map((guide) => guide.characterKey)}
|
||||||
|
/>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -68,26 +68,6 @@ export function AdminHeader() {
|
|||||||
<BookOpenIcon data-icon="inline-start" />
|
<BookOpenIcon data-icon="inline-start" />
|
||||||
<span className="hidden xl:inline">Glossary</span>
|
<span className="hidden xl:inline">Glossary</span>
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant={
|
variant={
|
||||||
pathname.startsWith("/admin/image") ? "secondary" : "ghost"
|
pathname.startsWith("/admin/image") ? "secondary" : "ghost"
|
||||||
@@ -100,6 +80,17 @@ export function AdminHeader() {
|
|||||||
<ImageIcon data-icon="inline-start" />
|
<ImageIcon data-icon="inline-start" />
|
||||||
<span className="hidden xl:inline">Image Generator</span>
|
<span className="hidden xl:inline">Image Generator</span>
|
||||||
</Button>
|
</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
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-sm"
|
size="icon-sm"
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export function AdminCommissionAvailabilityControl({ isOpen, closureMessage }: {
|
|||||||
|
|
||||||
return <Card><CardHeader>
|
return <Card><CardHeader>
|
||||||
<CardTitle>การรับ Commission</CardTitle>
|
<CardTitle>การรับ Commission</CardTitle>
|
||||||
<CardDescription>การปิดรับจะหยุดคำขอใหม่ ลูกค้าที่มีคำขออยู่แล้วชำระเงินต่อได้</CardDescription>
|
<CardDescription>การปิดรับจะหยุดคำขอใหม่ ลูกค้าที่มีคำขออยู่แล้วชำระเงินต่อได้ ทุก Ticket ยังคงคุยได้ตามปกติ</CardDescription>
|
||||||
</CardHeader><CardContent className="flex flex-col gap-3">
|
</CardHeader><CardContent className="flex flex-col gap-3">
|
||||||
<label htmlFor="commission-closure-message" className="text-sm font-medium">ข้อความเมื่อปิดรับ</label>
|
<label htmlFor="commission-closure-message" className="text-sm font-medium">ข้อความเมื่อปิดรับ</label>
|
||||||
<Textarea id="commission-closure-message" value={message} maxLength={1000}
|
<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 dragStart = useRef<{ pointerId: number; x: number; y: number; offsetX: number; offsetY: number } | null>(null);
|
||||||
const stats = character.baseStats.slice(0, 5);
|
const stats = character.baseStats.slice(0, 5);
|
||||||
const statBoxCount = Math.max(4, stats.length);
|
const statBoxCount = Math.max(4, stats.length);
|
||||||
const hasFiveStats = statBoxCount === 5;
|
|
||||||
const materials = mergePosterMaterials(
|
const materials = mergePosterMaterials(
|
||||||
[...character.ascension, ...character.leveling].flatMap((group) => group.items),
|
[...character.ascension, ...character.leveling].flatMap((group) => group.items),
|
||||||
);
|
);
|
||||||
@@ -218,7 +217,7 @@ export function MaterialPoster({
|
|||||||
</div>
|
</div>
|
||||||
</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>
|
<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>
|
<p className="mt-[1%] text-[4.5cqw] leading-none">LV.90</p>
|
||||||
<div className="mt-[5%] grid grid-cols-8 gap-[1.2%]">
|
<div className="mt-[5%] grid grid-cols-8 gap-[1.2%]">
|
||||||
|
|||||||
@@ -200,6 +200,7 @@ export const commissionPayments = commissionSchema.table("payment", {
|
|||||||
uniqueIndex("commission_payment_checkout_unique").on(table.checkoutId),
|
uniqueIndex("commission_payment_checkout_unique").on(table.checkoutId),
|
||||||
uniqueIndex("commission_payment_reference_unique").on(table.referenceId),
|
uniqueIndex("commission_payment_reference_unique").on(table.referenceId),
|
||||||
uniqueIndex("commission_payment_trans_ref_unique").on(table.transRef),
|
uniqueIndex("commission_payment_trans_ref_unique").on(table.transRef),
|
||||||
|
index("commission_payment_transferred_at_idx").on(table.transferredAt),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const commissionPushSubscriptions = commissionSchema.table("push_subscription", {
|
export const commissionPushSubscriptions = commissionSchema.table("push_subscription", {
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX "commission_payment_transferred_at_idx" ON "commission"."payment" USING btree ("transferred_at");
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -498,6 +498,13 @@
|
|||||||
"when": 1791015188934,
|
"when": 1791015188934,
|
||||||
"tag": "0070_outgoing_chimera",
|
"tag": "0070_outgoing_chimera",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 71,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1791100706838,
|
||||||
|
"tag": "0071_first_norman_osborn",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { commissionIncomeDay, commissionIncomeMonth, commissionIncomeShares } from "./income";
|
||||||
|
|
||||||
|
describe("commission income periods", () => {
|
||||||
|
it("uses Bangkok month boundaries and rolls across years", () => {
|
||||||
|
const december = commissionIncomeMonth("2026-12");
|
||||||
|
expect(december.from.toISOString()).toBe("2026-11-30T17:00:00.000Z");
|
||||||
|
expect(december.to.toISOString()).toBe("2026-12-31T17:00:00.000Z");
|
||||||
|
expect(december.next).toBe("2027-01");
|
||||||
|
expect(commissionIncomeMonth("2027-01").previous).toBe("2026-12");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("defaults to the current Bangkok month for an invalid query", () => {
|
||||||
|
expect(commissionIncomeMonth("2026-13", new Date("2026-09-30T18:00:00Z")).month).toBe("2026-10");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("assigns payments around midnight to the correct Bangkok day", () => {
|
||||||
|
expect(commissionIncomeDay(new Date("2026-10-01T16:59:59Z"))).toBe(1);
|
||||||
|
expect(commissionIncomeDay(new Date("2026-10-01T17:00:00Z"))).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calculates Buzz and Gunshin shares in satang", () => {
|
||||||
|
expect(commissionIncomeShares(101)).toEqual({ buzzSatang: 9090, gunshinSatang: 1010 });
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const BANGKOK_OFFSET = "+07:00";
|
||||||
|
|
||||||
|
export function commissionIncomeMonth(value: string | string[] | undefined, now = new Date()) {
|
||||||
|
const currentParts = new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: "Asia/Bangkok", year: "numeric", month: "2-digit",
|
||||||
|
}).formatToParts(now);
|
||||||
|
const current = `${currentParts.find((part) => part.type === "year")!.value}-${currentParts.find((part) => part.type === "month")!.value}`;
|
||||||
|
const month = typeof value === "string" && /^(20\d{2}|[3-9]\d{3})-(0[1-9]|1[0-2])$/.test(value) ? value : current;
|
||||||
|
const [year, number] = month.split("-").map(Number);
|
||||||
|
const next = number === 12 ? `${year + 1}-01` : `${year}-${String(number + 1).padStart(2, "0")}`;
|
||||||
|
const previous = number === 1 ? `${year - 1}-12` : `${year}-${String(number - 1).padStart(2, "0")}`;
|
||||||
|
return {
|
||||||
|
month, year, number, previous, next,
|
||||||
|
from: new Date(`${month}-01T00:00:00${BANGKOK_OFFSET}`),
|
||||||
|
to: new Date(`${next}-01T00:00:00${BANGKOK_OFFSET}`),
|
||||||
|
days: new Date(Date.UTC(year, number, 0)).getUTCDate(),
|
||||||
|
leadingDays: (new Date(Date.UTC(year, number - 1, 1)).getUTCDay() + 6) % 7,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commissionIncomeDay(date: Date) {
|
||||||
|
return Number(new Intl.DateTimeFormat("en-US", {
|
||||||
|
timeZone: "Asia/Bangkok", day: "numeric",
|
||||||
|
}).format(date));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function commissionIncomeShares(amountBaht: number) {
|
||||||
|
return { buzzSatang: amountBaht * 90, gunshinSatang: amountBaht * 10 };
|
||||||
|
}
|
||||||
@@ -218,7 +218,7 @@ describe("material poster text", () => {
|
|||||||
expect(html).toContain("Energy Recharge%");
|
expect(html).toContain("Energy Recharge%");
|
||||||
expect(html).toContain("132%");
|
expect(html).toContain("132%");
|
||||||
expect(html).toContain("h-[7cqw]");
|
expect(html).toContain("h-[7cqw]");
|
||||||
expect(html).toContain("top-[57%]");
|
expect(html).toContain("top-[49%]");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("optimizes scale-aware character artwork and bleeding layers for export", () => {
|
it("optimizes scale-aware character artwork and bleeding layers for export", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user