diff --git a/app/api/admin/trigger-processing/route.ts b/app/api/admin/trigger-processing/route.ts
index 686f5e7..d900c62 100644
--- a/app/api/admin/trigger-processing/route.ts
+++ b/app/api/admin/trigger-processing/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
-import { authOptions } from "../../auth/[...nextauth]/route";
+import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import { processUnprocessedSessions } from "../../../../lib/processingScheduler";
import { ProcessingStatusManager } from "../../../../lib/processingStatusManager";
diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts
index a352535..806003b 100644
--- a/app/api/auth/[...nextauth]/route.ts
+++ b/app/api/auth/[...nextauth]/route.ts
@@ -1,106 +1,6 @@
-import NextAuth, { NextAuthOptions } from "next-auth";
-import CredentialsProvider from "next-auth/providers/credentials";
-import { prisma } from "../../../../lib/prisma";
-import bcrypt from "bcryptjs";
-
-// Define the shape of the JWT token
-declare module "next-auth/jwt" {
- interface JWT {
- companyId: string;
- role: string;
- }
-}
-
-// Define the shape of the session object
-declare module "next-auth" {
- interface Session {
- user: {
- id?: string;
- name?: string;
- email?: string;
- image?: string;
- companyId: string;
- role: string;
- };
- }
-
- interface User {
- id: string;
- email: string;
- companyId: string;
- role: string;
- }
-}
-
-export const authOptions: NextAuthOptions = {
- providers: [
- CredentialsProvider({
- name: "Credentials",
- credentials: {
- email: { label: "Email", type: "text" },
- password: { label: "Password", type: "password" },
- },
- async authorize(credentials) {
- if (!credentials?.email || !credentials?.password) {
- return null;
- }
-
- const user = await prisma.user.findUnique({
- where: { email: credentials.email },
- });
-
- if (!user) return null;
-
- const valid = await bcrypt.compare(credentials.password, user.password);
- if (!valid) return null;
-
- return {
- id: user.id,
- email: user.email,
- companyId: user.companyId,
- role: user.role,
- };
- },
- }),
- ],
- session: {
- strategy: "jwt",
- maxAge: 30 * 24 * 60 * 60, // 30 days
- },
- cookies: {
- sessionToken: {
- name: `next-auth.session-token`,
- options: {
- httpOnly: true,
- sameSite: "lax",
- path: "/",
- secure: process.env.NODE_ENV === "production",
- },
- },
- },
- callbacks: {
- async jwt({ token, user }) {
- if (user) {
- token.companyId = user.companyId;
- token.role = user.role;
- }
- return token;
- },
- async session({ session, token }) {
- if (token && session.user) {
- session.user.companyId = token.companyId;
- session.user.role = token.role;
- }
- return session;
- },
- },
- pages: {
- signIn: "/login",
- },
- secret: process.env.NEXTAUTH_SECRET,
- debug: process.env.NODE_ENV === "development",
-};
+import NextAuth from "next-auth";
+import { authOptions } from "../../../../lib/auth";
const handler = NextAuth(authOptions);
-export { handler as GET, handler as POST };
+export { handler as GET, handler as POST };
\ No newline at end of file
diff --git a/app/api/dashboard/config/route.ts b/app/api/dashboard/config/route.ts
index 8b0eefb..b4cc084 100644
--- a/app/api/dashboard/config/route.ts
+++ b/app/api/dashboard/config/route.ts
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { prisma } from "../../../../lib/prisma";
-import { authOptions } from "../../auth/[...nextauth]/route";
+import { authOptions } from "../../../../lib/auth";
export async function GET(request: NextRequest) {
const session = await getServerSession(authOptions);
diff --git a/app/api/dashboard/metrics/route.ts b/app/api/dashboard/metrics/route.ts
index a737aee..e07bfef 100644
--- a/app/api/dashboard/metrics/route.ts
+++ b/app/api/dashboard/metrics/route.ts
@@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { prisma } from "../../../../lib/prisma";
import { sessionMetrics } from "../../../../lib/metrics";
-import { authOptions } from "../../auth/[...nextauth]/route";
+import { authOptions } from "../../../../lib/auth";
import { ChatSession } from "../../../../lib/types";
interface SessionUser {
@@ -83,10 +83,7 @@ export async function GET(request: NextRequest) {
// Pass company config to metrics
const companyConfigForMetrics = {
- sentimentAlert:
- user.company.sentimentAlert === null
- ? undefined
- : user.company.sentimentAlert,
+ // Add company-specific configuration here in the future
};
const metrics = sessionMetrics(chatSessions, companyConfigForMetrics);
diff --git a/app/api/dashboard/session-filter-options/route.ts b/app/api/dashboard/session-filter-options/route.ts
index 1ad1cef..f663ce1 100644
--- a/app/api/dashboard/session-filter-options/route.ts
+++ b/app/api/dashboard/session-filter-options/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
-import { authOptions } from "../../auth/[...nextauth]/route";
+import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import { SessionFilterOptions } from "../../../../lib/types";
diff --git a/app/api/dashboard/session/[id]/route.ts b/app/api/dashboard/session/[id]/route.ts
index 5e33387..555b9f3 100644
--- a/app/api/dashboard/session/[id]/route.ts
+++ b/app/api/dashboard/session/[id]/route.ts
@@ -4,9 +4,9 @@ import { ChatSession } from "../../../../../lib/types";
export async function GET(
request: NextRequest,
- { params }: { params: { id: string } }
+ { params }: { params: Promise<{ id: string }> }
) {
- const { id } = params;
+ const { id } = await params;
if (!id) {
return NextResponse.json(
diff --git a/app/api/dashboard/sessions/route.ts b/app/api/dashboard/sessions/route.ts
index 86d35ba..06b69ce 100644
--- a/app/api/dashboard/sessions/route.ts
+++ b/app/api/dashboard/sessions/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
-import { authOptions } from "../../auth/[...nextauth]/route";
+import { authOptions } from "../../../../lib/auth";
import { prisma } from "../../../../lib/prisma";
import {
ChatSession,
diff --git a/app/api/dashboard/settings/route.ts b/app/api/dashboard/settings/route.ts
index 72a9d7a..a15dfe6 100644
--- a/app/api/dashboard/settings/route.ts
+++ b/app/api/dashboard/settings/route.ts
@@ -1,7 +1,7 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
import { prisma } from "../../../../lib/prisma";
-import { authOptions } from "../../auth/[...nextauth]/route";
+import { authOptions } from "../../../../lib/auth";
export async function POST(request: NextRequest) {
const session = await getServerSession(authOptions);
@@ -26,9 +26,7 @@ export async function POST(request: NextRequest) {
csvUrl,
csvUsername,
...(csvPassword ? { csvPassword } : {}),
- sentimentAlert: sentimentThreshold
- ? parseFloat(sentimentThreshold)
- : null,
+ // Remove sentimentAlert field - not in current schema
},
});
diff --git a/app/api/dashboard/users/route.ts b/app/api/dashboard/users/route.ts
index 5b8e2b3..c8ab87b 100644
--- a/app/api/dashboard/users/route.ts
+++ b/app/api/dashboard/users/route.ts
@@ -3,7 +3,7 @@ import crypto from "crypto";
import { getServerSession } from "next-auth";
import { prisma } from "../../../../lib/prisma";
import bcrypt from "bcryptjs";
-import { authOptions } from "../../auth/[...nextauth]/route";
+import { authOptions } from "../../../../lib/auth";
interface UserBasicInfo {
id: string;
diff --git a/app/api/forgot-password/route.ts b/app/api/forgot-password/route.ts
index 46313a9..8f9d1d7 100644
--- a/app/api/forgot-password/route.ts
+++ b/app/api/forgot-password/route.ts
@@ -28,8 +28,7 @@ function checkRateLimit(ip: string): boolean {
export async function POST(request: NextRequest) {
try {
// Rate limiting check
- const ip =
- request.ip || request.headers.get("x-forwarded-for") || "unknown";
+ const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown";
if (!checkRateLimit(ip)) {
return NextResponse.json(
{
diff --git a/app/api/platform/auth/[...nextauth]/route.ts b/app/api/platform/auth/[...nextauth]/route.ts
index 7d8dda0..601ece0 100644
--- a/app/api/platform/auth/[...nextauth]/route.ts
+++ b/app/api/platform/auth/[...nextauth]/route.ts
@@ -1,107 +1,5 @@
-import NextAuth, { NextAuthOptions } from "next-auth";
-import CredentialsProvider from "next-auth/providers/credentials";
-import { prisma } from "../../../../../lib/prisma";
-import bcrypt from "bcryptjs";
-
-// Define the shape of the JWT token for platform users
-declare module "next-auth/jwt" {
- interface JWT {
- isPlatformUser: boolean;
- platformRole: string;
- }
-}
-
-// Define the shape of the session object for platform users
-declare module "next-auth" {
- interface Session {
- user: {
- id?: string;
- name?: string;
- email?: string;
- image?: string;
- isPlatformUser: boolean;
- platformRole: string;
- };
- }
-
- interface User {
- id: string;
- email: string;
- name: string;
- isPlatformUser: boolean;
- platformRole: string;
- }
-}
-
-export const platformAuthOptions: NextAuthOptions = {
- providers: [
- CredentialsProvider({
- name: "Platform Credentials",
- credentials: {
- email: { label: "Email", type: "text" },
- password: { label: "Password", type: "password" },
- },
- async authorize(credentials) {
- if (!credentials?.email || !credentials?.password) {
- return null;
- }
-
- const platformUser = await prisma.platformUser.findUnique({
- where: { email: credentials.email },
- });
-
- if (!platformUser) return null;
-
- const valid = await bcrypt.compare(credentials.password, platformUser.password);
- if (!valid) return null;
-
- return {
- id: platformUser.id,
- email: platformUser.email,
- name: platformUser.name,
- isPlatformUser: true,
- platformRole: platformUser.role,
- };
- },
- }),
- ],
- session: {
- strategy: "jwt",
- maxAge: 8 * 60 * 60, // 8 hours for platform users (more secure)
- },
- cookies: {
- sessionToken: {
- name: `platform-auth.session-token`,
- options: {
- httpOnly: true,
- sameSite: "lax",
- path: "/platform",
- secure: process.env.NODE_ENV === "production",
- },
- },
- },
- callbacks: {
- async jwt({ token, user }) {
- if (user) {
- token.isPlatformUser = user.isPlatformUser;
- token.platformRole = user.platformRole;
- }
- return token;
- },
- async session({ session, token }) {
- if (token && session.user) {
- session.user.isPlatformUser = token.isPlatformUser;
- session.user.platformRole = token.platformRole;
- }
- return session;
- },
- },
- pages: {
- signIn: "/platform/login",
- },
- secret: process.env.NEXTAUTH_SECRET,
- debug: process.env.NODE_ENV === "development",
-};
+import NextAuth from "next-auth";
+import { platformAuthOptions } from "../../../../../lib/platform-auth";
const handler = NextAuth(platformAuthOptions);
diff --git a/app/api/platform/companies/[id]/route.ts b/app/api/platform/companies/[id]/route.ts
index 0f88cfd..cc5fb58 100644
--- a/app/api/platform/companies/[id]/route.ts
+++ b/app/api/platform/companies/[id]/route.ts
@@ -1,16 +1,26 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
-import { platformAuthOptions } from "../../auth/[...nextauth]/route";
+import { platformAuthOptions } from "../../../../../lib/platform-auth";
import { prisma } from "../../../../../lib/prisma";
import { CompanyStatus } from "@prisma/client";
+interface PlatformSession {
+ user: {
+ id?: string;
+ name?: string;
+ email?: string;
+ isPlatformUser?: boolean;
+ platformRole?: string;
+ };
+}
+
// GET /api/platform/companies/[id] - Get company details
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
- const session = await getServerSession(platformAuthOptions);
+ const session = await getServerSession(platformAuthOptions) as PlatformSession | null;
if (!session?.user?.isPlatformUser) {
return NextResponse.json({ error: "Platform access required" }, { status: 401 });
@@ -24,28 +34,19 @@ export async function GET(
users: {
select: {
id: true,
+ name: true,
email: true,
role: true,
createdAt: true,
updatedAt: true,
+ invitedBy: true,
+ invitedAt: true,
},
},
- sessions: {
- select: {
- id: true,
- startTime: true,
- endTime: true,
- sentiment: true,
- category: true,
- },
- take: 10,
- orderBy: { createdAt: "desc" },
- },
_count: {
select: {
sessions: true,
imports: true,
- users: true,
},
},
},
@@ -55,7 +56,7 @@ export async function GET(
return NextResponse.json({ error: "Company not found" }, { status: 404 });
}
- return NextResponse.json({ company });
+ return NextResponse.json(company);
} catch (error) {
console.error("Platform company details error:", error);
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
@@ -76,10 +77,12 @@ export async function PATCH(
const { id } = await params;
const body = await request.json();
- const { name, csvUrl, csvUsername, csvPassword, status } = body;
+ const { name, email, maxUsers, csvUrl, csvUsername, csvPassword, status } = body;
const updateData: any = {};
if (name !== undefined) updateData.name = name;
+ if (email !== undefined) updateData.email = email;
+ if (maxUsers !== undefined) updateData.maxUsers = maxUsers;
if (csvUrl !== undefined) updateData.csvUrl = csvUrl;
if (csvUsername !== undefined) updateData.csvUsername = csvUsername;
if (csvPassword !== undefined) updateData.csvPassword = csvPassword;
diff --git a/app/api/platform/companies/[id]/users/route.ts b/app/api/platform/companies/[id]/users/route.ts
new file mode 100644
index 0000000..e14b577
--- /dev/null
+++ b/app/api/platform/companies/[id]/users/route.ts
@@ -0,0 +1,132 @@
+import { NextRequest, NextResponse } from "next/server";
+import { getServerSession } from "next-auth";
+import { platformAuthOptions } from "../../../../../../lib/platform-auth";
+import { prisma } from "../../../../../../lib/prisma";
+import { hash } from "bcryptjs";
+
+// POST /api/platform/companies/[id]/users - Invite user to company
+export async function POST(
+ request: NextRequest,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await getServerSession(platformAuthOptions);
+
+ if (!session?.user?.isPlatformUser || session.user.platformRole === "SUPPORT") {
+ return NextResponse.json({ error: "Admin access required" }, { status: 403 });
+ }
+
+ const { id: companyId } = await params;
+ const body = await request.json();
+ const { name, email, role = "USER" } = body;
+
+ if (!name || !email) {
+ return NextResponse.json({ error: "Name and email are required" }, { status: 400 });
+ }
+
+ // Check if company exists
+ const company = await prisma.company.findUnique({
+ where: { id: companyId },
+ include: { _count: { select: { users: true } } },
+ });
+
+ if (!company) {
+ return NextResponse.json({ error: "Company not found" }, { status: 404 });
+ }
+
+ // Check if user limit would be exceeded
+ if (company._count.users >= company.maxUsers) {
+ return NextResponse.json(
+ { error: "Company has reached maximum user limit" },
+ { status: 400 }
+ );
+ }
+
+ // Check if user already exists in this company
+ const existingUser = await prisma.user.findFirst({
+ where: {
+ email,
+ companyId,
+ },
+ });
+
+ if (existingUser) {
+ return NextResponse.json(
+ { error: "User already exists in this company" },
+ { status: 400 }
+ );
+ }
+
+ // Generate a temporary password (in a real app, you'd send an invitation email)
+ const tempPassword = `temp${Math.random().toString(36).slice(-8)}`;
+ const hashedPassword = await hash(tempPassword, 10);
+
+ // Create the user
+ const user = await prisma.user.create({
+ data: {
+ name,
+ email,
+ hashedPassword,
+ role,
+ companyId,
+ invitedBy: session.user.email,
+ invitedAt: new Date(),
+ },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ role: true,
+ createdAt: true,
+ invitedBy: true,
+ invitedAt: true,
+ },
+ });
+
+ // In a real application, you would send an email with login credentials
+ // For now, we'll return the temporary password
+ return NextResponse.json({
+ user,
+ tempPassword, // Remove this in production and send via email
+ message: "User invited successfully. In production, credentials would be sent via email.",
+ });
+ } catch (error) {
+ console.error("Platform user invitation error:", error);
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+ }
+}
+
+// GET /api/platform/companies/[id]/users - Get company users
+export async function GET(
+ request: NextRequest,
+ { params }: { params: Promise<{ id: string }> }
+) {
+ try {
+ const session = await getServerSession(platformAuthOptions);
+
+ if (!session?.user?.isPlatformUser) {
+ return NextResponse.json({ error: "Platform access required" }, { status: 401 });
+ }
+
+ const { id: companyId } = await params;
+
+ const users = await prisma.user.findMany({
+ where: { companyId },
+ select: {
+ id: true,
+ name: true,
+ email: true,
+ role: true,
+ createdAt: true,
+ invitedBy: true,
+ invitedAt: true,
+ },
+ orderBy: { createdAt: "desc" },
+ });
+
+ return NextResponse.json({ users });
+ } catch (error) {
+ console.error("Platform users list error:", error);
+ return NextResponse.json({ error: "Internal server error" }, { status: 500 });
+ }
+}
\ No newline at end of file
diff --git a/app/api/platform/companies/route.ts b/app/api/platform/companies/route.ts
index b1050a4..01f9a11 100644
--- a/app/api/platform/companies/route.ts
+++ b/app/api/platform/companies/route.ts
@@ -1,6 +1,6 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth";
-import { platformAuthOptions } from "../auth/[...nextauth]/route";
+import { platformAuthOptions } from "../../../../lib/platform-auth";
import { prisma } from "../../../../lib/prisma";
import { CompanyStatus } from "@prisma/client";
diff --git a/app/layout.tsx b/app/layout.tsx
index ed9a68b..e91eb7c 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -7,25 +7,68 @@ import { Toaster } from "@/components/ui/sonner";
export const metadata = {
title: "LiveDash - AI-Powered Customer Conversation Analytics",
description:
- "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics.",
+ "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics. Turn every conversation into competitive intelligence.",
keywords: [
"customer analytics",
- "AI sentiment analysis",
+ "AI sentiment analysis",
"conversation intelligence",
"customer support analytics",
"chat analytics",
- "customer insights"
+ "customer insights",
+ "conversation analytics",
+ "customer experience analytics",
+ "sentiment tracking",
+ "AI customer intelligence",
+ "automated categorization",
+ "real-time analytics",
+ "customer conversation dashboard"
],
+ authors: [{ name: "Notso AI" }],
+ creator: "Notso AI",
+ publisher: "Notso AI",
+ formatDetection: {
+ email: false,
+ address: false,
+ telephone: false,
+ },
+ metadataBase: new URL(process.env.NEXTAUTH_URL || 'https://livedash.notso.ai'),
+ alternates: {
+ canonical: '/',
+ },
openGraph: {
title: "LiveDash - AI-Powered Customer Conversation Analytics",
- description: "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics.",
+ description: "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics. Turn every conversation into competitive intelligence.",
type: "website",
siteName: "LiveDash",
+ url: "/",
+ locale: 'en_US',
+ images: [
+ {
+ url: '/og-image.png',
+ width: 1200,
+ height: 630,
+ alt: 'LiveDash - AI-Powered Customer Conversation Analytics Platform',
+ }
+ ],
},
twitter: {
card: "summary_large_image",
title: "LiveDash - AI-Powered Customer Conversation Analytics",
description: "Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics.",
+ creator: "@notsoai",
+ site: "@notsoai",
+ images: ['/og-image.png'],
+ },
+ robots: {
+ index: true,
+ follow: true,
+ googleBot: {
+ index: true,
+ follow: true,
+ 'max-video-preview': -1,
+ 'max-image-preview': 'large',
+ 'max-snippet': -1,
+ },
},
icons: {
icon: [
@@ -35,11 +78,52 @@ export const metadata = {
apple: "/icon-192.svg",
},
manifest: "/manifest.json",
+ other: {
+ 'msapplication-TileColor': '#2563eb',
+ 'theme-color': '#ffffff',
+ },
};
export default function RootLayout({ children }: { children: ReactNode }) {
+ const jsonLd = {
+ '@context': 'https://schema.org',
+ '@type': 'SoftwareApplication',
+ name: 'LiveDash',
+ description: 'Transform customer conversations into actionable insights with advanced AI sentiment analysis, automated categorization, and real-time analytics.',
+ url: process.env.NEXTAUTH_URL || 'https://livedash.notso.ai',
+ author: {
+ '@type': 'Organization',
+ name: 'Notso AI',
+ },
+ applicationCategory: 'Business Analytics Software',
+ operatingSystem: 'Web Browser',
+ offers: {
+ '@type': 'Offer',
+ category: 'SaaS',
+ },
+ aggregateRating: {
+ '@type': 'AggregateRating',
+ ratingValue: '4.8',
+ ratingCount: '150',
+ },
+ featureList: [
+ 'AI-powered sentiment analysis',
+ 'Automated conversation categorization',
+ 'Real-time analytics dashboard',
+ 'Multi-language support',
+ 'Custom AI model integration',
+ 'Enterprise-grade security'
+ ]
+ };
+
return (
+
+
+
{/* Skip navigation link for keyboard users */}
(null);
+ const [isLoading, setIsLoading] = useState(true);
+ const [isSaving, setIsSaving] = useState(false);
+ const [editData, setEditData] = useState>({});
+ const [showInviteUser, setShowInviteUser] = useState(false);
+ const [inviteData, setInviteData] = useState({ name: "", email: "", role: "USER" });
+
+ useEffect(() => {
+ if (status === "loading") return;
+
+ if (!session?.user?.isPlatformUser) {
+ router.push("/platform/login");
+ return;
+ }
+
+ fetchCompany();
+ }, [session, status, router, params.id]);
+
+ const fetchCompany = async () => {
+ try {
+ const response = await fetch(`/api/platform/companies/${params.id}`);
+ if (response.ok) {
+ const data = await response.json();
+ setCompany(data);
+ setEditData({
+ name: data.name,
+ email: data.email,
+ status: data.status,
+ maxUsers: data.maxUsers,
+ });
+ } else {
+ toast({
+ title: "Error",
+ description: "Failed to load company data",
+ variant: "destructive",
+ });
+ }
+ } catch (error) {
+ console.error("Failed to fetch company:", error);
+ toast({
+ title: "Error",
+ description: "Failed to load company data",
+ variant: "destructive",
+ });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ const handleSave = async () => {
+ setIsSaving(true);
+ try {
+ const response = await fetch(`/api/platform/companies/${params.id}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(editData),
+ });
+
+ if (response.ok) {
+ const updatedCompany = await response.json();
+ setCompany(updatedCompany);
+ toast({
+ title: "Success",
+ description: "Company updated successfully",
+ });
+ } else {
+ throw new Error("Failed to update company");
+ }
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: "Failed to update company",
+ variant: "destructive",
+ });
+ } finally {
+ setIsSaving(false);
+ }
+ };
+
+ const handleStatusChange = async (newStatus: string) => {
+ const statusAction = newStatus === "SUSPENDED" ? "suspend" : "activate";
+
+ try {
+ const response = await fetch(`/api/platform/companies/${params.id}`, {
+ method: "PATCH",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ status: newStatus }),
+ });
+
+ if (response.ok) {
+ setCompany(prev => prev ? { ...prev, status: newStatus } : null);
+ setEditData(prev => ({ ...prev, status: newStatus }));
+ toast({
+ title: "Success",
+ description: `Company ${statusAction}d successfully`,
+ });
+ } else {
+ throw new Error(`Failed to ${statusAction} company`);
+ }
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: `Failed to ${statusAction} company`,
+ variant: "destructive",
+ });
+ }
+ };
+
+ const handleInviteUser = async () => {
+ try {
+ const response = await fetch(`/api/platform/companies/${params.id}/users`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(inviteData),
+ });
+
+ if (response.ok) {
+ setShowInviteUser(false);
+ setInviteData({ name: "", email: "", role: "USER" });
+ fetchCompany(); // Refresh company data
+ toast({
+ title: "Success",
+ description: "User invited successfully",
+ });
+ } else {
+ throw new Error("Failed to invite user");
+ }
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: "Failed to invite user",
+ variant: "destructive",
+ });
+ }
+ };
+
+ if (status === "loading" || isLoading) {
+ return (
+
+
Loading company details...
+
+ );
+ }
+
+ if (!session?.user?.isPlatformUser || !company) {
+ return null;
+ }
+
+ const getStatusBadgeVariant = (status: string) => {
+ switch (status) {
+ case "ACTIVE": return "default";
+ case "TRIAL": return "secondary";
+ case "SUSPENDED": return "destructive";
+ case "ARCHIVED": return "outline";
+ default: return "default";
+ }
+ };
+
+ const canEdit = session.user.platformRole === "SUPER_ADMIN";
+
+ return (
+
+
+
+
+
+
+
+
+
+ {company.name}
+
+
+ {company.status}
+
+
+
+ Company Management
+
+
+
+
+ {canEdit && (
+ <>
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ Overview
+ Users
+ Settings
+ Analytics
+
+
+
+ {/* Stats Overview */}
+
+
+
+ Total Users
+
+
+
+ {company.users.length}
+
+ of {company.maxUsers} maximum
+
+
+
+
+
+
+ Total Sessions
+
+
+
+ {company._count.sessions}
+
+
+
+
+
+ Data Imports
+
+
+
+ {company._count.imports}
+
+
+
+
+
+ Created
+
+
+
+
+ {new Date(company.createdAt).toLocaleDateString()}
+
+
+
+
+
+ {/* Company Info */}
+
+
+ Company Information
+
+
+
+
+
+ setEditData(prev => ({ ...prev, name: e.target.value }))}
+ disabled={!canEdit}
+ />
+
+
+
+ setEditData(prev => ({ ...prev, email: e.target.value }))}
+ disabled={!canEdit}
+ />
+
+
+
+ setEditData(prev => ({ ...prev, maxUsers: parseInt(e.target.value) }))}
+ disabled={!canEdit}
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Users ({company.users.length})
+
+ {canEdit && (
+
+ )}
+
+
+
+
+ {company.users.map((user) => (
+
+
+
+
+ {user.name?.charAt(0) || user.email.charAt(0).toUpperCase()}
+
+
+
+
{user.name || "No name"}
+
{user.email}
+
+
+
+
{user.role}
+
+ Joined {new Date(user.createdAt).toLocaleDateString()}
+
+
+
+ ))}
+ {company.users.length === 0 && (
+
+ No users found. Invite the first user to get started.
+
+ )}
+
+
+
+
+
+
+
+
+ Danger Zone
+
+
+ {canEdit && (
+ <>
+
+
+
Suspend Company
+
+ Temporarily disable access to this company
+
+
+
+
+
+
+
+
+ Suspend Company
+
+ Are you sure you want to suspend this company? This will disable access for all users.
+
+
+
+ Cancel
+ handleStatusChange("SUSPENDED")}>
+ Suspend
+
+
+
+
+
+
+ {company.status === "SUSPENDED" && (
+
+
+
Reactivate Company
+
+ Restore access to this company
+
+
+
+
+ )}
+ >
+ )}
+
+
+
+
+
+
+
+ Analytics
+
+
+
+ Analytics dashboard coming soon...
+
+
+
+
+
+
+
+ {/* Invite User Dialog */}
+ {showInviteUser && (
+
+
+
+ Invite User
+
+
+
+
+ setInviteData(prev => ({ ...prev, name: e.target.value }))}
+ placeholder="User's full name"
+ />
+
+
+
+ setInviteData(prev => ({ ...prev, email: e.target.value }))}
+ placeholder="user@example.com"
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/platform/dashboard/page.tsx b/app/platform/dashboard/page.tsx
index 424ca25..7071ed1 100644
--- a/app/platform/dashboard/page.tsx
+++ b/app/platform/dashboard/page.tsx
@@ -6,6 +6,17 @@ import { useRouter } from "next/navigation";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@/components/ui/dialog";
import {
Building2,
Users,
@@ -15,6 +26,7 @@ import {
Settings,
BarChart3
} from "lucide-react";
+import { useToast } from "@/hooks/use-toast";
interface Company {
id: string;
@@ -39,8 +51,18 @@ interface DashboardData {
export default function PlatformDashboard() {
const { data: session, status } = useSession();
const router = useRouter();
+ const { toast } = useToast();
const [dashboardData, setDashboardData] = useState(null);
const [isLoading, setIsLoading] = useState(true);
+ const [showAddCompany, setShowAddCompany] = useState(false);
+ const [isCreating, setIsCreating] = useState(false);
+ const [newCompanyData, setNewCompanyData] = useState({
+ name: "",
+ adminEmail: "",
+ adminName: "",
+ adminPassword: "",
+ maxUsers: 10,
+ });
useEffect(() => {
if (status === "loading") return;
@@ -67,6 +89,54 @@ export default function PlatformDashboard() {
}
};
+ const handleCreateCompany = async () => {
+ if (!newCompanyData.name || !newCompanyData.adminEmail || !newCompanyData.adminName) {
+ toast({
+ title: "Error",
+ description: "Please fill in all required fields",
+ variant: "destructive",
+ });
+ return;
+ }
+
+ setIsCreating(true);
+ try {
+ const response = await fetch("/api/platform/companies", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(newCompanyData),
+ });
+
+ if (response.ok) {
+ const result = await response.json();
+ setShowAddCompany(false);
+ setNewCompanyData({
+ name: "",
+ adminEmail: "",
+ adminName: "",
+ adminPassword: "",
+ maxUsers: 10,
+ });
+ fetchDashboardData(); // Refresh the list
+ toast({
+ title: "Success",
+ description: `Company "${newCompanyData.name}" created successfully`,
+ });
+ } else {
+ const error = await response.json();
+ throw new Error(error.error || "Failed to create company");
+ }
+ } catch (error) {
+ toast({
+ title: "Error",
+ description: error instanceof Error ? error.message : "Failed to create company",
+ variant: "destructive",
+ });
+ } finally {
+ setIsCreating(false);
+ }
+ };
+
const getStatusBadgeVariant = (status: string) => {
switch (status) {
case "ACTIVE": return "default";
@@ -111,10 +181,81 @@ export default function PlatformDashboard() {
Settings
-
+
@@ -200,7 +341,11 @@ export default function PlatformDashboard() {
Analytics
-