Turn customers into users management with membership and row actions.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Alireza Hassani
2026-09-30 08:57:38 +03:30
co-authored by Cursor
parent e687e4502e
commit e5592ca997
9 changed files with 426 additions and 147 deletions
-2
View File
@@ -85,8 +85,6 @@ export default function CustomerHomePage() {
src="/images/image4.jpg" src="/images/image4.jpg"
alt="نمای کالکشن مانا" alt="نمای کالکشن مانا"
caption="Quiet elegance." caption="Quiet elegance."
hoverTitle="خریدهای من"
hoverSubtitle="My Orders"
priority priority
style={{ animationDelay: `${bannerDelay}ms` }} style={{ animationDelay: `${bannerDelay}ms` }}
/> />
+301 -21
View File
@@ -1,37 +1,72 @@
"use client"; "use client";
import { useCallback, useEffect, useState } from "react"; import { FormEvent, useCallback, useEffect, useState } from "react";
import ClothesShell from "@/components/clothes/ClothesShell"; import ClothesShell from "@/components/clothes/ClothesShell";
import { useConfirm } from "@/components/ui/ConfirmDialog";
import IconButton, { import IconButton, {
ClearFilterIcon, ClearFilterIcon,
EditIcon,
FilterIcon, FilterIcon,
RoleIcon,
SubscriptionIcon,
TrashIcon,
} from "@/components/ui/IconButton"; } from "@/components/ui/IconButton";
import { FormModal } from "@/components/ui/Modal";
import Pagination, { usePagination } from "@/components/ui/Pagination"; import Pagination, { usePagination } from "@/components/ui/Pagination";
import { import {
Customer, Customer,
formatJoinedDate, formatCount,
roleLabel,
subscriptionLabel,
SubscriptionStatus,
UserRole,
} from "@/data/customers"; } from "@/data/customers";
import { ApiError } from "@/lib/api"; import { ApiError } from "@/lib/api";
import { fetchCustomers } from "@/lib/customers-api"; import { useAuth } from "@/lib/auth";
import {
deleteCustomer,
fetchCustomers,
updateCustomer,
updateCustomerMembership,
updateCustomerRole,
} from "@/lib/customers-api";
const PAGE_SIZE = 5; const PAGE_SIZE = 5;
const ROLES: UserRole[] = ["CUSTOMER", "STORE_ADMIN", "SUPER_ADMIN"];
const MEMBERSHIPS: SubscriptionStatus[] = ["regular", "premium"];
type Filters = { type Filters = {
name: string; name: string;
phone: string; phone: string;
}; };
type Editor =
| { kind: "edit"; customer: Customer }
| { kind: "role"; customer: Customer }
| { kind: "membership"; customer: Customer }
| null;
const emptyFilters: Filters = { const emptyFilters: Filters = {
name: "", name: "",
phone: "", phone: "",
}; };
export default function CustomersPage() { export default function CustomersPage() {
const confirmDialog = useConfirm();
const { user } = useAuth();
const [items, setItems] = useState<Customer[]>([]); const [items, setItems] = useState<Customer[]>([]);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [draft, setDraft] = useState<Filters>(emptyFilters); const [draft, setDraft] = useState<Filters>(emptyFilters);
const [applied, setApplied] = useState<Filters>(emptyFilters); const [applied, setApplied] = useState<Filters>(emptyFilters);
const [editor, setEditor] = useState<Editor>(null);
const [firstName, setFirstName] = useState("");
const [lastName, setLastName] = useState("");
const [phone, setPhone] = useState("");
const [role, setRole] = useState<UserRole>("CUSTOMER");
const [membership, setMembership] = useState<SubscriptionStatus>("regular");
const reload = useCallback(async () => { const reload = useCallback(async () => {
setLoading(true); setLoading(true);
@@ -40,9 +75,7 @@ export default function CustomersPage() {
setItems(await fetchCustomers()); setItems(await fetchCustomers());
} catch (err) { } catch (err) {
setError( setError(
err instanceof ApiError err instanceof ApiError ? err.message : "بارگذاری کاربران ناموفق بود.",
? err.message
: "بارگذاری مشتریان ناموفق بود.",
); );
} finally { } finally {
setLoading(false); setLoading(false);
@@ -64,14 +97,111 @@ export default function CustomersPage() {
const { page, setPage, pageItems, totalPages, totalItems, pageSize } = const { page, setPage, pageItems, totalPages, totalItems, pageSize } =
usePagination(list, PAGE_SIZE, JSON.stringify(applied)); usePagination(list, PAGE_SIZE, JSON.stringify(applied));
function closeEditor() {
setEditor(null);
}
function openEdit(customer: Customer) {
setError(null);
setFirstName(customer.firstName);
setLastName(customer.lastName);
setPhone(customer.phone);
setEditor({ kind: "edit", customer });
}
function openRole(customer: Customer) {
setError(null);
setRole(customer.role);
setEditor({ kind: "role", customer });
}
function openMembership(customer: Customer) {
setError(null);
setMembership(customer.subscription);
setEditor({ kind: "membership", customer });
}
function replaceCustomer(next: Customer) {
setItems((prev) => prev.map((item) => (item.id === next.id ? next : item)));
}
async function handleEdit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!editor || editor.kind !== "edit") return;
if (!firstName.trim() || !lastName.trim() || !phone.trim()) return;
setSaving(true);
setError(null);
try {
replaceCustomer(
await updateCustomer(editor.customer.id, {
firstName: firstName.trim(),
lastName: lastName.trim(),
cellNumber: phone.trim(),
}),
);
closeEditor();
} catch (err) {
setError(err instanceof ApiError ? err.message : "ویرایش کاربر ناموفق بود.");
} finally {
setSaving(false);
}
}
async function handleRole(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!editor || editor.kind !== "role") return;
setSaving(true);
setError(null);
try {
replaceCustomer(await updateCustomerRole(editor.customer.id, role));
closeEditor();
} catch (err) {
setError(
err instanceof ApiError ? err.message : "تغییر نقش ناموفق بود.",
);
} finally {
setSaving(false);
}
}
async function handleMembership(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
if (!editor || editor.kind !== "membership") return;
setSaving(true);
setError(null);
try {
replaceCustomer(
await updateCustomerMembership(editor.customer.id, membership),
);
closeEditor();
} catch (err) {
setError(
err instanceof ApiError ? err.message : "تغییر عضویت ناموفق بود.",
);
} finally {
setSaving(false);
}
}
async function handleDelete(customer: Customer) {
if (!(await confirmDialog(`«${customer.nameFa}» حذف شود؟`))) return;
setError(null);
try {
await deleteCustomer(customer.id);
setItems((prev) => prev.filter((item) => item.id !== customer.id));
} catch (err) {
setError(err instanceof ApiError ? err.message : "حذف کاربر ناموفق بود.");
}
}
return ( return (
<ClothesShell <ClothesShell
eyebrow="Customers" eyebrow="Users"
title="مشتریان" title="کاربران"
description="مشتریان ثبت‌نام‌شده در مانا کالکشن." description="کاربران ثبت‌نام‌شده در مانا کالکشن."
breadcrumbs={[ breadcrumbs={[
{ label: "داشبورد", href: "/" }, { label: "داشبورد", href: "/" },
{ label: "مشتریان" }, { label: "کاربران" },
]} ]}
> >
{error ? ( {error ? (
@@ -133,7 +263,12 @@ export default function CustomersPage() {
) : ( ) : (
<> <>
<div className="space-y-1"> <div className="space-y-1">
{pageItems.map((customer, i) => ( {pageItems.map((customer, i) => {
const isSelf = user?.id === customer.id;
const protectedAdmin =
customer.role === "SUPER_ADMIN" && user?.role !== "SUPER_ADMIN";
const premium = customer.subscription === "premium";
return (
<article <article
key={customer.id} key={customer.id}
className="customer-card dash-enter" className="customer-card dash-enter"
@@ -149,24 +284,74 @@ export default function CustomersPage() {
> >
{customer.phone} {customer.phone}
</p> </p>
<p className="mt-1 text-[0.7rem] text-muted">
{roleLabel(customer.role)}
</p>
</div> </div>
<div className="customer-joined shrink-0"> <div className="customer-stats shrink-0">
<span className="customer-joined-label">عضویت</span> <div className="customer-stat">
<time <span className="customer-stat-label">عضویت</span>
dateTime={customer.joinedAt} <span
dir="ltr" className={
className="customer-joined-date" premium
? "customer-stat-value text-ink"
: "customer-stat-value"
}
> >
{formatJoinedDate(customer.joinedAt)} {subscriptionLabel(customer.subscription)}
</time> </span>
</div>
<div className="customer-stat">
<span className="customer-stat-label">سفارش‌ها</span>
<span className="customer-stat-value">
{formatCount(customer.orderCount)}
</span>
</div>
</div>
<div className="tree-row-actions shrink-0">
<IconButton
label="ویرایش"
variant="line"
disabled={protectedAdmin}
onClick={() => openEdit(customer)}
>
<EditIcon />
</IconButton>
<IconButton
label="تغییر نقش"
variant="line"
disabled={isSelf || protectedAdmin}
onClick={() => openRole(customer)}
>
<RoleIcon />
</IconButton>
<IconButton
label="عضویت"
variant="line"
className={premium ? "customer-sub-premium" : undefined}
disabled={protectedAdmin}
onClick={() => openMembership(customer)}
>
<SubscriptionIcon premium={premium} />
</IconButton>
<IconButton
label="حذف"
variant="line"
disabled={isSelf || protectedAdmin}
onClick={() => void handleDelete(customer)}
>
<TrashIcon />
</IconButton>
</div> </div>
</article> </article>
))} );
})}
{list.length === 0 ? ( {list.length === 0 ? (
<p className="rounded-lg border border-line bg-surface px-4 py-8 text-center text-sm text-muted"> <p className="rounded-lg border border-line bg-surface px-4 py-8 text-center text-sm text-muted">
مشتری‌ای با این فیلترها پیدا نشد. کاربری با این فیلترها پیدا نشد.
</p> </p>
) : null} ) : null}
</div> </div>
@@ -182,6 +367,101 @@ export default function CustomersPage() {
) : null} ) : null}
</> </>
)} )}
<FormModal
open={editor?.kind === "edit"}
title="ویرایش کاربر"
subtitle={editor?.kind === "edit" ? editor.customer.phone : undefined}
onClose={closeEditor}
onSubmit={handleEdit}
submitLabel={saving ? "..." : "ذخیره"}
>
<div className="clothes-field">
<label htmlFor="user-first-name">نام</label>
<input
id="user-first-name"
value={firstName}
onChange={(e) => setFirstName(e.target.value)}
required
/>
</div>
<div className="clothes-field">
<label htmlFor="user-last-name">نام خانوادگی</label>
<input
id="user-last-name"
value={lastName}
onChange={(e) => setLastName(e.target.value)}
required
/>
</div>
<div className="clothes-field">
<label htmlFor="user-phone">شماره موبایل</label>
<input
id="user-phone"
dir="ltr"
className="!text-end"
inputMode="tel"
value={phone}
onChange={(e) => setPhone(e.target.value)}
required
/>
</div>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
</FormModal>
<FormModal
open={editor?.kind === "role"}
title="تغییر نقش"
subtitle={editor?.customer.nameFa}
onClose={closeEditor}
onSubmit={handleRole}
submitLabel={saving ? "..." : "ذخیره"}
>
<div className="clothes-field">
<label htmlFor="user-role">نقش</label>
<select
id="user-role"
value={role}
onChange={(e) => setRole(e.target.value as UserRole)}
>
{ROLES.filter(
(item) => user?.role === "SUPER_ADMIN" || item !== "SUPER_ADMIN",
).map((item) => (
<option key={item} value={item}>
{roleLabel(item)}
</option>
))}
</select>
</div>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
</FormModal>
<FormModal
open={editor?.kind === "membership"}
title="عضویت"
subtitle={editor?.customer.nameFa}
onClose={closeEditor}
onSubmit={handleMembership}
submitLabel={saving ? "..." : "ذخیره"}
>
<div className="clothes-field">
<label htmlFor="user-membership">عضویت فعلی</label>
<select
id="user-membership"
value={membership}
onChange={(e) =>
setMembership(e.target.value as SubscriptionStatus)
}
>
{MEMBERSHIPS.map((item) => (
<option key={item} value={item}>
{subscriptionLabel(item)}
</option>
))}
</select>
</div>
{error ? <p className="text-sm text-red-600">{error}</p> : null}
</FormModal>
</ClothesShell> </ClothesShell>
); );
} }
+31 -18
View File
@@ -272,25 +272,8 @@ body {
color: #fff; color: #fff;
} }
/* Dashboard banner — white wipe overlay */
.dash-banner { .dash-banner {
cursor: pointer; cursor: default;
}
.dash-banner-wipe {
position: absolute;
inset: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
transform: translateY(101%);
transition: transform 0.32s cubic-bezier(0.7, 0, 0.84, 0.2);
}
.dash-banner:hover .dash-banner-wipe {
transform: translateY(0);
} }
/* Dashboard tiles */ /* Dashboard tiles */
@@ -646,6 +629,11 @@ body {
border-color 0.2s ease; border-color 0.2s ease;
} }
.icon-btn:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.icon-btn[data-tooltip]::before, .icon-btn[data-tooltip]::before,
.icon-btn[data-tooltip]::after { .icon-btn[data-tooltip]::after {
position: absolute; position: absolute;
@@ -764,6 +752,7 @@ body {
.customer-card { .customer-card {
display: flex; display: flex;
align-items: center; align-items: center;
flex-wrap: wrap;
gap: 1rem; gap: 1rem;
border: 1px solid var(--line); border: 1px solid var(--line);
background: #fff; background: #fff;
@@ -776,6 +765,30 @@ body {
border-color: #cfcfcf; border-color: #cfcfcf;
} }
.customer-stats {
display: flex;
align-items: center;
gap: 1.25rem;
}
.customer-stat {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 0.15rem;
min-width: 4.25rem;
}
.customer-stat-label {
font-size: 0.65rem;
color: color-mix(in srgb, var(--muted) 75%, white);
}
.customer-stat-value {
font-size: 0.85rem;
color: color-mix(in srgb, var(--ink) 55%, white);
}
.customer-joined { .customer-joined {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
+4 -4
View File
@@ -15,9 +15,9 @@ const tiles: DashTileItem[] = [
}, },
{ {
href: "/customers", href: "/customers",
title: "مشتریان من", title: "کاربران",
titleEn: "My Customers", titleEn: "Users",
description: "فهرست و جزئیات مشتریان", description: "فهرست، نقش و عضویت کاربران",
icon: <CustomersIcon />, icon: <CustomersIcon />,
}, },
{ {
@@ -74,7 +74,7 @@ export default function HomePage() {
</span> </span>
</h1> </h1>
<p className="mt-3 max-w-xl text-sm leading-7 text-muted sm:text-base"> <p className="mt-3 max-w-xl text-sm leading-7 text-muted sm:text-base">
از اینجا لباس‌ها، مشتریان، بلاگ، سفارش‌ها، وب‌سایت و پروفایل فروشگاه را از اینجا لباس‌ها، کاربران، بلاگ، سفارش‌ها، وب‌سایت و پروفایل فروشگاه را
مدیریت کنید. مدیریت کنید.
</p> </p>
</div> </div>
+2 -18
View File
@@ -5,8 +5,6 @@ type DashboardBannerProps = {
src: string; src: string;
alt: string; alt: string;
caption?: string; caption?: string;
hoverTitle?: string;
hoverSubtitle?: string;
className?: string; className?: string;
style?: CSSProperties; style?: CSSProperties;
priority?: boolean; priority?: boolean;
@@ -16,15 +14,13 @@ export default function DashboardBanner({
src, src,
alt, alt,
caption = "Quiet elegance.", caption = "Quiet elegance.",
hoverTitle = "مدیریت فروش ویژه ی من",
hoverSubtitle = "Special Sale",
className = "", className = "",
style, style,
priority = false, priority = false,
}: DashboardBannerProps) { }: DashboardBannerProps) {
return ( return (
<div <div
className={`dash-banner group relative mt-2 h-[150px] w-full overflow-hidden rounded-lg border border-line dash-enter ${className}`} className={`dash-banner relative mt-2 h-[150px] w-full overflow-hidden rounded-lg border border-line dash-enter ${className}`}
style={style} style={style}
> >
<Image <Image
@@ -39,23 +35,11 @@ export default function DashboardBanner({
{caption ? ( {caption ? (
<p <p
dir="ltr" dir="ltr"
className="absolute bottom-5 start-5 z-[1] font-display text-lg text-white/95 transition-opacity duration-300 group-hover:opacity-0 sm:text-xl" className="absolute bottom-5 start-5 z-[1] font-display text-lg text-white/95 sm:text-xl"
> >
{caption} {caption}
</p> </p>
) : null} ) : null}
<div className="dash-banner-wipe" aria-hidden>
<div className="flex flex-col items-center gap-1.5 px-6 text-center">
<p className="text-base text-ink sm:text-lg">{hoverTitle}</p>
<p
dir="ltr"
className="font-display text-sm tracking-[0.08em] text-muted sm:text-base"
>
{hoverSubtitle}
</p>
</div>
</div>
</div> </div>
); );
} }
+1 -1
View File
@@ -11,7 +11,7 @@ type NavLink = { href: string; label: string };
const ADMIN_LINKS: NavLink[] = [ const ADMIN_LINKS: NavLink[] = [
{ href: "/clothes", label: "لباس‌های من" }, { href: "/clothes", label: "لباس‌های من" },
{ href: "/customers", label: "مشتریان من" }, { href: "/customers", label: "کاربران" },
{ href: "/blog", label: "بلاگ من" }, { href: "/blog", label: "بلاگ من" },
{ href: "/blog/categories", label: "دسته‌بندی بلاگ" }, { href: "/blog/categories", label: "دسته‌بندی بلاگ" },
{ href: "/orders", label: "سفارش‌های من" }, { href: "/orders", label: "سفارش‌های من" },
+10
View File
@@ -48,6 +48,16 @@ export default function IconButton({
); );
} }
export function RoleIcon() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden>
<circle {...stroke} cx="9" cy="8" r="2.4" />
<path {...stroke} d="M4.5 17.5c.7-2.2 2.3-3.3 4.5-3.3s3.8 1.1 4.5 3.3" />
<path {...stroke} d="M15.5 9.5H20M17.75 7.25 20 9.5l-2.25 2.25" />
</svg>
);
}
export function EditIcon() { export function EditIcon() {
return ( return (
<svg width="20" height="20" viewBox="0 0 24 24" aria-hidden> <svg width="20" height="20" viewBox="0 0 24 24" aria-hidden>
+17 -58
View File
@@ -1,71 +1,20 @@
export type SubscriptionStatus = "regular" | "premium"; export type SubscriptionStatus = "regular" | "premium";
export type UserRole = "CUSTOMER" | "STORE_ADMIN" | "SUPER_ADMIN";
export type Customer = { export type Customer = {
id: string; id: string;
firstName: string;
lastName: string;
nameFa: string; nameFa: string;
phone: string; phone: string;
joinedAt: string; joinedAt: string;
subscription: SubscriptionStatus; subscription: SubscriptionStatus;
role: UserRole;
orderCount: number;
}; };
export const customers: Customer[] = [ export const customers: Customer[] = [];
{
id: "cu1",
nameFa: "سارا محمدی",
phone: "09121234567",
joinedAt: "2025-11-12",
subscription: "premium",
},
{
id: "cu2",
nameFa: "نیلوفر احمدی",
phone: "09351239876",
joinedAt: "2026-01-04",
subscription: "regular",
},
{
id: "cu3",
nameFa: "مریم کریمی",
phone: "09197654321",
joinedAt: "2025-08-21",
subscription: "premium",
},
{
id: "cu4",
nameFa: "الهام رضایی",
phone: "09014567890",
joinedAt: "2026-02-18",
subscription: "regular",
},
{
id: "cu5",
nameFa: "هانیه موسوی",
phone: "09128887766",
joinedAt: "2025-12-02",
subscription: "regular",
},
{
id: "cu6",
nameFa: "یاسمن نوری",
phone: "09361112233",
joinedAt: "2026-03-09",
subscription: "premium",
},
{
id: "cu7",
nameFa: "زهرا حسینی",
phone: "09201230000",
joinedAt: "2025-06-30",
subscription: "regular",
},
{
id: "cu8",
nameFa: "فرناز اکبری",
phone: "09193456789",
joinedAt: "2026-04-01",
subscription: "premium",
},
];
export function formatJoinedDate(isoDate: string) { export function formatJoinedDate(isoDate: string) {
const date = new Date(`${isoDate}T12:00:00`); const date = new Date(`${isoDate}T12:00:00`);
@@ -76,6 +25,16 @@ export function formatJoinedDate(isoDate: string) {
}).format(date); }).format(date);
} }
export function formatCount(value: number) {
return new Intl.NumberFormat("fa-IR").format(value);
}
export function subscriptionLabel(status: SubscriptionStatus) { export function subscriptionLabel(status: SubscriptionStatus) {
return status === "premium" ? "پرمیوم" : "عادی"; return status === "premium" ? "پرمیوم" : "عادی";
} }
export function roleLabel(role: UserRole) {
if (role === "SUPER_ADMIN") return "مدیر کل";
if (role === "STORE_ADMIN") return "مدیر فروشگاه";
return "مشتری";
}
+41 -6
View File
@@ -1,5 +1,5 @@
import { api } from "@/lib/api"; import { api } from "@/lib/api";
import type { Customer } from "@/data/customers"; import type { Customer, SubscriptionStatus, UserRole } from "@/data/customers";
export type ApiCustomer = { export type ApiCustomer = {
id: string; id: string;
@@ -8,17 +8,19 @@ export type ApiCustomer = {
name?: string; name?: string;
cellNumber: string; cellNumber: string;
email?: string | null; email?: string | null;
instagram?: string | null; role?: UserRole;
about?: string | null; membership?: SubscriptionStatus;
avatarUrl?: string | null; orderCount?: number;
disabled?: boolean; disabled?: boolean;
createdAt: string; createdAt: string;
}; };
export function mapCustomer(row: ApiCustomer): Customer { export function mapCustomer(row: ApiCustomer): Customer {
const firstName = row.firstName?.trim() ?? "";
const lastName = row.lastName?.trim() ?? "";
const name = const name =
row.name?.trim() || row.name?.trim() ||
`${row.firstName ?? ""} ${row.lastName ?? ""}`.trim() || `${firstName} ${lastName}`.trim() ||
"بدون نام"; "بدون نام";
const joined = row.createdAt const joined = row.createdAt
? new Date(row.createdAt).toISOString().slice(0, 10) ? new Date(row.createdAt).toISOString().slice(0, 10)
@@ -26,10 +28,14 @@ export function mapCustomer(row: ApiCustomer): Customer {
return { return {
id: row.id, id: row.id,
firstName,
lastName,
nameFa: name, nameFa: name,
phone: row.cellNumber, phone: row.cellNumber,
joinedAt: joined, joinedAt: joined,
subscription: "regular", subscription: row.membership === "premium" ? "premium" : "regular",
role: row.role ?? "CUSTOMER",
orderCount: row.orderCount ?? 0,
}; };
} }
@@ -37,3 +43,32 @@ export async function fetchCustomers() {
const rows = await api<ApiCustomer[]>("/customers"); const rows = await api<ApiCustomer[]>("/customers");
return rows.map(mapCustomer); return rows.map(mapCustomer);
} }
export async function updateCustomer(
id: string,
body: { firstName: string; lastName: string; cellNumber: string },
) {
return mapCustomer(await api<ApiCustomer>(`/customers/${id}`, { method: "PATCH", body }));
}
export async function updateCustomerRole(id: string, role: UserRole) {
return mapCustomer(
await api<ApiCustomer>(`/customers/${id}/role`, { method: "PATCH", body: { role } }),
);
}
export async function updateCustomerMembership(
id: string,
membership: SubscriptionStatus,
) {
return mapCustomer(
await api<ApiCustomer>(`/customers/${id}/membership`, {
method: "PATCH",
body: { membership },
}),
);
}
export async function deleteCustomer(id: string) {
await api(`/customers/${id}`, { method: "DELETE" });
}