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:
2025-06-29 07:35:45 +02:00
parent 831f344361
commit 93fbb44eec
118 changed files with 1445 additions and 938 deletions

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}

View File

@ -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>
);
}
}