mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 10:12:09 +01:00
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:
3
.gitignore
vendored
3
.gitignore
vendored
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -96,5 +96,6 @@ export function sessionMetrics(
|
||||
totalTokens,
|
||||
totalTokensEur,
|
||||
sentimentThreshold: threshold,
|
||||
lastUpdated: Date.now(), // Add current timestamp
|
||||
} as MetricsResult;
|
||||
}
|
||||
|
||||
@ -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> {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user