mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 11:32:13 +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
|
# 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
|
||||||
|
|||||||
@ -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>
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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> {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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 () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user