+ case 4:
+ const selectedTemplate = templates.find(
+ (t) => t.id === formData.templateId,
+ );
+ return (
+
-
-
-
Join BudStack
-
Launch your medical cannabis dispensary in minutes
-
+ default:
+ return null;
+ }
+ };
+
+ return (
+
+
+
+
+ Join BudStack
+
+
+ Launch your medical cannabis dispensary in minutes
+
+
-
-
- New Tenant Application
-
- Complete the steps below to create your dispensary store
-
-
-
- {renderStepIndicator()}
- {renderStep()}
-
-
-
- {currentStep < totalSteps ? (
-
- ) : (
-
- )}
-
-
-
-
-
-
- Already have an account?{' '}
-
- Sign in
-
-
-
+
+
+ New Tenant Application
+
+ Complete the steps below to create your dispensary store
+
+
+
+ {renderStepIndicator()}
+ {renderStep()}
+
+
+
+ {currentStep < totalSteps ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ Already have an account?{" "}
+
+ Sign in
+
+
- );
+
+
+ );
}
diff --git a/nextjs_space/app/onboarding/page.tsx b/nextjs_space/app/onboarding/page.tsx
index d4ee146..79279f4 100644
--- a/nextjs_space/app/onboarding/page.tsx
+++ b/nextjs_space/app/onboarding/page.tsx
@@ -1,8 +1,8 @@
-import { prisma } from '@/lib/db';
-import OnboardingForm from './onboarding-form';
+import { prisma } from "@/lib/db";
+import OnboardingForm from "./onboarding-form";
// Force dynamic rendering to ensure fresh template data
-export const dynamic = 'force-dynamic';
+export const dynamic = "force-dynamic";
export default async function OnboardingPage() {
// Fetch active, public templates from the database
@@ -19,7 +19,7 @@ export default async function OnboardingPage() {
previewUrl: true,
},
orderBy: {
- name: 'asc',
+ name: "asc",
},
});
diff --git a/nextjs_space/app/tenant-admin/analytics/page.tsx b/nextjs_space/app/tenant-admin/analytics/page.tsx
index 5bc7221..f512519 100644
--- a/nextjs_space/app/tenant-admin/analytics/page.tsx
+++ b/nextjs_space/app/tenant-admin/analytics/page.tsx
@@ -1,12 +1,18 @@
-'use client';
-
-import { useSession } from 'next-auth/react';
-import { useRouter } from 'next/navigation';
-import { useEffect, useState } from 'react';
-import dynamic from 'next/dynamic';
-import { Button } from '@/components/ui/button';
-import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
-import { Badge } from '@/components/ui/badge';
+"use client";
+
+import { useSession } from "next-auth/react";
+import { useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import dynamic from "next/dynamic";
+import { Button } from "@/components/ui/button";
+import {
+ Card,
+ CardContent,
+ CardDescription,
+ CardHeader,
+ CardTitle,
+} from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
import {
TrendingUp,
DollarSign,
@@ -17,11 +23,11 @@ import {
Leaf,
Calendar,
ArrowUpRight,
- ShoppingBag
-} from 'lucide-react';
-import { Breadcrumbs } from '@/components/admin/shared';
-import Link from 'next/link';
-import { cn } from '@/lib/utils';
+ ShoppingBag,
+} from "lucide-react";
+import { Breadcrumbs } from "@/components/admin/shared";
+import Link from "next/link";
+import { cn } from "@/lib/utils";
import {
LineChart,
Line,
@@ -36,11 +42,13 @@ import {
PieChart,
Pie,
Cell,
-} from 'recharts';
+} from "recharts";
// Dynamic import for Plotly to avoid SSR issues
// eslint-disable-next-line @typescript-eslint/no-explicit-any
-const Plot = dynamic(() => import('react-plotly.js') as any, { ssr: false }) as any;
+const Plot = dynamic(() => import("react-plotly.js") as any, {
+ ssr: false,
+}) as any;
interface AnalyticsData {
totalProducts: number;
@@ -70,7 +78,7 @@ interface RecentOrder {
orderNumber: string;
customer: string;
total: number;
- status: 'PENDING' | 'PROCESSING' | 'COMPLETED' | 'CANCELLED';
+ status: "PENDING" | "PROCESSING" | "COMPLETED" | "CANCELLED";
createdAt: Date;
}
@@ -94,59 +102,136 @@ const generateSalesTrendData = () => {
const weekendBoost = [0, 6].includes(date.getDay()) ? 200 : 0;
baseValue += Math.random() * 100 - 50;
data.push({
- date: date.toISOString().split('T')[0],
- sales: Math.max(200, baseValue + variance + weekendBoost)
+ date: date.toISOString().split("T")[0],
+ sales: Math.max(200, baseValue + variance + weekendBoost),
});
}
return data;
};
-const getRevenueMetrics = (analytics: AnalyticsData | null): RevenueMetric[] => {
+const getRevenueMetrics = (
+ analytics: AnalyticsData | null,
+): RevenueMetric[] => {
if (!analytics) {
return [
- { label: "Today's Revenue", value: 0, change: 0, period: 'vs yesterday' },
- { label: "This Week", value: 0, change: 0, period: 'vs last week' },
- { label: "This Month", value: 0, change: 0, period: 'vs last month' }
+ { label: "Today's Revenue", value: 0, change: 0, period: "vs yesterday" },
+ { label: "This Week", value: 0, change: 0, period: "vs last week" },
+ { label: "This Month", value: 0, change: 0, period: "vs last month" },
];
}
return [
- { label: "Today's Revenue", value: analytics.recentRevenue * 0.1, change: 12.5, period: 'vs yesterday' },
- { label: "This Week", value: analytics.recentRevenue * 0.7, change: 8.3, period: 'vs last week' },
- { label: "This Month", value: analytics.totalRevenue, change: 15.7, period: 'vs last month' }
+ {
+ label: "Today's Revenue",
+ value: analytics.recentRevenue * 0.1,
+ change: 12.5,
+ period: "vs yesterday",
+ },
+ {
+ label: "This Week",
+ value: analytics.recentRevenue * 0.7,
+ change: 8.3,
+ period: "vs last week",
+ },
+ {
+ label: "This Month",
+ value: analytics.totalRevenue,
+ change: 15.7,
+ period: "vs last month",
+ },
];
};
const generateRecentOrders = (): RecentOrder[] => [
- { id: '1', orderNumber: 'ORD-1247', customer: 'Sarah Chen', total: 85.50, status: 'COMPLETED', createdAt: new Date(Date.now() - 1000 * 60 * 15) },
- { id: '2', orderNumber: 'ORD-1246', customer: 'Marcus Johnson', total: 120.00, status: 'PROCESSING', createdAt: new Date(Date.now() - 1000 * 60 * 45) },
- { id: '3', orderNumber: 'ORD-1245', customer: 'Emma Williams', total: 65.75, status: 'COMPLETED', createdAt: new Date(Date.now() - 1000 * 60 * 120) },
- { id: '4', orderNumber: 'ORD-1244', customer: 'David Park', total: 95.25, status: 'PENDING', createdAt: new Date(Date.now() - 1000 * 60 * 180) },
- { id: '5', orderNumber: 'ORD-1243', customer: 'Lisa Anderson', total: 110.00, status: 'COMPLETED', createdAt: new Date(Date.now() - 1000 * 60 * 240) }
+ {
+ id: "1",
+ orderNumber: "ORD-1247",
+ customer: "Sarah Chen",
+ total: 85.5,
+ status: "COMPLETED",
+ createdAt: new Date(Date.now() - 1000 * 60 * 15),
+ },
+ {
+ id: "2",
+ orderNumber: "ORD-1246",
+ customer: "Marcus Johnson",
+ total: 120.0,
+ status: "PROCESSING",
+ createdAt: new Date(Date.now() - 1000 * 60 * 45),
+ },
+ {
+ id: "3",
+ orderNumber: "ORD-1245",
+ customer: "Emma Williams",
+ total: 65.75,
+ status: "COMPLETED",
+ createdAt: new Date(Date.now() - 1000 * 60 * 120),
+ },
+ {
+ id: "4",
+ orderNumber: "ORD-1244",
+ customer: "David Park",
+ total: 95.25,
+ status: "PENDING",
+ createdAt: new Date(Date.now() - 1000 * 60 * 180),
+ },
+ {
+ id: "5",
+ orderNumber: "ORD-1243",
+ customer: "Lisa Anderson",
+ total: 110.0,
+ status: "COMPLETED",
+ createdAt: new Date(Date.now() - 1000 * 60 * 240),
+ },
];
const generateRecentCustomers = (): RecentCustomer[] => [
- { id: '1', name: 'Alex Thompson', email: 'alex.t@email.com', createdAt: new Date(Date.now() - 1000 * 60 * 30) },
- { id: '2', name: 'Jordan Lee', email: 'jordan.lee@email.com', createdAt: new Date(Date.now() - 1000 * 60 * 120) },
- { id: '3', name: 'Taylor Martinez', email: 'taylor.m@email.com', createdAt: new Date(Date.now() - 1000 * 60 * 360) },
- { id: '4', name: 'Morgan Davis', email: 'morgan.d@email.com', createdAt: new Date(Date.now() - 1000 * 60 * 480) },
- { id: '5', name: 'Casey Wilson', email: 'casey.w@email.com', createdAt: new Date(Date.now() - 1000 * 60 * 720) }
+ {
+ id: "1",
+ name: "Alex Thompson",
+ email: "alex.t@email.com",
+ createdAt: new Date(Date.now() - 1000 * 60 * 30),
+ },
+ {
+ id: "2",
+ name: "Jordan Lee",
+ email: "jordan.lee@email.com",
+ createdAt: new Date(Date.now() - 1000 * 60 * 120),
+ },
+ {
+ id: "3",
+ name: "Taylor Martinez",
+ email: "taylor.m@email.com",
+ createdAt: new Date(Date.now() - 1000 * 60 * 360),
+ },
+ {
+ id: "4",
+ name: "Morgan Davis",
+ email: "morgan.d@email.com",
+ createdAt: new Date(Date.now() - 1000 * 60 * 480),
+ },
+ {
+ id: "5",
+ name: "Casey Wilson",
+ email: "casey.w@email.com",
+ createdAt: new Date(Date.now() - 1000 * 60 * 720),
+ },
];
const formatCurrency = (amount: number) => {
- return new Intl.NumberFormat('pt-PT', {
- style: 'currency',
- currency: 'EUR'
+ return new Intl.NumberFormat("pt-PT", {
+ style: "currency",
+ currency: "EUR",
}).format(amount);
};
const getStatusColor = (status: string) => {
const colors = {
- COMPLETED: 'bg-emerald-100 text-emerald-800 border-emerald-200',
- PROCESSING: 'bg-cyan-100 text-cyan-800 border-cyan-200',
- PENDING: 'bg-amber-100 text-amber-800 border-amber-200',
- CANCELLED: 'bg-slate-100 text-slate-800 border-slate-200'
+ COMPLETED: "bg-emerald-100 text-emerald-800 border-emerald-200",
+ PROCESSING: "bg-cyan-100 text-cyan-800 border-cyan-200",
+ PENDING: "bg-amber-100 text-amber-800 border-amber-200",
+ CANCELLED: "bg-slate-100 text-slate-800 border-slate-200",
};
return colors[status as keyof typeof colors] || colors.PENDING;
};
@@ -154,7 +239,7 @@ const getStatusColor = (status: string) => {
const formatTimeAgo = (date: Date) => {
const seconds = Math.floor((new Date().getTime() - date.getTime()) / 1000);
- if (seconds < 60) return 'just now';
+ if (seconds < 60) return "just now";
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
@@ -162,9 +247,9 @@ const formatTimeAgo = (date: Date) => {
const getInitials = (name: string) => {
return name
- .split(' ')
- .map(n => n[0])
- .join('')
+ .split(" ")
+ .map((n) => n[0])
+ .join("")
.toUpperCase()
.slice(0, 2);
};
@@ -176,18 +261,18 @@ export default function TenantAnalyticsPage() {
const router = useRouter();
const [loading, setLoading] = useState(true);
const [analytics, setAnalytics] = useState
(null);
- const [timeRange, setTimeRange] = useState<'7d' | '30d' | '90d'>('30d');
+ const [timeRange, setTimeRange] = useState<"7d" | "30d" | "90d">("30d");
const [salesTrendData, setSalesTrendData] = useState([]);
const [recentOrders, setRecentOrders] = useState([]);
const [recentCustomers, setRecentCustomers] = useState([]);
const [pendingConsultations] = useState(7);
useEffect(() => {
- if (status === 'unauthenticated') {
- router.push('/auth/login');
+ if (status === "unauthenticated") {
+ router.push("/auth/login");
}
- if (status === 'authenticated' && session?.user?.role !== 'TENANT_ADMIN') {
- router.push('/');
+ if (status === "authenticated" && session?.user?.role !== "TENANT_ADMIN") {
+ router.push("/");
}
}, [status, session, router]);
@@ -206,91 +291,111 @@ export default function TenantAnalyticsPage() {
const fetchAnalytics = async () => {
try {
- const response = await fetch(`/api/tenant-admin/analytics?timeRange=${timeRange}`);
+ const response = await fetch(
+ `/api/tenant-admin/analytics?timeRange=${timeRange}`,
+ );
if (response.ok) {
const data = await response.json();
setAnalytics(data);
} else {
- console.error('API error:', response.status, response.statusText);
+ console.error("API error:", response.status, response.statusText);
// Use mock data if API fails
setAnalytics({
totalProducts: 10,
totalOrders: 25,
totalCustomers: 15,
- totalRevenue: 2500.00,
+ totalRevenue: 2500.0,
recentOrders: 8,
recentCustomers: 5,
- recentRevenue: 850.00,
- avgOrderValue: 100.00,
+ recentRevenue: 850.0,
+ avgOrderValue: 100.0,
revenueByDay: Array.from({ length: 7 }, (_, i) => ({
date: `Day ${i + 1}`,
- revenue: Math.random() * 500 + 200
+ revenue: Math.random() * 500 + 200,
})),
ordersByDay: Array.from({ length: 7 }, (_, i) => ({
date: `Day ${i + 1}`,
- orders: Math.floor(Math.random() * 10) + 1
+ orders: Math.floor(Math.random() * 10) + 1,
})),
topProducts: [
- { id: '1', name: 'Product 1', quantity: 15, revenue: 450, orders: 8 },
- { id: '2', name: 'Product 2', quantity: 12, revenue: 360, orders: 6 },
- { id: '3', name: 'Product 3', quantity: 10, revenue: 300, orders: 5 },
+ {
+ id: "1",
+ name: "Product 1",
+ quantity: 15,
+ revenue: 450,
+ orders: 8,
+ },
+ {
+ id: "2",
+ name: "Product 2",
+ quantity: 12,
+ revenue: 360,
+ orders: 6,
+ },
+ {
+ id: "3",
+ name: "Product 3",
+ quantity: 10,
+ revenue: 300,
+ orders: 5,
+ },
],
customerGrowth: Array.from({ length: 7 }, (_, i) => ({
date: `Day ${i + 1}`,
- customers: Math.floor(Math.random() * 5)
+ customers: Math.floor(Math.random() * 5),
})),
ordersByStatus: [
- { name: 'COMPLETED', value: 15 },
- { name: 'PROCESSING', value: 5 },
- { name: 'PENDING', value: 3 },
- { name: 'CANCELLED', value: 2 }
- ]
+ { name: "COMPLETED", value: 15 },
+ { name: "PROCESSING", value: 5 },
+ { name: "PENDING", value: 3 },
+ { name: "CANCELLED", value: 2 },
+ ],
});
}
} catch (error) {
- console.error('Error fetching analytics:', error);
+ console.error("Error fetching analytics:", error);
// Use mock data on error
setAnalytics({
totalProducts: 10,
totalOrders: 25,
totalCustomers: 15,
- totalRevenue: 2500.00,
+ totalRevenue: 2500.0,
recentOrders: 8,
recentCustomers: 5,
- recentRevenue: 850.00,
- avgOrderValue: 100.00,
+ recentRevenue: 850.0,
+ avgOrderValue: 100.0,
revenueByDay: Array.from({ length: 7 }, (_, i) => ({
date: `Day ${i + 1}`,
- revenue: Math.random() * 500 + 200
+ revenue: Math.random() * 500 + 200,
})),
ordersByDay: Array.from({ length: 7 }, (_, i) => ({
date: `Day ${i + 1}`,
- orders: Math.floor(Math.random() * 10) + 1
+ orders: Math.floor(Math.random() * 10) + 1,
})),
topProducts: [
- { id: '1', name: 'Product 1', quantity: 15, revenue: 450, orders: 8 },
- { id: '2', name: 'Product 2', quantity: 12, revenue: 360, orders: 6 },
- { id: '3', name: 'Product 3', quantity: 10, revenue: 300, orders: 5 },
+ { id: "1", name: "Product 1", quantity: 15, revenue: 450, orders: 8 },
+ { id: "2", name: "Product 2", quantity: 12, revenue: 360, orders: 6 },
+ { id: "3", name: "Product 3", quantity: 10, revenue: 300, orders: 5 },
],
customerGrowth: Array.from({ length: 7 }, (_, i) => ({
date: `Day ${i + 1}`,
- customers: Math.floor(Math.random() * 5)
+ customers: Math.floor(Math.random() * 5),
})),
ordersByStatus: [
- { name: 'COMPLETED', value: 15 },
- { name: 'PROCESSING', value: 5 },
- { name: 'PENDING', value: 3 },
- { name: 'CANCELLED', value: 2 }
- ]
+ { name: "COMPLETED", value: 15 },
+ { name: "PROCESSING", value: 5 },
+ { name: "PENDING", value: 3 },
+ { name: "CANCELLED", value: 2 },
+ ],
});
} finally {
setLoading(false);
}
};
- const COLORS = ['#10b981', '#3b82f6', '#f59e0b', '#ef4444', '#8b5cf6'];
+ const COLORS = ["#10b981", "#3b82f6", "#f59e0b", "#ef4444", "#8b5cf6"];
- if (status === 'loading' || loading) {
+ if (status === "loading" || loading) {
return (
@@ -298,7 +403,9 @@ export default function TenantAnalyticsPage() {
-
Loading your garden of insights...
+
+ Loading your garden of insights...
+
);
@@ -312,104 +419,110 @@ export default function TenantAnalyticsPage() {
// Plotly chart configurations
const salesTrendTrace = {
- x: salesTrendData.map(d => d.date),
- y: salesTrendData.map(d => d.sales),
- type: 'scatter' as const,
- mode: 'lines' as const,
+ x: salesTrendData.map((d) => d.date),
+ y: salesTrendData.map((d) => d.sales),
+ type: "scatter" as const,
+ mode: "lines" as const,
line: {
- color: '#10b981',
+ color: "#10b981",
width: 3,
- shape: 'spline' as const
+ shape: "spline" as const,
},
- fill: 'tozeroy' as const,
- fillcolor: 'rgba(16, 185, 129, 0.1)',
- hovertemplate: '
%{x}€%{y:.2f}
'
+ fill: "tozeroy" as const,
+ fillcolor: "rgba(16, 185, 129, 0.1)",
+ hovertemplate: "
%{x}€%{y:.2f}
",
};
const salesTrendLayout = {
autosize: true,
margin: { l: 50, r: 20, t: 20, b: 40 },
- paper_bgcolor: 'rgba(0,0,0,0)',
- plot_bgcolor: 'rgba(0,0,0,0)',
+ paper_bgcolor: "rgba(0,0,0,0)",
+ plot_bgcolor: "rgba(0,0,0,0)",
xaxis: {
showgrid: false,
zeroline: false,
- tickfont: { size: 11, color: '#64748b' },
- tickformat: '%b %d'
+ tickfont: { size: 11, color: "#64748b" },
+ tickformat: "%b %d",
},
yaxis: {
showgrid: true,
- gridcolor: 'rgba(148, 163, 184, 0.1)',
+ gridcolor: "rgba(148, 163, 184, 0.1)",
zeroline: false,
- tickfont: { size: 11, color: '#64748b' },
- tickprefix: '€'
+ tickfont: { size: 11, color: "#64748b" },
+ tickprefix: "€",
},
- hovermode: 'x unified' as const
+ hovermode: "x unified" as const,
};
const topProductsTrace = {
x: analytics.topProducts.slice(0, 5).map((p: any) => p.revenue),
y: analytics.topProducts.slice(0, 5).map((p: any) => p.name),
- type: 'bar' as const,
- orientation: 'h' as const,
+ type: "bar" as const,
+ orientation: "h" as const,
marker: {
- color: ['#10b981', '#14b8a6', '#06b6d4', '#0ea5e9', '#3b82f6'],
- cornerradius: 8
+ color: ["#10b981", "#14b8a6", "#06b6d4", "#0ea5e9", "#3b82f6"],
+ cornerradius: 8,
},
- hovertemplate: '
%{y}Revenue: €%{x:.2f}
'
+ hovertemplate: "
%{y}Revenue: €%{x:.2f}
",
};
const topProductsLayout = {
autosize: true,
margin: { l: 150, r: 20, t: 20, b: 40 },
- paper_bgcolor: 'rgba(0,0,0,0)',
- plot_bgcolor: 'rgba(0,0,0,0)',
+ paper_bgcolor: "rgba(0,0,0,0)",
+ plot_bgcolor: "rgba(0,0,0,0)",
xaxis: {
showgrid: true,
- gridcolor: 'rgba(148, 163, 184, 0.1)',
+ gridcolor: "rgba(148, 163, 184, 0.1)",
zeroline: false,
- tickfont: { size: 11, color: '#64748b' },
- tickprefix: '€'
+ tickfont: { size: 11, color: "#64748b" },
+ tickprefix: "€",
},
yaxis: {
showgrid: false,
zeroline: false,
- tickfont: { size: 11, color: '#64748b' }
- }
+ tickfont: { size: 11, color: "#64748b" },
+ },
};
const orderStatusTrace = {
labels: analytics.ordersByStatus.map((s: any) => s.name),
values: analytics.ordersByStatus.map((s: any) => s.value),
- type: 'pie' as const,
+ type: "pie" as const,
hole: 0.5,
marker: {
- colors: ['#10b981', '#06b6d4', '#f59e0b', '#94a3b8']
+ colors: ["#10b981", "#06b6d4", "#f59e0b", "#94a3b8"],
},
- textinfo: 'label+percent' as const,
- textfont: { size: 12, color: '#1e293b' },
- hovertemplate: '
%{label}%{value} orders
%{percent}
'
+ textinfo: "label+percent" as const,
+ textfont: { size: 12, color: "#1e293b" },
+ hovertemplate:
+ "
%{label}%{value} orders
%{percent}
",
};
const orderStatusLayout = {
autosize: true,
margin: { l: 20, r: 20, t: 20, b: 20 },
- paper_bgcolor: 'rgba(0,0,0,0)',
- plot_bgcolor: 'rgba(0,0,0,0)',
+ paper_bgcolor: "rgba(0,0,0,0)",
+ plot_bgcolor: "rgba(0,0,0,0)",
showlegend: false,
- annotations: [{
- font: { size: 24, color: '#10b981', family: 'system-ui', weight: 700 },
- showarrow: false,
- text: String(analytics.ordersByStatus.reduce((a: any, b: any) => a + b.value, 0)),
- x: 0.5,
- y: 0.55
- }, {
- font: { size: 12, color: '#64748b', family: 'system-ui' },
- showarrow: false,
- text: 'Total Orders',
- x: 0.5,
- y: 0.42
- }]
+ annotations: [
+ {
+ font: { size: 24, color: "#10b981", family: "system-ui", weight: 700 },
+ showarrow: false,
+ text: String(
+ analytics.ordersByStatus.reduce((a: any, b: any) => a + b.value, 0),
+ ),
+ x: 0.5,
+ y: 0.55,
+ },
+ {
+ font: { size: 12, color: "#64748b", family: "system-ui" },
+ showarrow: false,
+ text: "Total Orders",
+ x: 0.5,
+ y: 0.42,
+ },
+ ],
};
return (
@@ -417,8 +530,8 @@ export default function TenantAnalyticsPage() {
{/* Breadcrumbs */}
@@ -436,30 +549,47 @@ export default function TenantAnalyticsPage() {
Store Analytics