✅ admin side for now | 🔄 started FE
This commit is contained in:
@@ -1,200 +0,0 @@
|
||||
import { trpcApiStore } from "$lib/stores/api";
|
||||
import { toast } from "svelte-sonner";
|
||||
import { get } from "svelte/store";
|
||||
import type {
|
||||
CouponModel,
|
||||
CreateCouponPayload,
|
||||
UpdateCouponPayload,
|
||||
} from "./data";
|
||||
import { DiscountType } from "./data";
|
||||
|
||||
export class CouponViewModel {
|
||||
coupons = $state<CouponModel[]>([]);
|
||||
loading = $state(false);
|
||||
formLoading = $state(false);
|
||||
|
||||
// Selected coupon for edit/delete operations
|
||||
selectedCoupon = $state<CouponModel | null>(null);
|
||||
|
||||
async fetchCoupons() {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast.error("API client not initialized");
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
try {
|
||||
const result = await api.coupon.getAllCoupons.query();
|
||||
if (result.error) {
|
||||
toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
console.log(result);
|
||||
|
||||
this.coupons = result.data || [];
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
toast.error("Failed to fetch coupons", {
|
||||
description: "Please try again later",
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async createCoupon(payload: CreateCouponPayload) {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast.error("API client not initialized");
|
||||
}
|
||||
|
||||
this.formLoading = true;
|
||||
try {
|
||||
const result = await api.coupon.createCoupon.mutate(payload);
|
||||
if (result.error) {
|
||||
toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
toast.success("Coupon created successfully");
|
||||
await this.fetchCoupons();
|
||||
return true;
|
||||
} catch (e) {
|
||||
toast.error("Failed to create coupon", {
|
||||
description: "Please try again later",
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
this.formLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async updateCoupon(payload: UpdateCouponPayload) {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast.error("API client not initialized");
|
||||
}
|
||||
|
||||
this.formLoading = true;
|
||||
try {
|
||||
const result = await api.coupon.updateCoupon.mutate(payload);
|
||||
if (result.error) {
|
||||
toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
toast.success("Coupon updated successfully");
|
||||
await this.fetchCoupons();
|
||||
return true;
|
||||
} catch (e) {
|
||||
toast.error("Failed to update coupon", {
|
||||
description: "Please try again later",
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
this.formLoading = false;
|
||||
this.selectedCoupon = null;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCoupon(id: number) {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast.error("API client not initialized");
|
||||
}
|
||||
|
||||
this.formLoading = true;
|
||||
try {
|
||||
const result = await api.coupon.deleteCoupon.mutate({ id });
|
||||
if (result.error) {
|
||||
toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
toast.success("Coupon deleted successfully");
|
||||
await this.fetchCoupons();
|
||||
return true;
|
||||
} catch (e) {
|
||||
toast.error("Failed to delete coupon", {
|
||||
description: "Please try again later",
|
||||
});
|
||||
return false;
|
||||
} finally {
|
||||
this.formLoading = false;
|
||||
this.selectedCoupon = null;
|
||||
}
|
||||
}
|
||||
|
||||
async toggleCouponStatus(id: number, isActive: boolean) {
|
||||
const api = get(trpcApiStore);
|
||||
if (!api) {
|
||||
return toast.error("API client not initialized");
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await api.coupon.toggleCouponStatus.mutate({
|
||||
id,
|
||||
isActive,
|
||||
});
|
||||
if (result.error) {
|
||||
toast.error(result.error.message, {
|
||||
description: result.error.userHint,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
toast.success(
|
||||
`Coupon ${isActive ? "activated" : "deactivated"} successfully`,
|
||||
);
|
||||
await this.fetchCoupons();
|
||||
return true;
|
||||
} catch (e) {
|
||||
toast.error(
|
||||
`Failed to ${isActive ? "activate" : "deactivate"} coupon`,
|
||||
{
|
||||
description: "Please try again later",
|
||||
},
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
selectCoupon(coupon: CouponModel) {
|
||||
this.selectedCoupon = { ...coupon };
|
||||
}
|
||||
|
||||
clearSelectedCoupon() {
|
||||
this.selectedCoupon = null;
|
||||
}
|
||||
|
||||
getDefaultCoupon(): CreateCouponPayload {
|
||||
const today = new Date();
|
||||
const nextMonth = new Date();
|
||||
nextMonth.setMonth(today.getMonth() + 1);
|
||||
|
||||
return {
|
||||
code: "",
|
||||
description: "",
|
||||
discountType: DiscountType.PERCENTAGE,
|
||||
discountValue: 10,
|
||||
maxUsageCount: null,
|
||||
minOrderValue: null,
|
||||
maxDiscountAmount: null,
|
||||
startDate: today.toISOString().split("T")[0],
|
||||
endDate: nextMonth.toISOString().split("T")[0],
|
||||
isActive: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const couponVM = new CouponViewModel();
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@pkg/logic/domains/coupon/data";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@pkg/logic/domains/coupon/repository";
|
||||
@@ -1,52 +0,0 @@
|
||||
import { createTRPCRouter } from "$lib/trpc/t";
|
||||
import { z } from "zod";
|
||||
import { getCouponUseCases } from "./usecases";
|
||||
import { createCouponPayload, updateCouponPayload } from "./data";
|
||||
import { protectedProcedure } from "$lib/server/trpc/t";
|
||||
|
||||
export const couponRouter = createTRPCRouter({
|
||||
getAllCoupons: protectedProcedure.query(async ({}) => {
|
||||
const controller = getCouponUseCases();
|
||||
return controller.getAllCoupons();
|
||||
}),
|
||||
|
||||
getCouponById: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.query(async ({ input }) => {
|
||||
const controller = getCouponUseCases();
|
||||
return controller.getCouponById(input.id);
|
||||
}),
|
||||
|
||||
createCoupon: protectedProcedure
|
||||
.input(createCouponPayload)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const controller = getCouponUseCases();
|
||||
return controller.createCoupon(ctx.user, input);
|
||||
}),
|
||||
|
||||
updateCoupon: protectedProcedure
|
||||
.input(updateCouponPayload)
|
||||
.mutation(async ({ input }) => {
|
||||
const controller = getCouponUseCases();
|
||||
return controller.updateCoupon(input);
|
||||
}),
|
||||
|
||||
deleteCoupon: protectedProcedure
|
||||
.input(z.object({ id: z.number() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const controller = getCouponUseCases();
|
||||
return controller.deleteCoupon(input.id);
|
||||
}),
|
||||
|
||||
toggleCouponStatus: protectedProcedure
|
||||
.input(z.object({ id: z.number(), isActive: z.boolean() }))
|
||||
.mutation(async ({ input }) => {
|
||||
const controller = getCouponUseCases();
|
||||
return controller.toggleCouponStatus(input.id, input.isActive);
|
||||
}),
|
||||
|
||||
getActiveCoupons: protectedProcedure.query(async ({}) => {
|
||||
const controller = getCouponUseCases();
|
||||
return controller.getActiveCoupons();
|
||||
}),
|
||||
});
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@pkg/logic/domains/coupon/usecases";
|
||||
@@ -1,187 +0,0 @@
|
||||
<script lang="ts">
|
||||
import Input from "$lib/components/ui/input/input.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import { Label } from "$lib/components/ui/label";
|
||||
import * as Select from "$lib/components/ui/select";
|
||||
import {
|
||||
type CreateCouponPayload,
|
||||
type UpdateCouponPayload,
|
||||
DiscountType,
|
||||
} from "$lib/domains/coupon/data";
|
||||
import { Textarea } from "$lib/components/ui/textarea";
|
||||
|
||||
export let formData: CreateCouponPayload | UpdateCouponPayload;
|
||||
export let loading = false;
|
||||
export let onSubmit: () => void;
|
||||
export let onCancel: () => void;
|
||||
|
||||
const isNewCoupon = !("id" in formData);
|
||||
const discountTypes = [
|
||||
{ value: DiscountType.PERCENTAGE, label: "Percentage" },
|
||||
{ value: DiscountType.FIXED, label: "Fixed Amount" },
|
||||
];
|
||||
</script>
|
||||
|
||||
<form on:submit|preventDefault={onSubmit} class="space-y-6">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Code -->
|
||||
<div class="space-y-2">
|
||||
<Label for="code">Coupon Code</Label>
|
||||
<Input
|
||||
id="code"
|
||||
bind:value={formData.code}
|
||||
placeholder="e.g. SUMMER20"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Discount Type -->
|
||||
<div class="space-y-2">
|
||||
<Label for="discountType">Discount Type</Label>
|
||||
<Select.Root
|
||||
type="single"
|
||||
required
|
||||
bind:value={formData.discountType}
|
||||
>
|
||||
<Select.Trigger class="w-full">
|
||||
{discountTypes.find((t) => t.value === formData.discountType)
|
||||
?.label || "Select type"}
|
||||
</Select.Trigger>
|
||||
<Select.Content>
|
||||
{#each discountTypes as type}
|
||||
<Select.Item value={type.value}>{type.label}</Select.Item>
|
||||
{/each}
|
||||
</Select.Content>
|
||||
</Select.Root>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Discount Value -->
|
||||
<div class="space-y-2">
|
||||
<Label for="discountValue">
|
||||
{formData.discountType === DiscountType.PERCENTAGE
|
||||
? "Discount Percentage"
|
||||
: "Discount Amount"}
|
||||
</Label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
id="discountValue"
|
||||
type="number"
|
||||
min={0}
|
||||
max={formData.discountType === DiscountType.PERCENTAGE
|
||||
? 100
|
||||
: undefined}
|
||||
step="0.01"
|
||||
bind:value={formData.discountValue}
|
||||
required
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<span class="text-gray-500">
|
||||
{formData.discountType === DiscountType.PERCENTAGE
|
||||
? "%"
|
||||
: "$"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="space-y-2">
|
||||
<Label for="description">Description</Label>
|
||||
<Textarea
|
||||
id="description"
|
||||
bind:value={formData.description}
|
||||
placeholder="Describe what this coupon is for"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Start Date -->
|
||||
<div class="space-y-2">
|
||||
<Label for="startDate">Start Date</Label>
|
||||
<Input
|
||||
id="startDate"
|
||||
type="date"
|
||||
bind:value={formData.startDate}
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- End Date -->
|
||||
<div class="space-y-2">
|
||||
<Label for="endDate">End Date (Optional)</Label>
|
||||
<Input id="endDate" type="date" bind:value={formData.endDate} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<!-- Max Usage Count -->
|
||||
<div class="space-y-2">
|
||||
<Label for="maxUsageCount">Max Usage Count (Optional)</Label>
|
||||
<Input
|
||||
id="maxUsageCount"
|
||||
type="number"
|
||||
min={1}
|
||||
step={1}
|
||||
bind:value={formData.maxUsageCount}
|
||||
placeholder="Leave empty for unlimited"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Max Discount Amount -->
|
||||
<div class="space-y-2">
|
||||
<Label for="maxDiscountAmount">Max Discount Amount (Optional)</Label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
id="maxDiscountAmount"
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.01"
|
||||
bind:value={formData.maxDiscountAmount}
|
||||
placeholder="Leave empty for no limit"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<span class="text-gray-500">$</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Min Order Value -->
|
||||
<div class="space-y-2">
|
||||
<Label for="minOrderValue">Minimum Order Value (Optional)</Label>
|
||||
<div class="relative">
|
||||
<Input
|
||||
id="minOrderValue"
|
||||
type="number"
|
||||
min={0}
|
||||
step="0.01"
|
||||
bind:value={formData.minOrderValue}
|
||||
placeholder="Leave empty for no minimum"
|
||||
/>
|
||||
<div
|
||||
class="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<span class="text-gray-500">$</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="flex justify-end space-x-2">
|
||||
<Button variant="outline" onclick={onCancel} disabled={loading}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" disabled={loading}>
|
||||
{#if loading}
|
||||
Processing...
|
||||
{:else}
|
||||
{isNewCoupon ? "Create Coupon" : "Update Coupon"}
|
||||
{/if}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
@@ -1,297 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { couponVM } from "$lib/domains/coupon/coupon.vm.svelte";
|
||||
import Button from "$lib/components/ui/button/button.svelte";
|
||||
import * as Dialog from "$lib/components/ui/dialog";
|
||||
import * as AlertDialog from "$lib/components/ui/alert-dialog/index";
|
||||
import { Badge } from "$lib/components/ui/badge";
|
||||
import CouponForm from "./coupon-form.svelte";
|
||||
import Title from "$lib/components/atoms/title.svelte";
|
||||
import {
|
||||
PlusIcon,
|
||||
Pencil,
|
||||
Trash2,
|
||||
CheckCircle,
|
||||
XCircle,
|
||||
} from "@lucide/svelte";
|
||||
import Loader from "$lib/components/atoms/loader.svelte";
|
||||
import { DiscountType, type CreateCouponPayload } from "../data";
|
||||
|
||||
let createDialogOpen = $state(false);
|
||||
let editDialogOpen = $state(false);
|
||||
let deleteDialogOpen = $state(false);
|
||||
|
||||
let newCouponData = $state<CreateCouponPayload>(couponVM.getDefaultCoupon());
|
||||
|
||||
async function handleCreateCoupon() {
|
||||
const success = await couponVM.createCoupon(newCouponData);
|
||||
if (success) {
|
||||
createDialogOpen = false;
|
||||
newCouponData = couponVM.getDefaultCoupon();
|
||||
}
|
||||
}
|
||||
|
||||
async function handleUpdateCoupon() {
|
||||
if (!couponVM.selectedCoupon) return;
|
||||
|
||||
const success = await couponVM.updateCoupon({
|
||||
id: couponVM.selectedCoupon.id! ?? -1,
|
||||
...couponVM.selectedCoupon,
|
||||
});
|
||||
if (success) {
|
||||
editDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDeleteCoupon() {
|
||||
if (!couponVM.selectedCoupon) return;
|
||||
|
||||
const success = await couponVM.deleteCoupon(couponVM.selectedCoupon.id!);
|
||||
if (success) {
|
||||
deleteDialogOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateString: string | null | undefined) {
|
||||
if (!dateString) return "None";
|
||||
return new Date(dateString).toLocaleDateString();
|
||||
}
|
||||
|
||||
function formatDiscountValue(type: DiscountType, value: number) {
|
||||
if (type === DiscountType.PERCENTAGE) {
|
||||
return `${value}%`;
|
||||
} else {
|
||||
return `$${value.toFixed(2)}`;
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
setTimeout(async () => {
|
||||
couponVM.fetchCoupons();
|
||||
}, 1000);
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mb-6 flex items-center justify-between">
|
||||
<Title size="h3" weight="semibold">Coupons</Title>
|
||||
<Button onclick={() => (createDialogOpen = true)}>
|
||||
<PlusIcon class="mr-2 h-4 w-4" />
|
||||
New Coupon
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{#if couponVM.loading}
|
||||
<div class="flex items-center justify-center py-20">
|
||||
<Loader />
|
||||
</div>
|
||||
{:else if couponVM.coupons.length === 0}
|
||||
<div class="rounded-lg border py-10 text-center">
|
||||
<p class="text-gray-500">
|
||||
No coupons found. Create your first coupon to get started.
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>Code</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>Discount</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>Valid Period</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-left text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>Status</th
|
||||
>
|
||||
<th
|
||||
class="px-6 py-3 text-right text-xs font-medium uppercase tracking-wider text-gray-500"
|
||||
>Actions</th
|
||||
>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-200 bg-white">
|
||||
{#each couponVM.coupons as coupon}
|
||||
<tr>
|
||||
<td class="whitespace-nowrap px-6 py-4">
|
||||
<div class="font-medium text-gray-900">
|
||||
{coupon.code}
|
||||
</div>
|
||||
{#if coupon.description}
|
||||
<div class="text-sm text-gray-500">
|
||||
{coupon.description}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<td class="whitespace-nowrap px-6 py-4">
|
||||
<Badge
|
||||
>{formatDiscountValue(
|
||||
coupon.discountType,
|
||||
coupon.discountValue,
|
||||
)}</Badge
|
||||
>
|
||||
{#if coupon.maxDiscountAmount}
|
||||
<div class="mt-1 text-xs text-gray-500">
|
||||
Max: ${coupon.maxDiscountAmount}
|
||||
</div>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<td class="whitespace-nowrap px-6 py-4">
|
||||
<div>From: {formatDate(coupon.startDate)}</div>
|
||||
<div>To: {formatDate(coupon.endDate)}</div>
|
||||
</td>
|
||||
|
||||
<td class="whitespace-nowrap px-6 py-4">
|
||||
{#if coupon.isActive}
|
||||
<Badge
|
||||
variant="success"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<CheckCircle class="h-3 w-3" />
|
||||
Active
|
||||
</Badge>
|
||||
{:else}
|
||||
<Badge
|
||||
variant="destructive"
|
||||
class="flex items-center gap-1"
|
||||
>
|
||||
<XCircle class="h-3 w-3" />
|
||||
Inactive
|
||||
</Badge>
|
||||
{/if}
|
||||
</td>
|
||||
|
||||
<td
|
||||
class="flex w-full items-center justify-end gap-2 whitespace-nowrap px-6 py-4 text-right"
|
||||
>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => {
|
||||
couponVM.selectCoupon(coupon);
|
||||
editDialogOpen = true;
|
||||
}}
|
||||
>
|
||||
<Pencil class="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onclick={() => {
|
||||
couponVM.selectCoupon(coupon);
|
||||
deleteDialogOpen = true;
|
||||
}}
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant={coupon.isActive
|
||||
? "destructive"
|
||||
: "default"}
|
||||
size="sm"
|
||||
onclick={() =>
|
||||
couponVM.toggleCouponStatus(
|
||||
coupon.id!,
|
||||
!coupon.isActive,
|
||||
)}
|
||||
>
|
||||
{coupon.isActive ? "Deactivate" : "Activate"}
|
||||
</Button>
|
||||
</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Create Coupon Dialog -->
|
||||
<Dialog.Root bind:open={createDialogOpen}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Create New Coupon</Dialog.Title>
|
||||
<Dialog.Description>
|
||||
Create a new coupon to offer discounts on flight tickets.
|
||||
</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
<CouponForm
|
||||
formData={newCouponData}
|
||||
loading={couponVM.formLoading}
|
||||
onSubmit={handleCreateCoupon}
|
||||
onCancel={() => (createDialogOpen = false)}
|
||||
/>
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<!-- Edit Coupon Dialog -->
|
||||
<Dialog.Root bind:open={editDialogOpen}>
|
||||
<Dialog.Content>
|
||||
<Dialog.Header>
|
||||
<Dialog.Title>Edit Coupon</Dialog.Title>
|
||||
<Dialog.Description>Update this coupon's details.</Dialog.Description>
|
||||
</Dialog.Header>
|
||||
|
||||
{#if couponVM.selectedCoupon}
|
||||
<CouponForm
|
||||
formData={couponVM.selectedCoupon}
|
||||
loading={couponVM.formLoading}
|
||||
onSubmit={handleUpdateCoupon}
|
||||
onCancel={() => (editDialogOpen = false)}
|
||||
/>
|
||||
{/if}
|
||||
</Dialog.Content>
|
||||
</Dialog.Root>
|
||||
|
||||
<!-- Delete Confirmation Dialog -->
|
||||
<AlertDialog.Root bind:open={deleteDialogOpen}>
|
||||
<AlertDialog.Content>
|
||||
<AlertDialog.Header>
|
||||
<AlertDialog.Title>Delete Coupon</AlertDialog.Title>
|
||||
<AlertDialog.Description>
|
||||
Are you sure you want to delete this coupon? This action cannot be
|
||||
undone.
|
||||
</AlertDialog.Description>
|
||||
</AlertDialog.Header>
|
||||
|
||||
{#if couponVM.selectedCoupon}
|
||||
<div class="mb-4 rounded-md bg-gray-100 p-4">
|
||||
<div class="font-bold">{couponVM.selectedCoupon.code}</div>
|
||||
{#if couponVM.selectedCoupon.description}
|
||||
<div class="text-sm text-gray-600">
|
||||
{couponVM.selectedCoupon.description}
|
||||
</div>
|
||||
{/if}
|
||||
<div class="mt-1">
|
||||
Discount: {formatDiscountValue(
|
||||
couponVM.selectedCoupon.discountType,
|
||||
couponVM.selectedCoupon.discountValue,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<AlertDialog.Footer>
|
||||
<AlertDialog.Cancel onclick={() => (deleteDialogOpen = false)}>
|
||||
Cancel
|
||||
</AlertDialog.Cancel>
|
||||
<AlertDialog.Action
|
||||
onclick={handleDeleteCoupon}
|
||||
disabled={couponVM.formLoading}
|
||||
>
|
||||
{couponVM.formLoading ? "Deleting..." : "Delete"}
|
||||
</AlertDialog.Action>
|
||||
</AlertDialog.Footer>
|
||||
</AlertDialog.Content>
|
||||
</AlertDialog.Root>
|
||||
@@ -1,6 +1,5 @@
|
||||
import { authRouter } from "$lib/domains/auth/domain/router";
|
||||
import { ckflowRouter } from "$lib/domains/ckflow/router";
|
||||
import { couponRouter } from "$lib/domains/coupon/router";
|
||||
import { customerInfoRouter } from "$lib/domains/customerinfo/router";
|
||||
import { orderRouter } from "$lib/domains/order/domain/router";
|
||||
import { productRouter } from "$lib/domains/product/router";
|
||||
@@ -12,7 +11,6 @@ export const router = createTRPCRouter({
|
||||
user: userRouter,
|
||||
order: orderRouter,
|
||||
ckflow: ckflowRouter,
|
||||
coupon: couponRouter,
|
||||
product: productRouter,
|
||||
customerInfo: customerInfoRouter,
|
||||
});
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
<script lang="ts">
|
||||
import { pageTitle } from "$lib/hooks/page-title.svelte";
|
||||
import CouponList from "$lib/domains/coupon/view/coupon-list.svelte";
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "$lib/components/ui/tabs/index";
|
||||
|
||||
pageTitle.set("Settings");
|
||||
</script>
|
||||
|
||||
<div class="container mx-auto py-8">
|
||||
<!-- <Tabs value="coupons" class="w-full">
|
||||
<TabsList class="mb-6">
|
||||
<TabsTrigger value="coupons">Coupon Management</TabsTrigger>
|
||||
<!-- Add more tabs here as needed -->
|
||||
<!-- </TabsList> -->
|
||||
|
||||
<!-- <TabsContent value="coupons"> -->
|
||||
<CouponList />
|
||||
<!-- </TabsContent> -->
|
||||
|
||||
<!-- Add more tab content here as needed -->
|
||||
<!-- </Tabs> -->
|
||||
</div>
|
||||
Reference in New Issue
Block a user