Skip to content

Commit 0b929cd

Browse files
committed
feat: allow creating promo offer from admin UI when none exists
1 parent 8ae142e commit 0b929cd

3 files changed

Lines changed: 79 additions & 9 deletions

File tree

client/src/pages/admin/free-audit-queue.tsx

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,16 @@ function AdminFreeAuditQueueContent({
124124
if (offer) {
125125
if (offer.startsAt) setStartsAt(new Date(offer.startsAt).toISOString().slice(0, 16));
126126
if (offer.endsAt) setEndsAt(new Date(offer.endsAt).toISOString().slice(0, 16));
127+
} else {
128+
// Set some defaults if no offer exists
129+
const tomorrow = new Date();
130+
tomorrow.setDate(tomorrow.getDate() + 1);
131+
tomorrow.setHours(9, 0, 0, 0);
132+
setStartsAt(tomorrow.toISOString().slice(0, 16));
133+
134+
const nextWeek = new Date(tomorrow);
135+
nextWeek.setDate(nextWeek.getDate() + 7);
136+
setEndsAt(nextWeek.toISOString().slice(0, 16));
127137
}
128138
}, [offer]);
129139

@@ -148,8 +158,32 @@ function AdminFreeAuditQueueContent({
148158
}
149159
});
150160

151-
const handleUpdateOffer = () => {
152-
updateOfferMutation.mutate({ startsAt, endsAt });
161+
const createOfferMutation = useMutation({
162+
mutationFn: async (values: { startsAt: string, endsAt: string }) => {
163+
const res = await fetch(`/api/admin/promo-offer`, {
164+
method: "POST",
165+
headers: { "Content-Type": "application/json" },
166+
body: JSON.stringify(values),
167+
});
168+
if (!res.ok) throw new Error("Failed to create offer");
169+
return res.json();
170+
},
171+
onSuccess: () => {
172+
toast({ title: "Campaign Created", description: "A new promo offer has been started." });
173+
queryClient.invalidateQueries({ queryKey: ["/api/admin/promo-offer"] });
174+
queryClient.invalidateQueries({ queryKey: ["/api/public/promo-offer"] });
175+
},
176+
onError: (error: any) => {
177+
toast({ title: "Creation Failed", description: error.message, variant: "destructive" });
178+
}
179+
});
180+
181+
const handleSaveOffer = () => {
182+
if (offer) {
183+
updateOfferMutation.mutate({ startsAt, endsAt });
184+
} else {
185+
createOfferMutation.mutate({ startsAt, endsAt });
186+
}
153187
};
154188

155189
const requests = data?.requests || [];
@@ -197,7 +231,7 @@ function AdminFreeAuditQueueContent({
197231
<div className="flex items-center gap-2 text-sm text-muted-foreground">
198232
<Loader2 className="h-4 w-4 animate-spin" /> Loading campaign data...
199233
</div>
200-
) : offer ? (
234+
) : (
201235
<div className="flex flex-col sm:flex-row items-end gap-4 max-w-2xl">
202236
<div className="space-y-1.5 w-full sm:w-auto flex-1">
203237
<Label htmlFor="startsAt" className="text-xs">Start Date & Time</Label>
@@ -220,16 +254,18 @@ function AdminFreeAuditQueueContent({
220254
/>
221255
</div>
222256
<Button
223-
onClick={handleUpdateOffer}
224-
disabled={updateOfferMutation.isPending}
257+
onClick={handleSaveOffer}
258+
disabled={updateOfferMutation.isPending || createOfferMutation.isPending}
225259
className="w-full sm:w-auto h-9"
226260
>
227-
{updateOfferMutation.isPending ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Save className="w-4 h-4 mr-2" />}
228-
Save Changes
261+
{updateOfferMutation.isPending || createOfferMutation.isPending ? (
262+
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
263+
) : (
264+
<Save className="w-4 h-4 mr-2" />
265+
)}
266+
{offer ? "Save Changes" : "Create Campaign"}
229267
</Button>
230268
</div>
231-
) : (
232-
<p className="text-sm text-muted-foreground">No active promo offer found. Please seed the database.</p>
233269
)}
234270
</CardContent>
235271
</Card>

server/routes/admin.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,28 @@ router.patch("/promo-offer/:id", async (req, res) => {
143143
}
144144
});
145145

146+
router.post("/promo-offer", async (req, res) => {
147+
try {
148+
const { startsAt, endsAt } = req.body;
149+
150+
// Check if one already exists
151+
const existing = await storage.getActivePromoOffer();
152+
if (existing) {
153+
return res.status(400).json({ error: "An active promo offer already exists. Update it instead." });
154+
}
155+
156+
const created = await storage.createPromoOffer({
157+
startsAt: startsAt ? new Date(startsAt) : new Date(),
158+
endsAt: endsAt ? new Date(endsAt) : new Date(Date.now() + 7 * 24 * 60 * 60 * 1000),
159+
});
160+
161+
await logAdminAction(req.user!.id, "create_promo_offer", created.id, null, { startsAt, endsAt });
162+
res.json(created);
163+
} catch (error: any) {
164+
res.status(500).json({ error: error.message });
165+
}
166+
});
167+
146168
router.get("/overview", async (req, res) => {
147169
try {
148170
const [{ count: totalUsers }] = await db.select({ count: sql<number>`count(*)` }).from(users);

server/storage.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,18 @@ export class DatabaseStorage implements IStorage {
691691
return updated || undefined;
692692
}
693693

694+
async createPromoOffer(data: Partial<PromoOffer>): Promise<PromoOffer> {
695+
const [created] = await db.insert(promoOffers).values({
696+
name: data.name || "admin-created-offer",
697+
description: data.description || "Admin created promo offer",
698+
startsAt: data.startsAt || new Date(),
699+
endsAt: data.endsAt || new Date(Date.now() + 7 * 24 * 60 * 60 * 1000), // default 7 days
700+
status: "active",
701+
grantsUsed: 0
702+
}).returning();
703+
return created;
704+
}
705+
694706
async createFreeAuditRequest(request: InsertFreeAuditRequest): Promise<FreeAuditRequest> {
695707
const [created] = await db.insert(freeAuditRequests).values(request).returning();
696708
return created;

0 commit comments

Comments
 (0)