Skip to content

Commit 8e7ea60

Browse files
committed
Admin Services tab: create/edit services (name, desc, price, scheduling type) #10
1 parent 6d579ff commit 8e7ea60

File tree

15 files changed

+14384
-7
lines changed

15 files changed

+14384
-7
lines changed

app/actions/services.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"use server";
2+
3+
import { db } from "@/lib/db";
4+
import { services } from "@/lib/db/schema";
5+
import { stripe } from "@/lib/stripe";
6+
import { eq } from "drizzle-orm";
7+
import { revalidatePath } from "next/cache";
8+
9+
export type CreateServicePayload = {
10+
title: string;
11+
description?: string;
12+
type: "booking" | "coaching_session";
13+
durationMinutes: number;
14+
price: number;
15+
scheduledAt?: any;
16+
};
17+
18+
export async function createService(data: CreateServicePayload) {
19+
try {
20+
// 1. Create product in Stripe
21+
const stripeProduct = await stripe.products.create({
22+
name: data.title,
23+
description: data.description || undefined,
24+
});
25+
26+
// 2. Create price in Stripe (Stripe expects amount in cents)
27+
const stripePrice = await stripe.prices.create({
28+
product: stripeProduct.id,
29+
unit_amount: data.price,
30+
currency: "usd",
31+
});
32+
33+
// 3. Insert into DB
34+
const [newService] = await db
35+
.insert(services)
36+
.values({
37+
title: data.title,
38+
description: data.description || null,
39+
type: data.type,
40+
durationMinutes: data.durationMinutes,
41+
price: data.price,
42+
scheduledAt: data.scheduledAt || null,
43+
stripeProductId: stripeProduct.id,
44+
stripePriceId: stripePrice.id,
45+
isActive: true,
46+
})
47+
.returning();
48+
49+
revalidatePath("/dashboard/services");
50+
return { success: true, service: newService };
51+
} catch (error: any) {
52+
console.error("Failed to create service:", error);
53+
return { success: false, error: error.message };
54+
}
55+
}
56+
57+
export async function updateService(id: string, data: Partial<CreateServicePayload>) {
58+
try {
59+
// 1. Fetch existing to compare price and get Stripe Product ID
60+
const existingServiceList = await db
61+
.select()
62+
.from(services)
63+
.where(eq(services.id, id));
64+
65+
const existingService = existingServiceList[0];
66+
if (!existingService) throw new Error("Service not found");
67+
68+
let newStripePriceId = existingService.stripePriceId;
69+
70+
// 2. Sync to Stripe if product details changed
71+
if ((data.title !== undefined || data.description !== undefined) && existingService.stripeProductId) {
72+
await stripe.products.update(existingService.stripeProductId, {
73+
name: data.title !== undefined ? data.title : existingService.title,
74+
description: data.description !== undefined ? data.description : existingService.description || undefined,
75+
});
76+
}
77+
78+
// 3. If price changed, create new Price in Stripe and archive old
79+
if (data.price !== undefined && data.price !== existingService.price && existingService.stripeProductId) {
80+
const stripePrice = await stripe.prices.create({
81+
product: existingService.stripeProductId,
82+
unit_amount: data.price,
83+
currency: "usd",
84+
});
85+
86+
// Archive old price
87+
if (existingService.stripePriceId) {
88+
await stripe.prices.update(existingService.stripePriceId, { active: false });
89+
}
90+
91+
newStripePriceId = stripePrice.id;
92+
}
93+
94+
// 4. Update DB
95+
const [updatedService] = await db
96+
.update(services)
97+
.set({
98+
title: data.title,
99+
description: data.description,
100+
type: data.type,
101+
durationMinutes: data.durationMinutes,
102+
price: data.price,
103+
scheduledAt: data.scheduledAt,
104+
stripePriceId: newStripePriceId,
105+
updatedAt: new Date(),
106+
})
107+
.where(eq(services.id, id))
108+
.returning();
109+
110+
revalidatePath("/dashboard/services");
111+
return { success: true, service: updatedService };
112+
} catch (error: any) {
113+
console.error("Failed to update service:", error);
114+
return { success: false, error: error.message };
115+
}
116+
}
117+
118+
export async function deleteService(id: string) {
119+
try {
120+
const existingServiceList = await db
121+
.select()
122+
.from(services)
123+
.where(eq(services.id, id));
124+
125+
const existingService = existingServiceList[0];
126+
if (!existingService) throw new Error("Service not found");
127+
128+
// 1. Soft delete on Stripe
129+
if (existingService.stripeProductId) {
130+
await stripe.products.update(existingService.stripeProductId, { active: false });
131+
}
132+
133+
// 2. Soft delete in DB
134+
await db
135+
.update(services)
136+
.set({ isActive: false, updatedAt: new Date() })
137+
.where(eq(services.id, id));
138+
139+
revalidatePath("/dashboard/services");
140+
return { success: true };
141+
} catch (error: any) {
142+
console.error("Failed to delete service:", error);
143+
return { success: false, error: error.message };
144+
}
145+
}

app/dashboard/page.tsx

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,27 @@
1+
import Link from "next/link";
2+
import { Button } from "@/components/ui/button";
3+
14
export default async function DashboardPage() {
25
return (
3-
<main className="flex min-h-screen flex-col p-8">
4-
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
5-
<p className="mt-2 text-muted-foreground">
6-
You are admin
7-
</p>
6+
<main className="flex min-h-screen flex-col p-8 gap-6 max-w-6xl mx-auto w-full">
7+
<div>
8+
<h1 className="text-3xl font-bold">Admin Dashboard</h1>
9+
<p className="mt-2 text-muted-foreground">
10+
You are admin. Select a module to manage.
11+
</p>
12+
</div>
13+
14+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
15+
<div className="rounded-xl border bg-card p-6 shadow-sm flex flex-col gap-4">
16+
<div className="flex flex-col gap-2">
17+
<h3 className="text-xl font-semibold">Services</h3>
18+
<p className="text-sm text-muted-foreground">Manage your coaching sessions, preset date bookings, and prices.</p>
19+
</div>
20+
<Button asChild className="w-full mt-auto">
21+
<Link href="/dashboard/services">Manage Services</Link>
22+
</Button>
23+
</div>
24+
</div>
825
</main>
926
);
1027
}

0 commit comments

Comments
 (0)