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
*.db
*.db-shm
*.db-wal
*.sqlite?
# IDE
@ -232,6 +234,7 @@ next-env.d.ts
.idea/
*.sublime-project
*.sublime-workspace
*-instructions.*
# logs
logs

View File

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

View File

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

View File

@ -87,11 +87,18 @@ export async function fetchAndParseCsv(
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
return records.map((r) => ({
id: r.session_id,
startTime: new Date(r.start_time),
endTime: r.end_time ? new Date(r.end_time) : null,
startTime: safeParseDate(r.start_time) || new Date(), // Fallback to current date if invalid
endTime: safeParseDate(r.end_time),
ipAddress: r.ip_address,
country: r.country,
language: r.language,

View File

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

View File

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

View File

@ -70,14 +70,26 @@ export default async function handler(
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
await prisma.session.create({
data: {
id: sessionData.id,
companyId: sessionData.companyId,
startTime: sessionData.startTime,
// endTime is required in the schema, so use startTime if not available
endTime: session.endTime || new Date(),
startTime: startTime,
// endTime is required in the schema, so use valid startTime if not available
endTime: endTime,
ipAddress: session.ipAddress || null,
country: session.country || null,
language: session.language || null,

View File

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