Improves dashboard data handling and settings

Refactors the dashboard to improve data fetching, error handling, and overall user experience.

- Prevents errors on refresh by validating company ID.
- Improves date handling from CSV by using a `safeParseDate` function to avoid "Invalid Date" errors.
- Adds a timestamp for when metrics were last updated.
- Fixes a bug where the refresh was failing silently.
- Improves settings page by wrapping form elements with form tags.
- Adds autocomplete attributes on settings page.
- Adds database files to `.gitignore`.
This commit is contained in:
2025-05-21 22:28:31 +02:00
parent 50b2fbda55
commit 52fbae23ba
8 changed files with 73 additions and 31 deletions

3
.gitignore vendored
View File

@ -224,6 +224,8 @@ next-env.d.ts
# Database files # Database files
*.db *.db
*.db-shm
*.db-wal
*.sqlite? *.sqlite?
# IDE # IDE
@ -232,6 +234,7 @@ next-env.d.ts
.idea/ .idea/
*.sublime-project *.sublime-project
*.sublime-workspace *.sublime-workspace
*-instructions.*
# logs # logs
logs logs

View File

@ -27,21 +27,21 @@ function DashboardContent() {
const [metrics, setMetrics] = useState<MetricsResult | null>(null); const [metrics, setMetrics] = useState<MetricsResult | null>(null);
const [company, setCompany] = useState<Company | null>(null); const [company, setCompany] = useState<Company | null>(null);
const [, setLoading] = useState<boolean>(false); const [, setLoading] = useState<boolean>(false);
const [csvUrl, setCsvUrl] = useState<string>(""); // Remove unused csvUrl state variable
const [refreshing, setRefreshing] = useState<boolean>(false); const [refreshing, setRefreshing] = useState<boolean>(false);
const isAdmin = session?.user?.role === "admin"; const isAdmin = session?.user?.role === "admin";
const isAuditor = session?.user?.role === "auditor"; const isAuditor = session?.user?.role === "auditor";
useEffect(() => { useEffect(() => {
// Fetch metrics, company, and CSV URL on mount // Fetch metrics and company on mount
const fetchData = async () => { const fetchData = async () => {
setLoading(true); setLoading(true);
const res = await fetch("/api/dashboard/metrics"); const res = await fetch("/api/dashboard/metrics");
const data = await res.json(); const data = await res.json();
setMetrics(data.metrics); setMetrics(data.metrics);
setCompany(data.company); setCompany(data.company);
setCsvUrl(data.csvUrl); // Removed unused csvUrl assignment
setLoading(false); setLoading(false);
}; };
fetchData(); fetchData();
@ -51,15 +51,27 @@ function DashboardContent() {
if (isAuditor) return; // Prevent auditors from refreshing if (isAuditor) return; // Prevent auditors from refreshing
try { try {
setRefreshing(true); setRefreshing(true);
// Make sure we have a company ID to send
if (!company?.id) {
console.error("Cannot refresh: Company ID is missing");
return;
}
const res = await fetch("/api/admin/refresh-sessions", { const res = await fetch("/api/admin/refresh-sessions", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
body: JSON.stringify({ companyId: company.id }),
}); });
if (res.ok) { if (res.ok) {
// Refetch metrics // Refetch metrics
const metricsRes = await fetch("/api/dashboard/metrics"); const metricsRes = await fetch("/api/dashboard/metrics");
const data = await metricsRes.json(); const data = await metricsRes.json();
setMetrics(data.metrics); setMetrics(data.metrics);
} else {
const errorData = await res.json();
console.error("Failed to refresh sessions:", errorData.error);
} }
} finally { } finally {
setRefreshing(false); setRefreshing(false);
@ -127,11 +139,11 @@ function DashboardContent() {
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white p-4 rounded-xl shadow"> <div className="bg-white p-4 rounded-xl shadow">
<h3 className="font-bold text-lg mb-3">Sessions by Day</h3> <h3 className="font-bold text-lg mb-3">Sessions by Day</h3>
<SessionsLineChart data={metrics.days || {}} /> <SessionsLineChart sessionsPerDay={metrics.days || {}} />
</div> </div>
<div className="bg-white p-4 rounded-xl shadow"> <div className="bg-white p-4 rounded-xl shadow">
<h3 className="font-bold text-lg mb-3">Categories</h3> <h3 className="font-bold text-lg mb-3">Categories</h3>
<CategoriesBarChart data={metrics.categories || {}} /> <CategoriesBarChart categories={metrics.categories || {}} />
</div> </div>
</div> </div>

View File

@ -42,18 +42,26 @@ export default function DashboardSettings({
return ( return (
<div className="bg-white p-6 rounded-xl shadow mb-6"> <div className="bg-white p-6 rounded-xl shadow mb-6">
<h2 className="font-bold text-lg mb-4">Company Config</h2> <h2 className="font-bold text-lg mb-4">Company Config</h2>
<div className="grid gap-4"> <form
className="grid gap-4"
onSubmit={(e) => {
e.preventDefault();
handleSave();
}}
>
<input <input
className="border px-3 py-2 rounded" className="border px-3 py-2 rounded"
placeholder="CSV URL" placeholder="CSV URL"
value={csvUrl} value={csvUrl}
onChange={(e) => setCsvUrl(e.target.value)} onChange={(e) => setCsvUrl(e.target.value)}
autoComplete="url"
/> />
<input <input
className="border px-3 py-2 rounded" className="border px-3 py-2 rounded"
placeholder="CSV Username" placeholder="CSV Username"
value={csvUsername} value={csvUsername}
onChange={(e) => setCsvUsername(e.target.value)} onChange={(e) => setCsvUsername(e.target.value)}
autoComplete="username"
/> />
<input <input
className="border px-3 py-2 rounded" className="border px-3 py-2 rounded"
@ -61,6 +69,7 @@ export default function DashboardSettings({
placeholder="CSV Password" placeholder="CSV Password"
value={csvPassword} value={csvPassword}
onChange={(e) => setCsvPassword(e.target.value)} onChange={(e) => setCsvPassword(e.target.value)}
autoComplete="new-password"
/> />
<input <input
className="border px-3 py-2 rounded" className="border px-3 py-2 rounded"
@ -69,14 +78,11 @@ export default function DashboardSettings({
value={sentimentThreshold} value={sentimentThreshold}
onChange={(e) => setSentimentThreshold(e.target.value)} onChange={(e) => setSentimentThreshold(e.target.value)}
/> />
<button <button type="submit" className="bg-blue-600 text-white rounded py-2">
className="bg-blue-600 text-white rounded py-2"
onClick={handleSave}
>
Save Settings Save Settings
</button> </button>
<div>{message}</div> <div>{message}</div>
</div> </form>
</div> </div>
); );
} }

View File

@ -87,11 +87,18 @@ export async function fetchAndParseCsv(
trim: true, trim: true,
}); });
// Helper function to safely parse dates
function safeParseDate(dateStr?: string): Date | null {
if (!dateStr) return null;
const date = new Date(dateStr);
return !isNaN(date.getTime()) ? date : null;
}
// Coerce types for relevant columns // Coerce types for relevant columns
return records.map((r) => ({ return records.map((r) => ({
id: r.session_id, id: r.session_id,
startTime: new Date(r.start_time), startTime: safeParseDate(r.start_time) || new Date(), // Fallback to current date if invalid
endTime: r.end_time ? new Date(r.end_time) : null, endTime: safeParseDate(r.end_time),
ipAddress: r.ip_address, ipAddress: r.ip_address,
country: r.country, country: r.country,
language: r.language, language: r.language,

View File

@ -96,5 +96,6 @@ export function sessionMetrics(
totalTokens, totalTokens,
totalTokensEur, totalTokensEur,
sentimentThreshold: threshold, sentimentThreshold: threshold,
lastUpdated: Date.now(), // Add current timestamp
} as MetricsResult; } as MetricsResult;
} }

View File

@ -85,6 +85,7 @@ export interface MetricsResult {
totalTokens?: number; totalTokens?: number;
totalTokensEur?: number; totalTokensEur?: number;
sentimentThreshold?: number | null; sentimentThreshold?: number | null;
lastUpdated?: number; // Timestamp for when metrics were last updated
} }
export interface ApiResponse<T> { export interface ApiResponse<T> {

View File

@ -70,14 +70,26 @@ export default async function handler(
startTime: session.startTime || new Date(), startTime: session.startTime || new Date(),
}; };
// Validate dates to prevent "Invalid Date" errors
const startTime =
sessionData.startTime instanceof Date &&
!isNaN(sessionData.startTime.getTime())
? sessionData.startTime
: new Date();
const endTime =
session.endTime instanceof Date && !isNaN(session.endTime.getTime())
? session.endTime
: new Date();
// Only include fields that are properly typed for Prisma // Only include fields that are properly typed for Prisma
await prisma.session.create({ await prisma.session.create({
data: { data: {
id: sessionData.id, id: sessionData.id,
companyId: sessionData.companyId, companyId: sessionData.companyId,
startTime: sessionData.startTime, startTime: startTime,
// endTime is required in the schema, so use startTime if not available // endTime is required in the schema, so use valid startTime if not available
endTime: session.endTime || new Date(), endTime: endTime,
ipAddress: session.ipAddress || null, ipAddress: session.ipAddress || null,
country: session.country || null, country: session.country || null,
language: session.language || null, language: session.language || null,

View File

@ -1,6 +1,6 @@
// seed.js - Create initial admin user and company // seed.js - Create initial admin user and company
import { PrismaClient } from '@prisma/client'; import { PrismaClient } from "@prisma/client";
import bcrypt from 'bcryptjs'; import bcrypt from "bcryptjs";
const prisma = new PrismaClient(); const prisma = new PrismaClient();
@ -8,30 +8,30 @@ async function main() {
// Create a company // Create a company
const company = await prisma.company.create({ const company = await prisma.company.create({
data: { data: {
name: 'Demo Company', name: "Demo Company",
csvUrl: 'https://example.com/data.csv', // Replace with a real URL if available csvUrl: "https://example.com/data.csv", // Replace with a real URL if available
} },
}); });
// Create an admin user // Create an admin user
const hashedPassword = await bcrypt.hash('admin123', 10); const hashedPassword = await bcrypt.hash("admin123", 10);
await prisma.user.create({ await prisma.user.create({
data: { data: {
email: 'admin@demo.com', email: "admin@demo.com",
password: hashedPassword, password: hashedPassword,
role: 'admin', role: "admin",
companyId: company.id companyId: company.id,
} },
}); });
console.log('Seed data created successfully:'); console.log("Seed data created successfully:");
console.log('Company: Demo Company'); console.log("Company: Demo Company");
console.log('Admin user: admin@demo.com (password: admin123)'); console.log("Admin user: admin@demo.com (password: admin123)");
} }
main() main()
.catch(e => { .catch((e) => {
console.error('Error seeding database:', e); console.error("Error seeding database:", e);
process.exit(1); process.exit(1);
}) })
.finally(async () => { .finally(async () => {