🎉 feat: initialization

This commit is contained in:
web
2025-11-26 19:56:16 -08:00
commit a801849fb2
553 changed files with 213088 additions and 0 deletions
@@ -0,0 +1,397 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@workspace/ui/components/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@workspace/ui/components/form";
import {
RadioGroup,
RadioGroupItem,
} from "@workspace/ui/components/radio-group";
import { ScrollArea } from "@workspace/ui/components/scroll-area";
import {
Sheet,
SheetContent,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@workspace/ui/components/sheet";
import { Combobox } from "@workspace/ui/composed/combobox";
import { DatePicker } from "@workspace/ui/composed/date-picker";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
import { unitConversion } from "@workspace/ui/utils/unit-conversions";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import { useSubscribe } from "@/stores/subscribe";
const formSchema = z.object({
name: z.string(),
code: z.string().optional(),
count: z.number().optional(),
type: z.number().optional(),
discount: z.number().optional(),
start_time: z.number().optional(),
expire_time: z.number().optional(),
subscribe: z.array(z.number()).nullish(),
user_limit: z.number().optional(),
});
interface CouponFormProps<T> {
onSubmit: (data: T) => Promise<boolean> | boolean;
initialValues?: T;
loading?: boolean;
trigger: string;
title: string;
}
export default function CouponForm<T extends Record<string, any>>({
onSubmit,
initialValues,
loading,
trigger,
title,
}: CouponFormProps<T>) {
const { t } = useTranslation("coupon");
const [open, setOpen] = useState(false);
const form = useForm({
resolver: zodResolver(formSchema),
defaultValues: {
type: 1,
...initialValues,
} as any,
});
useEffect(() => {
form?.reset(initialValues);
}, [form, initialValues]);
async function handleSubmit(data: { [x: string]: any }) {
const bool = await onSubmit(data as T);
if (bool) setOpen(false);
}
const type = form.watch("type");
const { subscribes } = useSubscribe();
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<Button
onClick={() => {
form.reset();
setOpen(true);
}}
>
{trigger}
</Button>
</SheetTrigger>
<SheetContent className="w-[500px] max-w-full md:max-w-screen-md">
<SheetHeader>
<SheetTitle>{title}</SheetTitle>
</SheetHeader>
<ScrollArea className="h-[calc(100vh-48px-36px-36px-env(safe-area-inset-top))]">
<Form {...form}>
<form
className="space-y-4 px-6 pt-4"
onSubmit={form.handleSubmit(handleSubmit)}
>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("form.name", "Name")}</FormLabel>
<FormControl>
<EnhancedInput
onValueChange={(value) => {
form.setValue(field.name, value);
}}
placeholder={t(
"form.enterCouponName",
"Enter Coupon Name"
)}
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="code"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("form.customCouponCode", "Custom Coupon Code")}
</FormLabel>
<FormControl>
<EnhancedInput
placeholder={t(
"form.customCouponCodePlaceholder",
"Custom Coupon Code (leave blank for auto-generation)"
)}
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="type"
render={({ field }) => (
<FormItem>
<FormLabel>{t("form.type", "Coupon Type")}</FormLabel>
<FormControl>
<RadioGroup
className="flex gap-2"
defaultValue={String(field.value)}
onValueChange={(value) => {
form.setValue(field.name, Number(value));
form.setValue("discount", "");
}}
>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="1" />
</FormControl>
<FormLabel className="font-normal">
{t(
"form.percentageDiscount",
"Percentage Discount"
)}
</FormLabel>
</FormItem>
<FormItem className="flex items-center space-x-3 space-y-0">
<FormControl>
<RadioGroupItem value="2" />
</FormControl>
<FormLabel className="font-normal">
{t("form.amountDiscount", "Amount Discount")}
</FormLabel>
</FormItem>
</RadioGroup>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{type === 1 && (
<FormField
control={form.control}
name="discount"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("form.percentageDiscount", "Percentage Discount")}
</FormLabel>
<FormControl>
<EnhancedInput
max={100}
min={1}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
placeholder={t("form.enterValue", "Enter Value")}
suffix="%"
type="number"
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
{type === 2 && (
<FormField
control={form.control}
name="discount"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("form.amountDiscount", "Amount Discount")}
</FormLabel>
<FormControl>
<EnhancedInput
formatInput={(value) =>
unitConversion("centsToDollars", value)
}
formatOutput={(value) =>
unitConversion("dollarsToCents", value)
}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
placeholder={t("form.enterValue", "Enter Value")}
type="number"
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
<FormField
control={form.control}
name="subscribe"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("form.specifiedServer", "Specified Subscription")}
</FormLabel>
<FormControl>
<Combobox<number, true>
multiple
onChange={(value) => {
form.setValue(field.name, value);
}}
options={subscribes?.map((item) => ({
value: item.id!,
label: item.name!,
}))}
placeholder={t(
"form.selectServer",
"Select Subscription"
)}
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="start_time"
render={({ field }) => (
<FormItem>
<FormLabel>{t("form.startTime", "Start Time")}</FormLabel>
<FormControl>
<DatePicker
disabled={(date: Date) =>
date < new Date(Date.now() - 24 * 60 * 60 * 1000)
}
onChange={(value: number | undefined) => {
form.setValue(field.name, value);
}}
placeholder={t("form.enterValue", "Enter Value")}
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expire_time"
render={({ field }) => (
<FormItem>
<FormLabel>{t("form.expireTime", "Expire Time")}</FormLabel>
<FormControl>
<DatePicker
onChange={(value: number | undefined) => {
form.setValue(field.name, value);
}}
placeholder={t("form.enterValue", "Enter Value")}
value={field.value}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="count"
render={({ field }) => (
<FormItem>
<FormLabel>{t("form.count", "Max Usage Count")}</FormLabel>
<FormControl>
<EnhancedInput
min={0}
placeholder={t(
"form.countPlaceholder",
"Max Usage Count (leave blank for no limit)"
)}
step={1}
type="number"
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="user_limit"
render={({ field }) => (
<FormItem>
<FormLabel>
{t("form.userLimit", "Max Usage Count per User")}
</FormLabel>
<FormControl>
<EnhancedInput
min={0}
placeholder={t(
"form.userLimitPlaceholder",
"Max Usage Count per User (leave blank for no limit)"
)}
step={1}
type="number"
{...field}
onValueChange={(value) => {
form.setValue(field.name, value);
}}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</form>
</Form>
</ScrollArea>
<SheetFooter className="flex-row justify-end gap-2 pt-3">
<Button
disabled={loading}
onClick={() => {
setOpen(false);
}}
variant="outline"
>
{t("form.cancel", "Cancel")}
</Button>
<Button disabled={loading} onClick={form.handleSubmit(handleSubmit)}>
{loading && (
<Icon className="mr-2 animate-spin" icon="mdi:loading" />
)}{" "}
{t("form.confirm", "Confirm")}
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
+240
View File
@@ -0,0 +1,240 @@
import { Badge } from "@workspace/ui/components/badge";
import { Button } from "@workspace/ui/components/button";
import { Switch } from "@workspace/ui/components/switch";
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
import {
ProTable,
type ProTableActions,
} from "@workspace/ui/composed/pro-table/pro-table";
import {
batchDeleteCoupon,
createCoupon,
deleteCoupon,
getCouponList,
updateCoupon,
} from "@workspace/ui/services/admin/coupon";
import { useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { Display } from "@/components/display";
import { useSubscribe } from "@/stores/subscribe";
import { formatDate } from "@/utils/common";
import CouponForm from "./coupon-form";
export default function Coupon() {
const { t } = useTranslation("coupon");
const [loading, setLoading] = useState(false);
const { subscribes } = useSubscribe();
const ref = useRef<ProTableActions>(null);
return (
<ProTable<API.Coupon, { group_id: number; query: string }>
action={ref}
actions={{
render: (row) => [
<CouponForm<API.UpdateCouponRequest>
initialValues={row}
key="edit"
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await updateCoupon({ ...row, ...values });
toast.success(t("updateSuccess", "Update Success"));
ref.current?.refresh();
setLoading(false);
return true;
} catch (_error) {
setLoading(false);
return false;
}
}}
title={t("editCoupon", "Edit Coupon")}
trigger={t("edit", "Edit")}
/>,
<ConfirmButton
cancelText={t("cancel", "Cancel")}
confirmText={t("confirm", "Confirm")}
description={t(
"deleteWarning",
"Once deleted, data cannot be recovered. Please proceed with caution."
)}
key="delete"
onConfirm={async () => {
await deleteCoupon({ id: row.id });
toast.success(t("deleteSuccess", "Delete Success"));
ref.current?.refresh();
}}
title={t("confirmDelete", "Are you sure you want to delete?")}
trigger={
<Button variant="destructive">{t("delete", "Delete")}</Button>
}
/>,
],
batchRender: (rows) => [
<ConfirmButton
cancelText={t("cancel", "Cancel")}
confirmText={t("confirm", "Confirm")}
description={t(
"deleteWarning",
"Once deleted, data cannot be recovered. Please proceed with caution."
)}
key="delete"
onConfirm={async () => {
await batchDeleteCoupon({ ids: rows.map((item) => item.id) });
toast.success(t("deleteSuccess", "Delete Success"));
ref.current?.reset();
}}
title={t("confirmDelete", "Are you sure you want to delete?")}
trigger={
<Button variant="destructive">{t("delete", "Delete")}</Button>
}
/>,
],
}}
columns={[
{
accessorKey: "enable",
header: t("enable", "Enable"),
cell: ({ row }) => (
<Switch
defaultChecked={row.getValue("enable")}
onCheckedChange={async (checked) => {
await updateCoupon({
...row.original,
enable: checked,
} as API.UpdateCouponRequest);
ref.current?.refresh();
}}
/>
),
},
{
accessorKey: "name",
header: t("name", "Name"),
},
{
accessorKey: "code",
header: t("code", "Code"),
},
{
accessorKey: "type",
header: t("type", "Type"),
cell: ({ row }) => (
<Badge
variant={row.getValue("type") === 1 ? "default" : "secondary"}
>
{row.getValue("type") === 1
? t("percentage", "Percentage")
: t("amount", "Amount")}
</Badge>
),
},
{
accessorKey: "discount",
header: t("discount", "Discount"),
cell: ({ row }) => (
<Badge
variant={row.getValue("type") === 1 ? "default" : "secondary"}
>
{row.getValue("type") === 1 ? (
`${row.original.discount} %`
) : (
<Display type="currency" value={row.original.discount} />
)}
</Badge>
),
},
{
accessorKey: "count",
header: t("count", "Count"),
cell: ({ row }) => (
<div className="flex flex-col">
<span>
{t("count", "Count")}:{" "}
{row.original.count === 0
? t("unlimited", "Unlimited")
: row.original.count}
</span>
<span>
{t("remainingTimes", "Remaining")}:{" "}
{row.original.count === 0
? t("unlimited", "Unlimited")
: row.original.count - row.original.used_count}
</span>
<span>
{t("usedTimes", "Usage Times")}: {row.original.used_count}
</span>
</div>
),
},
{
accessorKey: "expire",
header: t("validityPeriod", "Validity Period"),
cell: ({ row }) => {
const { start_time, expire_time } = row.original;
if (start_time) {
return expire_time ? (
<>
{formatDate(start_time)} - {formatDate(expire_time)}
</>
) : start_time ? (
formatDate(start_time)
) : (
"--"
);
}
return "--";
},
},
]}
header={{
toolbar: (
<CouponForm<API.CreateCouponRequest>
loading={loading}
onSubmit={async (values) => {
setLoading(true);
try {
await createCoupon({
...values,
enable: false,
});
toast.success(t("createSuccess", "Create Success"));
ref.current?.refresh();
setLoading(false);
return true;
} catch (_error) {
setLoading(false);
return false;
}
}}
title={t("createCoupon", "Create Coupon")}
trigger={t("create", "Create")}
/>
),
}}
params={[
{
key: "subscribe",
placeholder: t("subscribe", "Subscribe"),
options: subscribes?.map((item) => ({
label: item.name!,
value: String(item.id),
})),
},
{
key: "search",
},
]}
request={async (pagination, filters) => {
const { data } = await getCouponList({
...pagination,
...filters,
});
return {
list: data.data?.list || [],
total: data.data?.total || 0,
};
}}
/>
);
}