diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx
index 9a7e423..cf33d22 100644
--- a/app/(app)/dashboard/page.tsx
+++ b/app/(app)/dashboard/page.tsx
@@ -2,23 +2,17 @@
import { useQuery, useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
-import { Users, Wifi, FileText, DollarSign, Ticket, CheckSquare, TrendingUp, Database, RefreshCw } from "lucide-react";
+import { Users, Wifi, FileText, DollarSign, Ticket, TrendingUp, Database, RefreshCw, AlertCircle } from "lucide-react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { Button } from "@/components/ui/Button";
import { Badge } from "@/components/ui/Badge";
import { formatCurrency, formatDateTime } from "@/lib/utils";
+import { useAuthStore } from "@/lib/auth-store";
import { toast } from "sonner";
import api from "@/lib/api";
import type { DashboardSummary, PaginatedResponse, Ticket as TicketType } from "@/types";
import {
- LineChart,
- Line,
- XAxis,
- YAxis,
- CartesianGrid,
- Tooltip,
- ResponsiveContainer,
- Legend,
+ LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, Legend,
} from "recharts";
interface KpiCardProps {
@@ -46,44 +40,67 @@ function KpiCard({ title, value, icon: Icon, color, subtitle, href }: KpiCardPro
{value}
{subtitle && {subtitle}
}
- {href && (
- →
- )}
+ {href && →
}
);
}
+function KpiSkeleton() {
+ return (
+
+
+
+
+
+
+ );
+}
+
+function ErrorState({ message, onRetry }: { message: string; onRetry: () => void }) {
+ return (
+
+ );
+}
+
const priorityVariant: Record = {
- URGENT: "danger",
- urgent: "danger",
- HIGH: "warning",
- high: "warning",
- NORMAL: "default",
- normal: "default",
- MEDIUM: "default",
- medium: "default",
- LOW: "muted",
- low: "muted",
+ URGENT: "danger", urgent: "danger",
+ HIGH: "warning", high: "warning",
+ NORMAL: "default", normal: "default",
+ MEDIUM: "default", medium: "default",
+ LOW: "muted", low: "muted",
};
-// Build mock time-series data from revenue for the chart
function buildChartData(stats: DashboardSummary | undefined) {
if (!stats) return [];
- // Create a simple 2-month comparison from revenue data
const now = new Date();
const thisMonth = now.toLocaleString("default", { month: "short" });
const lastMonth = new Date(now.getFullYear(), now.getMonth() - 1).toLocaleString("default", { month: "short" });
return [
- { month: lastMonth, revenue: stats.revenue.lastMonth, clients: stats.subscribers.total },
- { month: thisMonth, revenue: stats.revenue.thisMonth, clients: stats.subscribers.active },
+ { month: lastMonth, revenue: stats.revenue.lastMonth },
+ { month: thisMonth, revenue: stats.revenue.thisMonth },
];
}
export default function DashboardPage() {
const router = useRouter();
+ const user = useAuthStore((s) => s.user);
+ const isAdmin = user?.roles?.some(r => r.toLowerCase() === "admin" || r.toLowerCase() === "super_admin");
- const { data: stats, isLoading: statsLoading, refetch: refetchStats } = useQuery({
+ const {
+ data: stats,
+ isLoading: statsLoading,
+ isError: statsError,
+ refetch: refetchStats,
+ } = useQuery({
queryKey: ["dashboard-summary"],
queryFn: async () => {
const res = await api.get("/api/v1/dashboard/summary");
@@ -91,7 +108,11 @@ export default function DashboardPage() {
},
});
- const { data: ticketsData, refetch: refetchTickets } = useQuery>({
+ const {
+ data: ticketsData,
+ isLoading: ticketsLoading,
+ refetch: refetchTickets,
+ } = useQuery>({
queryKey: ["recent-tickets"],
queryFn: async () => {
const res = await api.get>("/api/v1/tickets?page=1&limit=5");
@@ -106,7 +127,7 @@ export default function DashboardPage() {
refetchStats();
refetchTickets();
},
- onError: (err: { response?: { data?: { message?: string } } }) => {
+ onError: (err: any) => {
toast.error(err?.response?.data?.message || "Seed failed");
},
});
@@ -117,99 +138,60 @@ export default function DashboardPage() {
return (
-
+ {/* Header */}
+
Dashboard
Overview of your ISP operations
-
-
-
-
-
-
{/* KPI Cards */}
{statsLoading ? (
- {[1, 2, 3, 4].map((i) => (
-
-
-
-
-
- ))}
+ {[1,2,3,4].map(i =>
)}
+ ) : statsError ? (
+
+
+
) : (
-
-
-
-
+
+
+ {isAdmin && (
+ 0 ? "+" : ""}${stats.revenue.growth.toFixed(1)}% vs last month`
- : "vs last month"
- }
- href="/payments"
- />
+ : "vs last month"} href="/payments" />
+ )}
)}
{/* Secondary stats */}
-
-
router.push("/tickets?status=OPEN")}
- >
+
+
router.push("/tickets")}>
@@ -220,10 +202,8 @@ export default function DashboardPage() {
-
router.push("/tickets?status=IN_PROGRESS")}
- >
+ router.push("/tickets")}>
@@ -234,24 +214,10 @@ export default function DashboardPage() {
- router.push("/tasks")}
- >
-
-
-
-
-
-
Pending Tasks
-
{stats?.tasks.pending ?? 0}
-
-
-
- {/* Revenue Chart */}
- {!statsLoading && (
+ {/* Revenue Chart — admin only */}
+ {isAdmin && !statsLoading && (
Revenue Overview
@@ -262,33 +228,17 @@ export default function DashboardPage() {
- `₱${(v / 1000).toFixed(0)}k`} />
+ `₱${(v/1000).toFixed(0)}k`} />
formatCurrency(Number(v))} />
-
+
) : (
Revenue data will appear once payments are recorded.
-
seedMutation.mutate()}
- isLoading={seedMutation.isPending}
- >
-
- Seed Demo Data
-
)}
@@ -299,34 +249,38 @@ export default function DashboardPage() {
Recent Tickets
- router.push("/tickets")}>
- View all →
-
+ router.push("/tickets")}>View all →
- {recentTickets.length === 0 ? (
+ {ticketsLoading ? (
+
+ {[1,2,3].map(i => (
+
+ ))}
+
+ ) : recentTickets.length === 0 ? (
No tickets yet
) : (
{recentTickets.map((ticket) => (
-
router.push("/tickets")}
- >
+ onClick={() => router.push("/tickets")}>
{ticket.subject}
- {ticket.client
- ? `${ticket.client.firstName} ${ticket.client.lastName}`
- : "No client"}{" "}
- • {formatDateTime(ticket.createdAt)}
+ {ticket.client ? `${ticket.client.firstName} ${ticket.client.lastName}` : "No client"}
+ {" • "}{formatDateTime(ticket.createdAt)}
-
- {ticket.priority}
-
+ {ticket.priority}
{ticket.status}
diff --git a/e2e/dashboard.spec.ts b/e2e/dashboard.spec.ts
new file mode 100644
index 0000000..55e9add
--- /dev/null
+++ b/e2e/dashboard.spec.ts
@@ -0,0 +1,52 @@
+import { test, expect } from '@playwright/test';
+import { login } from './helpers/auth';
+
+test.describe('Dashboard', () => {
+ test.beforeEach(async ({ page }) => {
+ await login(page);
+ });
+
+ test('dashboard page loads with correct title', async ({ page }) => {
+ await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible();
+ await expect(page.locator('text=Overview of your ISP operations')).toBeVisible();
+ });
+
+ test('KPI cards render after loading', async ({ page }) => {
+ // Wait for skeletons to disappear
+ await page.waitForSelector('.skeleton', { state: 'detached', timeout: 15000 }).catch(() => {});
+ await expect(page.locator('text=Total Clients')).toBeVisible();
+ await expect(page.locator('text=Active Subscriptions')).toBeVisible();
+ await expect(page.locator('text=Overdue Invoices')).toBeVisible();
+ });
+
+ test('revenue card and chart only visible to admin', async ({ page }) => {
+ // Admin login — revenue card should show
+ await page.waitForSelector('.skeleton', { state: 'detached', timeout: 15000 }).catch(() => {});
+ await expect(page.locator('text=Monthly Revenue')).toBeVisible();
+ await expect(page.locator('text=Revenue Overview')).toBeVisible();
+ });
+
+ test('recent tickets section renders', async ({ page }) => {
+ await expect(page.locator('text=Recent Tickets')).toBeVisible();
+ await expect(page.locator('text=View all →')).toBeVisible();
+ });
+
+ test('refresh button works without crash', async ({ page }) => {
+ await page.click('button:has-text("Refresh")');
+ // Should not crash — page still has dashboard title
+ await expect(page.locator('h1:has-text("Dashboard")')).toBeVisible();
+ });
+
+ test('New Client button navigates to clients', async ({ page }) => {
+ await page.click('button:has-text("+ New Client")');
+ await expect(page).toHaveURL(/\/clients/);
+ });
+
+ test('KPI card click navigates correctly', async ({ page }) => {
+ await page.waitForSelector('.skeleton', { state: 'detached', timeout: 15000 }).catch(() => {});
+ // Click Total Clients card
+ const clientsCard = page.locator('text=Total Clients').first();
+ await clientsCard.click();
+ await expect(page).toHaveURL(/\/clients/);
+ });
+});