diff --git a/apps/http-backend/src/routes/userRoutes/userRoutes.ts b/apps/http-backend/src/routes/userRoutes/userRoutes.ts index 91ea6af..a274b5b 100644 --- a/apps/http-backend/src/routes/userRoutes/userRoutes.ts +++ b/apps/http-backend/src/routes/userRoutes/userRoutes.ts @@ -15,11 +15,43 @@ import { workflowUpdateSchema, ExecuteWorkflow, HOOKS_URL, + DashboardRangeSchema, } from "@repo/common/zod"; import { GoogleSheetsNodeExecutor } from "@repo/nodes"; import axios from "axios"; const router: Router = Router(); +const DASHBOARD_RANGE_DAYS = { + "7d": 7, + "30d": 30, + "90d": 90, +} as const; + +const FALLBACK_EXECUTION_QUOTA = 1000; + +const isTestingExecution = (metadata: unknown): boolean => { + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) { + return false; + } + + return (metadata as Record).isTesting === true; +}; + +const toDayKey = (date: Date): string => date.toISOString().slice(0, 10); + +const getExecutionQuota = (): number => { + const parsedQuota = Number.parseInt( + process.env.DASHBOARD_EXECUTION_QUOTA || "", + 10 + ); + + if (Number.isFinite(parsedQuota) && parsedQuota > 0) { + return parsedQuota; + } + + return FALLBACK_EXECUTION_QUOTA; +}; + router.post("/createAvaliableNode", async (req: AuthRequest, res: Response) => { try { const Data = req.body; @@ -234,6 +266,239 @@ router.get("/getAllCreds", } } ); + +router.get("/dashboard/overview", + userMiddleware, + async (req: AuthRequest, res: Response) => { + try { + const userId = req.user?.sub; + + if (!userId) { + return res.status(statusCodes.UNAUTHORIZED).json({ + message: "User is not authorized", + }); + } + + const [workflowCount, recentWorkflows, credentials, executionRows] = + await Promise.all([ + prismaClient.workflow.count({ + where: { userId }, + }), + prismaClient.workflow.findMany({ + where: { userId }, + orderBy: { createdAt: "desc" }, + take: 5, + select: { + id: true, + name: true, + description: true, + createdAt: true, + status: true, + }, + }), + prismaClient.credential.findMany({ + where: { userId }, + select: { type: true }, + }), + prismaClient.workflowExecution.findMany({ + where: { + workflow: { + userId, + }, + }, + select: { + status: true, + metadata: true, + }, + }), + ]); + + const executions = executionRows.filter( + (execution) => !isTestingExecution(execution.metadata) + ); + + const executionCount = executions.length; + const failedExecutions = executions.filter( + (execution) => execution.status === "Failed" + ).length; + const successfulExecutions = executions.filter( + (execution) => execution.status === "Completed" + ).length; + + const failedRate = executionCount + ? Number(((failedExecutions / executionCount) * 100).toFixed(2)) + : 0; + const successRate = executionCount + ? Number(((successfulExecutions / executionCount) * 100).toFixed(2)) + : 0; + + const executionQuota = getExecutionQuota(); + const remainingExecutions = Math.max(0, executionQuota - executionCount); + + const credentialTypes = new Set(credentials.map((credential) => credential.type)); + const hasSharedGoogleOAuth = credentialTypes.has("google_oauth"); + + const gmailConnected = + hasSharedGoogleOAuth || credentialTypes.has("gmail_oauth"); + const googleSheetsConnected = + hasSharedGoogleOAuth || credentialTypes.has("google_sheets_oauth"); + + return res.status(statusCodes.OK).json({ + message: "Dashboard overview fetched successfully", + data: { + workflowCount, + executionCount, + failedRate, + successRate, + executionQuota, + remainingExecutions, + integrations: [ + { + key: "gmail", + label: "Gmail", + connected: gmailConnected, + }, + { + key: "googleSheets", + label: "Google Sheets", + connected: googleSheetsConnected, + }, + ], + recentWorkflows, + }, + }); + } catch (error) { + console.log("Error fetching dashboard overview", error); + return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ + message: "Internal server error while fetching dashboard overview", + }); + } + } +); + +router.get("/dashboard/executions/trend", + userMiddleware, + async (req: AuthRequest, res: Response) => { + try { + const userId = req.user?.sub; + + if (!userId) { + return res.status(statusCodes.UNAUTHORIZED).json({ + message: "User is not authorized", + }); + } + + const rangeInput = req.query.range ?? "7d"; + const parsedRange = DashboardRangeSchema.safeParse(rangeInput); + + if (!parsedRange.success) { + return res.status(statusCodes.BAD_REQUEST).json({ + message: "Invalid range. Supported values: 7d, 30d, 90d", + }); + } + + const range = parsedRange.data; + const days = DASHBOARD_RANGE_DAYS[range]; + + const startDate = new Date(); + startDate.setHours(0, 0, 0, 0); + startDate.setDate(startDate.getDate() - (days - 1)); + + const executionRows = await prismaClient.workflowExecution.findMany({ + where: { + workflow: { + userId, + }, + startAt: { + gte: startDate, + }, + }, + select: { + startAt: true, + status: true, + metadata: true, + }, + orderBy: { + startAt: "asc", + }, + }); + + const executions = executionRows.filter( + (execution) => !isTestingExecution(execution.metadata) + ); + + const pointsMap = new Map< + string, + { + date: string; + total: number; + completed: number; + failed: number; + inFlight: number; + } + >(); + + for (let dayOffset = 0; dayOffset < days; dayOffset += 1) { + const currentDate = new Date(startDate); + currentDate.setDate(startDate.getDate() + dayOffset); + const dayKey = toDayKey(currentDate); + + pointsMap.set(dayKey, { + date: dayKey, + total: 0, + completed: 0, + failed: 0, + inFlight: 0, + }); + } + + for (const execution of executions) { + const dayKey = toDayKey(execution.startAt); + const point = pointsMap.get(dayKey); + + if (!point) { + continue; + } + + point.total += 1; + + if (execution.status === "Completed") { + point.completed += 1; + } else if (execution.status === "Failed") { + point.failed += 1; + } else { + point.inFlight += 1; + } + } + + const points = Array.from(pointsMap.values()); + const totals = points.reduce( + (acc, point) => { + acc.total += point.total; + acc.completed += point.completed; + acc.failed += point.failed; + acc.inFlight += point.inFlight; + return acc; + }, + { total: 0, completed: 0, failed: 0, inFlight: 0 } + ); + + return res.status(statusCodes.OK).json({ + message: "Dashboard trend fetched successfully", + data: { + range, + points, + totals, + }, + }); + } catch (error) { + console.log("Error fetching dashboard trend", error); + return res.status(statusCodes.INTERNAL_SERVER_ERROR).json({ + message: "Internal server error while fetching dashboard trend", + }); + } + } +); // ----------------------------------- CREATE WORKFLOW --------------------------------- router.post("/create/workflow", diff --git a/apps/web/app/components/ExecutionHistoryFooter.tsx b/apps/web/app/components/ExecutionHistoryFooter.tsx index 2cc58f0..b59a21e 100644 --- a/apps/web/app/components/ExecutionHistoryFooter.tsx +++ b/apps/web/app/components/ExecutionHistoryFooter.tsx @@ -18,12 +18,12 @@ interface ExecutionHistoryFooterProps { // ─── Status Badge ─── const StatusBadge: React.FC<{ status: string }> = ({ status }) => { const styles: Record = { - Completed: 'background:linear-gradient(135deg,#059669,#10b981);color:#ecfdf5;', - Failed: 'background:linear-gradient(135deg,#dc2626,#ef4444);color:#fef2f2;', - InProgress: 'background:linear-gradient(135deg,#2563eb,#3b82f6);color:#eff6ff;', - Pending: 'background:linear-gradient(135deg,#d97706,#f59e0b);color:#fffbeb;', - Start: 'background:linear-gradient(135deg,#6b7280,#9ca3af);color:#f9fafb;', - ReConnecting: 'background:linear-gradient(135deg,#ea580c,#f97316);color:#fff7ed;', + Completed: 'background:linear-gradient(135deg,#355126,#4a7a2e);color:#baf266;', + Failed: 'background:linear-gradient(135deg,#7f1d1d,#991b1b);color:#fca5a5;', + InProgress: 'background:linear-gradient(135deg,#1a3a18,#2a5a25);color:#82c246;', + Pending: 'background:linear-gradient(135deg,#5a4a10,#7a6420);color:#f59e0b;', + Start: 'background:linear-gradient(135deg,#2a3525,#3a4a32);color:#8a9178;', + ReConnecting: 'background:linear-gradient(135deg,#5a3510,#7a4a18);color:#f97316;', }; return ( = ({ isTest }) => { if (!isTest) return ; return ( = ({ da
 setSelectedExecution(null)}
                     style={{
                       background: 'none',
-                      border: '1px solid #334155',
-                      color: '#94a3b8',
+                      border: '1px solid #2a3525',
+                      color: '#8a9178',
                       padding: '2px 8px',
                       borderRadius: '4px',
                       cursor: 'pointer',
@@ -487,8 +487,8 @@ export default function ExecutionHistoryFooter({
               ;
+}> = [
+    { key: "profile", label: "Profile", icon: CircleUserRound },
+    { key: "dashboard", label: "Dashboard", icon: LayoutDashboard },
+    { key: "automations", label: "Automations", icon: Zap },
+    { key: "executions", label: "Executions", icon: Activity },
+    { key: "integrations", label: "Integrations", icon: Plug },
+  ];
+
+const PRIMARY_TABS: DashboardTab[] = ["profile", "dashboard", "automations"];
+const SECONDARY_TABS: DashboardTab[] = ["executions", "integrations"];
+
+interface DashboardSidebarProps {
+  activeTab: DashboardTab;
+  onTabChange: (tab: DashboardTab) => void;
+  onRefresh: () => void;
+}
+
+export default function DashboardSidebar({
+  activeTab,
+  onTabChange,
+  onRefresh,
+}: DashboardSidebarProps) {
+  const user = useAppSelector((state) => state.user);
+
+  return (
+    
+      
+        
+        BuildFlow
+        {/*  */}
+      
+
+      
+        
+          
+            Main Menu
+          
+          
+            
+              {PRIMARY_TABS.map((tabKey) => {
+                const tab = DASHBOARD_TABS.find((t) => t.key === tabKey)!;
+                const Icon = tab.icon;
+                const active = activeTab === tab.key;
+                return (
+                  
+                     onTabChange(tab.key)}
+                      isActive={active}
+                      tooltip={tab.label}
+                      className={`w-full gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group-data-[state=collapsed]:gap-0 group-data-[state=collapsed]:px-0 group-data-[state=collapsed]:justify-center ${active
+                        ? "bg-[#baf266]/10 text-[#baf266] hover:bg-[#baf266]/15 hover:text-[#baf266]"
+                        : "text-[#8a9178] hover:text-[#c8d4a8] hover:bg-[#1a2118]/60"
+                        }`}
+                    >
+                      
+                      {tab.label}
+                    
+                  
+                );
+              })}
+            
+          
+        
+
+        
+          
+            System
+          
+          
+            
+              {SECONDARY_TABS.map((tabKey) => {
+                const tab = DASHBOARD_TABS.find((t) => t.key === tabKey)!;
+                const Icon = tab.icon;
+                const active = activeTab === tab.key;
+                return (
+                  
+                     onTabChange(tab.key)}
+                      isActive={active}
+                      tooltip={tab.label}
+                      className={`w-full gap-3 px-3 py-2.5 rounded-xl transition-all duration-200 group-data-[state=collapsed]:gap-0 group-data-[state=collapsed]:px-0 group-data-[state=collapsed]:justify-center ${active
+                        ? "bg-[#baf266]/10 text-[#baf266] hover:bg-[#baf266]/15 hover:text-[#baf266]"
+                        : "text-[#8a9178] hover:text-[#c8d4a8] hover:bg-[#1a2118]/60"
+                        }`}
+                    >
+                      
+                      {tab.label}
+                    
+                  
+                );
+              })}
+
+              
+                
+                  
+                  Refresh
+                
+              
+            
+          
+        
+      
+
+      
+        
+
+
+ {user.name?.[0] || 'U'} +
+
+ {user.name || 'User'} + {user.email || 'user@example.com'} +
+
+ +
+
+
+ ); +} diff --git a/apps/web/app/components/dashboard/DashboardStatCard.tsx b/apps/web/app/components/dashboard/DashboardStatCard.tsx new file mode 100644 index 0000000..058dbac --- /dev/null +++ b/apps/web/app/components/dashboard/DashboardStatCard.tsx @@ -0,0 +1,44 @@ +"use client"; + +import { TrendingUp, TrendingDown } from "lucide-react"; + +interface StatCardProps { + border: boolean; + icon: React.ReactNode; + label: string; + value: string; + hint: string; + trend?: { value: string; positive: boolean }; + delay?: number; +} + +export default function DashboardStatCard({ label, value, hint, trend, icon, border, delay = 0 }: StatCardProps) { + return ( +
+
+ {icon} +
+
+
+

{label}

+
+
+

{value}

+ {trend && ( + + {trend.positive ? : } + {trend.value} + + )} +
+

{hint}

+
+
+ ); +} diff --git a/apps/web/app/components/dashboard/ExecutionHealthGauge.tsx b/apps/web/app/components/dashboard/ExecutionHealthGauge.tsx new file mode 100644 index 0000000..62f082c --- /dev/null +++ b/apps/web/app/components/dashboard/ExecutionHealthGauge.tsx @@ -0,0 +1,87 @@ +"use client"; + +import { useMemo } from "react"; +import { PieChart, Pie, Cell, ResponsiveContainer } from "recharts"; +import { MoreHorizontal } from "lucide-react"; +import { DashboardOverview } from "@/app/types/dashboard.types"; + +interface ExecutionHealthProps { + overview: DashboardOverview | null; +} + +export default function ExecutionHealthGauge({ overview }: ExecutionHealthProps) { + const successRate = overview?.successRate ?? 0; + const failedRate = overview?.failedRate ?? 0; + const delayedRate = Math.max(0, 100 - successRate - failedRate); + + const gaugeData = useMemo(() => [ + { name: "Success", value: successRate, color: "#baf266" }, + { name: "Delayed", value: delayedRate, color: "#f59e0b" }, + { name: "Failed", value: failedRate || 0.5, color: "#f87171" }, + ], [successRate, failedRate, delayedRate]); + + return ( +
+
+
+
+

Execution Health

+
+ +
+ + {/* Gauge */} +
+ + + + {gaugeData.map((e) => ( + + ))} + + + + {/* Center text */} +
+ {successRate.toFixed(1)}% + + ▼ 0% + +
+
+ + {/* Legend */} +
+
+
+ Success +
+
+
+ Delayed +
+
+
+ Failed +
+
+ +

+ Health is calculated based on the past 7 days execution +

+
+ ); +} diff --git a/apps/web/app/components/dashboard/IntegrationStatus.tsx b/apps/web/app/components/dashboard/IntegrationStatus.tsx new file mode 100644 index 0000000..733938b --- /dev/null +++ b/apps/web/app/components/dashboard/IntegrationStatus.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { MoreHorizontal } from "lucide-react"; +import { DashboardOverview } from "@/app/types/dashboard.types"; + +interface IntegrationStatusProps { + overview: DashboardOverview | null; +} + +export default function IntegrationStatus({ overview }: IntegrationStatusProps) { + const integrations = overview?.integrations ?? []; + + // Integration icon mapping + const iconMap: Record = { + gmail: { label: "Gmail", color: "#f87171" }, + googleSheets: { label: "Google Drive", color: "#baf266" }, + }; + + return ( +
+
+

Integration Status

+ +
+ + {/* Bar visualization */} +
+ {integrations.map((int) => { + const info = iconMap[int.key] || { label: int.label, color: "#5a6350" }; + return ( +
+
+
+
+
+ ); + })} +
+ + {/* Labels */} +
+ {integrations.map((int) => { + const info = iconMap[int.key] || { label: int.label, color: "#5a6350" }; + return ( +
+
+ {info.label[0]} +
+ {info.label} +
+ ); + })} +
+
+ ); +} diff --git a/apps/web/app/components/dashboard/RecentWorkflows.tsx b/apps/web/app/components/dashboard/RecentWorkflows.tsx new file mode 100644 index 0000000..04d246c --- /dev/null +++ b/apps/web/app/components/dashboard/RecentWorkflows.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { MoreHorizontal, CheckCircle2, Clock, Circle } from "lucide-react"; +import { DashboardOverview } from "@/app/types/dashboard.types"; + +interface RecentWorkflowsProps { + overview: DashboardOverview | null; +} + +const formatDate = (dateValue: string) => { + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return dateValue; + return date.toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }); +}; + +const getTimeDuration = (dateValue: string) => { + const date = new Date(dateValue); + if (Number.isNaN(date.getTime())) return ""; + const diff = Date.now() - date.getTime(); + const days = Math.floor(diff / (1000 * 60 * 60 * 24)); + if (days === 0) return "Today"; + if (days === 1) return "1 day ago"; + if (days < 7) return `${days} days ago`; + if (days < 30) return `${Math.floor(days / 7)} weeks ago`; + return `${Math.floor(days / 30)} month ago`; +}; + +export default function RecentWorkflows({ overview }: RecentWorkflowsProps) { + const workflows = overview?.recentWorkflows ?? []; + + const getStatusIcon = (status?: string | null) => { + switch (status?.toLowerCase()) { + case "active": + case "completed": + return ; + case "pending": + case "draft": + return ; + default: + return ; + } + }; + + const getStatusDot = (status?: string | null) => { + const colors: Record = { + active: "#baf266", + completed: "#baf266", + pending: "#f59e0b", + draft: "#5a6350", + }; + const c = colors[status?.toLowerCase() || ""] || "#5a6350"; + return ( + + + {status || "Draft"} + + ); + }; + + return ( +
+
+

Recent Workflows

+ +
+ + {workflows.length === 0 ? ( +
+ No workflows yet +
+ ) : ( +
+
+ + + + + + + + + + {workflows.slice(0, 5).map((wf) => ( + + + + + + + ))} + +
NameTriggered atStatusDuration
+
+ {getStatusIcon(wf.status)} + + {wf.name} + +
+
{formatDate(wf.createdAt)}{getStatusDot(wf.status)}{getTimeDuration(wf.createdAt)}
+ + )} + + ); +} diff --git a/apps/web/app/components/dashboard/WorkflowActivityChart.tsx b/apps/web/app/components/dashboard/WorkflowActivityChart.tsx new file mode 100644 index 0000000..9530e1e --- /dev/null +++ b/apps/web/app/components/dashboard/WorkflowActivityChart.tsx @@ -0,0 +1,127 @@ +"use client"; + +import { useMemo } from "react"; +import { + AreaChart, + Area, + CartesianGrid, + ResponsiveContainer, + Tooltip, + XAxis, + YAxis, +} from "recharts"; +import { MoreHorizontal } from "lucide-react"; +import { DashboardExecutionTrend, DashboardRange } from "@/app/types/dashboard.types"; + +const RANGE_OPTIONS: DashboardRange[] = ["7d", "30d", "90d"]; + +interface WorkflowChartProps { + trend: DashboardExecutionTrend | null; + trendLoading: boolean; + trendError: string | null; + selectedRange: DashboardRange; + onRangeChange: (range: DashboardRange) => void; +} + +export default function WorkflowActivityChart({ + trend, + trendLoading, + trendError, + selectedRange, + onRangeChange, +}: WorkflowChartProps) { + const chartData = useMemo(() => { + if (!trend?.points) return []; + return trend.points.map((p) => ({ + ...p, + label: new Date(p.date).toLocaleDateString("en-US", { weekday: "short" }).charAt(0), + })); + }, [trend]); + + const peakValue = useMemo(() => { + if (!chartData.length) return { value: 0, label: "" }; + const max = chartData.reduce((a, b) => (b.completed > a.completed ? b : a), chartData[0]!); + return { value: max.completed, label: max.label }; + }, [chartData]); + + return ( +
+
+
+
+

Workflow Activity Trend

+
+ +
+ + {/* Peak indicator */} + {peakValue.value > 0 && ( +
+ CRM + {peakValue.value} + + ▲ {((peakValue.value / (trend?.totals.total || 1)) * 100).toFixed(0)}% + +
+ )} + + {/* Range toggles */} +
+ {RANGE_OPTIONS.map((r) => ( + + ))} +
+ + {/* Chart */} +
+ {trendLoading ? ( +
Loading...
+ ) : trendError ? ( +
{trendError}
+ ) : ( + + + + + + + + + + + + + + + + + + + + + )} +
+
+ ); +} diff --git a/apps/web/app/components/ui/Inputbox.tsx b/apps/web/app/components/ui/Inputbox.tsx index 30dfa17..8ff2c2d 100644 --- a/apps/web/app/components/ui/Inputbox.tsx +++ b/apps/web/app/components/ui/Inputbox.tsx @@ -25,22 +25,22 @@ const Input = forwardRef( return (
{label && ( -
); diff --git a/apps/web/app/components/ui/app-sidebar.tsx b/apps/web/app/components/ui/app-sidebar.tsx index 1cc38c9..29a4a7b 100644 --- a/apps/web/app/components/ui/app-sidebar.tsx +++ b/apps/web/app/components/ui/app-sidebar.tsx @@ -210,7 +210,7 @@ export function AppSidebar() { side="top" className="w-[--radix-popper-anchor-width] flex gap-1 justify-between" > - + router.push('/dashboard')}> Dashboard diff --git a/apps/web/app/components/workflows/WorkflowListView.tsx b/apps/web/app/components/workflows/WorkflowListView.tsx new file mode 100644 index 0000000..1979465 --- /dev/null +++ b/apps/web/app/components/workflows/WorkflowListView.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@workspace/ui/components/card"; +import { Button } from "@workspace/ui/components/button"; +import ParentComponent from "@/app/components/ui/Design/WorkflowButton"; +import { api } from "@/app/lib/api"; + +interface WorkflowListViewProps { + title?: string; + showTitle?: boolean; + showCreateButton?: boolean; +} + +export default function WorkflowListView({ + title = "User Workflows", + showTitle = true, + showCreateButton = true, +}: WorkflowListViewProps) { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const router = useRouter(); + + useEffect(() => { + const fetchWorkflows = async () => { + try { + setLoading(true); + setError(null); + const response = await api.user.get(); + const workflows = response.data.Data || response.data || []; + setData(Array.isArray(workflows) ? workflows : []); + } catch (fetchError) { + setError("Failed to fetch workflows"); + console.error("Failed to fetch workflows:", fetchError); + } finally { + setLoading(false); + } + }; + + fetchWorkflows(); + }, []); + + return ( +
+ {showTitle &&

{title}

} + {loading ? ( +
Loading...
+ ) : error ? ( +
{error}
+ ) : data.length === 0 ? ( +
No workflows found.
+ ) : ( +
+ {data.map((workflow: any) => ( + + + + {workflow.name || workflow.Name || "Untitled Workflow"} + + {workflow.description && ( + + {workflow.description} + + )} + + +
+                  {(() => {
+                    const config = workflow.config ?? workflow.Config;
+                    if (
+                      config == null ||
+                      (typeof config === "object" &&
+                        !Array.isArray(config) &&
+                        Object.keys(config).length === 0) ||
+                      (Array.isArray(config) && config.length === 0)
+                    ) {
+                      return "Not Configured";
+                    }
+                    return JSON.stringify(config, null, 2);
+                  })()}
+                
+
+ + + +
+ ))} +
+ )} + + {showCreateButton && ( +
+
+ +
+
+ )} +
+ ); +} diff --git a/apps/web/app/dashboard/page.tsx b/apps/web/app/dashboard/page.tsx new file mode 100644 index 0000000..59b2dff --- /dev/null +++ b/apps/web/app/dashboard/page.tsx @@ -0,0 +1,267 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { LucideBadgeCheck, LucideTimer, Plus, RefreshCcwDot, Sparkles, Workflow } from "lucide-react"; +import { api } from "@/app/lib/api"; +import { useAppSelector } from "@/app/hooks/redux"; +import { + DashboardExecutionTrend, + DashboardOverview, + DashboardRange, + DashboardTab, +} from "@/app/types/dashboard.types"; +import WorkflowListView from "@/app/components/workflows/WorkflowListView"; +import DashboardSidebar from "../components/dashboard/DashboardSidebar"; +import DashboardStatCard from "../components/dashboard/DashboardStatCard"; +import WorkflowActivityChart from "../components/dashboard/WorkflowActivityChart"; +import ExecutionHealthGauge from "../components/dashboard/ExecutionHealthGauge"; +import IntegrationStatus from "../components/dashboard/IntegrationStatus"; +import RecentWorkflows from "../components/dashboard/RecentWorkflows"; +import { SidebarProvider, SidebarInset, SidebarTrigger } from "@workspace/ui/components/sidebar"; +import { CardDemo } from "../components/ui/Design/WorkflowCard"; + +const DASHBOARD_TABS: DashboardTab[] = [ + "profile", + "dashboard", + "automations", + "executions", + "integrations", +]; + +const isDashboardTab = (v: string | null): v is DashboardTab => + DASHBOARD_TABS.includes(v as DashboardTab); + +const formatPercent = (v: number) => `${v.toFixed(1)}%`; + +function PlaceholderPanel({ title, description }: { title: string; description: string }) { + return ( +
+

{title}

+

{description}

+
+ ); +} + +export default function DashboardPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const user = useAppSelector((state) => state.user); + + const [selectedRange, setSelectedRange] = useState("7d"); + const [overview, setOverview] = useState(null); + const [trend, setTrend] = useState(null); + const [overviewLoading, setOverviewLoading] = useState(true); + const [trendLoading, setTrendLoading] = useState(true); + const [overviewError, setOverviewError] = useState(null); + const [trendError, setTrendError] = useState(null); + + const activeTab: DashboardTab = useMemo(() => { + const p = searchParams.get("tab"); + return isDashboardTab(p) ? p : "dashboard"; + }, [searchParams]); + + const [enableCreateButton, setEnableCreateButton] = useState(false) + + const setTab = (tab: DashboardTab) => { + const params = new URLSearchParams(searchParams.toString()); + params.set("tab", tab); + router.replace(`/dashboard?${params.toString()}`); + }; + + const fetchOverview = async () => { + setOverviewLoading(true); + setOverviewError(null); + try { + setOverview(await api.dashboard.getOverview()); + } catch (e: any) { + setOverviewError(e?.message || "Failed to load overview"); + } finally { + setOverviewLoading(false); + } + }; + + const fetchTrend = async (range: DashboardRange) => { + setTrendLoading(true); + setTrendError(null); + try { + setTrend(await api.dashboard.getExecutionTrend(range)); + } catch (e: any) { + setTrendError(e?.message || "Failed to load trend"); + } finally { + setTrendLoading(false); + } + }; + + useEffect(() => { fetchOverview(); }, []); + useEffect(() => { fetchTrend(selectedRange); }, [selectedRange]); + + const handleRefresh = () => { + fetchOverview(); + fetchTrend(selectedRange); + }; + + const renderDashboard = () => { + + if (overviewLoading) { + return ( +
+
+
+

Loading dashboard...

+
+
+ ); + } + + if (overviewError || !overview) { + return ( +
+ {overviewError || "Unable to load dashboard"} +
+ ); + } + + return ( +
+ {/* Stat Cards Row */} +
+ } + label="Active Workflows" + value={String(overview.workflowCount)} + hint="from last month" + trend={{ value: "12%", positive: true }} + delay={0} + border={true} + /> + } + label="Automation Executed" + value={String(overview.executionCount).replace(/\B(?=(\d{3})+(?!\d))/g, ",")} + hint="from last month" + trend={{ value: `${overview.executionCount > 0 ? "↑" : ""} ${overview.executionCount}`, positive: true }} + delay={80} + border={true} + /> + } + label="Avg Execution Time" + value="3.0s" + hint="from last month" + trend={{ value: "-0.5s", positive: false }} + delay={160} + border={true} + /> + } + label="Success Rate" + value={formatPercent(overview.successRate)} + hint="from last month" + trend={{ value: `${overview.successRate > 95 ? "6.6%" : "1%"}`, positive: overview.successRate >= 95 }} + delay={240} + border={false} + /> +
+ + {/* Charts Row */} +
+ + +
+ + {/* Bottom Row */} +
+ + +
+
+ ); + }; + + const renderContent = () => { + if (activeTab === "dashboard") return renderDashboard(); + if (activeTab === "automations") { + return ( +
+

Automations

+

Your workflows list.

+
+
+ ); + } + if (activeTab === "profile") return ; + if (activeTab === "executions") return ; + return ; + }; + + return ( +
+ {/* Sidebar */} + + + + + {/* Main Content */} + + {/* Top Bar */} +
+ +
+
+

+ Welcome back, {user.name || "User"} 👋 +

+

+ Here's a quick summary of your automation workflows today. +

+
+
+ + +
+
+
+ + {/* Mobile tabs */} +
+ {DASHBOARD_TABS.map((tab) => ( + + ))} +
+ + {/* Content area */} +
+ {renderContent()} +
+ {enableCreateButton && ( + setEnableCreateButton(false)} /> + )} +
+
+
+ ); +} diff --git a/apps/web/app/lib/api.ts b/apps/web/app/lib/api.ts index d9b17e4..d41f2c1 100644 --- a/apps/web/app/lib/api.ts +++ b/apps/web/app/lib/api.ts @@ -103,6 +103,28 @@ export const api = { headers: { "Content-Type": "application/json" }, }), }, + dashboard: { + getOverview: async () => { + const res = await axios.get(`${BACKEND_URL}/user/dashboard/overview`, { + withCredentials: true, + headers: { "Content-Type": "application/json" }, + }); + + return res.data.data; + }, + getExecutionTrend: async (range: "7d" | "30d" | "90d" = "7d") => { + const res = await axios.get( + `${BACKEND_URL}/user/dashboard/executions/trend`, + { + params: { range }, + withCredentials: true, + headers: { "Content-Type": "application/json" }, + } + ); + + return res.data.data; + }, + }, google: { getDocuments: async (CredentialId : string) => { const data = await axios.get(`${BACKEND_URL}/node/getDocuments/${CredentialId}`,{ diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index 5ebc250..c36e80b 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -69,10 +69,10 @@ const Page = () => { } } return ( -
- +
+ {error.auth && ( - ! {error.auth} + ! {error.auth} )} { type="password" onChange={(e)=>{setPassword(e.target.value)}} /> - forgot password? + forgot password?
) diff --git a/apps/web/app/register/page.tsx b/apps/web/app/register/page.tsx index 2faf64f..4668dea 100644 --- a/apps/web/app/register/page.tsx +++ b/apps/web/app/register/page.tsx @@ -85,10 +85,10 @@ const pages = () => { } return ( <> -
- +
+ {error.auth && ( - ! {error.auth} + ! {error.auth} )} { text={isLoading ? 'Registering...' : 'Register'} variant='solid' size='md' - bgColor='bg-green-600' - className='w-full mt-4' + bgColor='bg-[#baf266]' + textColor='text-[#0a0d0a]' + className='w-full mt-4 font-semibold hover:opacity-90' disabled={isLoading} /> -

Already registered? Login

+

Already registered? Login

diff --git a/apps/web/app/types/dashboard.types.ts b/apps/web/app/types/dashboard.types.ts new file mode 100644 index 0000000..f109c3e --- /dev/null +++ b/apps/web/app/types/dashboard.types.ts @@ -0,0 +1,54 @@ +export type DashboardTab = + | "profile" + | "dashboard" + | "automations" + | "executions" + | "integrations"; + +export type DashboardRange = "7d" | "30d" | "90d"; + +export interface DashboardIntegration { + key: "gmail" | "googleSheets"; + label: string; + connected: boolean; +} + +export interface DashboardRecentWorkflow { + id: string; + name: string; + description?: string | null; + createdAt: string; + status?: string | null; +} + +export interface DashboardOverview { + workflowCount: number; + executionCount: number; + failedRate: number; + successRate: number; + executionQuota: number; + remainingExecutions: number; + integrations: DashboardIntegration[]; + recentWorkflows: DashboardRecentWorkflow[]; +} + +export interface DashboardTrendPoint { + date: string; + total: number; + completed: number; + failed: number; + inFlight: number; +} + +export interface DashboardTrendTotals { + total: number; + completed: number; + failed: number; + inFlight: number; +} + +export interface DashboardExecutionTrend { + range: DashboardRange; + points: DashboardTrendPoint[]; + totals: DashboardTrendTotals; +} diff --git a/apps/web/app/workflows/page.tsx b/apps/web/app/workflows/page.tsx index ed99f14..aaa5e1a 100644 --- a/apps/web/app/workflows/page.tsx +++ b/apps/web/app/workflows/page.tsx @@ -1,123 +1,7 @@ -"use client" -import { useEffect, useState } from "react"; -import { useAppSelector } from "../hooks/redux"; -import { api } from "../lib/api"; -import { - Card, - CardAction, - CardContent, - CardDescription, - CardFooter, - CardHeader, - CardTitle, -} from "@workspace/ui/components/card" -import { Button } from "@workspace/ui/components/button" -import { useRouter } from "next/navigation"; -import ParentComponent from "../components/ui/Design/WorkflowButton"; -// Removed: import { Router } from "next/router"; +"use client"; -export const UserWorkflows = () => { - const [data, setData] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const userId = useAppSelector((select) => select.user.userId) as string; - const router = useRouter(); // FIX: Call useRouter as a hook to get the router instance +import WorkflowListView from "@/app/components/workflows/WorkflowListView"; - useEffect(() => { - const fetchWorkflows = async () => { - try { - setLoading(true); - setError(null); - const response = await api.user.get(); - const workflows = response.data.Data || response.data || []; - setData(Array.isArray(workflows) ? workflows : []); - console.log("The workflow data is", workflows); - } catch (error) { - setError("Failed to fetch workflows"); - console.error("Failed to fetch workflows:", error); - } finally { - setLoading(false); - } - }; - if (userId) { - fetchWorkflows(); - } - }, [userId]); - - return ( -
-

User Workflows

- {loading ? ( -
Loading...
- ) : error ? ( -
{error}
- ) : data.length === 0 ? ( -
No workflows found.
- ) : ( - // Center and grid the square cards -
- {data.map((workflow: any) => ( - - - {workflow.name || workflow.Name || "Untitled Workflow"} - {workflow.description && ( - {workflow.description} - )} - - -
-                                    {(() => {
-                                        const config = workflow.config ?? workflow.Config;
-                                        if (
-                                            config == null ||
-                                            (typeof config === 'object' &&
-                                                !Array.isArray(config) &&
-                                                Object.keys(config).length === 0
-                                            ) ||
-                                            (Array.isArray(config) && config.length === 0)
-                                        ) {
-                                            return "Not Configured";
-                                        }
-                                        return JSON.stringify(config, null, 2);
-                                    })()}
-                                
-
- - - -
- ))} -
- )} - -
-
- -
-
- -
- ); -}; - -export default UserWorkflows \ No newline at end of file +export default function UserWorkflows() { + return ; +} \ No newline at end of file diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts index 461ae26..d844696 100644 --- a/packages/common/src/index.ts +++ b/packages/common/src/index.ts @@ -1,5 +1,4 @@ import z from "zod"; -import { number } from "zod/v4"; // Export interpolation utilities export * from "./interpolation"; @@ -120,6 +119,62 @@ export const WorkflowExecutionResponseSchema = z.object({ data: z.array(WorkflowExecutionSchema), }); +export const DashboardRangeSchema = z.enum(["7d", "30d", "90d"]); + +export const DashboardIntegrationSchema = z.object({ + key: z.enum(["gmail", "googleSheets"]), + label: z.string(), + connected: z.boolean(), +}); + +export const DashboardRecentWorkflowSchema = z.object({ + id: z.string(), + name: z.string(), + description: z.string().nullable().optional(), + createdAt: z.string().or(z.date()), + status: z.string().nullable().optional(), +}); + +export const DashboardOverviewSchema = z.object({ + workflowCount: z.number(), + executionCount: z.number(), + failedRate: z.number(), + successRate: z.number(), + executionQuota: z.number(), + remainingExecutions: z.number(), + integrations: z.array(DashboardIntegrationSchema), + recentWorkflows: z.array(DashboardRecentWorkflowSchema), +}); + +export const DashboardOverviewResponseSchema = z.object({ + message: z.string(), + data: DashboardOverviewSchema, +}); + +export const DashboardExecutionTrendPointSchema = z.object({ + date: z.string(), + total: z.number(), + completed: z.number(), + failed: z.number(), + inFlight: z.number(), +}); + +export const DashboardExecutionTrendSchema = z.object({ + range: DashboardRangeSchema, + points: z.array(DashboardExecutionTrendPointSchema), + totals: z.object({ + total: z.number(), + completed: z.number(), + failed: z.number(), + inFlight: z.number(), + }), +}); + +export const DashboardExecutionTrendResponseSchema = z.object({ + message: z.string(), + data: DashboardExecutionTrendSchema, +}); + export enum statusCodes { OK = 200, CREATED = 201, diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index a5c6ce3..f5cbd56 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -1,4 +1,5 @@ @import "tailwindcss"; +@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800&display=swap'); @source "../../../apps/**/*.{ts,tsx}"; @source "../../../components/**/*.{ts,tsx}"; @source "../**/*.{ts,tsx}"; @@ -44,38 +45,38 @@ } .dark { - --background: oklch(0.145 0 0); - --foreground: oklch(0.985 0 0); - --card: oklch(0.145 0 0); - --card-foreground: oklch(0.985 0 0); - --popover: oklch(0.145 0 0); - --popover-foreground: oklch(0.985 0 0); - --primary: oklch(0.985 0 0); - --primary-foreground: oklch(0.205 0 0); - --secondary: oklch(0.269 0 0); - --secondary-foreground: oklch(0.985 0 0); - --muted: oklch(0.269 0 0); - --muted-foreground: oklch(0.708 0 0); - --accent: oklch(0.269 0 0); - --accent-foreground: oklch(0.985 0 0); - --destructive: oklch(0.396 0.141 25.723); - --destructive-foreground: oklch(0.637 0.237 25.331); - --border: oklch(0.269 0 0); - --input: oklch(0.269 0 0); - --ring: oklch(0.556 0 0); - --chart-1: oklch(0.488 0.243 264.376); - --chart-2: oklch(0.696 0.17 162.48); - --chart-3: oklch(0.769 0.188 70.08); - --chart-4: oklch(0.627 0.265 303.9); - --chart-5: oklch(0.645 0.246 16.439); - --sidebar: oklch(0.205 0 0); - --sidebar-foreground: oklch(0.985 0 0); - --sidebar-primary: oklch(0.488 0.243 264.376); - --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.269 0 0); - --sidebar-accent-foreground: oklch(0.985 0 0); - --sidebar-border: oklch(0.269 0 0); - --sidebar-ring: oklch(0.439 0 0); + --background: #0a0d0a; + --foreground: #f0f0e8; + --card: #111611; + --card-foreground: #f0f0e8; + --popover: #111611; + --popover-foreground: #f0f0e8; + --primary: #baf266; + --primary-foreground: #0a0d0a; + --secondary: #1a2118; + --secondary-foreground: #f0f0e8; + --muted: #1a2118; + --muted-foreground: #8a9178; + --accent: #1a2118; + --accent-foreground: #f0f0e8; + --destructive: #7f1d1d; + --destructive-foreground: #fca5a5; + --border: #2a3525; + --input: #2a3525; + --ring: #baf266; + --chart-1: #baf266; + --chart-2: #82c246; + --chart-3: #5a9a30; + --chart-4: #355126; + --chart-5: #f59e0b; + --sidebar: #111611; + --sidebar-foreground: #f0f0e8; + --sidebar-primary: #baf266; + --sidebar-primary-foreground: #0a0d0a; + --sidebar-accent: #1a2118; + --sidebar-accent-foreground: #f0f0e8; + --sidebar-border: #2a3525; + --sidebar-ring: #baf266; } @theme inline { @@ -123,5 +124,52 @@ } body { @apply bg-background text-foreground; + font-family: 'Inter', system-ui, -apple-system, sans-serif; + } + + /* Custom scrollbar for dark theme */ + .dark ::-webkit-scrollbar { + width: 6px; + height: 6px; + } + .dark ::-webkit-scrollbar-track { + background: #0a0d0a; + } + .dark ::-webkit-scrollbar-thumb { + background: #2a3525; + border-radius: 3px; + } + .dark ::-webkit-scrollbar-thumb:hover { + background: #3a4a32; + } +} + +/* Dashboard-specific animations */ +@keyframes fadeInUp { + from { + opacity: 0; + transform: translateY(12px); } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes shimmer { + 0% { background-position: -200% 0; } + 100% { background-position: 200% 0; } +} + +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 20px rgba(186, 242, 102, 0.05); } + 50% { box-shadow: 0 0 30px rgba(186, 242, 102, 0.12); } +} + +.animate-fade-in-up { + animation: fadeInUp 0.5s ease-out forwards; +} + +.animate-pulse-glow { + animation: pulse-glow 3s ease-in-out infinite; }