feat: initialize project with Next.js, Prisma, and Tailwind CSS

- Add package.json with dependencies and scripts for Next.js and Prisma
- Implement API routes for session management, user authentication, and company configuration
- Create database schema for Company, User, and Session models in Prisma
- Set up authentication with NextAuth and JWT
- Add password reset functionality and user registration endpoint
- Configure Tailwind CSS and PostCSS for styling
- Implement metrics and dashboard settings API endpoints
This commit is contained in:
2025-05-21 20:44:56 +02:00
commit cdaa3ea19d
36 changed files with 7662 additions and 0 deletions

165
app/dashboard/page.tsx Normal file
View File

@ -0,0 +1,165 @@
// Main dashboard page: metrics, refresh, config
'use client';
import { useEffect, useState } from 'react';
import { signOut, useSession } from 'next-auth/react';
import { SessionsLineChart, CategoriesBarChart } from '../../components/Charts';
import DashboardSettings from './settings';
import UserManagement from './users';
interface MetricsCardProps {
label: string;
value: string | number | null | undefined;
}
function MetricsCard({ label, value }: MetricsCardProps) {
return (
<div className="bg-white rounded-xl p-4 shadow-md flex flex-col items-center">
<span className="text-2xl font-bold">{value ?? '-'}</span>
<span className="text-gray-500">{label}</span>
</div>
);
}
export default function DashboardPage() {
const { data: session } = useSession();
const [metrics, setMetrics] = useState<Record<string, unknown> | null>(null);
const [company, setCompany] = useState<Record<string, unknown> | null>(null);
// Loading state used in the fetchData function
const [, setLoading] = useState<boolean>(false);
const [csvUrl, setCsvUrl] = useState<string>('');
const [refreshing, setRefreshing] = useState<boolean>(false);
const isAdmin = session?.user?.role === 'admin';
const isAuditor = session?.user?.role === 'auditor';
useEffect(() => {
// Fetch metrics, company, and CSV URL on mount
const fetchData = async () => {
setLoading(true);
const res = await fetch('/api/dashboard/metrics');
const data = await res.json();
setMetrics(data.metrics);
setCompany(data.company);
setCsvUrl(data.csvUrl);
setLoading(false);
};
fetchData();
}, []);
async function handleRefresh() {
if (isAuditor) return; // Prevent auditors from refreshing
setRefreshing(true);
await fetch('/api/admin/refresh-sessions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ companyId: company?.id }),
});
setRefreshing(false);
window.location.reload();
}
async function handleSaveConfig() {
if (isAuditor) return; // Prevent auditors from changing config
await fetch('/api/dashboard/config', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ csvUrl }),
});
window.location.reload();
}
return (
<div>
<div className="flex items-center justify-between mb-6">
<h1 className="text-3xl font-bold">Analytics Dashboard</h1>
<button className="text-sm underline" onClick={() => signOut()}>
Log out
</button>
</div>
{/* Admin-only settings and user management */}
{company && isAdmin && (
<>
<DashboardSettings company={company} session={session} />
<UserManagement session={session} />
</>
)}
<div className="bg-white p-4 rounded-xl shadow mb-6 flex items-center gap-4">
<input
className="flex-1 px-3 py-2 rounded border"
value={csvUrl}
onChange={(e) => setCsvUrl(e.target.value)}
placeholder="CSV feed URL (with basic auth if set in backend)"
readOnly={isAuditor}
/>
{!isAuditor && (
<>
<button
className="px-4 py-2 bg-blue-600 text-white rounded"
onClick={handleSaveConfig}
>
Save Config
</button>
<button
className="px-4 py-2 bg-green-600 text-white rounded"
onClick={handleRefresh}
disabled={refreshing}
>
{refreshing ? 'Refreshing...' : 'Manual Refresh'}
</button>
</>
)}
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6 mb-10">
<MetricsCard label="Total Sessions" value={metrics?.totalSessions} />
<MetricsCard label="Escalated" value={metrics?.escalatedCount} />
<MetricsCard
label="Avg. Sentiment"
value={metrics?.avgSentiment?.toFixed(2)}
/>
<MetricsCard
label="Total Tokens (€)"
value={metrics?.totalTokensEur?.toFixed(2)}
/>
<MetricsCard
label="Below Sentiment Threshold"
value={metrics?.belowSentimentThreshold}
/>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
<div>
<h2 className="font-bold mb-2">Sessions Per Day</h2>
{(
metrics?.sessionsPerDay &&
Object.keys(metrics.sessionsPerDay).length > 0
) ?
<SessionsLineChart sessionsPerDay={metrics.sessionsPerDay} />
: <span>No data</span>}
</div>
<div>
<h2 className="font-bold mb-2">Top Categories</h2>
{metrics?.categories && Object.keys(metrics.categories).length > 0 ?
<CategoriesBarChart categories={metrics.categories} />
: <span>No data</span>}
</div>
<div>
<h2 className="font-bold mb-2">Languages</h2>
{metrics?.languages ?
Object.entries(metrics.languages).map(([lang, n]) => (
<div key={lang} className="flex justify-between">
<span>{lang}</span>
<span>{String(n)}</span>
</div>
))
: <span>No data</span>}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,82 @@
'use client';
import { useState } from 'react';
import { Company } from '../../lib/types';
import { Session } from 'next-auth';
interface DashboardSettingsProps {
company: Company;
session: Session;
}
export default function DashboardSettings({
company,
session,
}: DashboardSettingsProps) {
const [csvUrl, setCsvUrl] = useState<string>(company.csvUrl);
const [csvUsername, setCsvUsername] = useState<string>(
company.csvUsername || ''
);
const [csvPassword, setCsvPassword] = useState<string>('');
const [sentimentThreshold, setSentimentThreshold] = useState<string>(
company.sentimentAlert?.toString() || ''
);
const [message, setMessage] = useState<string>('');
async function handleSave() {
const res = await fetch('/api/dashboard/settings', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
csvUrl,
csvUsername,
csvPassword,
sentimentThreshold,
}),
});
if (res.ok) setMessage('Settings saved!');
else setMessage('Failed.');
}
if (session.user.role !== 'admin') return null;
return (
<div className="bg-white p-6 rounded-xl shadow mb-6">
<h2 className="font-bold text-lg mb-4">Company Config</h2>
<div className="grid gap-4">
<input
className="border px-3 py-2 rounded"
placeholder="CSV URL"
value={csvUrl}
onChange={(e) => setCsvUrl(e.target.value)}
/>
<input
className="border px-3 py-2 rounded"
placeholder="CSV Username"
value={csvUsername}
onChange={(e) => setCsvUsername(e.target.value)}
/>
<input
className="border px-3 py-2 rounded"
type="password"
placeholder="CSV Password"
value={csvPassword}
onChange={(e) => setCsvPassword(e.target.value)}
/>
<input
className="border px-3 py-2 rounded"
placeholder="Sentiment Alert Threshold"
type="number"
value={sentimentThreshold}
onChange={(e) => setSentimentThreshold(e.target.value)}
/>
<button
className="bg-blue-600 text-white rounded py-2"
onClick={handleSave}
>
Save Settings
</button>
<div>{message}</div>
</div>
</div>
);
}

76
app/dashboard/users.tsx Normal file
View File

@ -0,0 +1,76 @@
'use client';
import { useState, useEffect } from 'react';
import { UserSession } from '../../lib/types';
interface UserItem {
id: string;
email: string;
role: string;
}
interface UserManagementProps {
session: UserSession;
}
export default function UserManagement({ session }: UserManagementProps) {
const [users, setUsers] = useState<UserItem[]>([]);
const [email, setEmail] = useState<string>('');
const [role, setRole] = useState<string>('user');
const [msg, setMsg] = useState<string>('');
useEffect(() => {
fetch('/api/dashboard/users')
.then((r) => r.json())
.then((data) => setUsers(data.users));
}, []);
async function inviteUser() {
const res = await fetch('/api/dashboard/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, role }),
});
if (res.ok) setMsg('User invited.');
else setMsg('Failed.');
}
if (session.user.role !== 'admin') return null;
return (
<div className="bg-white p-6 rounded-xl shadow mb-6">
<h2 className="font-bold text-lg mb-4">User Management</h2>
<div className="flex gap-2 mb-3">
<input
className="border px-3 py-2 rounded"
placeholder="Email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<select
className="border px-3 py-2 rounded"
value={role}
onChange={(e) => setRole(e.target.value)}
>
<option value="user">User</option>
<option value="admin">Admin</option>
<option value="auditor">Auditor</option>
</select>
<button
className="bg-blue-600 text-white rounded px-4"
onClick={inviteUser}
>
Invite
</button>
</div>
<div>{msg}</div>
<ul className="mt-4">
{users.map((u) => (
<li key={u.id} className="flex justify-between border-b py-1">
{u.email}{' '}
<span className="text-xs bg-gray-200 px-2 rounded">{u.role}</span>
</li>
))}
</ul>
</div>
);
}