From 523d46874b19e2adeae3e130a78110bc8e3e5380 Mon Sep 17 00:00:00 2001 From: luorijun Date: Tue, 24 Mar 2026 17:14:50 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E4=BB=B7=E6=A0=BC=E6=8A=98?= =?UTF-8?q?=E6=89=A3=E5=8A=A8=E6=80=81=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/actions/product.ts | 46 +++++ src/actions/product_discount.ts | 47 +++++ src/app/(root)/discount/create.tsx | 122 ++++++++++++ src/app/(root)/discount/page.tsx | 131 +++++++++++++ src/app/(root)/discount/update.tsx | 140 ++++++++++++++ src/app/(root)/navigation.tsx | 7 + src/app/(root)/product/batch-discount.tsx | 144 ++++++++++++++ src/app/(root)/product/create.tsx | 220 +++++++++++++++++++++ src/app/(root)/product/page.tsx | 148 +++++++++----- src/app/(root)/product/update.tsx | 225 ++++++++++++++++++++++ src/components/ui/button.tsx | 6 +- src/models/product_discount.ts | 6 + src/models/product_sku.ts | 3 +- 13 files changed, 1189 insertions(+), 56 deletions(-) create mode 100644 src/actions/product_discount.ts create mode 100644 src/app/(root)/discount/create.tsx create mode 100644 src/app/(root)/discount/page.tsx create mode 100644 src/app/(root)/discount/update.tsx create mode 100644 src/app/(root)/product/batch-discount.tsx create mode 100644 src/app/(root)/product/create.tsx create mode 100644 src/app/(root)/product/update.tsx create mode 100644 src/models/product_discount.ts diff --git a/src/actions/product.ts b/src/actions/product.ts index 8156848..07a0261 100644 --- a/src/actions/product.ts +++ b/src/actions/product.ts @@ -19,3 +19,49 @@ export async function getPageProductSku(params: { params, ) } + +export async function createProductSku(data: { + product_id: number + code: string + name: string + price: string + discount_id?: number +}) { + return callByUser("/api/admin/product/sku/create", { + product_id: data.product_id, + code: data.code, + name: data.name, + price: data.price, + discount_id: data.discount_id, + }) +} + +export async function updateProductSku(data: { + id: number + code?: string + name?: string + price?: string + discount_id?: number | null +}) { + return callByUser("/api/admin/product/sku/update", { + id: data.id, + code: data.code, + name: data.name, + price: data.price, + discount_id: data.discount_id, + }) +} + +export async function deleteProductSku(id: number) { + return callByUser("/api/admin/product/sku/remove", { id }) +} + +export async function batchUpdateProductSkuDiscount(data: { + product_id: number + discount_id: number | null +}) { + return callByUser("/api/admin/product/sku/update/discount/batch", { + product_id: data.product_id, + discount_id: data.discount_id, + }) +} diff --git a/src/actions/product_discount.ts b/src/actions/product_discount.ts new file mode 100644 index 0000000..7c7c244 --- /dev/null +++ b/src/actions/product_discount.ts @@ -0,0 +1,47 @@ +"use server" + +import type { PageRecord } from "@/lib/api" +import type { ProductDiscount } from "@/models/product_discount" +import { callByUser } from "./base" + +export async function getAllProductDiscount() { + return callByUser("/api/admin/product/discount/all") +} + +export async function getPageProductDiscount(params: { + page: number + size: number +}) { + return callByUser>( + "/api/admin/product/discount/page", + params, + ) +} + +export async function createProductDiscount(data: { + name: string + discount: string +}) { + return callByUser("/api/admin/product/discount/create", { + name: data.name, + discount: Number(data.discount), + }) +} + +export async function updateProductDiscount(data: { + id: number + name?: string + discount?: string +}) { + return callByUser("/api/admin/product/discount/update", { + id: data.id, + name: data.name, + discount: data.discount ? Number(data.discount) : undefined, + }) +} + +export async function deleteProductDiscount(id: number) { + return callByUser("/api/admin/product/discount/remove", { + id, + }) +} diff --git a/src/app/(root)/discount/create.tsx b/src/app/(root)/discount/create.tsx new file mode 100644 index 0000000..8e6e8fe --- /dev/null +++ b/src/app/(root)/discount/create.tsx @@ -0,0 +1,122 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" +import z from "zod" +import { createProductDiscount } from "@/actions/product_discount" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Input } from "@/components/ui/input" + +const schema = z.object({ + name: z.string().min(1, "请输入折扣名称"), + discount: z.string().min(1, "请输入折扣代码"), +}) + +export function CreateDiscount(props: { onSuccess?: () => void }) { + const [open, setOpen] = useState(false) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: "", + discount: "", + }, + }) + + const onSubmit = async (data: z.infer) => { + try { + const resp = await createProductDiscount(data) + if (resp.success) { + form.reset() + toast.success("折扣创建成功") + props.onSuccess?.() + setOpen(false) + } else { + toast.error(resp.message) + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } + } + + return ( + + + + + + + + 创建折扣 + + +
+ + ( + + 名称 + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + + 代码 + + + {fieldState.invalid && ( + + )} + + )} + /> + +
+ + + + + + + +
+
+ ) +} diff --git a/src/app/(root)/discount/page.tsx b/src/app/(root)/discount/page.tsx new file mode 100644 index 0000000..686b3b7 --- /dev/null +++ b/src/app/(root)/discount/page.tsx @@ -0,0 +1,131 @@ +"use client" +import { format } from "date-fns" +import { Suspense, useState } from "react" +import { toast } from "sonner" +import { + deleteProductDiscount, + getPageProductDiscount, +} from "@/actions/product_discount" +import { DataTable, useDataTable } from "@/components/data-table" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" +import { Button } from "@/components/ui/button" +import type { ProductDiscount } from "@/models/product_discount" +import { CreateDiscount } from "./create" +import { UpdateDiscount } from "./update" + +export default function DiscountPage() { + const table = useDataTable((page, size) => + getPageProductDiscount({ page, size }), + ) + + return ( +
+ {/* 操作栏 */} +
+
+ +
+
+ + {/* 数据表 */} + + + {...table} + columns={[ + { header: "名称", accessorKey: "name" }, + { header: "折扣", accessorKey: "discount" }, + { + header: "创建时间", + accessorKey: "created_at", + cell: ({ row }) => + format(new Date(row.original.created_at), "yyyy-MM-dd HH:mm"), + }, + { + header: "更新时间", + accessorKey: "updated_at", + cell: ({ row }) => + format(new Date(row.original.updated_at), "yyyy-MM-dd HH:mm"), + }, + { + header: "操作", + cell: ({ row }) => ( +
+ + +
+ ), + }, + ]} + /> +
+
+ ) +} + +function DeleteButton({ + discount, + onSuccess, +}: { + discount: ProductDiscount + onSuccess?: () => void +}) { + const [loading, setLoading] = useState(false) + + const handleConfirm = async () => { + setLoading(true) + try { + const resp = await deleteProductDiscount(discount.id) + if (resp.success) { + toast.success("删除成功") + onSuccess?.() + } else { + toast.error(resp.message ?? "删除失败") + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } finally { + setLoading(false) + } + } + + return ( + + + + + + + 确认删除 + + 确定要删除折扣「{discount.name}」吗?此操作不可撤销。 + + + + 取消 + + 删除 + + + + + ) +} diff --git a/src/app/(root)/discount/update.tsx b/src/app/(root)/discount/update.tsx new file mode 100644 index 0000000..e8ab94d --- /dev/null +++ b/src/app/(root)/discount/update.tsx @@ -0,0 +1,140 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" +import z from "zod" +import { updateProductDiscount } from "@/actions/product_discount" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import type { ProductDiscount } from "@/models/product_discount" + +const schema = z.object({ + name: z.string().min(1, "请输入折扣名称"), + discount: z.string().min(1, "请输入折扣代码"), +}) + +export function UpdateDiscount(props: { + discount: ProductDiscount + onSuccess?: () => void +}) { + const [open, setOpen] = useState(false) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: props.discount.name, + discount: String(props.discount.discount), + }, + }) + + const onSubmit = async (data: z.infer) => { + try { + const resp = await updateProductDiscount({ + id: props.discount.id, + ...data, + }) + if (resp.success) { + toast.success("折扣修改成功") + props.onSuccess?.() + setOpen(false) + } else { + toast.error(resp.message) + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } + } + + const handleOpenChange = (value: boolean) => { + if (value) { + form.reset({ + name: props.discount.name, + discount: String(props.discount.discount), + }) + } + setOpen(value) + } + + return ( + + + + + + + + 修改折扣 + + +
+ + ( + + 名称 + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + + 代码 + + + {fieldState.invalid && ( + + )} + + )} + /> + +
+ + + + + + + +
+
+ ) +} diff --git a/src/app/(root)/navigation.tsx b/src/app/(root)/navigation.tsx index 1356ca8..763d8b2 100644 --- a/src/app/(root)/navigation.tsx +++ b/src/app/(root)/navigation.tsx @@ -14,6 +14,8 @@ import { Package, Shield, ShoppingBag, + SquarePercent, + SquarePercentIcon, Users, } from "lucide-react" import Link from "next/link" @@ -194,6 +196,11 @@ export default function Navigation() { {/* 运营 */} + diff --git a/src/app/(root)/product/batch-discount.tsx b/src/app/(root)/product/batch-discount.tsx new file mode 100644 index 0000000..4988bae --- /dev/null +++ b/src/app/(root)/product/batch-discount.tsx @@ -0,0 +1,144 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useEffect, useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" +import z from "zod" +import { batchUpdateProductSkuDiscount } from "@/actions/product" +import { getAllProductDiscount } from "@/actions/product_discount" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import type { ProductDiscount } from "@/models/product_discount" + +const schema = z.object({ + discount_id: z.string().min(1, "请选择折扣"), +}) + +export function BatchUpdateDiscount(props: { + productId: number + onSuccess?: () => void +}) { + const [open, setOpen] = useState(false) + const [discounts, setDiscounts] = useState([]) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + discount_id: "", + }, + }) + + useEffect(() => { + if (open) { + getAllProductDiscount().then(resp => { + if (resp.success) { + setDiscounts(resp.data) + } + }) + } + }, [open]) + + const onSubmit = async (data: z.infer) => { + try { + const resp = await batchUpdateProductSkuDiscount({ + product_id: props.productId, + discount_id: + data.discount_id === "none" ? null : Number(data.discount_id), + }) + if (resp.success) { + toast.success("批量配置折扣成功") + props.onSuccess?.() + setOpen(false) + } else { + toast.error(resp.message ?? "操作失败") + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } + } + + const handleOpenChange = (value: boolean) => { + if (!value) { + form.reset() + } + setOpen(value) + } + + return ( + + + + + + + + 批量配置折扣 + + +
+ + ( + + 折扣 + + {fieldState.invalid && ( + + )} + + )} + /> + +
+ + + + + + + +
+
+ ) +} diff --git a/src/app/(root)/product/create.tsx b/src/app/(root)/product/create.tsx new file mode 100644 index 0000000..b2015db --- /dev/null +++ b/src/app/(root)/product/create.tsx @@ -0,0 +1,220 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useEffect, useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" +import z from "zod" +import { createProductSku } from "@/actions/product" +import { getAllProductDiscount } from "@/actions/product_discount" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import type { ProductDiscount } from "@/models/product_discount" + +const schema = z.object({ + code: z.string().min(1, "请输入套餐编码"), + name: z.string().min(1, "请输入套餐名称"), + price: z + .string() + .min(1, "请输入单价") + .refine( + v => !Number.isNaN(Number(v)) && Number(v) > 0, + "请输入有效的正数单价", + ), + discount_id: z.string().optional(), +}) + +export function CreateProductSku(props: { + productId: number + onSuccess?: () => void +}) { + const [open, setOpen] = useState(false) + const [discounts, setDiscounts] = useState([]) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + code: "", + name: "", + price: "", + discount_id: "", + }, + }) + + useEffect(() => { + if (open) { + getAllProductDiscount() + .then(resp => { + if (resp.success) { + setDiscounts(resp.data) + } + }) + .catch(e => toast.error(e.message)) + } + }, [open]) + + const onSubmit = async (data: z.infer) => { + try { + const resp = await createProductSku({ + product_id: props.productId, + code: data.code, + name: data.name, + price: data.price, + discount_id: + data.discount_id && data.discount_id !== "" + ? Number(data.discount_id) + : undefined, + }) + if (resp.success) { + form.reset() + toast.success("套餐创建成功") + props.onSuccess?.() + setOpen(false) + } else { + toast.error(resp.message) + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } + } + + const handleOpenChange = (value: boolean) => { + if (!value) { + form.reset() + } + setOpen(value) + } + + return ( + + + + + + + + 新建套餐 + + +
+ + ( + + 套餐编码 + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + 套餐名称 + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + 单价 + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + 折扣 + + {fieldState.invalid && ( + + )} + + )} + /> + +
+ + + + + + + +
+
+ ) +} diff --git a/src/app/(root)/product/page.tsx b/src/app/(root)/product/page.tsx index 9c08b4a..1bb93c8 100644 --- a/src/app/(root)/product/page.tsx +++ b/src/app/(root)/product/page.tsx @@ -1,19 +1,33 @@ "use client" import { format } from "date-fns" -import { EyeIcon, EyeOffIcon, PlusIcon, TrashIcon } from "lucide-react" import { Suspense, useCallback, useEffect, useMemo, useState } from "react" -import { getAllProduct, getPageProductSku } from "@/actions/product" +import { toast } from "sonner" +import { + deleteProductSku, + getAllProduct, + getPageProductSku, +} from "@/actions/product" import { DataTable, useDataTable } from "@/components/data-table" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" import { Badge } from "@/components/ui/badge" import { Button } from "@/components/ui/button" -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip" import { cn } from "@/lib/utils" import type { Product } from "@/models/product" +import type { ProductSku } from "@/models/product_sku" +import { BatchUpdateDiscount } from "./batch-discount" +import { CreateProductSku } from "./create" +import { UpdateProductSku } from "./update" export default function ProductPage() { const [selected, setSelected] = useState(undefined) @@ -36,7 +50,6 @@ function Products(props: { const resp = await getAllProduct() if (resp.success) { setList(resp.data) - console.log(resp.data) } }, []) @@ -52,46 +65,10 @@ function Products(props: {

产品列表

-
- - - - - -

新建产品

-
-
- - {!!selected && ( - - - - - -

{selected.status ? "禁用产品" : "启用产品"}

-
-
- )} - - - - - - -

删除产品

-
-
-
-
    +
      {list.map(item => ( -
    • +
    • +
      + +
      - classNames={{ root: "overflow-auto", }} @@ -138,10 +122,13 @@ function ProductSkus(props: { selected?: number }) { { header: "套餐编码", accessorKey: "code" }, { header: "套餐名称", accessorKey: "name" }, { header: "单价", accessorKey: "price" }, - { header: "折扣", accessorKey: "discount" }, + { header: "折扣", accessorFn: row => row.discount?.name ?? "—" }, { header: "最终价格", - accessorFn: row => Number(row.price) * row.discount, + accessorFn: row => + row.discount + ? (Number(row.price) * Number(row.discount.discount)) / 100 + : Number(row.price), }, { header: "创建时间", @@ -151,10 +138,67 @@ function ProductSkus(props: { selected?: number }) { header: "更新时间", accessorFn: row => format(row.updated_at, "yyyy-MM-dd HH:mm"), }, - { header: "操作", cell: () =>
      }, + { + header: "操作", + cell: ({ row }) => ( +
      + + +
      + ), + }, ]} />
      ) } + +function DeleteButton(props: { sku: ProductSku; onSuccess?: () => void }) { + const [loading, setLoading] = useState(false) + + const handleConfirm = async () => { + setLoading(true) + try { + const resp = await deleteProductSku(props.sku.id) + if (resp.success) { + toast.success("删除成功") + props.onSuccess?.() + } else { + toast.error(resp.message ?? "删除失败") + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } finally { + setLoading(false) + } + } + + return ( + + + + + + + 确认删除 + + 确定要删除套餐「{props.sku.name}」吗?此操作不可撤销。 + + + + 取消 + + 删除 + + + + + ) +} diff --git a/src/app/(root)/product/update.tsx b/src/app/(root)/product/update.tsx new file mode 100644 index 0000000..73ffaaf --- /dev/null +++ b/src/app/(root)/product/update.tsx @@ -0,0 +1,225 @@ +import { zodResolver } from "@hookform/resolvers/zod" +import { useEffect, useState } from "react" +import { Controller, useForm } from "react-hook-form" +import { toast } from "sonner" +import z from "zod" +import { updateProductSku } from "@/actions/product" +import { getAllProductDiscount } from "@/actions/product_discount" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + Field, + FieldError, + FieldGroup, + FieldLabel, +} from "@/components/ui/field" +import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import type { ProductDiscount } from "@/models/product_discount" +import type { ProductSku } from "@/models/product_sku" + +const schema = z.object({ + code: z.string().min(1, "请输入套餐编码"), + name: z.string().min(1, "请输入套餐名称"), + price: z + .string() + .min(1, "请输入单价") + .refine( + v => !Number.isNaN(Number(v)) && Number(v) > 0, + "请输入有效的正数单价", + ), + discount_id: z.string().optional(), +}) + +export function UpdateProductSku(props: { + sku: ProductSku + onSuccess?: () => void +}) { + const [open, setOpen] = useState(false) + const [discounts, setDiscounts] = useState([]) + + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + code: props.sku.code, + name: props.sku.name, + price: props.sku.price, + discount_id: props.sku.discount ? String(props.sku.discount.id) : "", + }, + }) + + useEffect(() => { + if (open) { + getAllProductDiscount().then(resp => { + if (resp.success) { + setDiscounts(resp.data) + } + }) + } + }, [open]) + + const onSubmit = async (data: z.infer) => { + try { + const resp = await updateProductSku({ + id: props.sku.id, + code: data.code, + name: data.name, + price: data.price, + discount_id: + data.discount_id && data.discount_id !== "" + ? Number(data.discount_id) + : null, + }) + if (resp.success) { + toast.success("套餐修改成功") + props.onSuccess?.() + setOpen(false) + } else { + toast.error(resp.message) + } + } catch (error) { + const message = error instanceof Error ? error.message : error + toast.error(`接口请求错误: ${message}`) + } + } + + const handleOpenChange = (value: boolean) => { + if (value) { + form.reset({ + code: props.sku.code, + name: props.sku.name, + price: props.sku.price, + discount_id: props.sku.discount ? String(props.sku.discount.id) : "", + }) + } + setOpen(value) + } + + return ( + + + + + + + + 修改套餐 + + +
      + + ( + + 套餐编码 + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + 套餐名称 + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + 单价 + + {fieldState.invalid && ( + + )} + + )} + /> + + ( + + 折扣 + + {fieldState.invalid && ( + + )} + + )} + /> + +
      + + + + + + + +
      +
      + ) +} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 37a7d4b..3bda484 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -1,6 +1,6 @@ -import * as React from "react" import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" +import type * as React from "react" import { cn } from "@/lib/utils" @@ -13,7 +13,7 @@ const buttonVariants = cva( destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + "border bg-background hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: @@ -33,7 +33,7 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } + }, ) function Button({ diff --git a/src/models/product_discount.ts b/src/models/product_discount.ts new file mode 100644 index 0000000..95ebbd7 --- /dev/null +++ b/src/models/product_discount.ts @@ -0,0 +1,6 @@ +import type { Model } from "./base/model" + +export type ProductDiscount = Model & { + name: string + discount: number +} diff --git a/src/models/product_sku.ts b/src/models/product_sku.ts index 048ce3f..27d246b 100644 --- a/src/models/product_sku.ts +++ b/src/models/product_sku.ts @@ -1,12 +1,13 @@ import type { Model } from "./base/model" import type { Product } from "./product" +import type { ProductDiscount } from "./product_discount" export type ProductSku = Model & { product_id: number code: string name: string price: string - discount: number product?: Product + discount?: ProductDiscount }