fix all error
Browse files- .vscode/settings.json +3 -0
- app/admin/actions.ts +24 -15
- app/api/admin/analytics/route.ts +5 -3
- app/api/admin/users/[id]/route.ts +2 -1
- app/api/auth/otp/send/route.ts +3 -0
- app/api/notifications/[id]/route.ts +4 -2
- app/api/social/automations/[id]/route.ts +2 -1
- app/api/social/automations/create/route.ts +13 -11
- app/api/social/automations/route.ts +25 -0
- app/api/social/callback/[provider]/route.ts +2 -1
- app/api/social/connect/[provider]/route.ts +2 -1
- app/api/webhooks/email/route.ts +10 -5
- app/dashboard/whatsapp/page.tsx +219 -0
- components/dashboard/sidebar.tsx +2 -0
- components/mobile-nav.tsx +2 -1
- components/node-editor/workflow-node.tsx +7 -1
- components/settings/whatsapp-settings.tsx +79 -75
- lib/api-response-helpers.ts +6 -2
- lib/auth.ts +5 -1
- lib/queue.ts +13 -6
- lib/whatsapp/webhook.ts +18 -4
- lib/workflow-executor.ts +6 -3
.vscode/settings.json
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"agq.showPromptCredits": true
|
| 3 |
+
}
|
app/admin/actions.ts
CHANGED
|
@@ -14,20 +14,33 @@ export async function sendGlobalNotification(formData: FormData) {
|
|
| 14 |
|
| 15 |
const title = formData.get("title") as string;
|
| 16 |
const message = formData.get("message") as string;
|
| 17 |
-
const
|
| 18 |
|
| 19 |
if (!title || !message) return { error: "Missing fields" };
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
// --- Banners ---
|
|
@@ -39,10 +52,6 @@ export async function createBanner(formData: FormData) {
|
|
| 39 |
const message = formData.get("message") as string;
|
| 40 |
if (!message) return { error: "Message required" };
|
| 41 |
|
| 42 |
-
// Deactivate other banners if we want only one active?
|
| 43 |
-
// User didn't specify, but usually marquee is one at a time or list.
|
| 44 |
-
// I'll leave others active unless requested.
|
| 45 |
-
|
| 46 |
await db.insert(banners).values({
|
| 47 |
message,
|
| 48 |
isActive: true,
|
|
|
|
| 14 |
|
| 15 |
const title = formData.get("title") as string;
|
| 16 |
const message = formData.get("message") as string;
|
| 17 |
+
const level = (formData.get("type") as "info" | "warning" | "success") || "info";
|
| 18 |
|
| 19 |
if (!title || !message) return { error: "Missing fields" };
|
| 20 |
|
| 21 |
+
try {
|
| 22 |
+
const allUsers = await db.query.users.findMany({
|
| 23 |
+
columns: { id: true }
|
| 24 |
+
});
|
| 25 |
+
|
| 26 |
+
if (allUsers.length > 0) {
|
| 27 |
+
const notificationsData = allUsers.map(user => ({
|
| 28 |
+
userId: user.id,
|
| 29 |
+
title,
|
| 30 |
+
message,
|
| 31 |
+
category: "system",
|
| 32 |
+
level,
|
| 33 |
+
}));
|
| 34 |
+
|
| 35 |
+
await db.insert(notifications).values(notificationsData);
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
revalidatePath("/dashboard");
|
| 39 |
+
return { success: true };
|
| 40 |
+
} catch (error) {
|
| 41 |
+
console.error("Failed to send global notifications:", error);
|
| 42 |
+
return { error: "Failed to send notifications" };
|
| 43 |
+
}
|
| 44 |
}
|
| 45 |
|
| 46 |
// --- Banners ---
|
|
|
|
| 52 |
const message = formData.get("message") as string;
|
| 53 |
if (!message) return { error: "Message required" };
|
| 54 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
await db.insert(banners).values({
|
| 56 |
message,
|
| 57 |
isActive: true,
|
app/api/admin/analytics/route.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { db } from "@/db";
|
|
| 4 |
import { users } from "@/db/schema";
|
| 5 |
import { sql } from "drizzle-orm";
|
| 6 |
|
|
|
|
| 7 |
export async function GET(request: NextRequest) {
|
| 8 |
const session = await auth();
|
| 9 |
|
|
@@ -24,10 +25,11 @@ export async function GET(request: NextRequest) {
|
|
| 24 |
`);
|
| 25 |
|
| 26 |
let cumulativeUsers = 0;
|
| 27 |
-
const userGrowth = usersByDate.map((row
|
| 28 |
-
|
|
|
|
| 29 |
return {
|
| 30 |
-
date: new Date(
|
| 31 |
users: cumulativeUsers
|
| 32 |
};
|
| 33 |
});
|
|
|
|
| 4 |
import { users } from "@/db/schema";
|
| 5 |
import { sql } from "drizzle-orm";
|
| 6 |
|
| 7 |
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
| 8 |
export async function GET(request: NextRequest) {
|
| 9 |
const session = await auth();
|
| 10 |
|
|
|
|
| 25 |
`);
|
| 26 |
|
| 27 |
let cumulativeUsers = 0;
|
| 28 |
+
const userGrowth = usersByDate.rows.map((row) => {
|
| 29 |
+
const typedRow = row as unknown as { date: string, count: number };
|
| 30 |
+
cumulativeUsers += Number(typedRow.count);
|
| 31 |
return {
|
| 32 |
+
date: new Date(typedRow.date).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
|
| 33 |
users: cumulativeUsers
|
| 34 |
};
|
| 35 |
});
|
app/api/admin/users/[id]/route.ts
CHANGED
|
@@ -6,8 +6,9 @@ import { eq } from "drizzle-orm";
|
|
| 6 |
|
| 7 |
export async function PATCH(
|
| 8 |
request: NextRequest,
|
| 9 |
-
|
| 10 |
) {
|
|
|
|
| 11 |
const session = await auth();
|
| 12 |
const { id } = await params;
|
| 13 |
|
|
|
|
| 6 |
|
| 7 |
export async function PATCH(
|
| 8 |
request: NextRequest,
|
| 9 |
+
props: { params: Promise<{ id: string }> }
|
| 10 |
) {
|
| 11 |
+
const params = await props.params;
|
| 12 |
const session = await auth();
|
| 13 |
const { id } = await params;
|
| 14 |
|
app/api/auth/otp/send/route.ts
CHANGED
|
@@ -32,6 +32,9 @@ export async function POST(req: NextRequest) {
|
|
| 32 |
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
| 33 |
|
| 34 |
// Store in Redis (TTL 5 mins)
|
|
|
|
|
|
|
|
|
|
| 35 |
await redis.set(`otp:${phoneNumber}`, code, "EX", 300);
|
| 36 |
|
| 37 |
// Send via WhatsApp
|
|
|
|
| 32 |
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
| 33 |
|
| 34 |
// Store in Redis (TTL 5 mins)
|
| 35 |
+
if (!redis) {
|
| 36 |
+
throw new Error("Redis client not initialized");
|
| 37 |
+
}
|
| 38 |
await redis.set(`otp:${phoneNumber}`, code, "EX", 300);
|
| 39 |
|
| 40 |
// Send via WhatsApp
|
app/api/notifications/[id]/route.ts
CHANGED
|
@@ -4,8 +4,9 @@ import { NotificationService } from "@/lib/notifications/notification-service";
|
|
| 4 |
|
| 5 |
export async function PATCH(
|
| 6 |
request: Request,
|
| 7 |
-
|
| 8 |
) {
|
|
|
|
| 9 |
try {
|
| 10 |
const session = await auth();
|
| 11 |
if (!session?.user?.id) {
|
|
@@ -25,8 +26,9 @@ export async function PATCH(
|
|
| 25 |
|
| 26 |
export async function DELETE(
|
| 27 |
request: Request,
|
| 28 |
-
|
| 29 |
) {
|
|
|
|
| 30 |
try {
|
| 31 |
const session = await auth();
|
| 32 |
if (!session?.user?.id) {
|
|
|
|
| 4 |
|
| 5 |
export async function PATCH(
|
| 6 |
request: Request,
|
| 7 |
+
props: { params: Promise<{ id: string }> }
|
| 8 |
) {
|
| 9 |
+
const params = await props.params;
|
| 10 |
try {
|
| 11 |
const session = await auth();
|
| 12 |
if (!session?.user?.id) {
|
|
|
|
| 26 |
|
| 27 |
export async function DELETE(
|
| 28 |
request: Request,
|
| 29 |
+
props: { params: Promise<{ id: string }> }
|
| 30 |
) {
|
| 31 |
+
const params = await props.params;
|
| 32 |
try {
|
| 33 |
const session = await auth();
|
| 34 |
if (!session?.user?.id) {
|
app/api/social/automations/[id]/route.ts
CHANGED
|
@@ -6,8 +6,9 @@ import { apiSuccess, apiError } from "@/lib/api-response-helpers";
|
|
| 6 |
|
| 7 |
export async function DELETE(
|
| 8 |
request: Request,
|
| 9 |
-
|
| 10 |
) {
|
|
|
|
| 11 |
try {
|
| 12 |
const session = await auth();
|
| 13 |
|
|
|
|
| 6 |
|
| 7 |
export async function DELETE(
|
| 8 |
request: Request,
|
| 9 |
+
props: { params: Promise<{ id: string }> }
|
| 10 |
) {
|
| 11 |
+
const params = await props.params;
|
| 12 |
try {
|
| 13 |
const session = await auth();
|
| 14 |
|
app/api/social/automations/create/route.ts
CHANGED
|
@@ -18,21 +18,23 @@ export async function POST(req: NextRequest) {
|
|
| 18 |
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
| 19 |
}
|
| 20 |
|
| 21 |
-
// For
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
}
|
| 32 |
|
| 33 |
await db.insert(socialAutomations).values({
|
| 34 |
userId: session.user.id,
|
| 35 |
-
connectedAccountId:
|
| 36 |
name,
|
| 37 |
triggerType,
|
| 38 |
keywords, // Array of strings
|
|
|
|
| 18 |
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
|
| 19 |
}
|
| 20 |
|
| 21 |
+
// For whatsapp_command, we don't need a connected account as it uses the user's global WhatsApp config
|
| 22 |
+
let accountId = null;
|
| 23 |
+
|
| 24 |
+
if (triggerType !== 'whatsapp_command') {
|
| 25 |
+
const account = await db.query.connectedAccounts.findFirst({
|
| 26 |
+
where: eq(connectedAccounts.userId, session.user.id)
|
| 27 |
+
});
|
| 28 |
+
|
| 29 |
+
if (!account) {
|
| 30 |
+
return NextResponse.json({ error: "No connected social account found" }, { status: 400 });
|
| 31 |
+
}
|
| 32 |
+
accountId = account.id;
|
| 33 |
}
|
| 34 |
|
| 35 |
await db.insert(socialAutomations).values({
|
| 36 |
userId: session.user.id,
|
| 37 |
+
connectedAccountId: accountId, // Linking to first account found for now or null for global commands
|
| 38 |
name,
|
| 39 |
triggerType,
|
| 40 |
keywords, // Array of strings
|
app/api/social/automations/route.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
import { NextResponse } from "next/server";
|
| 3 |
+
import { auth } from "@/auth";
|
| 4 |
+
import { db } from "@/db";
|
| 5 |
+
import { socialAutomations } from "@/db/schema";
|
| 6 |
+
import { eq, desc } from "drizzle-orm";
|
| 7 |
+
|
| 8 |
+
export async function GET() {
|
| 9 |
+
const session = await auth();
|
| 10 |
+
if (!session?.user?.id) {
|
| 11 |
+
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
| 12 |
+
}
|
| 13 |
+
|
| 14 |
+
try {
|
| 15 |
+
const automations = await db.query.socialAutomations.findMany({
|
| 16 |
+
where: eq(socialAutomations.userId, session.user.id),
|
| 17 |
+
orderBy: [desc(socialAutomations.createdAt)]
|
| 18 |
+
});
|
| 19 |
+
|
| 20 |
+
return NextResponse.json(automations);
|
| 21 |
+
} catch (error) {
|
| 22 |
+
console.error("Error fetching automations:", error);
|
| 23 |
+
return NextResponse.json({ error: "Failed to fetch automations" }, { status: 500 });
|
| 24 |
+
}
|
| 25 |
+
}
|
app/api/social/callback/[provider]/route.ts
CHANGED
|
@@ -8,8 +8,9 @@ import { eq, and } from "drizzle-orm";
|
|
| 8 |
setDefaultResultOrder("ipv4first");
|
| 9 |
export async function GET(
|
| 10 |
req: NextRequest,
|
| 11 |
-
|
| 12 |
) {
|
|
|
|
| 13 |
const session = await auth();
|
| 14 |
|
| 15 |
// Facebook callbacks might not have session cookies if same-site strict?
|
|
|
|
| 8 |
setDefaultResultOrder("ipv4first");
|
| 9 |
export async function GET(
|
| 10 |
req: NextRequest,
|
| 11 |
+
props: { params: Promise<{ provider: string }> }
|
| 12 |
) {
|
| 13 |
+
const params = await props.params;
|
| 14 |
const session = await auth();
|
| 15 |
|
| 16 |
// Facebook callbacks might not have session cookies if same-site strict?
|
app/api/social/connect/[provider]/route.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { auth } from "@/auth";
|
|
| 3 |
|
| 4 |
export async function GET(
|
| 5 |
req: NextRequest,
|
| 6 |
-
|
| 7 |
) {
|
|
|
|
| 8 |
const session = await auth();
|
| 9 |
if (!session?.user?.id) {
|
| 10 |
return NextResponse.redirect(new URL("/auth/signin", req.url));
|
|
|
|
| 3 |
|
| 4 |
export async function GET(
|
| 5 |
req: NextRequest,
|
| 6 |
+
props: { params: Promise<{ provider: string }> }
|
| 7 |
) {
|
| 8 |
+
const params = await props.params;
|
| 9 |
const session = await auth();
|
| 10 |
if (!session?.user?.id) {
|
| 11 |
return NextResponse.redirect(new URL("/auth/signin", req.url));
|
app/api/webhooks/email/route.ts
CHANGED
|
@@ -46,7 +46,8 @@ export async function POST(request: Request) {
|
|
| 46 |
userId: business.userId,
|
| 47 |
title: "Email Opened",
|
| 48 |
message: `${business.name} opened your email`,
|
| 49 |
-
|
|
|
|
| 50 |
});
|
| 51 |
break;
|
| 52 |
|
|
@@ -69,7 +70,8 @@ export async function POST(request: Request) {
|
|
| 69 |
userId: business.userId,
|
| 70 |
title: "Link Clicked",
|
| 71 |
message: `${business.name} clicked a link in your email`,
|
| 72 |
-
|
|
|
|
| 73 |
});
|
| 74 |
break;
|
| 75 |
|
|
@@ -91,7 +93,8 @@ export async function POST(request: Request) {
|
|
| 91 |
userId: business.userId,
|
| 92 |
title: "Email Bounced",
|
| 93 |
message: `Email to ${business.name} bounced`,
|
| 94 |
-
|
|
|
|
| 95 |
});
|
| 96 |
break;
|
| 97 |
|
|
@@ -113,7 +116,8 @@ export async function POST(request: Request) {
|
|
| 113 |
userId: business.userId,
|
| 114 |
title: "Spam Report",
|
| 115 |
message: `${business.name} reported your email as spam`,
|
| 116 |
-
|
|
|
|
| 117 |
});
|
| 118 |
break;
|
| 119 |
|
|
@@ -127,7 +131,8 @@ export async function POST(request: Request) {
|
|
| 127 |
userId: business.userId,
|
| 128 |
title: "Unsubscribed",
|
| 129 |
message: `${business.name} unsubscribed`,
|
| 130 |
-
|
|
|
|
| 131 |
});
|
| 132 |
break;
|
| 133 |
|
|
|
|
| 46 |
userId: business.userId,
|
| 47 |
title: "Email Opened",
|
| 48 |
message: `${business.name} opened your email`,
|
| 49 |
+
level: "info",
|
| 50 |
+
category: "email",
|
| 51 |
});
|
| 52 |
break;
|
| 53 |
|
|
|
|
| 70 |
userId: business.userId,
|
| 71 |
title: "Link Clicked",
|
| 72 |
message: `${business.name} clicked a link in your email`,
|
| 73 |
+
level: "success",
|
| 74 |
+
category: "email",
|
| 75 |
});
|
| 76 |
break;
|
| 77 |
|
|
|
|
| 93 |
userId: business.userId,
|
| 94 |
title: "Email Bounced",
|
| 95 |
message: `Email to ${business.name} bounced`,
|
| 96 |
+
level: "error",
|
| 97 |
+
category: "email",
|
| 98 |
});
|
| 99 |
break;
|
| 100 |
|
|
|
|
| 116 |
userId: business.userId,
|
| 117 |
title: "Spam Report",
|
| 118 |
message: `${business.name} reported your email as spam`,
|
| 119 |
+
level: "error",
|
| 120 |
+
category: "email",
|
| 121 |
});
|
| 122 |
break;
|
| 123 |
|
|
|
|
| 131 |
userId: business.userId,
|
| 132 |
title: "Unsubscribed",
|
| 133 |
message: `${business.name} unsubscribed`,
|
| 134 |
+
level: "warning",
|
| 135 |
+
category: "email",
|
| 136 |
});
|
| 137 |
break;
|
| 138 |
|
app/dashboard/whatsapp/page.tsx
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"use client";
|
| 2 |
+
|
| 3 |
+
import { useEffect, useState, useCallback } from "react";
|
| 4 |
+
import { Button } from "@/components/ui/button";
|
| 5 |
+
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
| 6 |
+
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
|
| 7 |
+
import { Input } from "@/components/ui/input";
|
| 8 |
+
import { Label } from "@/components/ui/label";
|
| 9 |
+
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
| 10 |
+
import { Textarea } from "@/components/ui/textarea";
|
| 11 |
+
import { useApi } from "@/hooks/use-api";
|
| 12 |
+
import { Loader2, Plus, Terminal, Trash2 } from "lucide-react";
|
| 13 |
+
import { useToast } from "@/hooks/use-toast";
|
| 14 |
+
import { Badge } from "@/components/ui/badge";
|
| 15 |
+
|
| 16 |
+
interface Automation {
|
| 17 |
+
id: string;
|
| 18 |
+
name: string;
|
| 19 |
+
triggerType: string;
|
| 20 |
+
keywords: string[];
|
| 21 |
+
responseTemplate: string;
|
| 22 |
+
isActive: boolean;
|
| 23 |
+
createdAt: string;
|
| 24 |
+
}
|
| 25 |
+
|
| 26 |
+
export default function WhatsAppCommandCenter() {
|
| 27 |
+
const { toast } = useToast();
|
| 28 |
+
const { get, post, del, loading } = useApi<Automation[]>();
|
| 29 |
+
const [commands, setCommands] = useState<Automation[]>([]);
|
| 30 |
+
const [isDialogOpen, setIsDialogOpen] = useState(false);
|
| 31 |
+
const [formData, setFormData] = useState({
|
| 32 |
+
name: "",
|
| 33 |
+
command: "/",
|
| 34 |
+
response: ""
|
| 35 |
+
});
|
| 36 |
+
|
| 37 |
+
const fetchCommands = useCallback(async () => {
|
| 38 |
+
const data = await get("/api/social/automations");
|
| 39 |
+
if (data) {
|
| 40 |
+
// Filter only commands
|
| 41 |
+
setCommands(data.filter(a => a.triggerType === "whatsapp_command"));
|
| 42 |
+
}
|
| 43 |
+
}, [get]);
|
| 44 |
+
|
| 45 |
+
useEffect(() => {
|
| 46 |
+
const timer = setTimeout(() => {
|
| 47 |
+
fetchCommands();
|
| 48 |
+
}, 0);
|
| 49 |
+
return () => clearTimeout(timer);
|
| 50 |
+
}, [fetchCommands]);
|
| 51 |
+
|
| 52 |
+
const handleCreate = async () => {
|
| 53 |
+
if (!formData.name || !formData.command || !formData.response) {
|
| 54 |
+
toast({ title: "Error", description: "All fields are required", variant: "destructive" });
|
| 55 |
+
return;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
if (!formData.command.startsWith("/")) {
|
| 59 |
+
toast({ title: "Error", description: "Command must start with /", variant: "destructive" });
|
| 60 |
+
return;
|
| 61 |
+
}
|
| 62 |
+
|
| 63 |
+
const payload = {
|
| 64 |
+
name: formData.name,
|
| 65 |
+
triggerType: "whatsapp_command",
|
| 66 |
+
actionType: "whatsapp_reply",
|
| 67 |
+
keywords: [formData.command.toLowerCase().trim()],
|
| 68 |
+
responseTemplate: formData.response
|
| 69 |
+
};
|
| 70 |
+
|
| 71 |
+
const result = await post("/api/social/automations/create", payload, {
|
| 72 |
+
successMessage: "Command created successfully"
|
| 73 |
+
});
|
| 74 |
+
|
| 75 |
+
if (result) {
|
| 76 |
+
setIsDialogOpen(false);
|
| 77 |
+
setFormData({ name: "", command: "/", response: "" });
|
| 78 |
+
fetchCommands();
|
| 79 |
+
}
|
| 80 |
+
};
|
| 81 |
+
|
| 82 |
+
const handleDelete = async (id: string) => {
|
| 83 |
+
if (confirm("Are you sure you want to delete this command?")) {
|
| 84 |
+
const result = await del(`/api/social/automations/${id}`, {
|
| 85 |
+
successMessage: "Command deleted"
|
| 86 |
+
});
|
| 87 |
+
if (result) {
|
| 88 |
+
fetchCommands();
|
| 89 |
+
}
|
| 90 |
+
}
|
| 91 |
+
};
|
| 92 |
+
|
| 93 |
+
return (
|
| 94 |
+
<div className="space-y-6">
|
| 95 |
+
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
| 96 |
+
<div>
|
| 97 |
+
<h1 className="text-3xl font-bold">WhatsApp Command Center</h1>
|
| 98 |
+
<p className="text-muted-foreground">Manage slash commands for your WhatsApp chatbot.</p>
|
| 99 |
+
</div>
|
| 100 |
+
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
|
| 101 |
+
<DialogTrigger asChild>
|
| 102 |
+
<Button>
|
| 103 |
+
<Plus className="mr-2 h-4 w-4" />
|
| 104 |
+
New Command
|
| 105 |
+
</Button>
|
| 106 |
+
</DialogTrigger>
|
| 107 |
+
<DialogContent className="sm:max-w-[425px]">
|
| 108 |
+
<DialogHeader>
|
| 109 |
+
<DialogTitle>Create New Command</DialogTitle>
|
| 110 |
+
<DialogDescription>
|
| 111 |
+
Define a command that users can send to trigger a specific response.
|
| 112 |
+
</DialogDescription>
|
| 113 |
+
</DialogHeader>
|
| 114 |
+
<div className="grid gap-4 py-4">
|
| 115 |
+
<div className="grid gap-2">
|
| 116 |
+
<Label htmlFor="name">Friendly Name</Label>
|
| 117 |
+
<Input
|
| 118 |
+
id="name"
|
| 119 |
+
placeholder="e.g. Start Menu"
|
| 120 |
+
value={formData.name}
|
| 121 |
+
onChange={(e) => setFormData({ ...formData, name: e.target.value })}
|
| 122 |
+
/>
|
| 123 |
+
</div>
|
| 124 |
+
<div className="grid gap-2">
|
| 125 |
+
<Label htmlFor="command">Command Trigger</Label>
|
| 126 |
+
<div className="relative">
|
| 127 |
+
<Terminal className="absolute left-3 top-2.5 h-4 w-4 text-muted-foreground" />
|
| 128 |
+
<Input
|
| 129 |
+
id="command"
|
| 130 |
+
placeholder="/start"
|
| 131 |
+
className="pl-9"
|
| 132 |
+
value={formData.command}
|
| 133 |
+
onChange={(e) => setFormData({ ...formData, command: e.target.value })}
|
| 134 |
+
/>
|
| 135 |
+
</div>
|
| 136 |
+
<p className="text-xs text-muted-foreground">Must start with /</p>
|
| 137 |
+
</div>
|
| 138 |
+
<div className="grid gap-2">
|
| 139 |
+
<Label htmlFor="response">Response Message</Label>
|
| 140 |
+
<Textarea
|
| 141 |
+
id="response"
|
| 142 |
+
placeholder="Hello! Welcome to our service. Here are the options..."
|
| 143 |
+
rows={4}
|
| 144 |
+
value={formData.response}
|
| 145 |
+
onChange={(e) => setFormData({ ...formData, response: e.target.value })}
|
| 146 |
+
/>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
<DialogFooter>
|
| 150 |
+
<Button variant="outline" onClick={() => setIsDialogOpen(false)}>Cancel</Button>
|
| 151 |
+
<Button onClick={handleCreate} disabled={loading}>
|
| 152 |
+
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
| 153 |
+
Create Command
|
| 154 |
+
</Button>
|
| 155 |
+
</DialogFooter>
|
| 156 |
+
</DialogContent>
|
| 157 |
+
</Dialog>
|
| 158 |
+
</div>
|
| 159 |
+
|
| 160 |
+
<Card>
|
| 161 |
+
<CardHeader>
|
| 162 |
+
<CardTitle>Active Commands</CardTitle>
|
| 163 |
+
<CardDescription>
|
| 164 |
+
{commands.length} active commands available to your users.
|
| 165 |
+
</CardDescription>
|
| 166 |
+
</CardHeader>
|
| 167 |
+
<CardContent>
|
| 168 |
+
<div className="overflow-x-auto">
|
| 169 |
+
<Table>
|
| 170 |
+
<TableHeader>
|
| 171 |
+
<TableRow>
|
| 172 |
+
<TableHead>Command</TableHead>
|
| 173 |
+
<TableHead className="hidden sm:table-cell">Name</TableHead>
|
| 174 |
+
<TableHead className="hidden md:table-cell">Response Preview</TableHead>
|
| 175 |
+
<TableHead className="w-[100px]">Status</TableHead>
|
| 176 |
+
<TableHead className="w-[100px] text-right">Actions</TableHead>
|
| 177 |
+
</TableRow>
|
| 178 |
+
</TableHeader>
|
| 179 |
+
<TableBody>
|
| 180 |
+
{commands.length === 0 ? (
|
| 181 |
+
<TableRow>
|
| 182 |
+
<TableCell colSpan={5} className="text-center text-muted-foreground h-24">
|
| 183 |
+
No commands found. Create one to get started.
|
| 184 |
+
</TableCell>
|
| 185 |
+
</TableRow>
|
| 186 |
+
) : (
|
| 187 |
+
commands.map((cmd) => (
|
| 188 |
+
<TableRow key={cmd.id}>
|
| 189 |
+
<TableCell className="font-mono font-medium">{cmd.keywords[0]}</TableCell>
|
| 190 |
+
<TableCell className="hidden sm:table-cell">{cmd.name}</TableCell>
|
| 191 |
+
<TableCell className="hidden md:table-cell max-w-[300px] truncate" title={cmd.responseTemplate}>
|
| 192 |
+
{cmd.responseTemplate}
|
| 193 |
+
</TableCell>
|
| 194 |
+
<TableCell>
|
| 195 |
+
<Badge variant={cmd.isActive ? "default" : "secondary"}>
|
| 196 |
+
{cmd.isActive ? "Active" : "Inactive"}
|
| 197 |
+
</Badge>
|
| 198 |
+
</TableCell>
|
| 199 |
+
<TableCell className="text-right">
|
| 200 |
+
<Button
|
| 201 |
+
variant="ghost"
|
| 202 |
+
size="icon"
|
| 203 |
+
className="h-8 w-8 text-destructive hover:text-destructive"
|
| 204 |
+
onClick={() => handleDelete(cmd.id)}
|
| 205 |
+
>
|
| 206 |
+
<Trash2 className="h-4 w-4" />
|
| 207 |
+
</Button>
|
| 208 |
+
</TableCell>
|
| 209 |
+
</TableRow>
|
| 210 |
+
))
|
| 211 |
+
)}
|
| 212 |
+
</TableBody>
|
| 213 |
+
</Table>
|
| 214 |
+
</div>
|
| 215 |
+
</CardContent>
|
| 216 |
+
</Card>
|
| 217 |
+
</div>
|
| 218 |
+
);
|
| 219 |
+
}
|
components/dashboard/sidebar.tsx
CHANGED
|
@@ -15,6 +15,7 @@ import {
|
|
| 15 |
CheckSquare,
|
| 16 |
Share2,
|
| 17 |
Search,
|
|
|
|
| 18 |
} from "lucide-react";
|
| 19 |
import { Button } from "@/components/ui/button";
|
| 20 |
import { useState } from "react";
|
|
@@ -32,6 +33,7 @@ const navigation = [
|
|
| 32 |
{ name: "Analytics", href: "/dashboard/analytics", icon: FileText },
|
| 33 |
{ name: "Tasks", href: "/dashboard/tasks", icon: CheckSquare },
|
| 34 |
{ name: "Social Suite", href: "/dashboard/social", icon: Share2 },
|
|
|
|
| 35 |
{ name: "Settings", href: "/dashboard/settings", icon: Settings },
|
| 36 |
];
|
| 37 |
|
|
|
|
| 15 |
CheckSquare,
|
| 16 |
Share2,
|
| 17 |
Search,
|
| 18 |
+
MessageCircle,
|
| 19 |
} from "lucide-react";
|
| 20 |
import { Button } from "@/components/ui/button";
|
| 21 |
import { useState } from "react";
|
|
|
|
| 33 |
{ name: "Analytics", href: "/dashboard/analytics", icon: FileText },
|
| 34 |
{ name: "Tasks", href: "/dashboard/tasks", icon: CheckSquare },
|
| 35 |
{ name: "Social Suite", href: "/dashboard/social", icon: Share2 },
|
| 36 |
+
{ name: "WhatsApp", href: "/dashboard/whatsapp", icon: MessageCircle },
|
| 37 |
{ name: "Settings", href: "/dashboard/settings", icon: Settings },
|
| 38 |
];
|
| 39 |
|
components/mobile-nav.tsx
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import { useState } from "react";
|
| 4 |
-
import { Menu, LogOut, LayoutDashboard, Building2, Workflow, Mail, FileText, CheckSquare, Settings, Share2, Search } from "lucide-react";
|
| 5 |
import { Button } from "@/components/ui/button";
|
| 6 |
import { cn } from "@/lib/utils";
|
| 7 |
import Link from "next/link";
|
|
@@ -19,6 +19,7 @@ const navigation = [
|
|
| 19 |
{ name: "Analytics", href: "/dashboard/analytics", icon: FileText },
|
| 20 |
{ name: "Tasks", href: "/dashboard/tasks", icon: CheckSquare },
|
| 21 |
{ name: "Social Suite", href: "/dashboard/social", icon: Share2 },
|
|
|
|
| 22 |
{ name: "Settings", href: "/dashboard/settings", icon: Settings },
|
| 23 |
];
|
| 24 |
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import { useState } from "react";
|
| 4 |
+
import { Menu, LogOut, LayoutDashboard, Building2, Workflow, Mail, FileText, CheckSquare, Settings, Share2, Search, MessageCircle } from "lucide-react";
|
| 5 |
import { Button } from "@/components/ui/button";
|
| 6 |
import { cn } from "@/lib/utils";
|
| 7 |
import Link from "next/link";
|
|
|
|
| 19 |
{ name: "Analytics", href: "/dashboard/analytics", icon: FileText },
|
| 20 |
{ name: "Tasks", href: "/dashboard/tasks", icon: CheckSquare },
|
| 21 |
{ name: "Social Suite", href: "/dashboard/social", icon: Share2 },
|
| 22 |
+
{ name: "WhatsApp", href: "/dashboard/whatsapp", icon: MessageCircle },
|
| 23 |
{ name: "Settings", href: "/dashboard/settings", icon: Settings },
|
| 24 |
];
|
| 25 |
|
components/node-editor/workflow-node.tsx
CHANGED
|
@@ -23,7 +23,10 @@ const nodeColors = {
|
|
| 23 |
linkedinMessage: "#0077b5",
|
| 24 |
abSplit: "#ec4899",
|
| 25 |
whatsappNode: "#25D366",
|
| 26 |
-
database: "#60a5fa",
|
|
|
|
|
|
|
|
|
|
| 27 |
};
|
| 28 |
|
| 29 |
const nodeIcons = {
|
|
@@ -47,6 +50,9 @@ const nodeIcons = {
|
|
| 47 |
abSplit: "🔀",
|
| 48 |
whatsappNode: "📱",
|
| 49 |
database: "💾",
|
|
|
|
|
|
|
|
|
|
| 50 |
};
|
| 51 |
|
| 52 |
export const WorkflowNode = memo(({ data, selected }: NodeProps<NodeData>) => {
|
|
|
|
| 23 |
linkedinMessage: "#0077b5",
|
| 24 |
abSplit: "#ec4899",
|
| 25 |
whatsappNode: "#25D366",
|
| 26 |
+
database: "#60a5fa",
|
| 27 |
+
social_post: "#1877F2",
|
| 28 |
+
social_reply: "#25D366",
|
| 29 |
+
social_monitor: "#1877F2",
|
| 30 |
};
|
| 31 |
|
| 32 |
const nodeIcons = {
|
|
|
|
| 50 |
abSplit: "🔀",
|
| 51 |
whatsappNode: "📱",
|
| 52 |
database: "💾",
|
| 53 |
+
social_post: "📮",
|
| 54 |
+
social_reply: "💬",
|
| 55 |
+
social_monitor:"🔍"
|
| 56 |
};
|
| 57 |
|
| 58 |
export const WorkflowNode = memo(({ data, selected }: NodeProps<NodeData>) => {
|
components/settings/whatsapp-settings.tsx
CHANGED
|
@@ -1,4 +1,3 @@
|
|
| 1 |
-
|
| 2 |
"use client";
|
| 3 |
|
| 4 |
import { useState } from "react";
|
|
@@ -35,20 +34,25 @@ export function WhatsAppSettings({ businessPhone, isConfigured: initialConfigure
|
|
| 35 |
return;
|
| 36 |
}
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
});
|
| 45 |
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
const copyWebhookUrl = () => {
|
| 54 |
const url = `${window.location.origin}/api/webhooks/whatsapp`;
|
|
@@ -86,69 +90,69 @@ export function WhatsAppSettings({ businessPhone, isConfigured: initialConfigure
|
|
| 86 |
</p>
|
| 87 |
</div>
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
|
| 116 |
-
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
|
| 128 |
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
| 141 |
-
|
| 142 |
-
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
}
|
|
|
|
|
|
|
| 1 |
"use client";
|
| 2 |
|
| 3 |
import { useState } from "react";
|
|
|
|
| 34 |
return;
|
| 35 |
}
|
| 36 |
|
| 37 |
+
const data: {
|
| 38 |
+
whatsappBusinessPhone: string;
|
| 39 |
+
whatsappAccessToken?: string;
|
| 40 |
+
whatsappVerifyToken?: string;
|
| 41 |
+
} = { whatsappBusinessPhone: phoneId };
|
| 42 |
|
| 43 |
+
if (accessToken) data.whatsappAccessToken = accessToken;
|
| 44 |
+
if (verifyToken) data.whatsappVerifyToken = verifyToken;
|
|
|
|
| 45 |
|
| 46 |
+
const result = await patch("/api/settings", data, {
|
| 47 |
+
successMessage: "WhatsApp configuration saved successfully"
|
| 48 |
+
});
|
| 49 |
+
|
| 50 |
+
if (result) {
|
| 51 |
+
setIsConfigured(true);
|
| 52 |
+
setAccessToken(""); // Clear sensitive data after save
|
| 53 |
+
setVerifyToken("");
|
| 54 |
+
}
|
| 55 |
+
};
|
| 56 |
|
| 57 |
const copyWebhookUrl = () => {
|
| 58 |
const url = `${window.location.origin}/api/webhooks/whatsapp`;
|
|
|
|
| 90 |
</p>
|
| 91 |
</div>
|
| 92 |
|
| 93 |
+
<div className="space-y-2">
|
| 94 |
+
<Label htmlFor="wa-token">Permanent Access Token</Label>
|
| 95 |
+
<div className="flex gap-2">
|
| 96 |
+
<div className="relative flex-1">
|
| 97 |
+
<Input
|
| 98 |
+
id="wa-token"
|
| 99 |
+
type={showToken ? "text" : "password"}
|
| 100 |
+
placeholder={isConfigured ? "••••••••••••••••••••••••" : "EAAG..."}
|
| 101 |
+
value={accessToken}
|
| 102 |
+
onChange={(e) => setAccessToken(e.target.value)}
|
| 103 |
+
/>
|
| 104 |
+
<Button
|
| 105 |
+
type="button"
|
| 106 |
+
variant="ghost"
|
| 107 |
+
size="sm"
|
| 108 |
+
className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
|
| 109 |
+
onClick={() => setShowToken(!showToken)}
|
| 110 |
+
>
|
| 111 |
+
{showToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
|
| 112 |
+
</Button>
|
| 113 |
+
</div>
|
| 114 |
+
</div>
|
| 115 |
+
<p className="text-xs text-muted-foreground">
|
| 116 |
+
The System User Access Token from Meta Business Settings.
|
| 117 |
+
</p>
|
| 118 |
+
</div>
|
| 119 |
|
| 120 |
+
<div className="space-y-2">
|
| 121 |
+
<Label htmlFor="wa-verify">Webhook Verify Token</Label>
|
| 122 |
+
<Input
|
| 123 |
+
id="wa-verify"
|
| 124 |
+
placeholder="Your custom verify token"
|
| 125 |
+
value={verifyToken}
|
| 126 |
+
onChange={(e) => setVerifyToken(e.target.value)}
|
| 127 |
+
/>
|
| 128 |
+
<p className="text-xs text-muted-foreground">
|
| 129 |
+
Set this in Meta Dashboard when configuring the Webhook.
|
| 130 |
+
</p>
|
| 131 |
+
</div>
|
| 132 |
|
| 133 |
+
<div className="pt-4 pb-2">
|
| 134 |
+
<div className="rounded-md bg-muted p-3">
|
| 135 |
+
<div className="flex items-center justify-between mb-1">
|
| 136 |
+
<span className="text-sm font-medium">Webhook URL</span>
|
| 137 |
+
<Button variant="ghost" size="icon" className="h-6 w-6" onClick={copyWebhookUrl}>
|
| 138 |
+
<Copy className="h-3 w-3" />
|
| 139 |
+
</Button>
|
| 140 |
+
</div>
|
| 141 |
+
<code className="text-xs break-all block text-muted-foreground">
|
| 142 |
+
{typeof window !== 'undefined' ? `${window.location.origin}/api/webhooks/whatsapp` : '/api/webhooks/whatsapp'}
|
| 143 |
+
</code>
|
| 144 |
+
</div>
|
| 145 |
+
<div className="mt-2 text-xs text-muted-foreground flex items-center gap-1">
|
| 146 |
+
<ExternalLink className="h-3 w-3" />
|
| 147 |
+
Configure in <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground">Meta App Dashboard</a>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
|
| 151 |
+
<Button onClick={handleSave} disabled={loading} className="w-full sm:w-auto">
|
| 152 |
+
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
| 153 |
+
Save Configuration
|
| 154 |
+
</Button>
|
| 155 |
+
</CardContent>
|
| 156 |
+
</Card>
|
| 157 |
+
);
|
| 158 |
}
|
lib/api-response-helpers.ts
CHANGED
|
@@ -13,6 +13,10 @@ export interface StandardAPIResponse<T = unknown> {
|
|
| 13 |
timestamp?: string;
|
| 14 |
}
|
| 15 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
/**
|
| 17 |
* Success response
|
| 18 |
*/
|
|
@@ -100,11 +104,11 @@ export function apiRateLimitError(
|
|
| 100 |
* Wrapper for API route handlers with error handling
|
| 101 |
*/
|
| 102 |
export function withErrorHandling(
|
| 103 |
-
handler: (req: Request, context?: { params: Record<string, string> }) => Promise<NextResponse>
|
| 104 |
) {
|
| 105 |
return async (
|
| 106 |
req: Request,
|
| 107 |
-
context?: { params: Record<string, string> }
|
| 108 |
): Promise<NextResponse> => {
|
| 109 |
try {
|
| 110 |
return await handler(req, context);
|
|
|
|
| 13 |
timestamp?: string;
|
| 14 |
}
|
| 15 |
|
| 16 |
+
export interface RouteContext {
|
| 17 |
+
params: Promise<Record<string, string>>;
|
| 18 |
+
}
|
| 19 |
+
|
| 20 |
/**
|
| 21 |
* Success response
|
| 22 |
*/
|
|
|
|
| 104 |
* Wrapper for API route handlers with error handling
|
| 105 |
*/
|
| 106 |
export function withErrorHandling(
|
| 107 |
+
handler: (req: Request, context?: { params: Promise<Record<string, string>> }) => Promise<NextResponse>
|
| 108 |
) {
|
| 109 |
return async (
|
| 110 |
req: Request,
|
| 111 |
+
context?: { params: Promise<Record<string, string>> }
|
| 112 |
): Promise<NextResponse> => {
|
| 113 |
try {
|
| 114 |
return await handler(req, context);
|
lib/auth.ts
CHANGED
|
@@ -83,7 +83,8 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
| 83 |
linkedinSessionCookie: null,
|
| 84 |
whatsappBusinessPhone: null,
|
| 85 |
whatsappAccessToken: null,
|
| 86 |
-
whatsappVerifyToken: null
|
|
|
|
| 87 |
};
|
| 88 |
}
|
| 89 |
|
|
@@ -125,6 +126,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({
|
|
| 125 |
const { redis } = await import("@/lib/redis");
|
| 126 |
|
| 127 |
// Verify Code
|
|
|
|
|
|
|
|
|
|
| 128 |
const storedCode = await redis.get(`otp:${phoneNumber}`);
|
| 129 |
if (!storedCode || storedCode !== code) {
|
| 130 |
throw new Error("Invalid or expired OTP");
|
|
|
|
| 83 |
linkedinSessionCookie: null,
|
| 84 |
whatsappBusinessPhone: null,
|
| 85 |
whatsappAccessToken: null,
|
| 86 |
+
whatsappVerifyToken: null,
|
| 87 |
+
role: "admin"
|
| 88 |
};
|
| 89 |
}
|
| 90 |
|
|
|
|
| 126 |
const { redis } = await import("@/lib/redis");
|
| 127 |
|
| 128 |
// Verify Code
|
| 129 |
+
if (!redis) {
|
| 130 |
+
throw new Error("Redis client not initialized");
|
| 131 |
+
}
|
| 132 |
const storedCode = await redis.get(`otp:${phoneNumber}`);
|
| 133 |
if (!storedCode || storedCode !== code) {
|
| 134 |
throw new Error("Invalid or expired OTP");
|
lib/queue.ts
CHANGED
|
@@ -2,17 +2,24 @@
|
|
| 2 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 3 |
import { Queue, Worker, Job } from "bullmq";
|
| 4 |
import { redis as connection } from "./redis";
|
|
|
|
| 5 |
import { db } from "@/db";
|
| 6 |
import { emailLogs, businesses, emailTemplates, users } from "@/db/schema";
|
| 7 |
import { eq, sql, and, gte, lt } from "drizzle-orm";
|
| 8 |
import { interpolateTemplate, sendColdEmail } from "./email";
|
| 9 |
import type { ScraperSourceName } from "./scrapers/types";
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
// Email queue
|
| 12 |
-
export const emailQueue = new Queue("email-outreach", { connection:
|
| 13 |
|
| 14 |
// Scraping queue
|
| 15 |
-
export const scrapingQueue = new Queue("google-maps-scraping", { connection:
|
| 16 |
|
| 17 |
interface EmailJobData {
|
| 18 |
userId: string;
|
|
@@ -213,7 +220,7 @@ export const emailWorker = new Worker(
|
|
| 213 |
throw new Error(errorMessage);
|
| 214 |
}
|
| 215 |
},
|
| 216 |
-
{ connection:
|
| 217 |
);
|
| 218 |
|
| 219 |
/**
|
|
@@ -364,7 +371,7 @@ export const scrapingWorker = new Worker(
|
|
| 364 |
}
|
| 365 |
},
|
| 366 |
{
|
| 367 |
-
connection:
|
| 368 |
concurrency: 5 // Allow 5 concurrent jobs
|
| 369 |
}
|
| 370 |
);
|
|
@@ -382,7 +389,7 @@ scrapingWorker.on("failed", (job, err) => {
|
|
| 382 |
/**
|
| 383 |
* Workflow execution queue
|
| 384 |
*/
|
| 385 |
-
export const workflowQueue = new Queue("workflow-execution", { connection:
|
| 386 |
|
| 387 |
interface WorkflowJobData {
|
| 388 |
workflowId: string;
|
|
@@ -502,7 +509,7 @@ export const workflowWorker = new Worker(
|
|
| 502 |
throw new Error(msg);
|
| 503 |
}
|
| 504 |
},
|
| 505 |
-
{ connection:
|
| 506 |
);
|
| 507 |
|
| 508 |
workflowWorker.on("completed", (job) => {
|
|
|
|
| 2 |
/* eslint-disable @typescript-eslint/no-explicit-any */
|
| 3 |
import { Queue, Worker, Job } from "bullmq";
|
| 4 |
import { redis as connection } from "./redis";
|
| 5 |
+
import Redis from "ioredis";
|
| 6 |
import { db } from "@/db";
|
| 7 |
import { emailLogs, businesses, emailTemplates, users } from "@/db/schema";
|
| 8 |
import { eq, sql, and, gte, lt } from "drizzle-orm";
|
| 9 |
import { interpolateTemplate, sendColdEmail } from "./email";
|
| 10 |
import type { ScraperSourceName } from "./scrapers/types";
|
| 11 |
|
| 12 |
+
// Ensure we have a valid Redis instance for BullMQ (even if disconnected/null in lib/redis)
|
| 13 |
+
const safeConnection = connection || new Redis({
|
| 14 |
+
maxRetriesPerRequest: null,
|
| 15 |
+
lazyConnect: true
|
| 16 |
+
});
|
| 17 |
+
|
| 18 |
// Email queue
|
| 19 |
+
export const emailQueue = new Queue("email-outreach", { connection: safeConnection as any });
|
| 20 |
|
| 21 |
// Scraping queue
|
| 22 |
+
export const scrapingQueue = new Queue("google-maps-scraping", { connection: safeConnection as any });
|
| 23 |
|
| 24 |
interface EmailJobData {
|
| 25 |
userId: string;
|
|
|
|
| 220 |
throw new Error(errorMessage);
|
| 221 |
}
|
| 222 |
},
|
| 223 |
+
{ connection: safeConnection as any }
|
| 224 |
);
|
| 225 |
|
| 226 |
/**
|
|
|
|
| 371 |
}
|
| 372 |
},
|
| 373 |
{
|
| 374 |
+
connection: safeConnection as any,
|
| 375 |
concurrency: 5 // Allow 5 concurrent jobs
|
| 376 |
}
|
| 377 |
);
|
|
|
|
| 389 |
/**
|
| 390 |
* Workflow execution queue
|
| 391 |
*/
|
| 392 |
+
export const workflowQueue = new Queue("workflow-execution", { connection: safeConnection as any });
|
| 393 |
|
| 394 |
interface WorkflowJobData {
|
| 395 |
workflowId: string;
|
|
|
|
| 509 |
throw new Error(msg);
|
| 510 |
}
|
| 511 |
},
|
| 512 |
+
{ connection: safeConnection as any, concurrency: 10 }
|
| 513 |
);
|
| 514 |
|
| 515 |
workflowWorker.on("completed", (job) => {
|
lib/whatsapp/webhook.ts
CHANGED
|
@@ -125,23 +125,37 @@ async function processMessage(message: WhatsAppMessage) {
|
|
| 125 |
|
| 126 |
if (type !== "text") return; // Only automate text for now
|
| 127 |
|
| 128 |
-
// 2. Find Automation Rules (Auto-Reply)
|
| 129 |
const rules = await db.query.socialAutomations.findMany({
|
| 130 |
where: and(
|
| 131 |
-
eq(socialAutomations.triggerType, "whatsapp_keyword"),
|
| 132 |
eq(socialAutomations.isActive, true)
|
| 133 |
)
|
| 134 |
});
|
| 135 |
|
| 136 |
for (const rule of rules) {
|
| 137 |
-
|
| 138 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 139 |
|
| 140 |
// Send Reply
|
| 141 |
await sendWhatsAppMessage({
|
| 142 |
to: senderPhone,
|
| 143 |
text: rule.responseTemplate || ""
|
| 144 |
});
|
|
|
|
|
|
|
|
|
|
| 145 |
}
|
| 146 |
}
|
| 147 |
}
|
|
|
|
| 125 |
|
| 126 |
if (type !== "text") return; // Only automate text for now
|
| 127 |
|
| 128 |
+
// 2. Find Automation Rules (Auto-Reply & Commands)
|
| 129 |
const rules = await db.query.socialAutomations.findMany({
|
| 130 |
where: and(
|
|
|
|
| 131 |
eq(socialAutomations.isActive, true)
|
| 132 |
)
|
| 133 |
});
|
| 134 |
|
| 135 |
for (const rule of rules) {
|
| 136 |
+
let matched = false;
|
| 137 |
+
|
| 138 |
+
if (rule.triggerType === "whatsapp_keyword") {
|
| 139 |
+
if (rule.keywords && rule.keywords.some(k => text.toLowerCase().includes(k.toLowerCase()))) {
|
| 140 |
+
matched = true;
|
| 141 |
+
}
|
| 142 |
+
} else if (rule.triggerType === "whatsapp_command") {
|
| 143 |
+
if (rule.keywords && rule.keywords.some(k => text.toLowerCase().trim() === k.toLowerCase().trim() || text.toLowerCase().startsWith(k.toLowerCase() + " "))) {
|
| 144 |
+
matched = true;
|
| 145 |
+
}
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
if (matched) {
|
| 149 |
+
console.log(`✅ Matched WhatsApp Rule: ${rule.name} (${rule.triggerType})`);
|
| 150 |
|
| 151 |
// Send Reply
|
| 152 |
await sendWhatsAppMessage({
|
| 153 |
to: senderPhone,
|
| 154 |
text: rule.responseTemplate || ""
|
| 155 |
});
|
| 156 |
+
|
| 157 |
+
// Stop after first match? Maybe for commands, yes.
|
| 158 |
+
if (rule.triggerType === "whatsapp_command") break;
|
| 159 |
}
|
| 160 |
}
|
| 161 |
}
|
lib/workflow-executor.ts
CHANGED
|
@@ -46,7 +46,8 @@ export class WorkflowExecutor {
|
|
| 46 |
userId: this.context.userId,
|
| 47 |
title: "Workflow Completed Successfully",
|
| 48 |
message: `Workflow successfully processed business: ${business?.name}`,
|
| 49 |
-
|
|
|
|
| 50 |
});
|
| 51 |
} else {
|
| 52 |
// Count recent failures to alert on repeated errors
|
|
@@ -64,7 +65,8 @@ export class WorkflowExecutor {
|
|
| 64 |
userId: this.context.userId,
|
| 65 |
title: "⚠️ Workflow Repeated Failures",
|
| 66 |
message: `Workflow has failed ${recentFailures.length} times recently. Please check configuration and logs.`,
|
| 67 |
-
|
|
|
|
| 68 |
});
|
| 69 |
}
|
| 70 |
}
|
|
@@ -78,7 +80,8 @@ export class WorkflowExecutor {
|
|
| 78 |
userId: this.context.userId,
|
| 79 |
title: "Workflow Execution Failed",
|
| 80 |
message: `Workflow failed: ${errorMsg}`,
|
| 81 |
-
|
|
|
|
| 82 |
});
|
| 83 |
|
| 84 |
return { success: false, logs };
|
|
|
|
| 46 |
userId: this.context.userId,
|
| 47 |
title: "Workflow Completed Successfully",
|
| 48 |
message: `Workflow successfully processed business: ${business?.name}`,
|
| 49 |
+
level: "success",
|
| 50 |
+
category: "workflow",
|
| 51 |
});
|
| 52 |
} else {
|
| 53 |
// Count recent failures to alert on repeated errors
|
|
|
|
| 65 |
userId: this.context.userId,
|
| 66 |
title: "⚠️ Workflow Repeated Failures",
|
| 67 |
message: `Workflow has failed ${recentFailures.length} times recently. Please check configuration and logs.`,
|
| 68 |
+
level: "warning",
|
| 69 |
+
category: "workflow",
|
| 70 |
});
|
| 71 |
}
|
| 72 |
}
|
|
|
|
| 80 |
userId: this.context.userId,
|
| 81 |
title: "Workflow Execution Failed",
|
| 82 |
message: `Workflow failed: ${errorMsg}`,
|
| 83 |
+
level: "error",
|
| 84 |
+
category: "workflow",
|
| 85 |
});
|
| 86 |
|
| 87 |
return { success: false, logs };
|