feat: add settings UI for webhook toggles
- Add settings store (src/lib/settings.ts) for persistent configuration - Add settings API route for CRUD operations - Add settings page with webhook toggles for order events - Update webhook handlers to check settings before sending emails - Settings include: webhook enable/disable, admin/customer notification toggles - Email configuration: from name, from email, admin emails, store/dashboard URLs
This commit is contained in:
Generated
+699
-27
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,133 @@
|
|||||||
|
import { promises as fs } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export interface WebhookSettings {
|
||||||
|
orderCreated: {
|
||||||
|
enabled: boolean;
|
||||||
|
sendAdminNotification: boolean;
|
||||||
|
sendCustomerNotification: boolean;
|
||||||
|
};
|
||||||
|
orderFulfilled: {
|
||||||
|
enabled: boolean;
|
||||||
|
sendAdminNotification: boolean;
|
||||||
|
sendCustomerNotification: boolean;
|
||||||
|
};
|
||||||
|
orderCancelled: {
|
||||||
|
enabled: boolean;
|
||||||
|
sendAdminNotification: boolean;
|
||||||
|
sendCustomerNotification: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmailSettings {
|
||||||
|
fromEmail: string;
|
||||||
|
fromName: string;
|
||||||
|
adminEmails: string[];
|
||||||
|
siteUrl: string;
|
||||||
|
dashboardUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppSettings {
|
||||||
|
webhooks: WebhookSettings;
|
||||||
|
email: EmailSettings;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DEFAULT_SETTINGS: AppSettings = {
|
||||||
|
webhooks: {
|
||||||
|
orderCreated: {
|
||||||
|
enabled: true,
|
||||||
|
sendAdminNotification: true,
|
||||||
|
sendCustomerNotification: true,
|
||||||
|
},
|
||||||
|
orderFulfilled: {
|
||||||
|
enabled: true,
|
||||||
|
sendAdminNotification: true,
|
||||||
|
sendCustomerNotification: true,
|
||||||
|
},
|
||||||
|
orderCancelled: {
|
||||||
|
enabled: true,
|
||||||
|
sendAdminNotification: true,
|
||||||
|
sendCustomerNotification: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
fromEmail: process.env.FROM_EMAIL || "support@mail.manoonoils.com",
|
||||||
|
fromName: process.env.FROM_NAME || "ManoonOils",
|
||||||
|
adminEmails: process.env.ADMIN_EMAILS?.split(",").map((e) => e.trim()).filter(Boolean) || [],
|
||||||
|
siteUrl: process.env.SITE_URL || "https://manoonoils.com",
|
||||||
|
dashboardUrl: process.env.DASHBOARD_URL || "https://dashboard.manoonoils.com",
|
||||||
|
},
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
class SettingsStore {
|
||||||
|
private filePath: string;
|
||||||
|
private settings: AppSettings | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.filePath = process.env.SETTINGS_FILE_PATH || "/tmp/.app-settings.json";
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ensureFileExists(): Promise<void> {
|
||||||
|
try {
|
||||||
|
await fs.access(this.filePath);
|
||||||
|
} catch {
|
||||||
|
const dir = path.dirname(this.filePath);
|
||||||
|
await fs.mkdir(dir, { recursive: true });
|
||||||
|
await fs.writeFile(this.filePath, JSON.stringify(DEFAULT_SETTINGS, null, 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async get(): Promise<AppSettings> {
|
||||||
|
if (this.settings) {
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this.ensureFileExists();
|
||||||
|
const data = await fs.readFile(this.filePath, "utf-8");
|
||||||
|
this.settings = JSON.parse(data) as AppSettings;
|
||||||
|
return this.settings!;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error reading settings:", error);
|
||||||
|
this.settings = DEFAULT_SETTINGS;
|
||||||
|
return this.settings;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async set(newSettings: Partial<AppSettings>): Promise<AppSettings> {
|
||||||
|
const current = await this.get();
|
||||||
|
const updated: AppSettings = {
|
||||||
|
...current,
|
||||||
|
...newSettings,
|
||||||
|
updatedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.writeFile(this.filePath, JSON.stringify(updated, null, 2));
|
||||||
|
this.settings = updated;
|
||||||
|
return updated;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error writing settings:", error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async isWebhookEnabled(webhook: keyof WebhookSettings): Promise<boolean> {
|
||||||
|
const settings = await this.get();
|
||||||
|
return settings.webhooks[webhook].enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
async shouldSendAdminNotification(webhook: keyof WebhookSettings): Promise<boolean> {
|
||||||
|
const settings = await this.get();
|
||||||
|
return settings.webhooks[webhook].sendAdminNotification;
|
||||||
|
}
|
||||||
|
|
||||||
|
async shouldSendCustomerNotification(webhook: keyof WebhookSettings): Promise<boolean> {
|
||||||
|
const settings = await this.get();
|
||||||
|
return settings.webhooks[webhook].sendCustomerNotification;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const settingsStore = new SettingsStore();
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
import { settingsStore, AppSettings } from "@/lib/settings";
|
||||||
|
|
||||||
|
export async function GET() {
|
||||||
|
try {
|
||||||
|
const settings = await settingsStore.get();
|
||||||
|
return NextResponse.json(settings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error getting settings:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to get settings" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PUT(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json() as Partial<AppSettings>;
|
||||||
|
const settings = await settingsStore.set(body);
|
||||||
|
return NextResponse.json(settings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error updating settings:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to update settings" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function PATCH(request: NextRequest) {
|
||||||
|
try {
|
||||||
|
const body = await request.json() as Partial<AppSettings>;
|
||||||
|
const settings = await settingsStore.set(body);
|
||||||
|
return NextResponse.json(settings);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error patching settings:", error);
|
||||||
|
return NextResponse.json({ error: "Failed to patch settings" }, { status: 500 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@/generated/graphql";
|
} from "@/generated/graphql";
|
||||||
import { saleorApp } from "@/saleor-app";
|
import { saleorApp } from "@/saleor-app";
|
||||||
import { sendOrderCancelledEmail, formatPrice } from "@/lib/resend";
|
import { sendOrderCancelledEmail, formatPrice } from "@/lib/resend";
|
||||||
|
import { settingsStore } from "@/lib/settings";
|
||||||
|
|
||||||
export const orderCancelledWebhook = new SaleorAsyncWebhook<OrderCancelledWebhookPayloadFragment>({
|
export const orderCancelledWebhook = new SaleorAsyncWebhook<OrderCancelledWebhookPayloadFragment>({
|
||||||
name: "Order Cancelled in Saleor",
|
name: "Order Cancelled in Saleor",
|
||||||
@@ -23,7 +24,17 @@ export default orderCancelledWebhook.createHandler(async (req, res, ctx) => {
|
|||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Order ${order.number} cancelled for customer: ${order.userEmail}`);
|
const webhookEnabled = await settingsStore.isWebhookEnabled("orderCancelled");
|
||||||
|
const sendAdmin = await settingsStore.shouldSendAdminNotification("orderCancelled");
|
||||||
|
const sendCustomer = await settingsStore.shouldSendCustomerNotification("orderCancelled");
|
||||||
|
|
||||||
|
console.log(`❌ Order ${order.number} cancelled for customer: ${order.userEmail}`);
|
||||||
|
console.log(`📋 Webhook settings - enabled: ${webhookEnabled}, admin: ${sendAdmin}, customer: ${sendCustomer}`);
|
||||||
|
|
||||||
|
if (!webhookEnabled) {
|
||||||
|
console.log("⏭️ Webhook disabled, skipping notifications");
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
const items = ((order as any).lines || []).map((line: any) => ({
|
const items = ((order as any).lines || []).map((line: any) => ({
|
||||||
id: line.id,
|
id: line.id,
|
||||||
@@ -42,16 +53,36 @@ export default orderCancelledWebhook.createHandler(async (req, res, ctx) => {
|
|||||||
total: formatPrice((order as any).total?.gross?.amount || 0, (order as any).total?.gross?.currency || "USD"),
|
total: formatPrice((order as any).total?.gross?.amount || 0, (order as any).total?.gross?.currency || "USD"),
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
if (sendAdmin) {
|
||||||
if (order.userEmail) {
|
try {
|
||||||
await sendOrderCancelledEmail({
|
const adminEmails = process.env.ADMIN_EMAILS?.split(",").map(e => e.trim()).filter(e => e) || [];
|
||||||
to: order.userEmail,
|
|
||||||
orderData,
|
if (adminEmails.length > 0) {
|
||||||
});
|
await sendOrderCancelledEmail({
|
||||||
console.log(`Customer notification sent for cancelled order ${order.number}`);
|
to: adminEmails,
|
||||||
|
orderData,
|
||||||
|
});
|
||||||
|
console.log(`✅ Admin notification sent for cancelled order ${order.number}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to send admin email:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
console.error("Failed to send email:", error);
|
|
||||||
|
if (sendCustomer) {
|
||||||
|
try {
|
||||||
|
if (order.userEmail) {
|
||||||
|
await sendOrderCancelledEmail({
|
||||||
|
to: order.userEmail,
|
||||||
|
orderData,
|
||||||
|
});
|
||||||
|
console.log(`✅ Customer notification sent for cancelled order ${order.number}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to send customer email:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("⏭️ Customer notification disabled, skipping");
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@/generated/graphql";
|
} from "@/generated/graphql";
|
||||||
import { saleorApp } from "@/saleor-app";
|
import { saleorApp } from "@/saleor-app";
|
||||||
import { sendOrderConfirmationEmail, formatPrice } from "@/lib/resend";
|
import { sendOrderConfirmationEmail, formatPrice } from "@/lib/resend";
|
||||||
|
import { settingsStore } from "@/lib/settings";
|
||||||
|
|
||||||
export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderCreatedWebhookPayloadFragment>({
|
export const orderCreatedWebhook = new SaleorAsyncWebhook<OrderCreatedWebhookPayloadFragment>({
|
||||||
name: "Order Created in Saleor",
|
name: "Order Created in Saleor",
|
||||||
@@ -23,7 +24,17 @@ export default orderCreatedWebhook.createHandler(async (req, res, ctx) => {
|
|||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const webhookEnabled = await settingsStore.isWebhookEnabled("orderCreated");
|
||||||
|
const sendAdmin = await settingsStore.shouldSendAdminNotification("orderCreated");
|
||||||
|
const sendCustomer = await settingsStore.shouldSendCustomerNotification("orderCreated");
|
||||||
|
|
||||||
console.log(`🎉 Order #${order.number} created for customer: ${order.userEmail} (${order.languageCode || "EN"})`);
|
console.log(`🎉 Order #${order.number} created for customer: ${order.userEmail} (${order.languageCode || "EN"})`);
|
||||||
|
console.log(`📋 Webhook settings - enabled: ${webhookEnabled}, admin: ${sendAdmin}, customer: ${sendCustomer}`);
|
||||||
|
|
||||||
|
if (!webhookEnabled) {
|
||||||
|
console.log("⏭️ Webhook disabled, skipping notifications");
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
const items = ((order as any).lines || []).map((line: any) => ({
|
const items = ((order as any).lines || []).map((line: any) => ({
|
||||||
id: line.id,
|
id: line.id,
|
||||||
@@ -50,38 +61,44 @@ export default orderCreatedWebhook.createHandler(async (req, res, ctx) => {
|
|||||||
phone: (order as any).shippingAddress?.phone,
|
phone: (order as any).shippingAddress?.phone,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Send admin notification
|
if (sendAdmin) {
|
||||||
try {
|
try {
|
||||||
const adminEmails = process.env.ADMIN_EMAILS?.split(",").map(e => e.trim()).filter(e => e) || [];
|
const adminEmails = process.env.ADMIN_EMAILS?.split(",").map(e => e.trim()).filter(e => e) || [];
|
||||||
|
|
||||||
if (adminEmails.length > 0) {
|
if (adminEmails.length > 0) {
|
||||||
await sendOrderConfirmationEmail({
|
await sendOrderConfirmationEmail({
|
||||||
to: adminEmails,
|
to: adminEmails,
|
||||||
orderData,
|
orderData,
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
});
|
});
|
||||||
console.log(`✅ Admin notification sent for order #${order.number} to: ${adminEmails.join(", ")}`);
|
console.log(`✅ Admin notification sent for order #${order.number} to: ${adminEmails.join(", ")}`);
|
||||||
} else {
|
} else {
|
||||||
console.log("⚠️ No admin emails configured, skipping admin notification");
|
console.log("⚠️ No admin emails configured, skipping admin notification");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to send admin email:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error("❌ Failed to send admin email:", error);
|
console.log("⏭️ Admin notification disabled, skipping");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send customer confirmation
|
if (sendCustomer) {
|
||||||
try {
|
try {
|
||||||
if (order.userEmail) {
|
if (order.userEmail) {
|
||||||
await sendOrderConfirmationEmail({
|
await sendOrderConfirmationEmail({
|
||||||
to: order.userEmail,
|
to: order.userEmail,
|
||||||
orderData,
|
orderData,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
});
|
});
|
||||||
console.log(`✅ Customer confirmation sent for order #${order.number} to: ${order.userEmail}`);
|
console.log(`✅ Customer confirmation sent for order #${order.number} to: ${order.userEmail}`);
|
||||||
} else {
|
} else {
|
||||||
console.log("⚠️ No customer email found, skipping customer notification");
|
console.log("⚠️ No customer email found, skipping customer notification");
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to send customer email:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} else {
|
||||||
console.error("❌ Failed to send customer email:", error);
|
console.log("⏭️ Customer notification disabled, skipping");
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from "@/generated/graphql";
|
} from "@/generated/graphql";
|
||||||
import { saleorApp } from "@/saleor-app";
|
import { saleorApp } from "@/saleor-app";
|
||||||
import { sendOrderShippedEmail, formatPrice } from "@/lib/resend";
|
import { sendOrderShippedEmail, formatPrice } from "@/lib/resend";
|
||||||
|
import { settingsStore } from "@/lib/settings";
|
||||||
|
|
||||||
export const orderFulfilledWebhook = new SaleorAsyncWebhook<OrderFulfilledWebhookPayloadFragment>({
|
export const orderFulfilledWebhook = new SaleorAsyncWebhook<OrderFulfilledWebhookPayloadFragment>({
|
||||||
name: "Order Fulfilled in Saleor",
|
name: "Order Fulfilled in Saleor",
|
||||||
@@ -23,7 +24,17 @@ export default orderFulfilledWebhook.createHandler(async (req, res, ctx) => {
|
|||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Order ${order.number} fulfilled for customer: ${order.userEmail}`);
|
const webhookEnabled = await settingsStore.isWebhookEnabled("orderFulfilled");
|
||||||
|
const sendAdmin = await settingsStore.shouldSendAdminNotification("orderFulfilled");
|
||||||
|
const sendCustomer = await settingsStore.shouldSendCustomerNotification("orderFulfilled");
|
||||||
|
|
||||||
|
console.log(`📦 Order ${order.number} fulfilled for customer: ${order.userEmail}`);
|
||||||
|
console.log(`📋 Webhook settings - enabled: ${webhookEnabled}, admin: ${sendAdmin}, customer: ${sendCustomer}`);
|
||||||
|
|
||||||
|
if (!webhookEnabled) {
|
||||||
|
console.log("⏭️ Webhook disabled, skipping notifications");
|
||||||
|
return res.status(200).end();
|
||||||
|
}
|
||||||
|
|
||||||
const items = ((order as any).lines || []).map((line: any) => ({
|
const items = ((order as any).lines || []).map((line: any) => ({
|
||||||
id: line.id,
|
id: line.id,
|
||||||
@@ -41,16 +52,36 @@ export default orderFulfilledWebhook.createHandler(async (req, res, ctx) => {
|
|||||||
items,
|
items,
|
||||||
};
|
};
|
||||||
|
|
||||||
try {
|
if (sendAdmin) {
|
||||||
if (order.userEmail) {
|
try {
|
||||||
await sendOrderShippedEmail({
|
const adminEmails = process.env.ADMIN_EMAILS?.split(",").map(e => e.trim()).filter(e => e) || [];
|
||||||
to: order.userEmail,
|
|
||||||
orderData,
|
if (adminEmails.length > 0) {
|
||||||
});
|
await sendOrderShippedEmail({
|
||||||
console.log(`Customer notification sent for fulfilled order ${order.number}`);
|
to: adminEmails,
|
||||||
|
orderData,
|
||||||
|
});
|
||||||
|
console.log(`✅ Admin notification sent for fulfilled order ${order.number}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to send admin email:", error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
}
|
||||||
console.error("Failed to send email:", error);
|
|
||||||
|
if (sendCustomer) {
|
||||||
|
try {
|
||||||
|
if (order.userEmail) {
|
||||||
|
await sendOrderShippedEmail({
|
||||||
|
to: order.userEmail,
|
||||||
|
orderData,
|
||||||
|
});
|
||||||
|
console.log(`✅ Customer notification sent for fulfilled order ${order.number}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("❌ Failed to send customer email:", error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log("⏭️ Customer notification disabled, skipping");
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).end();
|
return res.status(200).end();
|
||||||
|
|||||||
@@ -0,0 +1,410 @@
|
|||||||
|
import { Box, Button, Input, Text } from "@saleor/macaw-ui";
|
||||||
|
import { NextPage } from "next";
|
||||||
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
interface WebhookToggle {
|
||||||
|
enabled: boolean;
|
||||||
|
sendAdminNotification: boolean;
|
||||||
|
sendCustomerNotification: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WebhookSettings {
|
||||||
|
orderCreated: WebhookToggle;
|
||||||
|
orderFulfilled: WebhookToggle;
|
||||||
|
orderCancelled: WebhookToggle;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmailSettings {
|
||||||
|
fromEmail: string;
|
||||||
|
fromName: string;
|
||||||
|
adminEmails: string;
|
||||||
|
siteUrl: string;
|
||||||
|
dashboardUrl: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Settings {
|
||||||
|
webhooks: WebhookSettings;
|
||||||
|
email: EmailSettings;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSettings: Settings = {
|
||||||
|
webhooks: {
|
||||||
|
orderCreated: { enabled: true, sendAdminNotification: true, sendCustomerNotification: true },
|
||||||
|
orderFulfilled: { enabled: true, sendAdminNotification: true, sendCustomerNotification: true },
|
||||||
|
orderCancelled: { enabled: true, sendAdminNotification: true, sendCustomerNotification: true },
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
fromEmail: "",
|
||||||
|
fromName: "",
|
||||||
|
adminEmails: "",
|
||||||
|
siteUrl: "",
|
||||||
|
dashboardUrl: "",
|
||||||
|
},
|
||||||
|
updatedAt: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
const webhookLabels: Record<keyof WebhookSettings, { title: string; description: string }> = {
|
||||||
|
orderCreated: {
|
||||||
|
title: "Order Created",
|
||||||
|
description: "Send email notifications when a new order is placed",
|
||||||
|
},
|
||||||
|
orderFulfilled: {
|
||||||
|
title: "Order Fulfilled",
|
||||||
|
description: "Send email notifications when an order is shipped",
|
||||||
|
},
|
||||||
|
orderCancelled: {
|
||||||
|
title: "Order Cancelled",
|
||||||
|
description: "Send email notifications when an order is cancelled",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const Toggle: React.FC<{
|
||||||
|
checked: boolean;
|
||||||
|
onChange: (checked: boolean) => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
}> = ({ checked, onChange, disabled }) => (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="switch"
|
||||||
|
aria-checked={checked}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() => onChange(!checked)}
|
||||||
|
style={{
|
||||||
|
position: "relative",
|
||||||
|
width: "44px",
|
||||||
|
height: "24px",
|
||||||
|
borderRadius: "12px",
|
||||||
|
backgroundColor: checked ? "#22c55e" : "#d1d5db",
|
||||||
|
border: "none",
|
||||||
|
cursor: disabled ? "not-allowed" : "pointer",
|
||||||
|
opacity: disabled ? 0.5 : 1,
|
||||||
|
transition: "background-color 0.2s",
|
||||||
|
padding: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
position: "absolute",
|
||||||
|
top: "2px",
|
||||||
|
left: checked ? "22px" : "2px",
|
||||||
|
width: "20px",
|
||||||
|
height: "20px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "white",
|
||||||
|
transition: "left 0.2s",
|
||||||
|
boxShadow: "0 1px 3px rgba(0,0,0,0.2)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Card: React.FC<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
}> = ({ children, style }) => (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#ffffff",
|
||||||
|
borderRadius: "12px",
|
||||||
|
border: "1px solid #e5e7eb",
|
||||||
|
padding: "24px",
|
||||||
|
...style,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const SettingsPage: NextPage = () => {
|
||||||
|
const [settings, setSettings] = useState<Settings>(defaultSettings);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [saving, setSaving] = useState(false);
|
||||||
|
const [saved, setSaved] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const bgColor = "#f9fafb";
|
||||||
|
const textColor = "#111827";
|
||||||
|
const subtextColor = "#6b7280";
|
||||||
|
const borderColor = "#e5e7eb";
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("/api/settings")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((data: Settings) => {
|
||||||
|
setSettings({
|
||||||
|
...data,
|
||||||
|
email: {
|
||||||
|
...data.email,
|
||||||
|
adminEmails: Array.isArray(data.email.adminEmails)
|
||||||
|
? data.email.adminEmails.join(", ")
|
||||||
|
: data.email.adminEmails || "",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setLoading(false);
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Failed to load settings:", err);
|
||||||
|
setError("Failed to load settings");
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleWebhookChange = useCallback(
|
||||||
|
(webhook: keyof WebhookSettings, field: keyof WebhookToggle) => {
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
webhooks: {
|
||||||
|
...prev.webhooks,
|
||||||
|
[webhook]: {
|
||||||
|
...prev.webhooks[webhook],
|
||||||
|
[field]: !prev.webhooks[webhook][field],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
setSaved(false);
|
||||||
|
},
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleEmailChange = useCallback((field: keyof EmailSettings, value: string) => {
|
||||||
|
setSettings((prev) => ({
|
||||||
|
...prev,
|
||||||
|
email: {
|
||||||
|
...prev.email,
|
||||||
|
[field]: value,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
setSaved(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSave = useCallback(async () => {
|
||||||
|
setSaving(true);
|
||||||
|
setError(null);
|
||||||
|
setSaved(false);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const toSave = {
|
||||||
|
...settings,
|
||||||
|
email: {
|
||||||
|
...settings.email,
|
||||||
|
adminEmails: settings.email.adminEmails
|
||||||
|
.split(",")
|
||||||
|
.map((e) => e.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const res = await fetch("/api/settings", {
|
||||||
|
method: "PUT",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify(toSave),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
throw new Error("Failed to save settings");
|
||||||
|
}
|
||||||
|
|
||||||
|
setSaved(true);
|
||||||
|
setTimeout(() => setSaved(false), 3000);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Failed to save settings:", err);
|
||||||
|
setError("Failed to save settings. Please try again.");
|
||||||
|
} finally {
|
||||||
|
setSaving(false);
|
||||||
|
}
|
||||||
|
}, [settings]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<Box padding={8} style={{ backgroundColor: bgColor, minHeight: "100vh" }}>
|
||||||
|
<Text size={5} style={{ color: textColor, fontWeight: "bold" }}>
|
||||||
|
Loading...
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box padding={8} style={{ backgroundColor: bgColor, minHeight: "100vh" }}>
|
||||||
|
<Box marginBottom={8}>
|
||||||
|
<Text size={7} style={{ color: textColor, fontWeight: "bold" }}>
|
||||||
|
Email Notifications
|
||||||
|
</Text>
|
||||||
|
<Text style={{ color: subtextColor, marginTop: "8px" }}>
|
||||||
|
Configure when to send email notifications for order events
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{error && (
|
||||||
|
<Box
|
||||||
|
padding={4}
|
||||||
|
marginBottom={6}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "#fee2e2",
|
||||||
|
borderRadius: "8px",
|
||||||
|
border: "1px solid #ef4444",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text style={{ color: "#991b1b" }}>{error}</Text>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Box marginBottom={6}>
|
||||||
|
<Text size={5} style={{ color: textColor, fontWeight: "bold", marginBottom: "16px" }}>
|
||||||
|
Webhooks
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{(Object.keys(webhookLabels) as Array<keyof WebhookSettings>).map((webhook) => {
|
||||||
|
const { title, description } = webhookLabels[webhook];
|
||||||
|
const webhookSettings = settings.webhooks[webhook];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card key={webhook} style={{ marginBottom: "16px" }}>
|
||||||
|
<Box display="flex" alignItems="flex-start" justifyContent="space-between">
|
||||||
|
<Box style={{ flex: 1 }}>
|
||||||
|
<Text style={{ color: textColor, fontWeight: "bold" }}>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
<Text size={3} style={{ color: subtextColor, marginTop: "4px" }}>
|
||||||
|
{description}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<Toggle
|
||||||
|
checked={webhookSettings.enabled}
|
||||||
|
onChange={() => handleWebhookChange(webhook, "enabled")}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{webhookSettings.enabled && (
|
||||||
|
<Box
|
||||||
|
marginTop={4}
|
||||||
|
paddingTop={4}
|
||||||
|
style={{ borderTop: `1px solid ${borderColor}` }}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
marginBottom={3}
|
||||||
|
>
|
||||||
|
<Text size={3} style={{ color: subtextColor }}>
|
||||||
|
Send to admin emails
|
||||||
|
</Text>
|
||||||
|
<Toggle
|
||||||
|
checked={webhookSettings.sendAdminNotification}
|
||||||
|
onChange={() => handleWebhookChange(webhook, "sendAdminNotification")}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
<Box display="flex" alignItems="center" justifyContent="space-between">
|
||||||
|
<Text size={3} style={{ color: subtextColor }}>
|
||||||
|
Send to customer
|
||||||
|
</Text>
|
||||||
|
<Toggle
|
||||||
|
checked={webhookSettings.sendCustomerNotification}
|
||||||
|
onChange={() => handleWebhookChange(webhook, "sendCustomerNotification")}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginBottom={6}>
|
||||||
|
<Text size={5} style={{ color: textColor, fontWeight: "bold", marginBottom: "16px" }}>
|
||||||
|
Email Configuration
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<Box marginBottom={4}>
|
||||||
|
<Text size={3} style={{ color: textColor, fontWeight: 500, marginBottom: "8px" }}>
|
||||||
|
From Name
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
value={settings.email.fromName}
|
||||||
|
onChange={(e) => handleEmailChange("fromName", e.target.value)}
|
||||||
|
placeholder="ManoonOils"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginBottom={4}>
|
||||||
|
<Text size={3} style={{ color: textColor, fontWeight: 500, marginBottom: "8px" }}>
|
||||||
|
From Email
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
value={settings.email.fromEmail}
|
||||||
|
onChange={(e) => handleEmailChange("fromEmail", e.target.value)}
|
||||||
|
placeholder="support@mail.manoonoils.com"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginBottom={4}>
|
||||||
|
<Text size={3} style={{ color: textColor, fontWeight: 500, marginBottom: "8px" }}>
|
||||||
|
Admin Emails (comma separated)
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
value={settings.email.adminEmails}
|
||||||
|
onChange={(e) => handleEmailChange("adminEmails", e.target.value)}
|
||||||
|
placeholder="admin@example.com, manager@example.com"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box marginBottom={4}>
|
||||||
|
<Text size={3} style={{ color: textColor, fontWeight: 500, marginBottom: "8px" }}>
|
||||||
|
Store URL
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
value={settings.email.siteUrl}
|
||||||
|
onChange={(e) => handleEmailChange("siteUrl", e.target.value)}
|
||||||
|
placeholder="https://manoonoils.com"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box>
|
||||||
|
<Text size={3} style={{ color: textColor, fontWeight: 500, marginBottom: "8px" }}>
|
||||||
|
Dashboard URL
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
type="url"
|
||||||
|
value={settings.email.dashboardUrl}
|
||||||
|
onChange={(e) => handleEmailChange("dashboardUrl", e.target.value)}
|
||||||
|
placeholder="https://dashboard.manoonoils.com"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Card>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box display="flex" alignItems="center" gap={4}>
|
||||||
|
<Button
|
||||||
|
variant="primary"
|
||||||
|
onClick={handleSave}
|
||||||
|
disabled={saving}
|
||||||
|
>
|
||||||
|
{saving ? "Saving..." : "Save Settings"}
|
||||||
|
</Button>
|
||||||
|
{saved && (
|
||||||
|
<Text size={3} style={{ color: "#22c55e" }}>
|
||||||
|
Settings saved successfully!
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{settings.updatedAt && (
|
||||||
|
<Text size={3} style={{ color: subtextColor, marginTop: "16px" }}>
|
||||||
|
Last updated: {new Date(settings.updatedAt).toLocaleString()}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsPage;
|
||||||
Reference in New Issue
Block a user