feat : updae

This commit is contained in:
2026-10-07 17:03:15 +07:00 Unverified
parent 1fa2f80a83
commit 5d195968ec
43 changed files with 515 additions and 243 deletions
+3 -3
View File
@@ -7,7 +7,7 @@ import { Separator } from "@/components/ui/separator";
const steps = [
{ title: "1. เลือกรายการ", description: "เลือกทีม อาวุธ หรือกลุ่มดาวที่ต้องการคำนวณ" },
{ title: "2. ชำระเงิน", description: "ตรวจสอบยอด สแกน PromptPay และส่งสลิป" },
{ title: "3. รับผลใน Ticket", description: "ติดตามคำขอและพูดคุยเรื่องผลลัพธ์ใน Ticket ของคุณ" },
{ title: "3. รับผลในแชตคำขอ", description: "ติดตามคำขอและพูดคุยเรื่องผลลัพธ์ในแชตของคำขอ (Ticket)" },
];
const sampleMembers = [
@@ -21,7 +21,7 @@ function CommissionTeamPreview() {
return <Card>
<CardHeader>
<CardTitle>ตัวอย่างทีม 4 ตัวละคร</CardTitle>
<CardDescription>โรเทชั่น 20 วิ · ข้อมูลสมมติสำหรับดูรูปแบบผลลัพธ์</CardDescription>
<CardDescription>โรเทชั่น 20 วิ - ข้อมูลสมมติสำหรับดูรูปแบบผลลัพธ์</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<div className="grid grid-cols-4 gap-1 sm:gap-3">
@@ -54,7 +54,7 @@ export function CommissionIntro() {
<div>
<h2 id="commission-how-it-works" className="font-heading text-2xl font-semibold">ขอคำนวณแบบที่ต้องการ</h2>
<p className="mt-2 text-muted-foreground">เลือกได้ทั้งจัดทีม เปรียบเทียบอาวุธ และเปรียบเทียบกลุ่มดาว โดยเห็นยอดก่อนชำระเงิน</p>
<p className="mt-2 text-sm text-muted-foreground">ระยะเวลาจัดทำ 1–7 วันหลังยืนยันการชำระเงิน ขึ้นอยู่กับขนาดคำขอ งานเล็กอาจเสร็จภายในประมาณ 4 ชั่วโมง ติดตามความคืบหน้าและสอบถามได้ใน Ticket</p>
<p className="mt-2 text-sm text-muted-foreground">ระยะเวลาจัดทำ 1–7 วันหลังยืนยันการชำระเงิน ขึ้นอยู่กับขนาดคำขอ งานเล็กอาจเสร็จภายในประมาณ 4 ชั่วโมง ติดตามความคืบหน้าและสอบถามได้ในแชตของคำขอ (Ticket)</p>
</div>
<div className="grid gap-3 sm:grid-cols-3">
{steps.map((step) => <Card key={step.title} size="sm">
+1 -1
View File
@@ -15,7 +15,7 @@ const date = new Intl.DateTimeFormat("th-TH", { timeZone: "Asia/Bangkok", day: "
function Totals({ amount }: { amount: number }) {
const shares = commissionIncomeShares(amount);
return <span>รวม {money(amount * 100)} · Buzz {money(shares.buzzSatang)} · Gunshin {money(shares.gunshinSatang)}</span>;
return <span>รวม {money(amount * 100)} - Buzz {money(shares.buzzSatang)} - Gunshin {money(shares.gunshinSatang)}</span>;
}
export function CommissionIncomeCalendar({ period, rows }: { period: Period; rows: IncomeRow[] }) {
+1 -1
View File
@@ -49,7 +49,7 @@ export function MobileSlipForm() {
{ready && <><p>ยอดชำระ ฿{amount}</p><form onSubmit={submit} className="flex flex-col gap-4">
<Field><FieldLabel htmlFor="mobile-slip">เลือกรูปสลิปจากแอปธนาคาร</FieldLabel>
<Input id="mobile-slip" name="file" type="file" accept="image/png,image/jpeg,image/webp" required disabled={busy} /></Field>
<Button type="submit" disabled={busy}>{busy ? "กำลังตรวจสอบ..." : "ตรวจสอบสลิป"}</Button>
<Button type="submit" className="min-h-11" disabled={busy}>{busy ? "กำลังตรวจสอบ..." : "ตรวจสอบสลิป"}</Button>
</form></>}
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
</div>;
@@ -70,15 +70,15 @@ export function MobileSlipHandoff({ checkoutId }: { checkoutId: string }) {
}, [checkoutId, link, router]);
return <div className="flex w-full flex-col items-start gap-3">
<Button type="button" variant="link" className="h-auto px-0 text-foreground underline" disabled={busy}
<Button type="button" variant="link" className="min-h-11 px-0 text-foreground underline" disabled={busy}
onClick={() => { if (link && !expired && secondsLeft > 0) setOpen(true); else void createLink(); }}>
{busy ? "กำลังสร้าง QR..." : "อัปโหลดจากมือถือ"}
{busy ? "กำลังสร้าง QR..." : "ใช้มือถืออีกเครื่องอัปโหลด"}
</Button>
<Dialog open={open} onOpenChange={setOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>อัปโหลดสลิปจากมือถือ</DialogTitle>
<DialogDescription>สแกน QR ด้วยมือถือ แล้วเลือกรูปสลิปจากแอปธนาคาร</DialogDescription>
<DialogTitle>ใช้มือถืออีกเครื่องอัปโหลดสลิป</DialogTitle>
<DialogDescription>สแกน QR นี้ด้วยมือถืออีกเครื่อง แล้วเลือกรูปสลิปจากแอปธนาคาร หากใช้มือถือเครื่องนี้ ให้ปิดหน้าต่างแล้วเลือกไฟล์สลิปได้โดยตรง</DialogDescription>
</DialogHeader>
{link && <div className="flex flex-col items-center gap-3">
{!expired && <>
@@ -86,7 +86,7 @@ export function MobileSlipHandoff({ checkoutId }: { checkoutId: string }) {
<img src={link.qr} alt="QR สำหรับเปิดหน้าอัปโหลดสลิปบนมือถือ" width={240} height={240} />
<p className="text-center text-sm text-muted-foreground">เวลาที่เหลือ {timeLeft}</p>
</>}
{expired && <p className="text-center text-sm text-muted-foreground">ลิงก์หมดอายุแล้ว ปิดหน้าต่างนี้แล้วกดอัปโหลดจากมือถืออีกครั้ง</p>}
{expired && <p className="text-center text-sm text-muted-foreground">ลิงก์หมดอายุแล้ว ปิดหน้าต่างนี้แล้วกดใช้มือถืออีกเครื่องอัปโหลดอีกครั้ง</p>}
</div>}
{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}
</DialogContent>
+3 -1
View File
@@ -53,7 +53,9 @@ export function CommissionPaymentForm({ checkoutId, amountBaht, qr, expiresAt, s
{/* Generated locally from the fixed checkout amount and configured recipient. */}
{/* eslint-disable-next-line @next/next/no-img-element */}
<img src={qr} alt={`PromptPay QR สำหรับชำระเงิน ${amountBaht} บาท`} width={300} height={300} />
<Button nativeButton={false} className="min-h-11" render={<a href={qr} download="promptpay-qr.png" />}>บันทึก QR สำหรับชำระเงิน</Button>
<strong>฿{amountBaht}</strong>
<p className="text-center text-sm text-muted-foreground">ใช้มือถือเครื่องนี้ชำระเงิน: บันทึก QR แล้วเลือกรูปในแอปธนาคาร</p>
<p className="text-sm text-muted-foreground">โอนเงินแล้วอัปโหลดรูปสลิปเพื่อยืนยัน</p>
<p className="text-sm text-muted-foreground">เวลาชำระเงินที่เหลือ {String(Math.floor(secondsLeft / 60)).padStart(2, "0")}:{String(secondsLeft % 60).padStart(2, "0")}</p>
</CardContent></Card>
@@ -61,6 +63,6 @@ export function CommissionPaymentForm({ checkoutId, amountBaht, qr, expiresAt, s
<Field><FieldLabel htmlFor="slip">อัปโหลดสลิปที่ชำระเงินแล้ว</FieldLabel>
<Input id="slip" name="file" type="file" accept="image/png,image/jpeg,image/webp" required />
<MobileSlipHandoff checkoutId={checkoutId} /></Field>
<Button type="submit" disabled={busy}>{busy ? "กำลังตรวจสอบ..." : "ตรวจสอบสลิปและเปิด Ticket"}</Button>
<Button type="submit" className="min-h-11" disabled={busy}>{busy ? "กำลังตรวจสอบ..." : "ตรวจสอบสลิปและเปิดแชตคำขอ"}</Button>
</FieldGroup>{error && <Alert variant="destructive"><AlertDescription>{error}</AlertDescription></Alert>}</form></>;
}
+7 -6
View File
@@ -171,11 +171,11 @@ export function CommissionRequestForm({ catalog, userId }: { catalog: Catalog; u
}} triggerProps={{ size: "sm", className: "h-6 min-w-0 flex-1 justify-center px-1 text-xs [&>svg]:hidden" }} />
</div>
<p className="mt-1 truncate text-center text-xs font-medium" title={character?.name}>{character?.name ?? "เลือกตัวละคร"}</p>
{errorsAt(`teams.${teamIndex}.members.${memberIndex}`).length > 0 && <FieldError>{errorsAt(`teams.${teamIndex}.members.${memberIndex}`).join(" · ")}</FieldError>}
{errorsAt(`teams.${teamIndex}.members.${memberIndex}`).length > 0 && <FieldError>{errorsAt(`teams.${teamIndex}.members.${memberIndex}`).join(" - ")}</FieldError>}
</div>;
})}
</div>
{issues.some((issue) => issue.path === `teams.${teamIndex}`) && <FieldError data-commission-errors tabIndex={-1}>{issues.filter((issue) => issue.path === `teams.${teamIndex}`).map((issue) => issue.message).join(" · ")}</FieldError>}
{issues.some((issue) => issue.path === `teams.${teamIndex}`) && <FieldError data-commission-errors tabIndex={-1}>{issues.filter((issue) => issue.path === `teams.${teamIndex}`).map((issue) => issue.message).join(" - ")}</FieldError>}
<Button variant="outline" onClick={() => patch({ teams: request.teams.filter((_, index) => index !== teamIndex) })}>ลบทีม</Button>
</CardContent></Card>)}
<Button variant="outline" onClick={() => patch({ teams: [...request.teams, blankTeam()] })}>เพิ่มทีม</Button>
@@ -234,7 +234,7 @@ export function CommissionRequestForm({ catalog, userId }: { catalog: Catalog; u
{catalog.characters.find((item) => item.key === set.characterKey)?.name ?? "เลือกตัวละคร"}</p>
</div>
</div>
{errorsAt(`weapons.${index}`).length > 0 && <FieldError data-commission-errors tabIndex={-1}>{errorsAt(`weapons.${index}`).join(" · ")}</FieldError>}
{errorsAt(`weapons.${index}`).length > 0 && <FieldError data-commission-errors tabIndex={-1}>{errorsAt(`weapons.${index}`).join(" - ")}</FieldError>}
<Button variant="outline" disabled={set.weaponKeys.length >= 100} onClick={() => {
const weapons = structuredClone(request.weapons); weapons[index].weaponKeys.push(""); patch({ weapons });
}}>เพิ่มอาวุธ</Button>
@@ -263,21 +263,22 @@ export function CommissionRequestForm({ catalog, userId }: { catalog: Catalog; u
{catalog.characters.find((item) => item.key === set.characterKey)?.name ?? "เลือกตัวละคร"}</p>
</div>
</div>
{errorsAt(`constellations.${index}`).length > 0 && <FieldError data-commission-errors tabIndex={-1}>{errorsAt(`constellations.${index}`).join(" · ")}</FieldError>}
{errorsAt(`constellations.${index}`).length > 0 && <FieldError data-commission-errors tabIndex={-1}>{errorsAt(`constellations.${index}`).join(" - ")}</FieldError>}
<Button variant="outline" onClick={() => patch({ constellations: request.constellations.filter((_, i) => i !== index) })}>ลบชุดนี้</Button>
</FieldGroup>)}
<Button variant="outline" onClick={() => patch({ constellations: [...request.constellations, { characterKey: "", levels: [] }] })}>เพิ่มชุดกลุ่มดาว</Button>
</CardContent></Card>
<Card className="sticky bottom-2 z-20 shadow-lg">
<CardHeader className="hidden sm:grid"><CardTitle>สรุปคำขอ</CardTitle><CardDescription>
{request.teams.length} ทีม · {request.weapons.reduce((sum, set) => sum + set.weaponKeys.filter(Boolean).length, 0)} อาวุธ · {request.constellations.reduce((sum, set) => sum + set.levels.length, 0)} ระดับกลุ่มดาว
{request.teams.length} ทีม - {request.weapons.reduce((sum, set) => sum + set.weaponKeys.filter(Boolean).length, 0)} อาวุธ - {request.constellations.reduce((sum, set) => sum + set.levels.length, 0)} ระดับกลุ่มดาว
</CardDescription></CardHeader>
<CardFooter className="flex flex-wrap items-center justify-between gap-3">
{error && <Alert variant="destructive" role="alert" className="w-full"><AlertDescription>{error}</AlertDescription></Alert>}
<div><strong className="text-lg">รวม ฿{commissionPrice(request)}</strong>
<p className="text-xs text-muted-foreground sm:hidden">{request.teams.length} ทีม - {request.weapons.reduce((sum, set) => sum + set.weaponKeys.filter(Boolean).length, 0)} อาวุธ - {request.constellations.reduce((sum, set) => sum + set.levels.length, 0)} ระดับกลุ่มดาว</p>
{request.teams.length === 0 && request.weapons.length + request.constellations.length > 0 && <p className="text-sm text-muted-foreground">รวมค่าบริการพื้นฐาน ฿100</p>}
</div>
<Button disabled={!hydrated || busy || commissionPrice(request) === 0} onClick={() => void submit()}>{busy ? "กำลังสร้างคำขอ…" : "ยืนยันและชำระเงิน"}</Button>
<Button className="min-h-11" disabled={!hydrated || busy || commissionPrice(request) === 0} onClick={() => void submit()}>{busy ? "กำลังสร้างคำขอ…" : "ยืนยันและชำระเงิน"}</Button>
</CardFooter>
</Card>
</FieldSet>;
+3 -2
View File
@@ -645,10 +645,11 @@ export function CommissionTicketChat({
<Card className="h-full min-h-0 min-w-0 gap-0 py-0">
<CardHeader className="shrink-0 border-b py-3">
<CardTitle>แชต{admin ? "กับลูกค้า" : "กับทีมงาน"}</CardTitle>
{!admin && <CardDescription>สถานะนี้บอกว่าส่งข้อความได้หรือไม่ สอบถามความคืบหน้างานได้ในแชต</CardDescription>}
<CardAction className="flex flex-wrap items-center gap-2">
<Badge variant={status === "open" ? "outline" : "destructive"}
className={status === "open" ? "border-success/30 bg-success/10 text-success" : undefined}>
{status === "open" ? "เปิดอยู่" : "ปิดแล้ว"}
{admin ? (status === "open" ? "เปิดอยู่" : "ปิดแล้ว") : (status === "open" ? "แชตเปิดอยู่" : "แชตปิดแล้ว")}
</Badge>
{admin &&
(status === "open" ? (
@@ -1080,7 +1081,7 @@ export function CommissionTicketChat({
</div>
))}
</div>
<p className="mt-1 text-xs text-muted-foreground">{images.length}/10 รูป · สูงสุด 10 MiB ต่อรูป</p>
<p className="mt-1 text-xs text-muted-foreground">{images.length}/10 รูป - สูงสุด 10 MiB ต่อรูป</p>
</div>
)}
<Field>