-
Notifications
You must be signed in to change notification settings - Fork 0
Feature/65 discount modal #73
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
Changes from all commits
d729ee5
2d7c532
89bb686
325d3e3
77dce06
6ee8f87
f662210
1fe876f
a48a970
b5da8b1
1ce9326
0c3962a
f1696aa
5df88f7
d5f73c5
b48522e
adaf3f5
63a8f57
ff50d4e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| "use client"; | ||
|
|
||
| import { useRef, useState } from "react"; | ||
| import { Pencil, Tag } from "lucide-react"; | ||
| import { Button } from "@/components/ui/button"; | ||
| import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; | ||
| import { DiscountModal, type ActiveDiscount, type DiscountService } from "@/components/discount-modal"; | ||
| import { | ||
| getUserDiscountModalData, | ||
| applyDiscountToCustomerProduct, | ||
| removeCouponById, | ||
| } from "@/app/(authenticated)/discounts/actions"; | ||
| import { toast } from "sonner"; | ||
| import { profileRoleLabel, type UserRow } from "../profile-role-label"; | ||
|
|
||
| interface UserActionsCellProps { | ||
| user: UserRow; | ||
| } | ||
|
|
||
| export function UserActionsCell({ user }: UserActionsCellProps) { | ||
| const [open, setOpen] = useState(false); | ||
| const [services, setServices] = useState<DiscountService[]>([]); | ||
| const [discounts, setDiscounts] = useState<ActiveDiscount[]>([]); | ||
| const [loading, setLoading] = useState(false); | ||
| const servicesFetched = useRef(false); | ||
|
|
||
| const fetchModalData = async (forceServices = false) => { | ||
| if (!user.stripeCustomerId) return; | ||
| setLoading(true); | ||
| try { | ||
| const data = await getUserDiscountModalData(user.stripeCustomerId); | ||
| if (forceServices || !servicesFetched.current) { | ||
| setServices(data.services); | ||
| servicesFetched.current = true; | ||
| } | ||
| setDiscounts(data.discounts); | ||
| } finally { | ||
| setLoading(false); | ||
| } | ||
| }; | ||
|
|
||
| const handleOpenChange = async (next: boolean) => { | ||
| setOpen(next); | ||
| if (next) await fetchModalData(); | ||
| }; | ||
|
|
||
| const handleApply = async (data: { | ||
| serviceId: string; | ||
| type: "percent" | "amount"; | ||
| value: number; | ||
| usageLimit: number; | ||
| }) => { | ||
| if (!user.stripeCustomerId) return; | ||
| const fd = new FormData(); | ||
| fd.append("product_id", data.serviceId); | ||
| fd.append("customer_id", user.stripeCustomerId); | ||
| fd.append("usage_limit", String(data.usageLimit)); | ||
| fd.append("discount_type", data.type); | ||
| const discountValue = data.type === "amount" ? Math.round(parseFloat(data.value.toFixed(2)) * 100) : data.value; | ||
| fd.append("discount_value", String(discountValue)); | ||
| if (data.type === "amount") fd.append("currency", "cad"); | ||
| const result = await applyDiscountToCustomerProduct(null, fd); | ||
| if (result?.errors) { | ||
| toast.error("Failed to apply discount", { | ||
| description: Object.values(result.errors).flat().join(" "), | ||
| }); | ||
| } else { | ||
| toast.success("Discount applied"); | ||
| await fetchModalData(); | ||
| } | ||
| }; | ||
|
|
||
| const handleRemove = async (couponId: string) => { | ||
| if (!user.stripeCustomerId) return; | ||
| const result = await removeCouponById(couponId, user.stripeCustomerId); | ||
| if (result?.errors) { | ||
| toast.error("Failed to remove discount", { | ||
| description: Object.values(result.errors).flat().join(" "), | ||
| }); | ||
| } else { | ||
| toast.success("Discount removed"); | ||
| setDiscounts((prev) => prev.filter((d) => d.id !== couponId)); | ||
| } | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="flex items-center justify-end gap-0.5"> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button variant="ghost" size="icon-sm" aria-label="Edit user" disabled> | ||
| <Pencil /> | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent>Edit (coming soon)</TooltipContent> | ||
| </Tooltip> | ||
| <Tooltip> | ||
| <TooltipTrigger asChild> | ||
| <Button | ||
| variant="ghost" | ||
| size="icon-sm" | ||
| aria-label="Manage discounts" | ||
| disabled={!user.stripeCustomerId || loading} | ||
| onClick={() => handleOpenChange(true)} | ||
| > | ||
| <Tag /> | ||
| </Button> | ||
| </TooltipTrigger> | ||
| <TooltipContent> | ||
| {user.stripeCustomerId ? "Manage discounts" : "No Stripe customer"} | ||
| </TooltipContent> | ||
| </Tooltip> | ||
| <DiscountModal | ||
| userName={`${user.firstName} ${user.lastName}`} | ||
| userEmail={user.email} | ||
| userRole={profileRoleLabel(user.role)} | ||
| discounts={discounts} | ||
| services={services} | ||
| loading={loading} | ||
| open={open} | ||
| onOpenChange={handleOpenChange} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. re-hits Stripe on every open no caching?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. wrap |
||
| onApply={handleApply} | ||
| onRemove={handleRemove} | ||
| /> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,4 +18,5 @@ export type UserRow = { | |
| role: string; | ||
| isActive: boolean; | ||
| lastLoginAt: Date; | ||
| stripeCustomerId: string | null; | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.