shubhjn commited on
Commit
e4885de
·
1 Parent(s): 688debd

fix all error

Browse files
.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 type = formData.get("type") as "info" | "warning" | "success" || "info";
18
 
19
  if (!title || !message) return { error: "Missing fields" };
20
 
21
- await db.insert(notifications).values({
22
- title,
23
- message,
24
- type,
25
- // userId: null implies global notification
26
- userId: null,
27
- });
28
-
29
- revalidatePath("/dashboard"); // Revalidate user dashboard to show new notif
30
- return { success: true };
 
 
 
 
 
 
 
 
 
 
 
 
 
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: any) => {
28
- cumulativeUsers += Number(row.count);
 
29
  return {
30
- date: new Date(row.date).toLocaleDateString("en-US", { month: "short", day: "numeric" }),
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
- { params }: { params: Promise<{ id: string }> }
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
- { params }: { params: { id: string } }
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
- { params }: { params: { id: string } }
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
- { params }: { params: { id: string } }
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 MVP, we automatically link to the first connected account or all?
22
- // The schema allows `connectedAccountId` to be null (meaning global?) or specific.
23
- // The form currently doesn't ask for account. Let's find a default one for now to link it,
24
- // or just pick the first one. A real app would let you choose "Apply to which account?".
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
 
33
  await db.insert(socialAutomations).values({
34
  userId: session.user.id,
35
- connectedAccountId: account.id, // Linking to first account found for now
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
- { params }: { params: Promise<{ provider: string }> }
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
- { params }: { params: Promise<{ provider: string }> }
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
- type: "info",
 
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
- type: "success",
 
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
- type: "error",
 
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
- type: "error",
 
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
- type: "warning",
 
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", // Blue-400
 
 
 
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
- const data = { whatsappBusinessPhone: phoneId };
39
- if (accessToken) data.whatsappAccessToken = accessToken;
40
- if (verifyToken) data.whatsappVerifyToken = verifyToken;
 
 
41
 
42
- const result = await patch("/api/settings", data, {
43
- successMessage: "WhatsApp configuration saved successfully"
44
- });
45
 
46
- if (result) {
47
- setIsConfigured(true);
48
- setAccessToken(""); // Clear sensitive data after save
49
- setVerifyToken("");
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
- <div className="space-y-2">
90
- <Label htmlFor="wa-token">Permanent Access Token</Label>
91
- <div className="flex gap-2">
92
- <div className="relative flex-1">
93
- <Input
94
- id="wa-token"
95
- type={showToken ? "text" : "password"}
96
- placeholder={isConfigured ? "••••••••••••••••••••••••" : "EAAG..."}
97
- value={accessToken}
98
- onChange={(e) => setAccessToken(e.target.value)}
99
- />
100
- <Button
101
- type="button"
102
- variant="ghost"
103
- size="sm"
104
- className="absolute right-0 top-0 h-full px-3 py-2 hover:bg-transparent"
105
- onClick={() => setShowToken(!showToken)}
106
- >
107
- {showToken ? <EyeOff className="h-4 w-4" /> : <Eye className="h-4 w-4" />}
108
- </Button>
109
- </div>
110
- </div>
111
- <p className="text-xs text-muted-foreground">
112
- The System User Access Token from Meta Business Settings.
113
- </p>
114
- </div>
115
 
116
- <div className="space-y-2">
117
- <Label htmlFor="wa-verify">Webhook Verify Token</Label>
118
- <Input
119
- id="wa-verify"
120
- placeholder="Your custom verify token"
121
- value={verifyToken}
122
- onChange={(e) => setVerifyToken(e.target.value)}
123
- />
124
- <p className="text-xs text-muted-foreground">
125
- Set this in Meta Dashboard when configuring the Webhook.
126
- </p>
127
- </div>
128
 
129
- <div className="pt-4 pb-2">
130
- <div className="rounded-md bg-muted p-3">
131
- <div className="flex items-center justify-between mb-1">
132
- <span className="text-sm font-medium">Webhook URL</span>
133
- <Button variant="ghost" size="icon" className="h-6 w-6" onClick={copyWebhookUrl}>
134
- <Copy className="h-3 w-3" />
135
- </Button>
136
- </div>
137
- <code className="text-xs break-all block text-muted-foreground">
138
- {typeof window !== 'undefined' ? `${window.location.origin}/api/webhooks/whatsapp` : '/api/webhooks/whatsapp'}
139
- </code>
140
- </div>
141
- <div className="mt-2 text-xs text-muted-foreground flex items-center gap-1">
142
- <ExternalLink className="h-3 w-3" />
143
- Configure in <a href="https://developers.facebook.com/apps/" target="_blank" rel="noopener noreferrer" className="underline hover:text-foreground">Meta App Dashboard</a>
144
- </div>
145
- </div>
146
 
147
- <Button onClick={handleSave} disabled={loading} className="w-full sm:w-auto">
148
- {loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
149
- Save Configuration
150
- </Button>
151
- </CardContent>
152
- </Card>
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: connection as any });
13
 
14
  // Scraping queue
15
- export const scrapingQueue = new Queue("google-maps-scraping", { connection: connection as any });
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: connection as any }
217
  );
218
 
219
  /**
@@ -364,7 +371,7 @@ export const scrapingWorker = new Worker(
364
  }
365
  },
366
  {
367
- connection: connection as any,
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: connection as any });
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: connection as any, concurrency: 10 }
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
- if (rule.keywords && rule.keywords.some(k => text.toLowerCase().includes(k.toLowerCase()))) {
138
- console.log(`✅ Matched WhatsApp Rule: ${rule.name}`);
 
 
 
 
 
 
 
 
 
 
 
 
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
- type: "success",
 
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
- type: "warning",
 
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
- type: "error",
 
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 };