mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 08:52:10 +01:00
feat: comprehensive Biome linting fixes and code quality improvements
Major code quality overhaul addressing 58% of all linting issues: • Type Safety Improvements: - Replace all any types with proper TypeScript interfaces - Fix Map component shadowing (renamed to CountryMap) - Add comprehensive custom error classes system - Enhance API route type safety • Accessibility Enhancements: - Add explicit button types to all interactive elements - Implement useId() hooks for form element accessibility - Add SVG title attributes for screen readers - Fix static element interactions with keyboard handlers • React Best Practices: - Resolve exhaustive dependencies warnings with useCallback - Extract nested component definitions to top level - Fix array index keys with proper unique identifiers - Improve component organization and prop typing • Code Organization: - Automatic import organization and type import optimization - Fix unused function parameters and variables - Enhanced error handling with structured error responses - Improve component reusability and maintainability Results: 248 → 104 total issues (58% reduction) - Fixed all critical type safety and security issues - Enhanced accessibility compliance significantly - Improved code maintainability and performance
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { fetchAndParseCsv } from "../../../../lib/csvFetcher";
|
||||
import { processQueuedImports } from "../../../../lib/importProcessor";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
@ -47,10 +47,10 @@ export async function POST(request: NextRequest) {
|
||||
// Check if company is active and can process data
|
||||
if (company.status !== "ACTIVE") {
|
||||
return NextResponse.json(
|
||||
{
|
||||
{
|
||||
error: `Data processing is disabled for ${company.status.toLowerCase()} companies`,
|
||||
companyStatus: company.status
|
||||
},
|
||||
companyStatus: company.status,
|
||||
},
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { ProcessingStage } from "@prisma/client";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { processUnprocessedSessions } from "../../../../lib/processingScheduler";
|
||||
import { ProcessingStatusManager } from "../../../../lib/processingStatusManager";
|
||||
import { ProcessingStage } from "@prisma/client";
|
||||
|
||||
interface SessionUser {
|
||||
email: string;
|
||||
@ -34,7 +34,7 @@ export async function POST(request: NextRequest) {
|
||||
id: true,
|
||||
name: true,
|
||||
status: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -86,7 +86,7 @@ export async function POST(request: NextRequest) {
|
||||
}
|
||||
|
||||
// Start processing (this will run asynchronously)
|
||||
const startTime = Date.now();
|
||||
const _startTime = Date.now();
|
||||
|
||||
// Note: We're calling the function but not awaiting it to avoid timeout
|
||||
// The processing will continue in the background
|
||||
|
||||
@ -3,4 +3,4 @@ import { authOptions } from "../../../../lib/auth";
|
||||
|
||||
const handler = NextAuth(authOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
export { handler as GET, handler as POST };
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
export async function GET(_request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user) {
|
||||
return NextResponse.json({ error: "Not logged in" }, { status: 401 });
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { sessionMetrics } from "../../../../lib/metrics";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { ChatSession } from "../../../../lib/types";
|
||||
import { sessionMetrics } from "../../../../lib/metrics";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import type { ChatSession } from "../../../../lib/types";
|
||||
|
||||
interface SessionUser {
|
||||
email: string;
|
||||
@ -31,7 +31,7 @@ export async function GET(request: NextRequest) {
|
||||
name: true,
|
||||
csvUrl: true,
|
||||
status: true,
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
@ -46,14 +46,20 @@ export async function GET(request: NextRequest) {
|
||||
const endDate = searchParams.get("endDate");
|
||||
|
||||
// Build where clause with optional date filtering
|
||||
const whereClause: any = {
|
||||
const whereClause: {
|
||||
companyId: string;
|
||||
startTime?: {
|
||||
gte: Date;
|
||||
lte: Date;
|
||||
};
|
||||
} = {
|
||||
companyId: user.companyId,
|
||||
};
|
||||
|
||||
if (startDate && endDate) {
|
||||
whereClause.startTime = {
|
||||
gte: new Date(startDate),
|
||||
lte: new Date(endDate + "T23:59:59.999Z"), // Include full end date
|
||||
lte: new Date(`${endDate}T23:59:59.999Z`), // Include full end date
|
||||
};
|
||||
}
|
||||
|
||||
@ -82,25 +88,28 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
|
||||
// Batch fetch questions for all sessions at once if needed for metrics
|
||||
const sessionIds = prismaSessions.map(s => s.id);
|
||||
const sessionIds = prismaSessions.map((s) => s.id);
|
||||
const sessionQuestions = await prisma.sessionQuestion.findMany({
|
||||
where: { sessionId: { in: sessionIds } },
|
||||
include: { question: true },
|
||||
orderBy: { order: 'asc' },
|
||||
orderBy: { order: "asc" },
|
||||
});
|
||||
|
||||
// Group questions by session
|
||||
const questionsBySession = sessionQuestions.reduce((acc, sq) => {
|
||||
if (!acc[sq.sessionId]) acc[sq.sessionId] = [];
|
||||
acc[sq.sessionId].push(sq.question.content);
|
||||
return acc;
|
||||
}, {} as Record<string, string[]>);
|
||||
const questionsBySession = sessionQuestions.reduce(
|
||||
(acc, sq) => {
|
||||
if (!acc[sq.sessionId]) acc[sq.sessionId] = [];
|
||||
acc[sq.sessionId].push(sq.question.content);
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, string[]>
|
||||
);
|
||||
|
||||
// Convert Prisma sessions to ChatSession[] type for sessionMetrics
|
||||
const chatSessions: ChatSession[] = prismaSessions.map((ps) => {
|
||||
// Get questions for this session or empty array
|
||||
const questions = questionsBySession[ps.id] || [];
|
||||
|
||||
|
||||
// Convert questions to mock messages for backward compatibility
|
||||
const mockMessages = questions.map((q, index) => ({
|
||||
id: `question-${index}`,
|
||||
@ -127,7 +136,8 @@ export async function GET(request: NextRequest) {
|
||||
ipAddress: ps.ipAddress || undefined,
|
||||
sentiment: ps.sentiment === null ? undefined : ps.sentiment,
|
||||
messagesSent: ps.messagesSent === null ? undefined : ps.messagesSent,
|
||||
avgResponseTime: ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
|
||||
avgResponseTime:
|
||||
ps.avgResponseTime === null ? undefined : ps.avgResponseTime,
|
||||
escalated: ps.escalated || false,
|
||||
forwardedHr: ps.forwardedHr || false,
|
||||
initialMsg: ps.initialMsg || undefined,
|
||||
|
||||
@ -1,10 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { SessionFilterOptions } from "../../../../lib/types";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
export async function GET(_request: NextRequest) {
|
||||
const authSession = await getServerSession(authOptions);
|
||||
|
||||
if (!authSession || !authSession.user?.companyId) {
|
||||
@ -17,23 +16,23 @@ export async function GET(request: NextRequest) {
|
||||
// Use groupBy for better performance with distinct values
|
||||
const [categoryGroups, languageGroups] = await Promise.all([
|
||||
prisma.session.groupBy({
|
||||
by: ['category'],
|
||||
by: ["category"],
|
||||
where: {
|
||||
companyId,
|
||||
category: { not: null },
|
||||
},
|
||||
orderBy: {
|
||||
category: 'asc',
|
||||
category: "asc",
|
||||
},
|
||||
}),
|
||||
prisma.session.groupBy({
|
||||
by: ['language'],
|
||||
by: ["language"],
|
||||
where: {
|
||||
companyId,
|
||||
language: { not: null },
|
||||
},
|
||||
orderBy: {
|
||||
language: 'asc',
|
||||
language: "asc",
|
||||
},
|
||||
}),
|
||||
]);
|
||||
@ -41,7 +40,7 @@ export async function GET(request: NextRequest) {
|
||||
const distinctCategories = categoryGroups
|
||||
.map((g) => g.category)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
|
||||
const distinctLanguages = languageGroups
|
||||
.map((g) => g.language)
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../../../lib/prisma";
|
||||
import { ChatSession } from "../../../../../lib/types";
|
||||
import type { ChatSession } from "../../../../../lib/types";
|
||||
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
const { id } = await params;
|
||||
|
||||
@ -1,13 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth/next";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import {
|
||||
ChatSession,
|
||||
SessionApiResponse,
|
||||
SessionQuery,
|
||||
} from "../../../../lib/types";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import type { ChatSession } from "../../../../lib/types";
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const authSession = await getServerSession(authOptions);
|
||||
@ -48,7 +44,7 @@ export async function GET(request: NextRequest) {
|
||||
// Category Filter
|
||||
if (category && category.trim() !== "") {
|
||||
// Cast to SessionCategory enum if it's a valid value
|
||||
whereClause.category = category as any;
|
||||
whereClause.category = category;
|
||||
}
|
||||
|
||||
// Language Filter
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "crypto";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import crypto from "node:crypto";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { authOptions } from "../../../../lib/auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
|
||||
interface UserBasicInfo {
|
||||
id: string;
|
||||
@ -11,7 +11,7 @@ interface UserBasicInfo {
|
||||
role: string;
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
export async function GET(_request: NextRequest) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session?.user || session.user.role !== "ADMIN") {
|
||||
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "node:crypto";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { sendEmail } from "../../../lib/sendEmail";
|
||||
import { forgotPasswordSchema, validateInput } from "../../../lib/validation";
|
||||
import crypto from "crypto";
|
||||
|
||||
// In-memory rate limiting for password reset requests
|
||||
const resetAttempts = new Map<string, { count: number; resetTime: number }>();
|
||||
@ -28,7 +28,10 @@ function checkRateLimit(ip: string): boolean {
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
// Rate limiting check
|
||||
const ip = request.headers.get("x-forwarded-for") || request.headers.get("x-real-ip") || "unknown";
|
||||
const ip =
|
||||
request.headers.get("x-forwarded-for") ||
|
||||
request.headers.get("x-real-ip") ||
|
||||
"unknown";
|
||||
if (!checkRateLimit(ip)) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
|
||||
@ -3,4 +3,4 @@ import { platformAuthOptions } from "../../../../../lib/platform-auth";
|
||||
|
||||
const handler = NextAuth(platformAuthOptions);
|
||||
|
||||
export { handler as GET, handler as POST };
|
||||
export { handler as GET, handler as POST };
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { CompanyStatus } from "@prisma/client";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { platformAuthOptions } from "../../../../../lib/platform-auth";
|
||||
import { prisma } from "../../../../../lib/prisma";
|
||||
import { CompanyStatus } from "@prisma/client";
|
||||
|
||||
interface PlatformSession {
|
||||
user: {
|
||||
@ -16,14 +16,19 @@ interface PlatformSession {
|
||||
|
||||
// GET /api/platform/companies/[id] - Get company details
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions) as PlatformSession | null;
|
||||
const session = (await getServerSession(
|
||||
platformAuthOptions
|
||||
)) as PlatformSession | null;
|
||||
|
||||
if (!session?.user?.isPlatformUser) {
|
||||
return NextResponse.json({ error: "Platform access required" }, { status: 401 });
|
||||
return NextResponse.json(
|
||||
{ error: "Platform access required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
@ -59,7 +64,10 @@ export async function GET(
|
||||
return NextResponse.json(company);
|
||||
} catch (error) {
|
||||
console.error("Platform company details error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,15 +79,30 @@ export async function PATCH(
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
|
||||
if (!session?.user?.isPlatformUser || session.user.platformRole === "SUPPORT") {
|
||||
return NextResponse.json({ error: "Admin access required" }, { status: 403 });
|
||||
if (
|
||||
!session?.user?.isPlatformUser ||
|
||||
session.user.platformRole === "SUPPORT"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const body = await request.json();
|
||||
const { name, email, maxUsers, csvUrl, csvUsername, csvPassword, status } = body;
|
||||
const { name, email, maxUsers, csvUrl, csvUsername, csvPassword, status } =
|
||||
body;
|
||||
|
||||
const updateData: any = {};
|
||||
const updateData: {
|
||||
name?: string;
|
||||
email?: string;
|
||||
maxUsers?: number;
|
||||
csvUrl?: string;
|
||||
csvUsername?: string;
|
||||
csvPassword?: string;
|
||||
status?: CompanyStatus;
|
||||
} = {};
|
||||
if (name !== undefined) updateData.name = name;
|
||||
if (email !== undefined) updateData.email = email;
|
||||
if (maxUsers !== undefined) updateData.maxUsers = maxUsers;
|
||||
@ -96,20 +119,29 @@ export async function PATCH(
|
||||
return NextResponse.json({ company });
|
||||
} catch (error) {
|
||||
console.error("Platform company update error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// DELETE /api/platform/companies/[id] - Delete company (archives instead)
|
||||
export async function DELETE(
|
||||
request: NextRequest,
|
||||
_request: NextRequest,
|
||||
{ params }: { params: Promise<{ id: string }> }
|
||||
) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
|
||||
if (!session?.user?.isPlatformUser || session.user.platformRole !== "SUPER_ADMIN") {
|
||||
return NextResponse.json({ error: "Super admin access required" }, { status: 403 });
|
||||
if (
|
||||
!session?.user?.isPlatformUser ||
|
||||
session.user.platformRole !== "SUPER_ADMIN"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Super admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
@ -123,6 +155,9 @@ export async function DELETE(
|
||||
return NextResponse.json({ company });
|
||||
} catch (error) {
|
||||
console.error("Platform company archive error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { hash } from "bcryptjs";
|
||||
import { type 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(
|
||||
@ -12,8 +12,14 @@ export async function POST(
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
|
||||
if (!session?.user?.isPlatformUser || session.user.platformRole === "SUPPORT") {
|
||||
return NextResponse.json({ error: "Admin access required" }, { status: 403 });
|
||||
if (
|
||||
!session?.user?.isPlatformUser ||
|
||||
session.user.platformRole === "SUPPORT"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id: companyId } = await params;
|
||||
@ -21,7 +27,10 @@ export async function POST(
|
||||
const { name, email, role = "USER" } = body;
|
||||
|
||||
if (!name || !email) {
|
||||
return NextResponse.json({ error: "Name and email are required" }, { status: 400 });
|
||||
return NextResponse.json(
|
||||
{ error: "Name and email are required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Check if company exists
|
||||
@ -88,24 +97,31 @@ export async function POST(
|
||||
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.",
|
||||
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 });
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// GET /api/platform/companies/[id]/users - Get company users
|
||||
export async function GET(
|
||||
request: NextRequest,
|
||||
_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 });
|
||||
return NextResponse.json(
|
||||
{ error: "Platform access required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { id: companyId } = await params;
|
||||
@ -127,6 +143,9 @@ export async function GET(
|
||||
return NextResponse.json({ users });
|
||||
} catch (error) {
|
||||
console.error("Platform users list error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import type { CompanyStatus } from "@prisma/client";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getServerSession } from "next-auth";
|
||||
import { platformAuthOptions } from "../../../../lib/platform-auth";
|
||||
import { prisma } from "../../../../lib/prisma";
|
||||
import { CompanyStatus } from "@prisma/client";
|
||||
|
||||
// GET /api/platform/companies - List all companies
|
||||
export async function GET(request: NextRequest) {
|
||||
@ -10,7 +10,10 @@ export async function GET(request: NextRequest) {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
|
||||
if (!session?.user?.isPlatformUser) {
|
||||
return NextResponse.json({ error: "Platform access required" }, { status: 401 });
|
||||
return NextResponse.json(
|
||||
{ error: "Platform access required" },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
const { searchParams } = new URL(request.url);
|
||||
@ -20,7 +23,13 @@ export async function GET(request: NextRequest) {
|
||||
const limit = parseInt(searchParams.get("limit") || "20");
|
||||
const offset = (page - 1) * limit;
|
||||
|
||||
const where: any = {};
|
||||
const where: {
|
||||
status?: CompanyStatus;
|
||||
name?: {
|
||||
contains: string;
|
||||
mode: "insensitive";
|
||||
};
|
||||
} = {};
|
||||
if (status) where.status = status;
|
||||
if (search) {
|
||||
where.name = {
|
||||
@ -65,7 +74,10 @@ export async function GET(request: NextRequest) {
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Platform companies list error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,33 +86,46 @@ export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await getServerSession(platformAuthOptions);
|
||||
|
||||
if (!session?.user?.isPlatformUser || session.user.platformRole === "SUPPORT") {
|
||||
return NextResponse.json({ error: "Admin access required" }, { status: 403 });
|
||||
if (
|
||||
!session?.user?.isPlatformUser ||
|
||||
session.user.platformRole === "SUPPORT"
|
||||
) {
|
||||
return NextResponse.json(
|
||||
{ error: "Admin access required" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const {
|
||||
name,
|
||||
csvUrl,
|
||||
csvUsername,
|
||||
csvPassword,
|
||||
const {
|
||||
name,
|
||||
csvUrl,
|
||||
csvUsername,
|
||||
csvPassword,
|
||||
adminEmail,
|
||||
adminName,
|
||||
adminPassword,
|
||||
maxUsers = 10,
|
||||
status = "TRIAL"
|
||||
status = "TRIAL",
|
||||
} = body;
|
||||
|
||||
if (!name || !csvUrl) {
|
||||
return NextResponse.json({ error: "Name and CSV URL required" }, { status: 400 });
|
||||
return NextResponse.json(
|
||||
{ error: "Name and CSV URL required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!adminEmail || !adminName) {
|
||||
return NextResponse.json({ error: "Admin email and name required" }, { status: 400 });
|
||||
return NextResponse.json(
|
||||
{ error: "Admin email and name required" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// Generate password if not provided
|
||||
const finalAdminPassword = adminPassword || `Temp${Math.random().toString(36).slice(2, 8)}!`;
|
||||
const finalAdminPassword =
|
||||
adminPassword || `Temp${Math.random().toString(36).slice(2, 8)}!`;
|
||||
|
||||
// Hash the admin password
|
||||
const bcrypt = await import("bcryptjs");
|
||||
@ -133,20 +158,30 @@ export async function POST(request: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
return { company, adminUser, generatedPassword: adminPassword ? null : finalAdminPassword };
|
||||
return {
|
||||
company,
|
||||
adminUser,
|
||||
generatedPassword: adminPassword ? null : finalAdminPassword,
|
||||
};
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
company: result.company,
|
||||
adminUser: {
|
||||
email: result.adminUser.email,
|
||||
name: result.adminUser.name,
|
||||
role: result.adminUser.role,
|
||||
return NextResponse.json(
|
||||
{
|
||||
company: result.company,
|
||||
adminUser: {
|
||||
email: result.adminUser.email,
|
||||
name: result.adminUser.name,
|
||||
role: result.adminUser.role,
|
||||
},
|
||||
generatedPassword: result.generatedPassword,
|
||||
},
|
||||
generatedPassword: result.generatedPassword,
|
||||
}, { status: 201 });
|
||||
{ status: 201 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Platform company creation error:", error);
|
||||
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
|
||||
return NextResponse.json(
|
||||
{ error: "Internal server error" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { registerSchema, validateInput } from "../../../lib/validation";
|
||||
import bcrypt from "bcryptjs";
|
||||
|
||||
// In-memory rate limiting (for production, use Redis or similar)
|
||||
const registrationAttempts = new Map<
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import crypto from "node:crypto";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { prisma } from "../../../lib/prisma";
|
||||
import { resetPasswordSchema, validateInput } from "../../../lib/validation";
|
||||
import bcrypt from "bcryptjs";
|
||||
import crypto from "crypto";
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { Database, Save, Settings, ShieldX } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Company } from "../../../lib/types";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { ShieldX, Settings, Save, Database } from "lucide-react";
|
||||
import type { Company } from "../../../lib/types";
|
||||
|
||||
export default function CompanySettingsPage() {
|
||||
const csvUrlId = useId();
|
||||
const csvUsernameId = useId();
|
||||
const csvPasswordId = useId();
|
||||
const { data: session, status } = useSession();
|
||||
// We store the full company object for future use and updates after save operations
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
const [company, setCompany] = useState<Company | null>(null);
|
||||
const [_company, setCompany] = useState<Company | null>(null);
|
||||
const [csvUrl, setCsvUrl] = useState<string>("");
|
||||
const [csvUsername, setCsvUsername] = useState<string>("");
|
||||
const [csvPassword, setCsvPassword] = useState<string>("");
|
||||
@ -156,9 +159,9 @@ export default function CompanySettingsPage() {
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="csvUrl">CSV Data Source URL</Label>
|
||||
<Label htmlFor={csvUrlId}>CSV Data Source URL</Label>
|
||||
<Input
|
||||
id="csvUrl"
|
||||
id={csvUrlId}
|
||||
type="text"
|
||||
value={csvUrl}
|
||||
onChange={(e) => setCsvUrl(e.target.value)}
|
||||
@ -168,9 +171,9 @@ export default function CompanySettingsPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="csvUsername">CSV Username</Label>
|
||||
<Label htmlFor={csvUsernameId}>CSV Username</Label>
|
||||
<Input
|
||||
id="csvUsername"
|
||||
id={csvUsernameId}
|
||||
type="text"
|
||||
value={csvUsername}
|
||||
onChange={(e) => setCsvUsername(e.target.value)}
|
||||
@ -180,9 +183,9 @@ export default function CompanySettingsPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="csvPassword">CSV Password</Label>
|
||||
<Label htmlFor={csvPasswordId}>CSV Password</Label>
|
||||
<Input
|
||||
id="csvPassword"
|
||||
id={csvPasswordId}
|
||||
type="password"
|
||||
value={csvPassword}
|
||||
onChange={(e) => setCsvPassword(e.target.value)}
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { ReactNode, useState, useEffect, useCallback } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { type ReactNode, useCallback, useEffect, useId, useState } from "react";
|
||||
import Sidebar from "../../components/Sidebar";
|
||||
|
||||
export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
const mainContentId = useId();
|
||||
const { status } = useSession();
|
||||
const router = useRouter();
|
||||
|
||||
@ -66,7 +67,7 @@ export default function DashboardLayout({ children }: { children: ReactNode }) {
|
||||
/>
|
||||
|
||||
<main
|
||||
id="main-content"
|
||||
id={mainContentId}
|
||||
className={`flex-1 overflow-auto transition-all duration-300 py-4 pr-4
|
||||
${
|
||||
isSidebarExpanded
|
||||
|
||||
@ -1,42 +1,42 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import {
|
||||
CheckCircle,
|
||||
Clock,
|
||||
Euro,
|
||||
Globe,
|
||||
LogOut,
|
||||
MessageCircle,
|
||||
MessageSquare,
|
||||
MoreVertical,
|
||||
RefreshCw,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Company, MetricsResult, WordCloudWord } from "../../../lib/types";
|
||||
import { formatEnumValue } from "@/lib/format-enums";
|
||||
import MetricCard from "../../../components/ui/metric-card";
|
||||
import ModernLineChart from "../../../components/charts/line-chart";
|
||||
import ModernBarChart from "../../../components/charts/bar-chart";
|
||||
import ModernDonutChart from "../../../components/charts/donut-chart";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import { useCallback, useEffect, useId, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
MessageSquare,
|
||||
Users,
|
||||
Clock,
|
||||
Zap,
|
||||
Euro,
|
||||
TrendingUp,
|
||||
CheckCircle,
|
||||
RefreshCw,
|
||||
LogOut,
|
||||
MoreVertical,
|
||||
Globe,
|
||||
MessageCircle,
|
||||
} from "lucide-react";
|
||||
import WordCloud from "../../../components/WordCloud";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { formatEnumValue } from "@/lib/format-enums";
|
||||
import ModernBarChart from "../../../components/charts/bar-chart";
|
||||
import ModernDonutChart from "../../../components/charts/donut-chart";
|
||||
import ModernLineChart from "../../../components/charts/line-chart";
|
||||
import GeographicMap from "../../../components/GeographicMap";
|
||||
import ResponseTimeDistribution from "../../../components/ResponseTimeDistribution";
|
||||
import TopQuestionsChart from "../../../components/TopQuestionsChart";
|
||||
import MetricCard from "../../../components/ui/metric-card";
|
||||
import WordCloud from "../../../components/WordCloud";
|
||||
import type { Company, MetricsResult, WordCloudWord } from "../../../lib/types";
|
||||
|
||||
// Safely wrapped component with useSession
|
||||
function DashboardContent() {
|
||||
@ -48,10 +48,11 @@ function DashboardContent() {
|
||||
const [refreshing, setRefreshing] = useState<boolean>(false);
|
||||
const [isInitialLoad, setIsInitialLoad] = useState<boolean>(true);
|
||||
|
||||
const refreshStatusId = useId();
|
||||
const isAuditor = session?.user?.role === "AUDITOR";
|
||||
|
||||
// Function to fetch metrics with optional date range
|
||||
const fetchMetrics = async (
|
||||
const fetchMetrics = useCallback(async (
|
||||
startDate?: string,
|
||||
endDate?: string,
|
||||
isInitial = false
|
||||
@ -78,7 +79,7 @@ function DashboardContent() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Redirect if not authenticated
|
||||
@ -91,7 +92,7 @@ function DashboardContent() {
|
||||
if (status === "authenticated" && isInitialLoad) {
|
||||
fetchMetrics(undefined, undefined, true);
|
||||
}
|
||||
}, [status, router, isInitialLoad]);
|
||||
}, [status, router, isInitialLoad, fetchMetrics]);
|
||||
|
||||
async function handleRefresh() {
|
||||
if (isAuditor) return;
|
||||
@ -243,7 +244,7 @@ function DashboardContent() {
|
||||
return {
|
||||
name:
|
||||
formattedName.length > 15
|
||||
? formattedName.substring(0, 15) + "..."
|
||||
? `${formattedName.substring(0, 15)}...`
|
||||
: formattedName,
|
||||
value: value as number,
|
||||
};
|
||||
@ -323,7 +324,7 @@ function DashboardContent() {
|
||||
? "Refreshing dashboard data"
|
||||
: "Refresh dashboard data"
|
||||
}
|
||||
aria-describedby={refreshing ? "refresh-status" : undefined}
|
||||
aria-describedby={refreshing ? refreshStatusId : undefined}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`h-4 w-4 ${refreshing ? "animate-spin" : ""}`}
|
||||
@ -332,7 +333,7 @@ function DashboardContent() {
|
||||
{refreshing ? "Refreshing..." : "Refresh"}
|
||||
</Button>
|
||||
{refreshing && (
|
||||
<div id="refresh-status" className="sr-only" aria-live="polite">
|
||||
<div id={refreshStatusId} className="sr-only" aria-live="polite">
|
||||
Dashboard data is being refreshed
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -1,22 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FC } from "react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
ArrowRight,
|
||||
BarChart3,
|
||||
MessageSquare,
|
||||
Settings,
|
||||
Users,
|
||||
ArrowRight,
|
||||
TrendingUp,
|
||||
Shield,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
|
||||
const DashboardPage: FC = () => {
|
||||
const { data: session, status } = useSession();
|
||||
@ -158,9 +157,9 @@ const DashboardPage: FC = () => {
|
||||
|
||||
{/* Navigation Cards */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{navigationCards.map((card, index) => (
|
||||
{navigationCards.map((card) => (
|
||||
<Card
|
||||
key={index}
|
||||
key={card.href}
|
||||
className={`relative overflow-hidden transition-all duration-300 hover:shadow-2xl hover:-translate-y-1 cursor-pointer group ${getCardClasses(
|
||||
card.variant
|
||||
)}`}
|
||||
@ -203,9 +202,9 @@ const DashboardPage: FC = () => {
|
||||
<CardContent className="relative space-y-4">
|
||||
{/* Features List */}
|
||||
<div className="space-y-2">
|
||||
{card.features.map((feature, featureIndex) => (
|
||||
{card.features.map((feature) => (
|
||||
<div
|
||||
key={featureIndex}
|
||||
key={feature}
|
||||
className="flex items-center gap-2 text-sm"
|
||||
>
|
||||
<Zap className="h-3 w-3 text-primary/60" />
|
||||
|
||||
@ -1,27 +1,27 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Activity,
|
||||
AlertCircle,
|
||||
ArrowLeft,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
Globe,
|
||||
MessageSquare,
|
||||
User,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import SessionDetails from "../../../../components/SessionDetails";
|
||||
import MessageViewer from "../../../../components/MessageViewer";
|
||||
import { ChatSession } from "../../../../lib/types";
|
||||
import { formatCategory } from "@/lib/format-enums";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
ArrowLeft,
|
||||
MessageSquare,
|
||||
Clock,
|
||||
Globe,
|
||||
ExternalLink,
|
||||
User,
|
||||
AlertCircle,
|
||||
FileText,
|
||||
Activity,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { formatCategory } from "@/lib/format-enums";
|
||||
import MessageViewer from "../../../../components/MessageViewer";
|
||||
import SessionDetails from "../../../../components/SessionDetails";
|
||||
import type { ChatSession } from "../../../../lib/types";
|
||||
|
||||
export default function SessionViewPage() {
|
||||
const params = useParams();
|
||||
|
||||
@ -1,26 +1,26 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { ChatSession } from "../../../lib/types";
|
||||
import Link from "next/link";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { formatCategory } from "@/lib/format-enums";
|
||||
import {
|
||||
MessageSquare,
|
||||
Search,
|
||||
Filter,
|
||||
ChevronDown,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Clock,
|
||||
Globe,
|
||||
Eye,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
Clock,
|
||||
Eye,
|
||||
Filter,
|
||||
Globe,
|
||||
MessageSquare,
|
||||
Search,
|
||||
} from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { formatCategory } from "@/lib/format-enums";
|
||||
import type { ChatSession } from "../../../lib/types";
|
||||
|
||||
// Placeholder for a SessionListItem component to be created later
|
||||
// For now, we'll display some basic info directly.
|
||||
@ -59,7 +59,7 @@ export default function SessionsPage() {
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
|
||||
const [pageSize, setPageSize] = useState(10); // Or make this configurable
|
||||
const [pageSize, _setPageSize] = useState(10); // Or make this configurable
|
||||
|
||||
// UI states
|
||||
const [filtersExpanded, setFiltersExpanded] = useState(false);
|
||||
@ -404,7 +404,7 @@ export default function SessionsPage() {
|
||||
|
||||
{/* Sessions List */}
|
||||
{!loading && !error && sessions.length > 0 && (
|
||||
<ul role="list" aria-label="Chat sessions" className="grid gap-4">
|
||||
<ul aria-label="Chat sessions" className="grid gap-4">
|
||||
{sessions.map((session) => (
|
||||
<li key={session.id}>
|
||||
<Card className="hover:shadow-md transition-shadow">
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
import type { Session } from "next-auth";
|
||||
import { useState } from "react";
|
||||
import { Company } from "../../lib/types";
|
||||
import { Session } from "next-auth";
|
||||
import type { Company } from "../../lib/types";
|
||||
|
||||
interface DashboardSettingsProps {
|
||||
company: Company;
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useState, useEffect } from "react";
|
||||
import { UserSession } from "../../lib/types";
|
||||
import { useEffect, useState } from "react";
|
||||
import type { UserSession } from "../../lib/types";
|
||||
|
||||
interface UserItem {
|
||||
id: string;
|
||||
@ -56,6 +56,7 @@ export default function UserManagement({ session }: UserManagementProps) {
|
||||
<option value="AUDITOR">Auditor</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
className="bg-blue-600 text-white rounded px-4 py-2 sm:py-0 w-full sm:w-auto"
|
||||
onClick={inviteUser}
|
||||
>
|
||||
|
||||
@ -1,13 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { AlertCircle, Eye, Shield, UserPlus, Users } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { useCallback, useEffect, useId, useState } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
@ -16,14 +24,6 @@ import {
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Users, UserPlus, Shield, Eye, AlertCircle } from "lucide-react";
|
||||
|
||||
interface UserItem {
|
||||
id: string;
|
||||
@ -38,20 +38,9 @@ export default function UserManagementPage() {
|
||||
const [role, setRole] = useState<string>("USER");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const [loading, setLoading] = useState(true);
|
||||
const emailId = useId();
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "authenticated") {
|
||||
if (session?.user?.role === "ADMIN") {
|
||||
fetchUsers();
|
||||
} else {
|
||||
setLoading(false); // Stop loading for non-admin users
|
||||
}
|
||||
} else if (status === "unauthenticated") {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [status, session?.user?.role]);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
const fetchUsers = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await fetch("/api/dashboard/users");
|
||||
@ -63,7 +52,19 @@ export default function UserManagementPage() {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "authenticated") {
|
||||
if (session?.user?.role === "ADMIN") {
|
||||
fetchUsers();
|
||||
} else {
|
||||
setLoading(false); // Stop loading for non-admin users
|
||||
}
|
||||
} else if (status === "unauthenticated") {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [status, session?.user?.role, fetchUsers]);
|
||||
|
||||
async function inviteUser() {
|
||||
setMessage("");
|
||||
@ -163,12 +164,11 @@ export default function UserManagementPage() {
|
||||
}}
|
||||
autoComplete="off"
|
||||
data-testid="invite-form"
|
||||
role="form"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Label htmlFor={emailId}>Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
id={emailId}
|
||||
type="email"
|
||||
placeholder="user@example.com"
|
||||
value={email}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
// Main app layout with basic global style
|
||||
import "./globals.css";
|
||||
import { ReactNode } from "react";
|
||||
import { Providers } from "./providers";
|
||||
import type { ReactNode } from "react";
|
||||
import { Toaster } from "@/components/ui/sonner";
|
||||
import { Providers } from "./providers";
|
||||
|
||||
export const metadata = {
|
||||
title: "LiveDash - AI-Powered Customer Conversation Analytics",
|
||||
@ -10,7 +10,7 @@ export const metadata = {
|
||||
"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",
|
||||
@ -21,7 +21,7 @@ export const metadata = {
|
||||
"AI customer intelligence",
|
||||
"automated categorization",
|
||||
"real-time analytics",
|
||||
"customer conversation dashboard"
|
||||
"customer conversation dashboard",
|
||||
],
|
||||
authors: [{ name: "Notso AI" }],
|
||||
creator: "Notso AI",
|
||||
@ -31,33 +31,37 @@ export const metadata = {
|
||||
address: false,
|
||||
telephone: false,
|
||||
},
|
||||
metadataBase: new URL(process.env.NEXTAUTH_URL || 'https://livedash.notso.ai'),
|
||||
metadataBase: new URL(
|
||||
process.env.NEXTAUTH_URL || "https://livedash.notso.ai"
|
||||
),
|
||||
alternates: {
|
||||
canonical: '/',
|
||||
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. Turn every conversation into competitive intelligence.",
|
||||
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',
|
||||
locale: "en_US",
|
||||
images: [
|
||||
{
|
||||
url: '/og-image.png',
|
||||
url: "/og-image.png",
|
||||
width: 1200,
|
||||
height: 630,
|
||||
alt: 'LiveDash - AI-Powered Customer Conversation Analytics Platform',
|
||||
}
|
||||
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.",
|
||||
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'],
|
||||
images: ["/og-image.png"],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
@ -65,9 +69,9 @@ export const metadata = {
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
'max-video-preview': -1,
|
||||
'max-image-preview': 'large',
|
||||
'max-snippet': -1,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
icons: {
|
||||
@ -79,41 +83,42 @@ export const metadata = {
|
||||
},
|
||||
manifest: "/manifest.json",
|
||||
other: {
|
||||
'msapplication-TileColor': '#2563eb',
|
||||
'theme-color': '#ffffff',
|
||||
"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',
|
||||
"@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',
|
||||
"@type": "Organization",
|
||||
name: "Notso AI",
|
||||
},
|
||||
applicationCategory: 'Business Analytics Software',
|
||||
operatingSystem: 'Web Browser',
|
||||
applicationCategory: "Business Analytics Software",
|
||||
operatingSystem: "Web Browser",
|
||||
offers: {
|
||||
'@type': 'Offer',
|
||||
category: 'SaaS',
|
||||
"@type": "Offer",
|
||||
category: "SaaS",
|
||||
},
|
||||
aggregateRating: {
|
||||
'@type': 'AggregateRating',
|
||||
ratingValue: '4.8',
|
||||
ratingCount: '150',
|
||||
"@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'
|
||||
]
|
||||
"AI-powered sentiment analysis",
|
||||
"Automated conversation categorization",
|
||||
"Real-time analytics dashboard",
|
||||
"Multi-language support",
|
||||
"Custom AI model integration",
|
||||
"Enterprise-grade security",
|
||||
],
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@ -1,9 +1,13 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import { BarChart3, Loader2, Shield, Zap } from "lucide-react";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useId, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@ -11,15 +15,16 @@ import {
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
||||
import { Loader2, Shield, BarChart3, Zap } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
export default function LoginPage() {
|
||||
const emailId = useId();
|
||||
const emailHelpId = useId();
|
||||
const passwordId = useId();
|
||||
const passwordHelpId = useId();
|
||||
const loadingStatusId = useId();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [error, setError] = useState("");
|
||||
@ -157,38 +162,38 @@ export default function LoginPage() {
|
||||
|
||||
<form onSubmit={handleLogin} className="space-y-4" noValidate>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Label htmlFor={emailId}>Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
id={emailId}
|
||||
type="email"
|
||||
placeholder="name@company.com"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
disabled={isLoading}
|
||||
required
|
||||
aria-describedby="email-help"
|
||||
aria-describedby={emailHelpId}
|
||||
aria-invalid={!!error}
|
||||
className="transition-all duration-200 focus:ring-2 focus:ring-primary/20"
|
||||
/>
|
||||
<div id="email-help" className="sr-only">
|
||||
<div id={emailHelpId} className="sr-only">
|
||||
Enter your company email address
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Label htmlFor={passwordId}>Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
id={passwordId}
|
||||
type="password"
|
||||
placeholder="Enter your password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
disabled={isLoading}
|
||||
required
|
||||
aria-describedby="password-help"
|
||||
aria-describedby={passwordHelpId}
|
||||
aria-invalid={!!error}
|
||||
className="transition-all duration-200 focus:ring-2 focus:ring-primary/20"
|
||||
/>
|
||||
<div id="password-help" className="sr-only">
|
||||
<div id={passwordHelpId} className="sr-only">
|
||||
Enter your account password
|
||||
</div>
|
||||
</div>
|
||||
@ -213,7 +218,7 @@ export default function LoginPage() {
|
||||
</Button>
|
||||
{isLoading && (
|
||||
<div
|
||||
id="loading-status"
|
||||
id={loadingStatusId}
|
||||
className="sr-only"
|
||||
aria-live="polite"
|
||||
>
|
||||
|
||||
179
app/page.tsx
179
app/page.tsx
@ -1,25 +1,21 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import {
|
||||
ArrowRight,
|
||||
BarChart3,
|
||||
Brain,
|
||||
Globe,
|
||||
MessageCircle,
|
||||
Shield,
|
||||
Zap,
|
||||
CheckCircle,
|
||||
Star,
|
||||
Sparkles,
|
||||
TrendingUp,
|
||||
Users,
|
||||
Globe,
|
||||
Sparkles
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
export default function LandingPage() {
|
||||
const { data: session, status } = useSession();
|
||||
@ -43,7 +39,11 @@ export default function LandingPage() {
|
||||
};
|
||||
|
||||
if (status === "loading") {
|
||||
return <div className="flex items-center justify-center min-h-screen">Loading...</div>;
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
Loading...
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
@ -93,9 +93,10 @@ export default function LandingPage() {
|
||||
</h1>
|
||||
|
||||
<p className="text-xl lg:text-2xl text-gray-600 dark:text-gray-300 mb-12 max-w-4xl mx-auto leading-relaxed">
|
||||
LiveDash analyzes your customer support conversations with advanced AI to deliver
|
||||
real-time sentiment analysis, automated categorization, and powerful analytics
|
||||
that drive better business decisions.
|
||||
LiveDash analyzes your customer support conversations with
|
||||
advanced AI to deliver real-time sentiment analysis, automated
|
||||
categorization, and powerful analytics that drive better business
|
||||
decisions.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center items-center">
|
||||
@ -129,7 +130,8 @@ export default function LandingPage() {
|
||||
Powerful Features for Modern Teams
|
||||
</h2>
|
||||
<p className="text-xl text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||
Everything you need to understand and optimize your customer interactions
|
||||
Everything you need to understand and optimize your customer
|
||||
interactions
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@ -138,16 +140,19 @@ export default function LandingPage() {
|
||||
<div className="relative">
|
||||
{/* Connection Lines */}
|
||||
<div className="absolute left-1/2 top-0 bottom-0 w-px bg-gradient-to-b from-blue-200 via-purple-200 to-transparent dark:from-blue-800 dark:via-purple-800 transform -translate-x-1/2 z-0"></div>
|
||||
|
||||
|
||||
{/* Feature Cards */}
|
||||
<div className="space-y-16 relative z-10">
|
||||
{/* AI Sentiment Analysis */}
|
||||
<div className="flex items-center gap-8 group">
|
||||
<div className="flex-1 text-right">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-lg border border-gray-100 dark:border-gray-700 group-hover:shadow-xl transition-all duration-300 group-hover:scale-105">
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">AI Sentiment Analysis</h3>
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||
AI Sentiment Analysis
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
Automatically analyze customer emotions and satisfaction levels across all conversations with 99.9% accuracy
|
||||
Automatically analyze customer emotions and satisfaction
|
||||
levels across all conversations with 99.9% accuracy
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -165,9 +170,12 @@ export default function LandingPage() {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-lg border border-gray-100 dark:border-gray-700 group-hover:shadow-xl transition-all duration-300 group-hover:scale-105">
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">Smart Categorization</h3>
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||
Smart Categorization
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
Intelligently categorize conversations by topic, urgency, and department automatically using advanced ML
|
||||
Intelligently categorize conversations by topic,
|
||||
urgency, and department automatically using advanced ML
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -177,9 +185,12 @@ export default function LandingPage() {
|
||||
<div className="flex items-center gap-8 group">
|
||||
<div className="flex-1 text-right">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-lg border border-gray-100 dark:border-gray-700 group-hover:shadow-xl transition-all duration-300 group-hover:scale-105">
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">Real-time Analytics</h3>
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||
Real-time Analytics
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
Get instant insights with beautiful dashboards and real-time performance metrics that update live
|
||||
Get instant insights with beautiful dashboards and
|
||||
real-time performance metrics that update live
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -197,9 +208,12 @@ export default function LandingPage() {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-lg border border-gray-100 dark:border-gray-700 group-hover:shadow-xl transition-all duration-300 group-hover:scale-105">
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">Enterprise Security</h3>
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||
Enterprise Security
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
Bank-grade security with GDPR compliance, SOC 2 certification, and end-to-end encryption
|
||||
Bank-grade security with GDPR compliance, SOC 2
|
||||
certification, and end-to-end encryption
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -209,9 +223,12 @@ export default function LandingPage() {
|
||||
<div className="flex items-center gap-8 group">
|
||||
<div className="flex-1 text-right">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-lg border border-gray-100 dark:border-gray-700 group-hover:shadow-xl transition-all duration-300 group-hover:scale-105">
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">Lightning Fast</h3>
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||
Lightning Fast
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
Process thousands of conversations in seconds with our optimized AI pipeline and global CDN
|
||||
Process thousands of conversations in seconds with our
|
||||
optimized AI pipeline and global CDN
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -229,9 +246,12 @@ export default function LandingPage() {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="bg-white dark:bg-gray-800 p-6 rounded-2xl shadow-lg border border-gray-100 dark:border-gray-700 group-hover:shadow-xl transition-all duration-300 group-hover:scale-105">
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">Global Scale</h3>
|
||||
<h3 className="text-2xl font-bold mb-3 text-gray-900 dark:text-white">
|
||||
Global Scale
|
||||
</h3>
|
||||
<p className="text-gray-600 dark:text-gray-300 text-lg">
|
||||
Multi-language support with global infrastructure for teams worldwide, serving 50+ countries
|
||||
Multi-language support with global infrastructure for
|
||||
teams worldwide, serving 50+ countries
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
@ -251,16 +271,26 @@ export default function LandingPage() {
|
||||
|
||||
<div className="grid md:grid-cols-3 gap-8 mb-16">
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold text-blue-600 mb-2">10,000+</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">Conversations Analyzed Daily</div>
|
||||
<div className="text-4xl font-bold text-blue-600 mb-2">
|
||||
10,000+
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">
|
||||
Conversations Analyzed Daily
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold text-purple-600 mb-2">99.9%</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">Accuracy Rate</div>
|
||||
<div className="text-4xl font-bold text-purple-600 mb-2">
|
||||
99.9%
|
||||
</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">
|
||||
Accuracy Rate
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="text-4xl font-bold text-green-600 mb-2">50+</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">Enterprise Customers</div>
|
||||
<div className="text-gray-600 dark:text-gray-300">
|
||||
Enterprise Customers
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -270,12 +300,11 @@ export default function LandingPage() {
|
||||
<section className="py-20 bg-gradient-to-r from-blue-600 to-purple-600">
|
||||
<div className="max-w-4xl mx-auto text-center px-4 sm:px-6 lg:px-8">
|
||||
<h2 className="text-4xl lg:text-5xl font-bold text-white mb-6">
|
||||
Ready to Transform Your
|
||||
Customer Insights?
|
||||
Ready to Transform Your Customer Insights?
|
||||
</h2>
|
||||
<p className="text-xl text-blue-100 mb-8 max-w-2xl mx-auto">
|
||||
Join thousands of teams already using LiveDash to make data-driven decisions
|
||||
and improve customer satisfaction.
|
||||
Join thousands of teams already using LiveDash to make data-driven
|
||||
decisions and improve customer satisfaction.
|
||||
</p>
|
||||
<div className="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<Button
|
||||
@ -318,30 +347,78 @@ export default function LandingPage() {
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Product</h3>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><a href="#" className="hover:text-white transition-colors">Features</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">Pricing</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">API</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">Integrations</a></li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Features
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Pricing
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
API
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Integrations
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Company</h3>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><a href="#" className="hover:text-white transition-colors">About</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">Blog</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">Careers</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">Contact</a></li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
About
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Blog
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Careers
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Contact
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="font-semibold mb-4">Support</h3>
|
||||
<ul className="space-y-2 text-gray-400">
|
||||
<li><a href="#" className="hover:text-white transition-colors">Documentation</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">Help Center</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">Privacy</a></li>
|
||||
<li><a href="#" className="hover:text-white transition-colors">Terms</a></li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Documentation
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Help Center
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Privacy
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="#" className="hover:text-white transition-colors">
|
||||
Terms
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Activity,
|
||||
ArrowLeft,
|
||||
Calendar,
|
||||
Database,
|
||||
Mail,
|
||||
Save,
|
||||
UserPlus,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { useRouter, useParams } 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@ -22,20 +24,19 @@ import {
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import {
|
||||
Building2,
|
||||
Users,
|
||||
Database,
|
||||
Settings,
|
||||
ArrowLeft,
|
||||
Save,
|
||||
Trash2,
|
||||
UserPlus,
|
||||
Mail,
|
||||
Shield,
|
||||
Activity,
|
||||
Calendar
|
||||
} from "lucide-react";
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
|
||||
interface User {
|
||||
@ -68,60 +69,73 @@ export default function CompanyManagement() {
|
||||
const router = useRouter();
|
||||
const params = useParams();
|
||||
const { toast } = useToast();
|
||||
|
||||
|
||||
const [company, setCompany] = useState<Company | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [editData, setEditData] = useState<Partial<Company>>({});
|
||||
const [originalData, setOriginalData] = useState<Partial<Company>>({});
|
||||
const [showInviteUser, setShowInviteUser] = useState(false);
|
||||
const [inviteData, setInviteData] = useState({ name: "", email: "", role: "USER" });
|
||||
const [showUnsavedChangesDialog, setShowUnsavedChangesDialog] = useState(false);
|
||||
const [pendingNavigation, setPendingNavigation] = useState<string | null>(null);
|
||||
const [inviteData, setInviteData] = useState({
|
||||
name: "",
|
||||
email: "",
|
||||
role: "USER",
|
||||
});
|
||||
const [showUnsavedChangesDialog, setShowUnsavedChangesDialog] =
|
||||
useState(false);
|
||||
const [pendingNavigation, setPendingNavigation] = useState<string | null>(
|
||||
null
|
||||
);
|
||||
|
||||
// Function to check if data has been modified
|
||||
const hasUnsavedChanges = useCallback(() => {
|
||||
// Normalize data for comparison (handle null/undefined/empty string equivalence)
|
||||
const normalizeValue = (value: any) => {
|
||||
const normalizeValue = (value: string | number | null | undefined) => {
|
||||
if (value === null || value === undefined || value === "") {
|
||||
return "";
|
||||
}
|
||||
return value;
|
||||
};
|
||||
|
||||
|
||||
const normalizedEditData = {
|
||||
name: normalizeValue(editData.name),
|
||||
email: normalizeValue(editData.email),
|
||||
status: normalizeValue(editData.status),
|
||||
maxUsers: editData.maxUsers || 0,
|
||||
};
|
||||
|
||||
|
||||
const normalizedOriginalData = {
|
||||
name: normalizeValue(originalData.name),
|
||||
email: normalizeValue(originalData.email),
|
||||
status: normalizeValue(originalData.status),
|
||||
maxUsers: originalData.maxUsers || 0,
|
||||
};
|
||||
|
||||
return JSON.stringify(normalizedEditData) !== JSON.stringify(normalizedOriginalData);
|
||||
|
||||
return (
|
||||
JSON.stringify(normalizedEditData) !==
|
||||
JSON.stringify(normalizedOriginalData)
|
||||
);
|
||||
}, [editData, originalData]);
|
||||
|
||||
// Handle navigation protection - must be at top level
|
||||
const handleNavigation = useCallback((url: string) => {
|
||||
// Allow navigation within the same company (different tabs, etc.)
|
||||
if (url.includes(`/platform/companies/${params.id}`)) {
|
||||
router.push(url);
|
||||
return;
|
||||
}
|
||||
const handleNavigation = useCallback(
|
||||
(url: string) => {
|
||||
// Allow navigation within the same company (different tabs, etc.)
|
||||
if (url.includes(`/platform/companies/${params.id}`)) {
|
||||
router.push(url);
|
||||
return;
|
||||
}
|
||||
|
||||
// If there are unsaved changes, show confirmation dialog
|
||||
if (hasUnsavedChanges()) {
|
||||
setPendingNavigation(url);
|
||||
setShowUnsavedChangesDialog(true);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
}, [router, params.id, hasUnsavedChanges]);
|
||||
// If there are unsaved changes, show confirmation dialog
|
||||
if (hasUnsavedChanges()) {
|
||||
setPendingNavigation(url);
|
||||
setShowUnsavedChangesDialog(true);
|
||||
} else {
|
||||
router.push(url);
|
||||
}
|
||||
},
|
||||
[router, params.id, hasUnsavedChanges]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (status === "loading") return;
|
||||
@ -132,7 +146,7 @@ export default function CompanyManagement() {
|
||||
}
|
||||
|
||||
fetchCompany();
|
||||
}, [session, status, router, params.id]);
|
||||
}, [session, status, router, fetchCompany]);
|
||||
|
||||
const fetchCompany = async () => {
|
||||
try {
|
||||
@ -193,7 +207,7 @@ export default function CompanyManagement() {
|
||||
} else {
|
||||
throw new Error("Failed to update company");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to update company",
|
||||
@ -206,7 +220,7 @@ export default function CompanyManagement() {
|
||||
|
||||
const handleStatusChange = async (newStatus: string) => {
|
||||
const statusAction = newStatus === "SUSPENDED" ? "suspend" : "activate";
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/platform/companies/${params.id}`, {
|
||||
method: "PATCH",
|
||||
@ -215,8 +229,8 @@ export default function CompanyManagement() {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
setCompany(prev => prev ? { ...prev, status: newStatus } : null);
|
||||
setEditData(prev => ({ ...prev, status: newStatus }));
|
||||
setCompany((prev) => (prev ? { ...prev, status: newStatus } : null));
|
||||
setEditData((prev) => ({ ...prev, status: newStatus }));
|
||||
toast({
|
||||
title: "Success",
|
||||
description: `Company ${statusAction}d successfully`,
|
||||
@ -224,7 +238,7 @@ export default function CompanyManagement() {
|
||||
} else {
|
||||
throw new Error(`Failed to ${statusAction} company`);
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: `Failed to ${statusAction} company`,
|
||||
@ -251,39 +265,42 @@ export default function CompanyManagement() {
|
||||
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
|
||||
if (hasUnsavedChanges()) {
|
||||
e.preventDefault();
|
||||
e.returnValue = '';
|
||||
e.returnValue = "";
|
||||
}
|
||||
};
|
||||
|
||||
const handlePopState = (e: PopStateEvent) => {
|
||||
if (hasUnsavedChanges()) {
|
||||
const confirmLeave = window.confirm(
|
||||
'You have unsaved changes. Are you sure you want to leave this page?'
|
||||
"You have unsaved changes. Are you sure you want to leave this page?"
|
||||
);
|
||||
if (!confirmLeave) {
|
||||
// Push the current state back to prevent navigation
|
||||
window.history.pushState(null, '', window.location.href);
|
||||
window.history.pushState(null, "", window.location.href);
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', handleBeforeUnload);
|
||||
window.addEventListener('popstate', handlePopState);
|
||||
window.addEventListener("beforeunload", handleBeforeUnload);
|
||||
window.addEventListener("popstate", handlePopState);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||
window.removeEventListener('popstate', handlePopState);
|
||||
window.removeEventListener("beforeunload", handleBeforeUnload);
|
||||
window.removeEventListener("popstate", handlePopState);
|
||||
};
|
||||
}, [hasUnsavedChanges]);
|
||||
|
||||
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),
|
||||
});
|
||||
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);
|
||||
@ -296,7 +313,7 @@ export default function CompanyManagement() {
|
||||
} else {
|
||||
throw new Error("Failed to invite user");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Failed to invite user",
|
||||
@ -307,11 +324,16 @@ export default function CompanyManagement() {
|
||||
|
||||
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";
|
||||
case "ACTIVE":
|
||||
return "default";
|
||||
case "TRIAL":
|
||||
return "secondary";
|
||||
case "SUSPENDED":
|
||||
return "destructive";
|
||||
case "ARCHIVED":
|
||||
return "outline";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
@ -335,9 +357,9 @@ export default function CompanyManagement() {
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex justify-between items-center py-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleNavigation("/platform/dashboard")}
|
||||
>
|
||||
<ArrowLeft className="w-4 h-4 mr-2" />
|
||||
@ -387,11 +409,15 @@ export default function CompanyManagement() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Users</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Users
|
||||
</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{company.users.length}</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{company.users.length}
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
of {company.maxUsers} maximum
|
||||
</p>
|
||||
@ -400,21 +426,29 @@ export default function CompanyManagement() {
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Sessions</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Sessions
|
||||
</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{company._count.sessions}</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{company._count.sessions}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Data Imports</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Data Imports
|
||||
</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{company._count.imports}</div>
|
||||
<div className="text-2xl font-bold">
|
||||
{company._count.imports}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -443,7 +477,12 @@ export default function CompanyManagement() {
|
||||
<Input
|
||||
id="name"
|
||||
value={editData.name || ""}
|
||||
onChange={(e) => setEditData(prev => ({ ...prev, name: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</div>
|
||||
@ -453,7 +492,12 @@ export default function CompanyManagement() {
|
||||
id="email"
|
||||
type="email"
|
||||
value={editData.email || ""}
|
||||
onChange={(e) => setEditData(prev => ({ ...prev, email: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
email: e.target.value,
|
||||
}))
|
||||
}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</div>
|
||||
@ -463,7 +507,12 @@ export default function CompanyManagement() {
|
||||
id="maxUsers"
|
||||
type="number"
|
||||
value={editData.maxUsers || 0}
|
||||
onChange={(e) => setEditData(prev => ({ ...prev, maxUsers: parseInt(e.target.value) }))}
|
||||
onChange={(e) =>
|
||||
setEditData((prev) => ({
|
||||
...prev,
|
||||
maxUsers: parseInt(e.target.value),
|
||||
}))
|
||||
}
|
||||
disabled={!canEdit}
|
||||
/>
|
||||
</div>
|
||||
@ -471,7 +520,9 @@ export default function CompanyManagement() {
|
||||
<Label htmlFor="status">Status</Label>
|
||||
<Select
|
||||
value={editData.status}
|
||||
onValueChange={(value) => setEditData(prev => ({ ...prev, status: value }))}
|
||||
onValueChange={(value) =>
|
||||
setEditData((prev) => ({ ...prev, status: value }))
|
||||
}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
<SelectTrigger>
|
||||
@ -496,10 +547,7 @@ export default function CompanyManagement() {
|
||||
>
|
||||
Cancel Changes
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
>
|
||||
<Button onClick={handleSave} disabled={isSaving}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{isSaving ? "Saving..." : "Save Changes"}
|
||||
</Button>
|
||||
@ -535,12 +583,17 @@ export default function CompanyManagement() {
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-10 h-10 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center">
|
||||
<span className="text-sm font-medium text-blue-600 dark:text-blue-300">
|
||||
{user.name?.charAt(0) || user.email.charAt(0).toUpperCase()}
|
||||
{user.name?.charAt(0) ||
|
||||
user.email.charAt(0).toUpperCase()}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium">{user.name || "No name"}</div>
|
||||
<div className="text-sm text-muted-foreground">{user.email}</div>
|
||||
<div className="font-medium">
|
||||
{user.name || "No name"}
|
||||
</div>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
@ -564,7 +617,9 @@ export default function CompanyManagement() {
|
||||
<TabsContent value="settings" className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="text-red-600 dark:text-red-400">Danger Zone</CardTitle>
|
||||
<CardTitle className="text-red-600 dark:text-red-400">
|
||||
Danger Zone
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{canEdit && (
|
||||
@ -578,20 +633,28 @@ export default function CompanyManagement() {
|
||||
</div>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="destructive" disabled={company.status === "SUSPENDED"}>
|
||||
{company.status === "SUSPENDED" ? "Already Suspended" : "Suspend"}
|
||||
<Button
|
||||
variant="destructive"
|
||||
disabled={company.status === "SUSPENDED"}
|
||||
>
|
||||
{company.status === "SUSPENDED"
|
||||
? "Already Suspended"
|
||||
: "Suspend"}
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Suspend Company</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
Are you sure you want to suspend this company? This will disable access for all users.
|
||||
Are you sure you want to suspend this company?
|
||||
This will disable access for all users.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction onClick={() => handleStatusChange("SUSPENDED")}>
|
||||
<AlertDialogAction
|
||||
onClick={() => handleStatusChange("SUSPENDED")}
|
||||
>
|
||||
Suspend
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
@ -607,7 +670,10 @@ export default function CompanyManagement() {
|
||||
Restore access to this company
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="default" onClick={() => handleStatusChange("ACTIVE")}>
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={() => handleStatusChange("ACTIVE")}
|
||||
>
|
||||
Reactivate
|
||||
</Button>
|
||||
</div>
|
||||
@ -646,7 +712,9 @@ export default function CompanyManagement() {
|
||||
<Input
|
||||
id="inviteName"
|
||||
value={inviteData.name}
|
||||
onChange={(e) => setInviteData(prev => ({ ...prev, name: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setInviteData((prev) => ({ ...prev, name: e.target.value }))
|
||||
}
|
||||
placeholder="User's full name"
|
||||
/>
|
||||
</div>
|
||||
@ -656,7 +724,12 @@ export default function CompanyManagement() {
|
||||
id="inviteEmail"
|
||||
type="email"
|
||||
value={inviteData.email}
|
||||
onChange={(e) => setInviteData(prev => ({ ...prev, email: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setInviteData((prev) => ({
|
||||
...prev,
|
||||
email: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="user@example.com"
|
||||
/>
|
||||
</div>
|
||||
@ -664,7 +737,9 @@ export default function CompanyManagement() {
|
||||
<Label htmlFor="inviteRole">Role</Label>
|
||||
<Select
|
||||
value={inviteData.role}
|
||||
onValueChange={(value) => setInviteData(prev => ({ ...prev, role: value }))}
|
||||
onValueChange={(value) =>
|
||||
setInviteData((prev) => ({ ...prev, role: value }))
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
@ -698,12 +773,16 @@ export default function CompanyManagement() {
|
||||
)}
|
||||
|
||||
{/* Unsaved Changes Dialog */}
|
||||
<AlertDialog open={showUnsavedChangesDialog} onOpenChange={setShowUnsavedChangesDialog}>
|
||||
<AlertDialog
|
||||
open={showUnsavedChangesDialog}
|
||||
onOpenChange={setShowUnsavedChangesDialog}
|
||||
>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Unsaved Changes</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
You have unsaved changes that will be lost if you leave this page. Are you sure you want to continue?
|
||||
You have unsaved changes that will be lost if you leave this page.
|
||||
Are you sure you want to continue?
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
@ -718,4 +797,4 @@ export default function CompanyManagement() {
|
||||
</AlertDialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,12 +1,22 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
Activity,
|
||||
BarChart3,
|
||||
Building2,
|
||||
Check,
|
||||
Copy,
|
||||
Database,
|
||||
Plus,
|
||||
Search,
|
||||
Settings,
|
||||
Users,
|
||||
} from "lucide-react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -16,19 +26,10 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Building2,
|
||||
Users,
|
||||
Database,
|
||||
Activity,
|
||||
Plus,
|
||||
Settings,
|
||||
BarChart3,
|
||||
Search
|
||||
} from "lucide-react";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
||||
import { useToast } from "@/hooks/use-toast";
|
||||
import { Copy, Check } from "lucide-react";
|
||||
|
||||
interface Company {
|
||||
id: string;
|
||||
@ -50,17 +51,29 @@ interface DashboardData {
|
||||
};
|
||||
}
|
||||
|
||||
interface PlatformSession {
|
||||
user: {
|
||||
id: string;
|
||||
email: string;
|
||||
name?: string;
|
||||
isPlatformUser: boolean;
|
||||
platformRole: string;
|
||||
};
|
||||
}
|
||||
|
||||
// Custom hook for platform session
|
||||
function usePlatformSession() {
|
||||
const [session, setSession] = useState<any>(null);
|
||||
const [status, setStatus] = useState<"loading" | "authenticated" | "unauthenticated">("loading");
|
||||
const [session, setSession] = useState<PlatformSession | null>(null);
|
||||
const [status, setStatus] = useState<
|
||||
"loading" | "authenticated" | "unauthenticated"
|
||||
>("loading");
|
||||
|
||||
useEffect(() => {
|
||||
const fetchSession = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/platform/auth/session");
|
||||
const sessionData = await response.json();
|
||||
|
||||
|
||||
if (sessionData?.user?.isPlatformUser) {
|
||||
setSession(sessionData);
|
||||
setStatus("authenticated");
|
||||
@ -85,7 +98,9 @@ export default function PlatformDashboard() {
|
||||
const { data: session, status } = usePlatformSession();
|
||||
const router = useRouter();
|
||||
const { toast } = useToast();
|
||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(null);
|
||||
const [dashboardData, setDashboardData] = useState<DashboardData | null>(
|
||||
null
|
||||
);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [showAddCompany, setShowAddCompany] = useState(false);
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
@ -112,12 +127,12 @@ export default function PlatformDashboard() {
|
||||
}
|
||||
|
||||
fetchDashboardData();
|
||||
}, [session, status, router]);
|
||||
}, [session, status, router, fetchDashboardData]);
|
||||
|
||||
const copyToClipboard = async (text: string, type: 'email' | 'password') => {
|
||||
const copyToClipboard = async (text: string, type: "email" | "password") => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
if (type === 'email') {
|
||||
if (type === "email") {
|
||||
setCopiedEmail(true);
|
||||
setTimeout(() => setCopiedEmail(false), 2000);
|
||||
} else {
|
||||
@ -125,14 +140,14 @@ export default function PlatformDashboard() {
|
||||
setTimeout(() => setCopiedPassword(false), 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
console.error("Failed to copy: ", err);
|
||||
}
|
||||
};
|
||||
|
||||
const getFilteredCompanies = () => {
|
||||
if (!dashboardData?.companies) return [];
|
||||
|
||||
return dashboardData.companies.filter(company =>
|
||||
|
||||
return dashboardData.companies.filter((company) =>
|
||||
company.name.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
};
|
||||
@ -152,7 +167,12 @@ export default function PlatformDashboard() {
|
||||
};
|
||||
|
||||
const handleCreateCompany = async () => {
|
||||
if (!newCompanyData.name || !newCompanyData.csvUrl || !newCompanyData.adminEmail || !newCompanyData.adminName) {
|
||||
if (
|
||||
!newCompanyData.name ||
|
||||
!newCompanyData.csvUrl ||
|
||||
!newCompanyData.adminEmail ||
|
||||
!newCompanyData.adminName
|
||||
) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: "Please fill in all required fields",
|
||||
@ -172,7 +192,7 @@ export default function PlatformDashboard() {
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
setShowAddCompany(false);
|
||||
|
||||
|
||||
const companyName = newCompanyData.name;
|
||||
setNewCompanyData({
|
||||
name: "",
|
||||
@ -184,43 +204,65 @@ export default function PlatformDashboard() {
|
||||
adminPassword: "",
|
||||
maxUsers: 10,
|
||||
});
|
||||
|
||||
|
||||
fetchDashboardData(); // Refresh the list
|
||||
|
||||
|
||||
// Show success message with copyable credentials
|
||||
if (result.generatedPassword) {
|
||||
toast({
|
||||
title: "Company Created Successfully!",
|
||||
description: (
|
||||
<div className="space-y-3">
|
||||
<p className="font-medium">Company "{companyName}" has been created.</p>
|
||||
<p className="font-medium">
|
||||
Company "{companyName}" has been created.
|
||||
</p>
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center justify-between bg-muted p-2 rounded">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-muted-foreground">Admin Email:</p>
|
||||
<p className="font-mono text-sm">{result.adminUser.email}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Admin Email:
|
||||
</p>
|
||||
<p className="font-mono text-sm">
|
||||
{result.adminUser.email}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(result.adminUser.email, 'email')}
|
||||
onClick={() =>
|
||||
copyToClipboard(result.adminUser.email, "email")
|
||||
}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{copiedEmail ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
||||
{copiedEmail ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex items-center justify-between bg-muted p-2 rounded">
|
||||
<div className="flex-1">
|
||||
<p className="text-xs text-muted-foreground">Admin Password:</p>
|
||||
<p className="font-mono text-sm">{result.generatedPassword}</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Admin Password:
|
||||
</p>
|
||||
<p className="font-mono text-sm">
|
||||
{result.generatedPassword}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
onClick={() => copyToClipboard(result.generatedPassword, 'password')}
|
||||
onClick={() =>
|
||||
copyToClipboard(result.generatedPassword, "password")
|
||||
}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{copiedPassword ? <Check className="h-3 w-3" /> : <Copy className="h-3 w-3" />}
|
||||
{copiedPassword ? (
|
||||
<Check className="h-3 w-3" />
|
||||
) : (
|
||||
<Copy className="h-3 w-3" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@ -241,7 +283,8 @@ export default function PlatformDashboard() {
|
||||
} catch (error) {
|
||||
toast({
|
||||
title: "Error",
|
||||
description: error instanceof Error ? error.message : "Failed to create company",
|
||||
description:
|
||||
error instanceof Error ? error.message : "Failed to create company",
|
||||
variant: "destructive",
|
||||
});
|
||||
} finally {
|
||||
@ -251,11 +294,16 @@ export default function PlatformDashboard() {
|
||||
|
||||
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";
|
||||
case "ACTIVE":
|
||||
return "default";
|
||||
case "TRIAL":
|
||||
return "secondary";
|
||||
case "SUSPENDED":
|
||||
return "destructive";
|
||||
case "ARCHIVED":
|
||||
return "outline";
|
||||
default:
|
||||
return "default";
|
||||
}
|
||||
};
|
||||
|
||||
@ -273,8 +321,16 @@ export default function PlatformDashboard() {
|
||||
|
||||
const filteredCompanies = getFilteredCompanies();
|
||||
const totalCompanies = dashboardData?.pagination?.total || 0;
|
||||
const totalUsers = dashboardData?.companies?.reduce((sum, company) => sum + company._count.users, 0) || 0;
|
||||
const totalSessions = dashboardData?.companies?.reduce((sum, company) => sum + company._count.sessions, 0) || 0;
|
||||
const totalUsers =
|
||||
dashboardData?.companies?.reduce(
|
||||
(sum, company) => sum + company._count.users,
|
||||
0
|
||||
) || 0;
|
||||
const totalSessions =
|
||||
dashboardData?.companies?.reduce(
|
||||
(sum, company) => sum + company._count.sessions,
|
||||
0
|
||||
) || 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
@ -291,7 +347,7 @@ export default function PlatformDashboard() {
|
||||
</div>
|
||||
<div className="flex gap-4 items-center">
|
||||
<ThemeToggle />
|
||||
|
||||
|
||||
{/* Search Filter */}
|
||||
<div className="relative">
|
||||
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400 w-4 h-4" />
|
||||
@ -316,7 +372,9 @@ export default function PlatformDashboard() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Companies</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Companies
|
||||
</CardTitle>
|
||||
<Building2 className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -336,7 +394,9 @@ export default function PlatformDashboard() {
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Sessions</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Total Sessions
|
||||
</CardTitle>
|
||||
<Database className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@ -346,12 +406,15 @@ export default function PlatformDashboard() {
|
||||
|
||||
<Card>
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Companies</CardTitle>
|
||||
<CardTitle className="text-sm font-medium">
|
||||
Active Companies
|
||||
</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">
|
||||
{dashboardData?.companies?.filter(c => c.status === "ACTIVE").length || 0}
|
||||
{dashboardData?.companies?.filter((c) => c.status === "ACTIVE")
|
||||
.length || 0}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@ -396,7 +459,12 @@ export default function PlatformDashboard() {
|
||||
<Input
|
||||
id="companyName"
|
||||
value={newCompanyData.name}
|
||||
onChange={(e) => setNewCompanyData(prev => ({ ...prev, name: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewCompanyData((prev) => ({
|
||||
...prev,
|
||||
name: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Acme Corporation"
|
||||
/>
|
||||
</div>
|
||||
@ -405,7 +473,12 @@ export default function PlatformDashboard() {
|
||||
<Input
|
||||
id="csvUrl"
|
||||
value={newCompanyData.csvUrl}
|
||||
onChange={(e) => setNewCompanyData(prev => ({ ...prev, csvUrl: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewCompanyData((prev) => ({
|
||||
...prev,
|
||||
csvUrl: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="https://api.company.com/sessions.csv"
|
||||
/>
|
||||
</div>
|
||||
@ -414,7 +487,12 @@ export default function PlatformDashboard() {
|
||||
<Input
|
||||
id="csvUsername"
|
||||
value={newCompanyData.csvUsername}
|
||||
onChange={(e) => setNewCompanyData(prev => ({ ...prev, csvUsername: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewCompanyData((prev) => ({
|
||||
...prev,
|
||||
csvUsername: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Optional HTTP auth username"
|
||||
/>
|
||||
</div>
|
||||
@ -424,7 +502,12 @@ export default function PlatformDashboard() {
|
||||
id="csvPassword"
|
||||
type="password"
|
||||
value={newCompanyData.csvPassword}
|
||||
onChange={(e) => setNewCompanyData(prev => ({ ...prev, csvPassword: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewCompanyData((prev) => ({
|
||||
...prev,
|
||||
csvPassword: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Optional HTTP auth password"
|
||||
/>
|
||||
</div>
|
||||
@ -433,7 +516,12 @@ export default function PlatformDashboard() {
|
||||
<Input
|
||||
id="adminName"
|
||||
value={newCompanyData.adminName}
|
||||
onChange={(e) => setNewCompanyData(prev => ({ ...prev, adminName: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewCompanyData((prev) => ({
|
||||
...prev,
|
||||
adminName: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="John Doe"
|
||||
/>
|
||||
</div>
|
||||
@ -443,7 +531,12 @@ export default function PlatformDashboard() {
|
||||
id="adminEmail"
|
||||
type="email"
|
||||
value={newCompanyData.adminEmail}
|
||||
onChange={(e) => setNewCompanyData(prev => ({ ...prev, adminEmail: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewCompanyData((prev) => ({
|
||||
...prev,
|
||||
adminEmail: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="admin@acme.com"
|
||||
/>
|
||||
</div>
|
||||
@ -453,7 +546,12 @@ export default function PlatformDashboard() {
|
||||
id="adminPassword"
|
||||
type="password"
|
||||
value={newCompanyData.adminPassword}
|
||||
onChange={(e) => setNewCompanyData(prev => ({ ...prev, adminPassword: e.target.value }))}
|
||||
onChange={(e) =>
|
||||
setNewCompanyData((prev) => ({
|
||||
...prev,
|
||||
adminPassword: e.target.value,
|
||||
}))
|
||||
}
|
||||
placeholder="Leave empty to auto-generate"
|
||||
/>
|
||||
</div>
|
||||
@ -463,17 +561,28 @@ export default function PlatformDashboard() {
|
||||
id="maxUsers"
|
||||
type="number"
|
||||
value={newCompanyData.maxUsers}
|
||||
onChange={(e) => setNewCompanyData(prev => ({ ...prev, maxUsers: parseInt(e.target.value) || 10 }))}
|
||||
onChange={(e) =>
|
||||
setNewCompanyData((prev) => ({
|
||||
...prev,
|
||||
maxUsers: parseInt(e.target.value) || 10,
|
||||
}))
|
||||
}
|
||||
min="1"
|
||||
max="1000"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowAddCompany(false)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowAddCompany(false)}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleCreateCompany} disabled={isCreating}>
|
||||
<Button
|
||||
onClick={handleCreateCompany}
|
||||
disabled={isCreating}
|
||||
>
|
||||
{isCreating ? "Creating..." : "Create Company"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@ -500,7 +609,10 @@ export default function PlatformDashboard() {
|
||||
<span>{company._count.users} users</span>
|
||||
<span>{company._count.sessions} sessions</span>
|
||||
<span>{company._count.imports} imports</span>
|
||||
<span>Created {new Date(company.createdAt).toLocaleDateString()}</span>
|
||||
<span>
|
||||
Created{" "}
|
||||
{new Date(company.createdAt).toLocaleDateString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
@ -508,10 +620,12 @@ export default function PlatformDashboard() {
|
||||
<BarChart3 className="w-4 h-4 mr-2" />
|
||||
Analytics
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => router.push(`/platform/companies/${company.id}`)}
|
||||
onClick={() =>
|
||||
router.push(`/platform/companies/${company.id}`)
|
||||
}
|
||||
>
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Manage
|
||||
@ -525,7 +639,11 @@ export default function PlatformDashboard() {
|
||||
{searchTerm ? (
|
||||
<div className="space-y-2">
|
||||
<p>No companies match "{searchTerm}".</p>
|
||||
<Button variant="link" onClick={() => setSearchTerm("")} className="text-sm">
|
||||
<Button
|
||||
variant="link"
|
||||
onClick={() => setSearchTerm("")}
|
||||
className="text-sm"
|
||||
>
|
||||
Clear search to see all companies
|
||||
</Button>
|
||||
</div>
|
||||
@ -540,4 +658,4 @@ export default function PlatformDashboard() {
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
import { Toaster } from "@/components/ui/toaster";
|
||||
|
||||
export default function PlatformLayout({
|
||||
children,
|
||||
@ -22,4 +22,4 @@ export default function PlatformLayout({
|
||||
</SessionProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { signIn, getSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useId, useState } from "react";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { ThemeToggle } from "@/components/ui/theme-toggle";
|
||||
|
||||
export default function PlatformLoginPage() {
|
||||
const emailId = useId();
|
||||
const passwordId = useId();
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
@ -36,7 +38,7 @@ export default function PlatformLoginPage() {
|
||||
// Login successful, redirect to dashboard
|
||||
router.push("/platform/dashboard");
|
||||
}
|
||||
} catch (error) {
|
||||
} catch (_error) {
|
||||
setError("An error occurred during login");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
@ -64,9 +66,9 @@ export default function PlatformLoginPage() {
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Label htmlFor={emailId}>Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
id={emailId}
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
@ -77,9 +79,9 @@ export default function PlatformLoginPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
<Label htmlFor={passwordId}>Password</Label>
|
||||
<Input
|
||||
id="password"
|
||||
id={passwordId}
|
||||
type="password"
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
@ -89,11 +91,7 @@ export default function PlatformLoginPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isLoading}
|
||||
>
|
||||
<Button type="submit" className="w-full" disabled={isLoading}>
|
||||
{isLoading ? "Signing in..." : "Sign In"}
|
||||
</Button>
|
||||
</form>
|
||||
@ -101,4 +99,4 @@ export default function PlatformLoginPage() {
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function PlatformIndexPage() {
|
||||
const router = useRouter();
|
||||
@ -14,8 +14,10 @@ export default function PlatformIndexPage() {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="text-center">
|
||||
<p className="text-muted-foreground">Redirecting to platform dashboard...</p>
|
||||
<p className="text-muted-foreground">
|
||||
Redirecting to platform dashboard...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { ReactNode } from "react";
|
||||
import type { ReactNode } from "react";
|
||||
import { ThemeProvider } from "@/components/theme-provider";
|
||||
|
||||
export function Providers({ children }: { children: ReactNode }) {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
|
||||
export default function RegisterPage() {
|
||||
const [email, setEmail] = useState<string>("");
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
import { useState, Suspense } from "react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { Suspense, useState } from "react";
|
||||
|
||||
// Component that uses useSearchParams wrapped in Suspense
|
||||
function ResetPasswordForm() {
|
||||
|
||||
Reference in New Issue
Block a user