Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement delete checkout #4

Merged
merged 1 commit into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion apps/api/src/module/checkout/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { createCheckoutV1 } from "./route/v1.create";
import { checkoutPortalV1 } from "./route/v1.portal";
import { findOneCheckoutV1 } from "./route/v1.find-one";
import { findManyCheckoutV1 } from "./route/v1.find-many";
import { deleteCheckoutV1 } from "./route/v1.delete";

export const CheckoutRoute = new OpenAPIHono()
.route("/", createCheckoutV1)
.route("/", checkoutPortalV1)
.route("/", findOneCheckoutV1)
.route("/", findManyCheckoutV1);
.route("/", findManyCheckoutV1)
.route("/", deleteCheckoutV1);
39 changes: 39 additions & 0 deletions apps/api/src/module/checkout/route/v1.delete.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { validateAuth } from "@/lib/auth";
import { apiError } from "@/lib/error";
import { checkoutService } from "@/service/checkout.service";
import type { AppEnv } from "@/setup/context";
import { createRoute, OpenAPIHono, z } from "@hono/zod-openapi";

export const deleteCheckoutV1 = new OpenAPIHono<AppEnv>().openapi(
createRoute({
method: "delete",
path: "/v1/checkout/{id}",
tags: ["Checkout"],
description: "Delete a checkout",
operationId: "Delete One Checkout",
request: {
params: z.object({ id: z.string() }),
},
responses: {
200: {
description: "Checkout deleted",
},
},
}),
async (c) => {
const { user } = validateAuth(c);
const { id } = c.req.valid("param");

const deletedCheckout = await checkoutService.delete(id, user.id);

if (deletedCheckout.error) {
throw apiError({
status: 500,
message: "Could not delete checkout",
details: deletedCheckout.error.message,
});
}

return c.json(null);
},
);
22 changes: 22 additions & 0 deletions apps/api/src/service/checkout.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,28 @@ export class CheckoutService {

return ok({ checkouts, total });
}

async delete(checkoutId: string, userId: string) {
const condition = and(eq(TB_checkout.id, checkoutId), eq(TB_checkout.userId, userId));

const checkout = await db.query.TB_checkout.findFirst({ where: condition })
.prepare("find-checkout-for-deletion")
.execute();

if (!checkout) {
throw apiError({
status: 404,
message: "Checkout not found",
});
}

return db
.update(TB_checkout)
.set({ deletedAt: new Date() })
.where(condition)
.then(ok)
.catch(err);
}
}

export const checkoutService = new CheckoutService();
18 changes: 18 additions & 0 deletions apps/web/src/query/checkout/checkout.mutation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { api } from "@/lib/api";
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { checkoutKey } from "./checkout.config";

function deleteCheckout(checkoutId: string) {
return api.delete(`v1/checkout/${checkoutId}`).json<null>();
}

export function useDeleteCheckoutMutation() {
const queryClient = useQueryClient();

return useMutation({
mutationFn: deleteCheckout,
onSettled: async () => {
await queryClient.invalidateQueries({ queryKey: checkoutKey.all });
},
});
}
18 changes: 12 additions & 6 deletions apps/web/src/route/_app/checkouts/$checkoutId.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,15 +108,21 @@ function CheckoutDetailPage() {
<DataList.Item>
<DataList.Label>Status</DataList.Label>
<DataList.Value className="justify-end">
<Badge color={COLOR.SUCCESS} size="3" className="items-center">
<Badge
color={match(data.transactions.some((t) => t.status === "SUCCESS"))
.with(true, () => COLOR.SUCCESS)
.otherwise(() => COLOR.WARNING)}
size="3"
className="items-center"
>
{data.transactions.some((t) => t.status === "SUCCESS") ? (
<Fragment>
<span className="mt-px size-2 rounded-full bg-success" />
<span className="mt-px size-2 rounded-full bg-primary-10" />
<span>Paid</span>
</Fragment>
) : (
<Fragment>
<span className="mt-px size-2 rounded-full bg-warning" />
<span className="mt-px size-2 rounded-full bg-primary-10" />
<span>Pending</span>
</Fragment>
)}
Expand Down Expand Up @@ -266,7 +272,7 @@ function CheckoutDetailPage() {
size="2"
className="items-center text-2"
>
<span className="mt-px size-2 rounded-full bg-primary" />
<span className="mt-px size-2 rounded-full bg-primary-10" />
<span>{transaction.status}</span>
</Badge>
<Text className="ml-auto text-gray-foreground">
Expand All @@ -287,12 +293,12 @@ function CheckoutDetailPage() {
<CollapsibleTrigger className="group flex cursor-default items-center gap-x-4">
<Text className="text-gray-foreground">#{index + 1}</Text>
<Badge
size="2"
color={webhook.status >= 400 ? COLOR.DANGER : COLOR.SUCCESS}
data-success={webhook.status >= 200 && webhook.status < 300}
size="2"
className="group items-center text-2"
>
<span className="mt-px size-1.5 rounded-full bg-danger group-data-[success=true]:bg-success" />
<span className="mt-px size-1.5 rounded-full bg-primary-10" />
{webhook.status}
</Badge>
<Text className="ml-auto text-gray-foreground">
Expand Down
67 changes: 54 additions & 13 deletions apps/web/src/route/_app/checkouts/route.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,11 @@ import { formatCurrency } from "@/lib/currency";
import { formatDate } from "@/lib/date";
import { getInitial } from "@/lib/utils";
import { checkoutListQuery } from "@/query/checkout/checkout.query";
import {
Alarm,
DotsThreeVertical,
Faders,
MagnifyingGlass,
Plus,
Receipt,
} from "@phosphor-icons/react";
import { DotsThreeVertical, Faders, MagnifyingGlass, Plus, Trash } from "@phosphor-icons/react";
import {
Badge,
Checkbox,
DropdownMenu,
Flex,
Heading,
IconButton,
Expand All @@ -29,6 +23,9 @@ import { Fragment } from "react/jsx-runtime";
import { z } from "zod";
import { zodSearchValidator } from "@tanstack/router-zod-adapter";
import { useSuspenseQuery } from "@tanstack/react-query";
import { useDeleteCheckoutMutation } from "@/query/checkout/checkout.mutation";
import { toast } from "sonner";
import { confirmation } from "@/lib/confirmation";

const checkoutsSearchParams = z.object({
page: z.number({ coerce: true }).int().catch(1),
Expand All @@ -49,6 +46,8 @@ function CheckoutPage() {
const { page, perPage } = Route.useSearch();
const navigate = Route.useNavigate();

const { mutate } = useDeleteCheckoutMutation();

const {
data: { total, checkouts },
} = useSuspenseQuery({ ...checkoutListQuery({ page, perPage }) });
Expand All @@ -59,6 +58,23 @@ function CheckoutPage() {
navigate({ search: (prev) => ({ ...prev, page }) });
};

const handleDeleteCheckout = (id: string) => {
confirmation.create({
type: "danger",
title: "Delete Checkout",
description: "Are you sure you want to delete this checkout?",
onConfirm: () => {
const toastId = toast.loading("Deleting checkout...");
mutate(id, {
onSettled: (_, error) => {
if (error) return toast.error("Failed to delete checkout", { id: toastId });
toast.success("Checkout deleted", { id: toastId });
},
});
},
});
};

return (
<main className="flex flex-1 flex-col px-10 py-5">
<nav className="space-y-5">
Expand Down Expand Up @@ -157,21 +173,46 @@ function CheckoutPage() {
>
{checkout.transactions.some((t) => t.status === "SUCCESS") ? (
<Fragment>
<span className="mt-px size-2 rounded-full bg-success" />
<span className="mt-px size-2 rounded-full bg-primary-10" />
<span>Paid</span>
</Fragment>
) : (
<Fragment>
<span className="mt-px size-2 rounded-full bg-warning" />
<span className="mt-px size-2 rounded-full bg-primary-10" />
<span>Pending</span>
</Fragment>
)}
</Badge>
</Table.Cell>
<Table.Cell>
<IconButton color="gray" variant="outline">
<DotsThreeVertical size={22} weight="bold" />
</IconButton>
<DropdownMenu.Root>
<DropdownMenu.Trigger>
<IconButton color="gray" variant="outline">
<DotsThreeVertical size={22} weight="bold" />
</IconButton>
</DropdownMenu.Trigger>
<DropdownMenu.Content align="center">
<DropdownMenu.Item shortcut="⌘ E" disabled>
Edit
</DropdownMenu.Item>
<DropdownMenu.Item shortcut="⌘ D" disabled>
Duplicate
</DropdownMenu.Item>

<DropdownMenu.Separator />

<DropdownMenu.Item
color="red"
onClick={(e) => {
e.stopPropagation();
handleDeleteCheckout(checkout.id);
}}
>
Delete
<Trash size={16} className="my-auto ml-auto" />
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Root>
</Table.Cell>
</Table.Row>
))}
Expand Down