mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 12:52:09 +01:00
feat: update package.json scripts and add prisma seed command
refactor: improve refresh-sessions API handler for better readability and error handling fix: enhance NextAuth configuration with session token handling and cookie settings chore: update dashboard API handlers for consistency and improved error responses style: format dashboard API routes for better readability feat: implement forgot password and reset password functionality with security improvements feat: add user registration API with email existence check and initial company creation chore: create initial database migration and seed script for demo data style: clean up PostCSS and Tailwind CSS configuration files fix: update TypeScript configuration for stricter type checking chore: add development environment variables for NextAuth feat: create Providers component for session management in the app chore: initialize Prisma migration and seed files for database setup
This commit is contained in:
9
.env.development
Normal file
9
.env.development
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
# Development environment settings
|
||||||
|
# This file ensures NextAuth always has necessary environment variables in development
|
||||||
|
|
||||||
|
# NextAuth.js configuration
|
||||||
|
NEXTAUTH_URL=http://localhost:3000
|
||||||
|
NEXTAUTH_SECRET=this_is_a_fixed_secret_for_development_only
|
||||||
|
NODE_ENV=development
|
||||||
|
|
||||||
|
# Database connection - already configured in your prisma/schema.prisma
|
||||||
@ -1,11 +1,9 @@
|
|||||||
{
|
{
|
||||||
"extends": [
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
"next/core-web-vitals",
|
// ,
|
||||||
"next/typescript"
|
// "rules": {
|
||||||
],
|
// "@typescript-eslint/no-explicit-any": "off",
|
||||||
"rules": {
|
// "@typescript-eslint/no-unused-vars": "warn",
|
||||||
"@typescript-eslint/no-explicit-any": "off",
|
// "@typescript-eslint/ban-ts-comment": "off"
|
||||||
"@typescript-eslint/no-unused-vars": "warn",
|
// }
|
||||||
"@typescript-eslint/ban-ts-comment": "off"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
193
.gitignore
vendored
193
.gitignore
vendored
@ -1,3 +1,192 @@
|
|||||||
|
# Created by https://www.toptal.com/developers/gitignore/api/node,nextjs,react
|
||||||
|
# Edit at https://www.toptal.com/developers/gitignore?templates=node,nextjs,react
|
||||||
|
|
||||||
|
### NextJS ###
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env*.local
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
### Node ###
|
||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# Snowpack dependency directory (https://snowpack.dev/)
|
||||||
|
web_modules/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variable files
|
||||||
|
.env
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
|
.env.local
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# vuepress v2.x temp and cache directory
|
||||||
|
.temp
|
||||||
|
|
||||||
|
# Docusaurus cache and generated files
|
||||||
|
.docusaurus
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
### Node Patch ###
|
||||||
|
# Serverless Webpack directories
|
||||||
|
.webpack/
|
||||||
|
|
||||||
|
# Optional stylelint cache
|
||||||
|
|
||||||
|
# SvelteKit build / generate output
|
||||||
|
.svelte-kit
|
||||||
|
|
||||||
|
### react ###
|
||||||
|
.DS_*
|
||||||
|
**/*.backup.*
|
||||||
|
**/*.back.*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
*.sublime*
|
||||||
|
|
||||||
|
psd
|
||||||
|
thumb
|
||||||
|
sketch
|
||||||
|
|
||||||
|
# End of https://www.toptal.com/developers/gitignore/api/node,nextjs,react
|
||||||
|
|
||||||
# dependencies
|
# dependencies
|
||||||
/node_modules
|
/node_modules
|
||||||
/.pnp
|
/.pnp
|
||||||
@ -33,6 +222,10 @@ yarn-error.log*
|
|||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
next-env.d.ts
|
next-env.d.ts
|
||||||
|
|
||||||
|
# Database files
|
||||||
|
*.db
|
||||||
|
*.sqlite?
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|||||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@ -1,5 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": [
|
"recommendations": ["prisma.prisma"]
|
||||||
"prisma.prisma"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,12 +1,11 @@
|
|||||||
// Main dashboard page: metrics, refresh, config
|
"use client";
|
||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from "react";
|
||||||
import { signOut, useSession } from 'next-auth/react';
|
import { signOut, useSession } from "next-auth/react";
|
||||||
import { SessionsLineChart, CategoriesBarChart } from '../../components/Charts';
|
import { SessionsLineChart, CategoriesBarChart } from "../../components/Charts";
|
||||||
import DashboardSettings from './settings';
|
import DashboardSettings from "./settings";
|
||||||
import UserManagement from './users';
|
import UserManagement from "./users";
|
||||||
import { Company, MetricsResult } from '../../lib/types';
|
import { Company, MetricsResult } from "../../lib/types";
|
||||||
|
|
||||||
interface MetricsCardProps {
|
interface MetricsCardProps {
|
||||||
label: string;
|
label: string;
|
||||||
@ -16,29 +15,29 @@ interface MetricsCardProps {
|
|||||||
function MetricsCard({ label, value }: MetricsCardProps) {
|
function MetricsCard({ label, value }: MetricsCardProps) {
|
||||||
return (
|
return (
|
||||||
<div className="bg-white rounded-xl p-4 shadow-md flex flex-col items-center">
|
<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-2xl font-bold">{value ?? "-"}</span>
|
||||||
<span className="text-gray-500">{label}</span>
|
<span className="text-gray-500">{label}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function DashboardPage() {
|
// Safely wrapped component with useSession
|
||||||
const { data: session } = useSession() || { data: null };
|
function DashboardContent() {
|
||||||
|
const { data: session } = useSession();
|
||||||
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);
|
||||||
// Loading state used in the fetchData function
|
|
||||||
const [, setLoading] = useState<boolean>(false);
|
const [, setLoading] = useState<boolean>(false);
|
||||||
const [csvUrl, setCsvUrl] = useState<string>('');
|
const [csvUrl, setCsvUrl] = useState<string>("");
|
||||||
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, company, and CSV URL 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);
|
||||||
@ -50,122 +49,105 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
async function handleRefresh() {
|
async function handleRefresh() {
|
||||||
if (isAuditor) return; // Prevent auditors from refreshing
|
if (isAuditor) return; // Prevent auditors from refreshing
|
||||||
|
try {
|
||||||
setRefreshing(true);
|
setRefreshing(true);
|
||||||
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) {
|
||||||
|
// Refetch metrics
|
||||||
|
const metricsRes = await fetch("/api/dashboard/metrics");
|
||||||
|
const data = await metricsRes.json();
|
||||||
|
setMetrics(data.metrics);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
window.location.reload();
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleSaveConfig() {
|
if (!metrics || !company) {
|
||||||
if (isAuditor) return; // Prevent auditors from changing config
|
return <div className="text-center py-10">Loading dashboard...</div>;
|
||||||
|
|
||||||
await fetch('/api/dashboard/config', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ csvUrl }),
|
|
||||||
});
|
|
||||||
window.location.reload();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{/* Header with company info */}
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-6">
|
<h1 className="text-2xl font-bold">{company.name}</h1>
|
||||||
<h1 className="text-3xl font-bold">Analytics Dashboard</h1>
|
<p className="text-gray-600">
|
||||||
<button className="text-sm underline" onClick={() => signOut()}>
|
Dashboard updated{" "}
|
||||||
Log out
|
{new Date(metrics.lastUpdated || Date.now()).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<button
|
||||||
|
className="bg-blue-600 text-white py-2 px-4 rounded-lg shadow-sm hover:bg-blue-700 disabled:opacity-50"
|
||||||
|
onClick={handleRefresh}
|
||||||
|
disabled={refreshing || isAuditor}
|
||||||
|
>
|
||||||
|
{refreshing ? "Refreshing..." : "Refresh Data"}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="bg-gray-200 py-2 px-4 rounded-lg shadow-sm hover:bg-gray-300"
|
||||||
|
onClick={() => signOut()}
|
||||||
|
>
|
||||||
|
Sign Out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Admin-only settings and user management */}
|
{/* Metrics Cards */}
|
||||||
{company && isAdmin && (
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||||
|
<MetricsCard label="Total Sessions" value={metrics.totalSessions} />
|
||||||
|
<MetricsCard
|
||||||
|
label="Avg Sessions/Day"
|
||||||
|
value={metrics.avgSessionsPerDay?.toFixed(1)}
|
||||||
|
/>
|
||||||
|
<MetricsCard
|
||||||
|
label="Avg Session Time"
|
||||||
|
value={
|
||||||
|
metrics.avgSessionLength
|
||||||
|
? `${metrics.avgSessionLength.toFixed(1)} min`
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<MetricsCard
|
||||||
|
label="Avg Sentiment"
|
||||||
|
value={
|
||||||
|
metrics.avgSentiment
|
||||||
|
? metrics.avgSentiment.toFixed(2) + "/10"
|
||||||
|
: null
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Charts Row */}
|
||||||
|
<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 || {}} />
|
||||||
|
</div>
|
||||||
|
<div className="bg-white p-4 rounded-xl shadow">
|
||||||
|
<h3 className="font-bold text-lg mb-3">Categories</h3>
|
||||||
|
<CategoriesBarChart data={metrics.categories || {}} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Admin Controls */}
|
||||||
|
{isAdmin && (
|
||||||
<>
|
<>
|
||||||
<DashboardSettings company={company} session={session} />
|
<DashboardSettings company={company} session={session} />
|
||||||
<UserManagement 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 !== undefined ?
|
|
||||||
metrics.avgSentiment.toFixed(2)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<MetricsCard
|
|
||||||
label="Total Tokens (€)"
|
|
||||||
value={
|
|
||||||
metrics?.totalTokensEur !== undefined ?
|
|
||||||
metrics.totalTokensEur.toFixed(2)
|
|
||||||
: undefined
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<MetricsCard
|
|
||||||
label="Below Sentiment Threshold"
|
|
||||||
value={metrics?.belowThresholdCount}
|
|
||||||
/>
|
|
||||||
</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?.days && Object.keys(metrics.days).length > 0 ?
|
|
||||||
<SessionsLineChart sessionsPerDay={metrics.days} />
|
|
||||||
: <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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Our exported component
|
||||||
|
export default function DashboardPage() {
|
||||||
|
// We don't use useSession here to avoid the error outside the provider
|
||||||
|
return <DashboardContent />;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { Company } from '../../lib/types';
|
import { Company } from "../../lib/types";
|
||||||
import { Session } from 'next-auth';
|
import { Session } from "next-auth";
|
||||||
|
|
||||||
interface DashboardSettingsProps {
|
interface DashboardSettingsProps {
|
||||||
company: Company;
|
company: Company;
|
||||||
@ -14,18 +14,18 @@ export default function DashboardSettings({
|
|||||||
}: DashboardSettingsProps) {
|
}: DashboardSettingsProps) {
|
||||||
const [csvUrl, setCsvUrl] = useState<string>(company.csvUrl);
|
const [csvUrl, setCsvUrl] = useState<string>(company.csvUrl);
|
||||||
const [csvUsername, setCsvUsername] = useState<string>(
|
const [csvUsername, setCsvUsername] = useState<string>(
|
||||||
company.csvUsername || ''
|
company.csvUsername || "",
|
||||||
);
|
);
|
||||||
const [csvPassword, setCsvPassword] = useState<string>('');
|
const [csvPassword, setCsvPassword] = useState<string>("");
|
||||||
const [sentimentThreshold, setSentimentThreshold] = useState<string>(
|
const [sentimentThreshold, setSentimentThreshold] = useState<string>(
|
||||||
company.sentimentAlert?.toString() || ''
|
company.sentimentAlert?.toString() || "",
|
||||||
);
|
);
|
||||||
const [message, setMessage] = useState<string>('');
|
const [message, setMessage] = useState<string>("");
|
||||||
|
|
||||||
async function handleSave() {
|
async function handleSave() {
|
||||||
const res = await fetch('/api/dashboard/settings', {
|
const res = await fetch("/api/dashboard/settings", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
csvUrl,
|
csvUrl,
|
||||||
csvUsername,
|
csvUsername,
|
||||||
@ -33,11 +33,11 @@ export default function DashboardSettings({
|
|||||||
sentimentThreshold,
|
sentimentThreshold,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
if (res.ok) setMessage('Settings saved!');
|
if (res.ok) setMessage("Settings saved!");
|
||||||
else setMessage('Failed.');
|
else setMessage("Failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.user.role !== 'admin') return null;
|
if (session.user.role !== "admin") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-xl shadow mb-6">
|
<div className="bg-white p-6 rounded-xl shadow mb-6">
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect } from "react";
|
||||||
import { UserSession } from '../../lib/types';
|
import { UserSession } from "../../lib/types";
|
||||||
|
|
||||||
interface UserItem {
|
interface UserItem {
|
||||||
id: string;
|
id: string;
|
||||||
@ -14,27 +14,27 @@ interface UserManagementProps {
|
|||||||
|
|
||||||
export default function UserManagement({ session }: UserManagementProps) {
|
export default function UserManagement({ session }: UserManagementProps) {
|
||||||
const [users, setUsers] = useState<UserItem[]>([]);
|
const [users, setUsers] = useState<UserItem[]>([]);
|
||||||
const [email, setEmail] = useState<string>('');
|
const [email, setEmail] = useState<string>("");
|
||||||
const [role, setRole] = useState<string>('user');
|
const [role, setRole] = useState<string>("user");
|
||||||
const [msg, setMsg] = useState<string>('');
|
const [msg, setMsg] = useState<string>("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetch('/api/dashboard/users')
|
fetch("/api/dashboard/users")
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((data) => setUsers(data.users));
|
.then((data) => setUsers(data.users));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
async function inviteUser() {
|
async function inviteUser() {
|
||||||
const res = await fetch('/api/dashboard/users', {
|
const res = await fetch("/api/dashboard/users", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ email, role }),
|
body: JSON.stringify({ email, role }),
|
||||||
});
|
});
|
||||||
if (res.ok) setMsg('User invited.');
|
if (res.ok) setMsg("User invited.");
|
||||||
else setMsg('Failed.');
|
else setMsg("Failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.user.role !== 'admin') return null;
|
if (session.user.role !== "admin") return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-white p-6 rounded-xl shadow mb-6">
|
<div className="bg-white p-6 rounded-xl shadow mb-6">
|
||||||
@ -66,7 +66,7 @@ export default function UserManagement({ session }: UserManagementProps) {
|
|||||||
<ul className="mt-4">
|
<ul className="mt-4">
|
||||||
{users.map((u) => (
|
{users.map((u) => (
|
||||||
<li key={u.id} className="flex justify-between border-b py-1">
|
<li key={u.id} className="flex justify-between border-b py-1">
|
||||||
{u.email}{' '}
|
{u.email}{" "}
|
||||||
<span className="text-xs bg-gray-200 px-2 rounded">{u.role}</span>
|
<span className="text-xs bg-gray-200 px-2 rounded">{u.role}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,19 +1,19 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
|
|
||||||
export default function ForgotPasswordPage() {
|
export default function ForgotPasswordPage() {
|
||||||
const [email, setEmail] = useState<string>('');
|
const [email, setEmail] = useState<string>("");
|
||||||
const [message, setMessage] = useState<string>('');
|
const [message, setMessage] = useState<string>("");
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const res = await fetch('/api/forgot-password', {
|
const res = await fetch("/api/forgot-password", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ email }),
|
body: JSON.stringify({ email }),
|
||||||
});
|
});
|
||||||
if (res.ok) setMessage('If that email exists, a reset link has been sent.');
|
if (res.ok) setMessage("If that email exists, a reset link has been sent.");
|
||||||
else setMessage('Failed. Try again.');
|
else setMessage("Failed. Try again.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,18 +1,21 @@
|
|||||||
// Main app layout with basic global style
|
// Main app layout with basic global style
|
||||||
import './globals.css';
|
import "./globals.css";
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from "react";
|
||||||
|
import { Providers } from "./providers";
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: 'LiveDash-Node',
|
title: "LiveDash-Node",
|
||||||
description:
|
description:
|
||||||
'Multi-tenant dashboard system for tracking chat session metrics',
|
"Multi-tenant dashboard system for tracking chat session metrics",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body className="bg-gray-100 min-h-screen font-sans">
|
<body className="bg-gray-100 min-h-screen font-sans">
|
||||||
|
<Providers>
|
||||||
<div className="max-w-5xl mx-auto py-8">{children}</div>
|
<div className="max-w-5xl mx-auto py-8">{children}</div>
|
||||||
|
</Providers>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,23 +1,23 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from "next-auth/react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
const [email, setEmail] = useState('');
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function handleLogin(e: React.FormEvent) {
|
async function handleLogin(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const res = await signIn('credentials', {
|
const res = await signIn("credentials", {
|
||||||
email,
|
email,
|
||||||
password,
|
password,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
});
|
});
|
||||||
if (res?.ok) router.push('/dashboard');
|
if (res?.ok) router.push("/dashboard");
|
||||||
else setError('Invalid credentials.');
|
else setError("Invalid credentials.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
10
app/page.tsx
10
app/page.tsx
@ -1,9 +1,9 @@
|
|||||||
import { getServerSession } from 'next-auth';
|
import { getServerSession } from "next-auth";
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from "next/navigation";
|
||||||
import { authOptions } from '../pages/api/auth/[...nextauth]';
|
import { authOptions } from "../pages/api/auth/[...nextauth]";
|
||||||
|
|
||||||
export default async function HomePage() {
|
export default async function HomePage() {
|
||||||
const session = await getServerSession(authOptions);
|
const session = await getServerSession(authOptions);
|
||||||
if (session?.user) redirect('/dashboard');
|
if (session?.user) redirect("/dashboard");
|
||||||
else redirect('/login');
|
else redirect("/login");
|
||||||
}
|
}
|
||||||
|
|||||||
17
app/providers.tsx
Normal file
17
app/providers.tsx
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { SessionProvider } from "next-auth/react";
|
||||||
|
import { ReactNode } from "react";
|
||||||
|
|
||||||
|
export function Providers({ children }: { children: ReactNode }) {
|
||||||
|
// Including error handling and refetch interval for better user experience
|
||||||
|
return (
|
||||||
|
<SessionProvider
|
||||||
|
// Re-fetch session every 10 minutes
|
||||||
|
refetchInterval={10 * 60}
|
||||||
|
refetchOnWindowFocus={true}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SessionProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,25 +1,25 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export default function RegisterPage() {
|
export default function RegisterPage() {
|
||||||
const [email, setEmail] = useState<string>('');
|
const [email, setEmail] = useState<string>("");
|
||||||
const [company, setCompany] = useState<string>('');
|
const [company, setCompany] = useState<string>("");
|
||||||
const [password, setPassword] = useState<string>('');
|
const [password, setPassword] = useState<string>("");
|
||||||
const [csvUrl, setCsvUrl] = useState<string>('');
|
const [csvUrl, setCsvUrl] = useState<string>("");
|
||||||
const [role, setRole] = useState<string>('admin'); // Default to admin for company registration
|
const [role, setRole] = useState<string>("admin"); // Default to admin for company registration
|
||||||
const [error, setError] = useState<string>('');
|
const [error, setError] = useState<string>("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function handleRegister(e: React.FormEvent) {
|
async function handleRegister(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const res = await fetch('/api/register', {
|
const res = await fetch("/api/register", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ email, password, company, csvUrl, role }),
|
body: JSON.stringify({ email, password, company, csvUrl, role }),
|
||||||
});
|
});
|
||||||
if (res.ok) router.push('/login');
|
if (res.ok) router.push("/login");
|
||||||
else setError('Registration failed.');
|
else setError("Registration failed.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,26 +1,26 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { useState, Suspense } from 'react';
|
import { useState, Suspense } from "react";
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
|
||||||
// Component that uses useSearchParams wrapped in Suspense
|
// Component that uses useSearchParams wrapped in Suspense
|
||||||
function ResetPasswordForm() {
|
function ResetPasswordForm() {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const token = searchParams?.get('token');
|
const token = searchParams?.get("token");
|
||||||
const [password, setPassword] = useState<string>('');
|
const [password, setPassword] = useState<string>("");
|
||||||
const [message, setMessage] = useState<string>('');
|
const [message, setMessage] = useState<string>("");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const res = await fetch('/api/reset-password', {
|
const res = await fetch("/api/reset-password", {
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { "Content-Type": "application/json" },
|
||||||
body: JSON.stringify({ token, password }),
|
body: JSON.stringify({ token, password }),
|
||||||
});
|
});
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
setMessage('Password reset! Redirecting to login...');
|
setMessage("Password reset! Redirecting to login...");
|
||||||
setTimeout(() => router.push('/login'), 2000);
|
setTimeout(() => router.push("/login"), 2000);
|
||||||
} else setMessage('Invalid or expired link.');
|
} else setMessage("Invalid or expired link.");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from "react";
|
||||||
import Chart from 'chart.js/auto';
|
import Chart from "chart.js/auto";
|
||||||
|
|
||||||
interface SessionsData {
|
interface SessionsData {
|
||||||
[date: string]: number;
|
[date: string]: number;
|
||||||
@ -23,16 +23,16 @@ export function SessionsLineChart({ sessionsPerDay }: SessionsLineChartProps) {
|
|||||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || !sessionsPerDay) return;
|
if (!ref.current || !sessionsPerDay) return;
|
||||||
const ctx = ref.current.getContext('2d');
|
const ctx = ref.current.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const chart = new Chart(ctx, {
|
const chart = new Chart(ctx, {
|
||||||
type: 'line',
|
type: "line",
|
||||||
data: {
|
data: {
|
||||||
labels: Object.keys(sessionsPerDay),
|
labels: Object.keys(sessionsPerDay),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Sessions',
|
label: "Sessions",
|
||||||
data: Object.values(sessionsPerDay),
|
data: Object.values(sessionsPerDay),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
@ -53,16 +53,16 @@ export function CategoriesBarChart({ categories }: CategoriesBarChartProps) {
|
|||||||
const ref = useRef<HTMLCanvasElement | null>(null);
|
const ref = useRef<HTMLCanvasElement | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!ref.current || !categories) return;
|
if (!ref.current || !categories) return;
|
||||||
const ctx = ref.current.getContext('2d');
|
const ctx = ref.current.getContext("2d");
|
||||||
if (!ctx) return;
|
if (!ctx) return;
|
||||||
|
|
||||||
const chart = new Chart(ctx, {
|
const chart = new Chart(ctx, {
|
||||||
type: 'bar',
|
type: "bar",
|
||||||
data: {
|
data: {
|
||||||
labels: Object.keys(categories),
|
labels: Object.keys(categories),
|
||||||
datasets: [
|
datasets: [
|
||||||
{
|
{
|
||||||
label: 'Categories',
|
label: "Categories",
|
||||||
data: Object.values(categories),
|
data: Object.values(categories),
|
||||||
borderWidth: 2,
|
borderWidth: 2,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -43,8 +43,13 @@ interface SessionData {
|
|||||||
initialMsg?: string;
|
initialMsg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchAndParseCsv(url: string, username?: string, password?: string): Promise<Partial<SessionData>[]> {
|
export async function fetchAndParseCsv(
|
||||||
const authHeader = username && password
|
url: string,
|
||||||
|
username?: string,
|
||||||
|
password?: string,
|
||||||
|
): Promise<Partial<SessionData>[]> {
|
||||||
|
const authHeader =
|
||||||
|
username && password
|
||||||
? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
|
? "Basic " + Buffer.from(`${username}:${password}`).toString("base64")
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
@ -95,7 +100,9 @@ export async function fetchAndParseCsv(url: string, username?: string, password?
|
|||||||
escalated: r.escalated === "1" || r.escalated === "true",
|
escalated: r.escalated === "1" || r.escalated === "true",
|
||||||
forwardedHr: r.forwarded_hr === "1" || r.forwarded_hr === "true",
|
forwardedHr: r.forwarded_hr === "1" || r.forwarded_hr === "true",
|
||||||
fullTranscriptUrl: r.full_transcript_url,
|
fullTranscriptUrl: r.full_transcript_url,
|
||||||
avgResponseTime: r.avg_response_time ? parseFloat(r.avg_response_time) : null,
|
avgResponseTime: r.avg_response_time
|
||||||
|
? parseFloat(r.avg_response_time)
|
||||||
|
: null,
|
||||||
tokens: Number(r.tokens) || 0,
|
tokens: Number(r.tokens) || 0,
|
||||||
tokensEur: r.tokens_eur ? parseFloat(r.tokens_eur) : 0,
|
tokensEur: r.tokens_eur ? parseFloat(r.tokens_eur) : 0,
|
||||||
category: r.category,
|
category: r.category,
|
||||||
|
|||||||
@ -1,25 +1,38 @@
|
|||||||
// Functions to calculate metrics over sessions
|
// Functions to calculate metrics over sessions
|
||||||
import { ChatSession, DayMetrics, CategoryMetrics, LanguageMetrics, MetricsResult } from './types';
|
import {
|
||||||
|
ChatSession,
|
||||||
|
DayMetrics,
|
||||||
|
CategoryMetrics,
|
||||||
|
LanguageMetrics,
|
||||||
|
MetricsResult,
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
interface CompanyConfig {
|
interface CompanyConfig {
|
||||||
sentimentAlert?: number;
|
sentimentAlert?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sessionMetrics(sessions: ChatSession[], companyConfig: CompanyConfig = {}): MetricsResult {
|
export function sessionMetrics(
|
||||||
|
sessions: ChatSession[],
|
||||||
|
companyConfig: CompanyConfig = {},
|
||||||
|
): MetricsResult {
|
||||||
const total = sessions.length;
|
const total = sessions.length;
|
||||||
const byDay: DayMetrics = {};
|
const byDay: DayMetrics = {};
|
||||||
const byCategory: CategoryMetrics = {};
|
const byCategory: CategoryMetrics = {};
|
||||||
const byLanguage: LanguageMetrics = {};
|
const byLanguage: LanguageMetrics = {};
|
||||||
let escalated = 0, forwarded = 0;
|
let escalated = 0,
|
||||||
let totalSentiment = 0, sentimentCount = 0;
|
forwarded = 0;
|
||||||
let totalResponse = 0, responseCount = 0;
|
let totalSentiment = 0,
|
||||||
let totalTokens = 0, totalTokensEur = 0;
|
sentimentCount = 0;
|
||||||
|
let totalResponse = 0,
|
||||||
|
responseCount = 0;
|
||||||
|
let totalTokens = 0,
|
||||||
|
totalTokensEur = 0;
|
||||||
|
|
||||||
// Calculate total session duration in minutes
|
// Calculate total session duration in minutes
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
let durationCount = 0;
|
let durationCount = 0;
|
||||||
|
|
||||||
sessions.forEach(s => {
|
sessions.forEach((s) => {
|
||||||
const day = s.startTime.toISOString().slice(0, 10);
|
const day = s.startTime.toISOString().slice(0, 10);
|
||||||
byDay[day] = (byDay[day] || 0) + 1;
|
byDay[day] = (byDay[day] || 0) + 1;
|
||||||
|
|
||||||
@ -27,7 +40,8 @@ export function sessionMetrics(sessions: ChatSession[], companyConfig: CompanyCo
|
|||||||
if (s.language) byLanguage[s.language] = (byLanguage[s.language] || 0) + 1;
|
if (s.language) byLanguage[s.language] = (byLanguage[s.language] || 0) + 1;
|
||||||
|
|
||||||
if (s.endTime) {
|
if (s.endTime) {
|
||||||
const duration = (s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
|
const duration =
|
||||||
|
(s.endTime.getTime() - s.startTime.getTime()) / (1000 * 60); // minutes
|
||||||
totalDuration += duration;
|
totalDuration += duration;
|
||||||
durationCount++;
|
durationCount++;
|
||||||
}
|
}
|
||||||
@ -63,7 +77,8 @@ export function sessionMetrics(sessions: ChatSession[], companyConfig: CompanyCo
|
|||||||
const avgSessionsPerDay = dayCount > 0 ? total / dayCount : 0;
|
const avgSessionsPerDay = dayCount > 0 ? total / dayCount : 0;
|
||||||
|
|
||||||
// Calculate average session length
|
// Calculate average session length
|
||||||
const avgSessionLength = durationCount > 0 ? totalDuration / durationCount : null;
|
const avgSessionLength =
|
||||||
|
durationCount > 0 ? totalDuration / durationCount : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
totalSessions: total,
|
totalSessions: total,
|
||||||
|
|||||||
@ -15,7 +15,11 @@ export function startScheduler() {
|
|||||||
const companies = await prisma.company.findMany();
|
const companies = await prisma.company.findMany();
|
||||||
for (const company of companies) {
|
for (const company of companies) {
|
||||||
try {
|
try {
|
||||||
const sessions = await fetchAndParseCsv(company.csvUrl, company.csvUsername as string | undefined, company.csvPassword as string | undefined);
|
const sessions = await fetchAndParseCsv(
|
||||||
|
company.csvUrl,
|
||||||
|
company.csvUsername as string | undefined,
|
||||||
|
company.csvPassword as string | undefined,
|
||||||
|
);
|
||||||
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
||||||
|
|
||||||
for (const session of sessions) {
|
for (const session of sessions) {
|
||||||
@ -24,7 +28,7 @@ export function startScheduler() {
|
|||||||
companyId: company.id,
|
companyId: company.id,
|
||||||
id: session.id || session.sessionId || `sess_${Date.now()}`,
|
id: session.id || session.sessionId || `sess_${Date.now()}`,
|
||||||
// Ensure startTime is not undefined
|
// Ensure startTime is not undefined
|
||||||
startTime: session.startTime || new Date()
|
startTime: session.startTime || new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only include fields that are properly typed for Prisma
|
// Only include fields that are properly typed for Prisma
|
||||||
@ -38,13 +42,21 @@ export function startScheduler() {
|
|||||||
ipAddress: session.ipAddress || null,
|
ipAddress: session.ipAddress || null,
|
||||||
country: session.country || null,
|
country: session.country || null,
|
||||||
language: session.language || null,
|
language: session.language || null,
|
||||||
sentiment: typeof session.sentiment === 'number' ? session.sentiment : null,
|
sentiment:
|
||||||
messagesSent: typeof session.messagesSent === 'number' ? session.messagesSent : 0,
|
typeof session.sentiment === "number"
|
||||||
category: session.category || null
|
? session.sentiment
|
||||||
}
|
: null,
|
||||||
|
messagesSent:
|
||||||
|
typeof session.messagesSent === "number"
|
||||||
|
? session.messagesSent
|
||||||
|
: 0,
|
||||||
|
category: session.category || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
console.log(`[Scheduler] Refreshed sessions for company: ${company.name}`);
|
console.log(
|
||||||
|
`[Scheduler] Refreshed sessions for company: ${company.name}`,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`[Scheduler] Failed for company: ${company.name} - ${e}`);
|
console.error(`[Scheduler] Failed for company: ${company.name} - ${e}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,8 @@
|
|||||||
export async function sendEmail(to: string, subject: string, text: string): Promise<void> {
|
export async function sendEmail(
|
||||||
|
to: string,
|
||||||
|
subject: string,
|
||||||
|
text: string,
|
||||||
|
): Promise<void> {
|
||||||
// For demo: log to console. Use nodemailer/sendgrid/whatever in prod.
|
// For demo: log to console. Use nodemailer/sendgrid/whatever in prod.
|
||||||
console.log(`[Email to ${to}]: ${subject}\n${text}`);
|
console.log(`[Email to ${to}]: ${subject}\n${text}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Session as NextAuthSession } from 'next-auth';
|
import { Session as NextAuthSession } from "next-auth";
|
||||||
|
|
||||||
export interface UserSession extends NextAuthSession {
|
export interface UserSession extends NextAuthSession {
|
||||||
user: {
|
user: {
|
||||||
|
|||||||
158
package-lock.json
generated
158
package-lock.json
generated
@ -35,6 +35,7 @@
|
|||||||
"postcss": "^8.4.0",
|
"postcss": "^8.4.0",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^3.5.3",
|
||||||
"prisma": "^6.8.2",
|
"prisma": "^6.8.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -74,6 +75,30 @@
|
|||||||
"node": ">=6.9.0"
|
"node": ">=6.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support": {
|
||||||
|
"version": "0.8.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
|
||||||
|
"integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/trace-mapping": "0.3.9"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": {
|
||||||
|
"version": "0.3.9",
|
||||||
|
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz",
|
||||||
|
"integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@jridgewell/resolve-uri": "^3.0.3",
|
||||||
|
"@jridgewell/sourcemap-codec": "^1.4.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@emnapi/core": {
|
"node_modules/@emnapi/core": {
|
||||||
"version": "1.4.3",
|
"version": "1.4.3",
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.3.tgz",
|
||||||
@ -1400,6 +1425,34 @@
|
|||||||
"tailwindcss": "4.1.7"
|
"tailwindcss": "4.1.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@tsconfig/node10": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node12": {
|
||||||
|
"version": "1.0.11",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz",
|
||||||
|
"integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node14": {
|
||||||
|
"version": "1.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz",
|
||||||
|
"integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/@tsconfig/node16": {
|
||||||
|
"version": "1.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz",
|
||||||
|
"integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@tybys/wasm-util": {
|
"node_modules/@tybys/wasm-util": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.9.0.tgz",
|
||||||
@ -1978,6 +2031,19 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/acorn-walk": {
|
||||||
|
"version": "8.3.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz",
|
||||||
|
"integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"acorn": "^8.11.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/ajv": {
|
"node_modules/ajv": {
|
||||||
"version": "6.12.6",
|
"version": "6.12.6",
|
||||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||||
@ -2011,6 +2077,13 @@
|
|||||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/arg": {
|
||||||
|
"version": "4.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz",
|
||||||
|
"integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/argparse": {
|
"node_modules/argparse": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||||
@ -2547,6 +2620,13 @@
|
|||||||
"node": ">= 0.6"
|
"node": ">= 0.6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/create-require": {
|
||||||
|
"version": "1.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
|
||||||
|
"integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
@ -2716,6 +2796,16 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/diff": {
|
||||||
|
"version": "4.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
|
||||||
|
"integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.3.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dunder-proto": {
|
"node_modules/dunder-proto": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||||
@ -4808,6 +4898,13 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/make-error": {
|
||||||
|
"version": "1.3.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz",
|
||||||
|
"integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/math-intrinsics": {
|
"node_modules/math-intrinsics": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||||
@ -6344,6 +6441,50 @@
|
|||||||
"typescript": ">=4.8.4"
|
"typescript": ">=4.8.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/ts-node": {
|
||||||
|
"version": "10.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||||
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
|
"@tsconfig/node10": "^1.0.7",
|
||||||
|
"@tsconfig/node12": "^1.0.7",
|
||||||
|
"@tsconfig/node14": "^1.0.0",
|
||||||
|
"@tsconfig/node16": "^1.0.2",
|
||||||
|
"acorn": "^8.4.1",
|
||||||
|
"acorn-walk": "^8.1.1",
|
||||||
|
"arg": "^4.1.0",
|
||||||
|
"create-require": "^1.1.0",
|
||||||
|
"diff": "^4.0.1",
|
||||||
|
"make-error": "^1.1.1",
|
||||||
|
"v8-compile-cache-lib": "^3.0.1",
|
||||||
|
"yn": "3.1.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"ts-node": "dist/bin.js",
|
||||||
|
"ts-node-cwd": "dist/bin-cwd.js",
|
||||||
|
"ts-node-esm": "dist/bin-esm.js",
|
||||||
|
"ts-node-script": "dist/bin-script.js",
|
||||||
|
"ts-node-transpile-only": "dist/bin-transpile.js",
|
||||||
|
"ts-script": "dist/bin-script-deprecated.js"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@swc/core": ">=1.2.50",
|
||||||
|
"@swc/wasm": ">=1.2.50",
|
||||||
|
"@types/node": "*",
|
||||||
|
"typescript": ">=2.7"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@swc/core": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"@swc/wasm": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/tsconfig-paths": {
|
"node_modules/tsconfig-paths": {
|
||||||
"version": "3.15.0",
|
"version": "3.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
|
||||||
@ -6577,6 +6718,13 @@
|
|||||||
"uuid": "dist/bin/uuid"
|
"uuid": "dist/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/v8-compile-cache-lib": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/web-streams-polyfill": {
|
"node_modules/web-streams-polyfill": {
|
||||||
"version": "3.3.3",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
|
||||||
@ -6707,6 +6855,16 @@
|
|||||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
"node_modules/yn": {
|
||||||
|
"version": "3.1.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz",
|
||||||
|
"integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
||||||
|
|||||||
@ -4,13 +4,14 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev --turbopack",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "next lint",
|
"lint": "next lint",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev"
|
"prisma:migrate": "prisma migrate dev",
|
||||||
|
"prisma:seed": "node prisma/seed.mjs"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@prisma/client": "^6.8.2",
|
"@prisma/client": "^6.8.2",
|
||||||
|
|||||||
@ -11,7 +11,10 @@ interface SessionCreateData {
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
// Check if this is a POST request
|
// Check if this is a POST request
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
return res.status(405).json({ error: "Method not allowed" });
|
return res.status(405).json({ error: "Method not allowed" });
|
||||||
@ -24,8 +27,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
// Try to get user from prisma based on session cookie
|
// Try to get user from prisma based on session cookie
|
||||||
try {
|
try {
|
||||||
const session = await prisma.session.findFirst({
|
const session = await prisma.session.findFirst({
|
||||||
orderBy: { createdAt: 'desc' },
|
orderBy: { createdAt: "desc" },
|
||||||
where: { /* Add session check criteria here */ }
|
where: {
|
||||||
|
/* Add session check criteria here */
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (session) {
|
if (session) {
|
||||||
@ -44,7 +49,11 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
if (!company) return res.status(404).json({ error: "Company not found" });
|
if (!company) return res.status(404).json({ error: "Company not found" });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const sessions = await fetchAndParseCsv(company.csvUrl, company.csvUsername as string | undefined, company.csvPassword as string | undefined);
|
const sessions = await fetchAndParseCsv(
|
||||||
|
company.csvUrl,
|
||||||
|
company.csvUsername as string | undefined,
|
||||||
|
company.csvPassword as string | undefined,
|
||||||
|
);
|
||||||
|
|
||||||
// Replace all session rows for this company (for demo simplicity)
|
// Replace all session rows for this company (for demo simplicity)
|
||||||
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
await prisma.session.deleteMany({ where: { companyId: company.id } });
|
||||||
@ -53,9 +62,12 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const sessionData: SessionCreateData = {
|
const sessionData: SessionCreateData = {
|
||||||
...session,
|
...session,
|
||||||
companyId: company.id,
|
companyId: company.id,
|
||||||
id: session.id || session.sessionId || `sess_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
|
id:
|
||||||
|
session.id ||
|
||||||
|
session.sessionId ||
|
||||||
|
`sess_${Date.now()}_${Math.random().toString(36).substring(2, 7)}`,
|
||||||
// Ensure startTime is not undefined
|
// Ensure startTime is not undefined
|
||||||
startTime: session.startTime || new Date()
|
startTime: session.startTime || new Date(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Only include fields that are properly typed for Prisma
|
// Only include fields that are properly typed for Prisma
|
||||||
@ -69,16 +81,18 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
ipAddress: session.ipAddress || null,
|
ipAddress: session.ipAddress || null,
|
||||||
country: session.country || null,
|
country: session.country || null,
|
||||||
language: session.language || null,
|
language: session.language || null,
|
||||||
sentiment: typeof session.sentiment === 'number' ? session.sentiment : null,
|
sentiment:
|
||||||
messagesSent: typeof session.messagesSent === 'number' ? session.messagesSent : 0,
|
typeof session.sentiment === "number" ? session.sentiment : null,
|
||||||
category: session.category || null
|
messagesSent:
|
||||||
}
|
typeof session.messagesSent === "number" ? session.messagesSent : 0,
|
||||||
|
category: session.category || null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ ok: true, imported: sessions.length });
|
res.json({ ok: true, imported: sessions.length });
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
const error = e instanceof Error ? e.message : 'An unknown error occurred';
|
const error = e instanceof Error ? e.message : "An unknown error occurred";
|
||||||
res.status(500).json({ error });
|
res.status(500).json({ error });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ export const authOptions: NextAuthOptions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: credentials.email }
|
where: { email: credentials.email },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
@ -63,7 +63,21 @@ export const authOptions: NextAuthOptions = {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
session: { strategy: "jwt" },
|
session: {
|
||||||
|
strategy: "jwt",
|
||||||
|
maxAge: 30 * 24 * 60 * 60, // 30 days
|
||||||
|
},
|
||||||
|
cookies: {
|
||||||
|
sessionToken: {
|
||||||
|
name: `next-auth.session-token`,
|
||||||
|
options: {
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "lax",
|
||||||
|
path: "/",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
callbacks: {
|
callbacks: {
|
||||||
async jwt({ token, user }) {
|
async jwt({ token, user }) {
|
||||||
if (user) {
|
if (user) {
|
||||||
@ -83,7 +97,8 @@ export const authOptions: NextAuthOptions = {
|
|||||||
pages: {
|
pages: {
|
||||||
signIn: "/login",
|
signIn: "/login",
|
||||||
},
|
},
|
||||||
secret: process.env.NEXTAUTH_SECRET || "fallback-secret-key-change-in-production",
|
secret: process.env.NEXTAUTH_SECRET,
|
||||||
|
debug: process.env.NODE_ENV === "development",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default NextAuth(authOptions);
|
export default NextAuth(authOptions);
|
||||||
|
|||||||
@ -4,12 +4,15 @@ import { getServerSession } from "next-auth";
|
|||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
import { authOptions } from "../auth/[...nextauth]";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
if (!session?.user) return res.status(401).json({ error: "Not logged in" });
|
if (!session?.user) return res.status(401).json({ error: "Not logged in" });
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email as string }
|
where: { email: session.user.email as string },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return res.status(401).json({ error: "No user" });
|
if (!user) return res.status(401).json({ error: "No user" });
|
||||||
@ -18,7 +21,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const { csvUrl } = req.body;
|
const { csvUrl } = req.body;
|
||||||
await prisma.company.update({
|
await prisma.company.update({
|
||||||
where: { id: user.companyId },
|
where: { id: user.companyId },
|
||||||
data: { csvUrl }
|
data: { csvUrl },
|
||||||
});
|
});
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -16,20 +16,24 @@ interface SessionData {
|
|||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
res: NextApiResponse
|
res: NextApiResponse,
|
||||||
) {
|
) {
|
||||||
const session = await getServerSession(req, res, authOptions) as SessionData | null;
|
const session = (await getServerSession(
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
authOptions,
|
||||||
|
)) as SessionData | null;
|
||||||
if (!session?.user) return res.status(401).json({ error: "Not logged in" });
|
if (!session?.user) return res.status(401).json({ error: "Not logged in" });
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email },
|
where: { email: session.user.email },
|
||||||
include: { company: true }
|
include: { company: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return res.status(401).json({ error: "No user" });
|
if (!user) return res.status(401).json({ error: "No user" });
|
||||||
|
|
||||||
const sessions = await prisma.session.findMany({
|
const sessions = await prisma.session.findMany({
|
||||||
where: { companyId: user.companyId }
|
where: { companyId: user.companyId },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Pass company config to metrics
|
// Pass company config to metrics
|
||||||
@ -39,6 +43,6 @@ export default async function handler(
|
|||||||
res.json({
|
res.json({
|
||||||
metrics,
|
metrics,
|
||||||
csvUrl: user.company.csvUrl,
|
csvUrl: user.company.csvUrl,
|
||||||
company: user.company
|
company: user.company,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,16 @@ import { getServerSession } from "next-auth";
|
|||||||
import { prisma } from "../../../lib/prisma";
|
import { prisma } from "../../../lib/prisma";
|
||||||
import { authOptions } from "../auth/[...nextauth]";
|
import { authOptions } from "../auth/[...nextauth]";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
if (!session?.user || session.user.role !== "admin")
|
if (!session?.user || session.user.role !== "admin")
|
||||||
return res.status(403).json({ error: "Forbidden" });
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email as string }
|
where: { email: session.user.email as string },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return res.status(401).json({ error: "No user" });
|
if (!user) return res.status(401).json({ error: "No user" });
|
||||||
@ -22,8 +25,10 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
csvUrl,
|
csvUrl,
|
||||||
csvUsername,
|
csvUsername,
|
||||||
...(csvPassword ? { csvPassword } : {}),
|
...(csvPassword ? { csvPassword } : {}),
|
||||||
sentimentAlert: sentimentThreshold ? parseFloat(sentimentThreshold) : null,
|
sentimentAlert: sentimentThreshold
|
||||||
}
|
? parseFloat(sentimentThreshold)
|
||||||
|
: null,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -11,33 +11,36 @@ interface UserBasicInfo {
|
|||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
const session = await getServerSession(req, res, authOptions);
|
const session = await getServerSession(req, res, authOptions);
|
||||||
if (!session?.user || session.user.role !== "admin")
|
if (!session?.user || session.user.role !== "admin")
|
||||||
return res.status(403).json({ error: "Forbidden" });
|
return res.status(403).json({ error: "Forbidden" });
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: { email: session.user.email as string }
|
where: { email: session.user.email as string },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return res.status(401).json({ error: "No user" });
|
if (!user) return res.status(401).json({ error: "No user" });
|
||||||
|
|
||||||
if (req.method === "GET") {
|
if (req.method === "GET") {
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
where: { companyId: user.companyId }
|
where: { companyId: user.companyId },
|
||||||
});
|
});
|
||||||
|
|
||||||
const mappedUsers: UserBasicInfo[] = users.map(u => ({
|
const mappedUsers: UserBasicInfo[] = users.map((u) => ({
|
||||||
id: u.id,
|
id: u.id,
|
||||||
email: u.email,
|
email: u.email,
|
||||||
role: u.role
|
role: u.role,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
res.json({ users: mappedUsers });
|
res.json({ users: mappedUsers });
|
||||||
}
|
} else if (req.method === "POST") {
|
||||||
else if (req.method === "POST") {
|
|
||||||
const { email, role } = req.body;
|
const { email, role } = req.body;
|
||||||
if (!email || !role) return res.status(400).json({ error: "Missing fields" });
|
if (!email || !role)
|
||||||
|
return res.status(400).json({ error: "Missing fields" });
|
||||||
const exists = await prisma.user.findUnique({ where: { email } });
|
const exists = await prisma.user.findUnique({ where: { email } });
|
||||||
if (exists) return res.status(409).json({ error: "Email exists" });
|
if (exists) return res.status(409).json({ error: "Email exists" });
|
||||||
const tempPassword = Math.random().toString(36).slice(-8); // random initial password
|
const tempPassword = Math.random().toString(36).slice(-8); // random initial password
|
||||||
@ -47,10 +50,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
password: await bcrypt.hash(tempPassword, 10),
|
password: await bcrypt.hash(tempPassword, 10),
|
||||||
companyId: user.companyId,
|
companyId: user.companyId,
|
||||||
role,
|
role,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
// TODO: Email user their temp password (stub, for demo)
|
// TODO: Email user their temp password (stub, for demo)
|
||||||
res.json({ ok: true, tempPassword });
|
res.json({ ok: true, tempPassword });
|
||||||
}
|
} else res.status(405).end();
|
||||||
else res.status(405).end();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import { prisma } from "../../lib/prisma";
|
import { prisma } from "../../lib/prisma";
|
||||||
import { sendEmail } from "../../lib/sendEmail";
|
import { sendEmail } from "../../lib/sendEmail";
|
||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { IncomingMessage, ServerResponse } from 'http';
|
import type { IncomingMessage, ServerResponse } from "http";
|
||||||
|
|
||||||
type NextApiRequest = IncomingMessage & {
|
type NextApiRequest = IncomingMessage & {
|
||||||
body: {
|
body: {
|
||||||
@ -16,7 +16,10 @@ type NextApiResponse = ServerResponse & {
|
|||||||
end: () => void;
|
end: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
if (req.method !== "POST") return res.status(405).end();
|
if (req.method !== "POST") return res.status(405).end();
|
||||||
const { email } = req.body;
|
const { email } = req.body;
|
||||||
const user = await prisma.user.findUnique({ where: { email } });
|
const user = await prisma.user.findUnique({ where: { email } });
|
||||||
|
|||||||
@ -10,7 +10,10 @@ interface RegisterRequestBody {
|
|||||||
csvUrl?: string;
|
csvUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse<ApiResponse<{ success: boolean; } | { error: string; }>>) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse<ApiResponse<{ success: boolean } | { error: string }>>,
|
||||||
|
) {
|
||||||
if (req.method !== "POST") return res.status(405).end();
|
if (req.method !== "POST") return res.status(405).end();
|
||||||
|
|
||||||
const { email, password, company, csvUrl } = req.body as RegisterRequestBody;
|
const { email, password, company, csvUrl } = req.body as RegisterRequestBody;
|
||||||
@ -18,19 +21,19 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
if (!email || !password || !company) {
|
if (!email || !password || !company) {
|
||||||
return res.status(400).json({
|
return res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Missing required fields"
|
error: "Missing required fields",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if email exists
|
// Check if email exists
|
||||||
const exists = await prisma.user.findUnique({
|
const exists = await prisma.user.findUnique({
|
||||||
where: { email }
|
where: { email },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exists) {
|
if (exists) {
|
||||||
return res.status(409).json({
|
return res.status(409).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Email already exists"
|
error: "Email already exists",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -48,6 +51,6 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse<
|
|||||||
});
|
});
|
||||||
res.status(201).json({
|
res.status(201).json({
|
||||||
success: true,
|
success: true,
|
||||||
data: { success: true }
|
data: { success: true },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { prisma } from "../../lib/prisma";
|
import { prisma } from "../../lib/prisma";
|
||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import type { IncomingMessage, ServerResponse } from 'http';
|
import type { IncomingMessage, ServerResponse } from "http";
|
||||||
|
|
||||||
type NextApiRequest = IncomingMessage & {
|
type NextApiRequest = IncomingMessage & {
|
||||||
body: {
|
body: {
|
||||||
@ -16,14 +16,17 @@ type NextApiResponse = ServerResponse & {
|
|||||||
end: () => void;
|
end: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
if (req.method !== "POST") return res.status(405).end();
|
if (req.method !== "POST") return res.status(405).end();
|
||||||
const { token, password } = req.body;
|
const { token, password } = req.body;
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
resetToken: token,
|
resetToken: token,
|
||||||
resetTokenExpiry: { gte: new Date() }
|
resetTokenExpiry: { gte: new Date() },
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
if (!user) return res.status(400).json({ error: "Invalid or expired token" });
|
if (!user) return res.status(400).json({ error: "Invalid or expired token" });
|
||||||
|
|
||||||
@ -34,7 +37,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
password: hash,
|
password: hash,
|
||||||
resetToken: null,
|
resetToken: null,
|
||||||
resetTokenExpiry: null,
|
resetTokenExpiry: null,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
res.status(200).end();
|
res.status(200).end();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
plugins: {
|
plugins: {
|
||||||
'@tailwindcss/postcss': {},
|
"@tailwindcss/postcss": {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
50
prisma/migrations/20250521191702_init/migration.sql
Normal file
50
prisma/migrations/20250521191702_init/migration.sql
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Company" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"name" TEXT NOT NULL,
|
||||||
|
"csvUrl" TEXT NOT NULL,
|
||||||
|
"csvUsername" TEXT,
|
||||||
|
"csvPassword" TEXT,
|
||||||
|
"sentimentAlert" REAL,
|
||||||
|
"dashboardOpts" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
"updatedAt" DATETIME NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "User" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"email" TEXT NOT NULL,
|
||||||
|
"password" TEXT NOT NULL,
|
||||||
|
"companyId" TEXT NOT NULL,
|
||||||
|
"role" TEXT NOT NULL,
|
||||||
|
"resetToken" TEXT,
|
||||||
|
"resetTokenExpiry" DATETIME,
|
||||||
|
CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "Session" (
|
||||||
|
"id" TEXT NOT NULL PRIMARY KEY,
|
||||||
|
"companyId" TEXT NOT NULL,
|
||||||
|
"startTime" DATETIME NOT NULL,
|
||||||
|
"endTime" DATETIME NOT NULL,
|
||||||
|
"ipAddress" TEXT,
|
||||||
|
"country" TEXT,
|
||||||
|
"language" TEXT,
|
||||||
|
"messagesSent" INTEGER,
|
||||||
|
"sentiment" REAL,
|
||||||
|
"escalated" BOOLEAN,
|
||||||
|
"forwardedHr" BOOLEAN,
|
||||||
|
"fullTranscriptUrl" TEXT,
|
||||||
|
"avgResponseTime" REAL,
|
||||||
|
"tokens" INTEGER,
|
||||||
|
"tokensEur" REAL,
|
||||||
|
"category" TEXT,
|
||||||
|
"initialMsg" TEXT,
|
||||||
|
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT "Session_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
|
||||||
3
prisma/migrations/migration_lock.toml
Normal file
3
prisma/migrations/migration_lock.toml
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Please do not edit this file manually
|
||||||
|
# It should be added in your version-control system (e.g., Git)
|
||||||
|
provider = "sqlite"
|
||||||
39
prisma/seed.js
Normal file
39
prisma/seed.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// seed.js - Create initial admin user and company
|
||||||
|
import { PrismaClient } from '@prisma/client';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an admin user
|
||||||
|
const hashedPassword = await bcrypt.hash('admin123', 10);
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: 'admin@demo.com',
|
||||||
|
password: hashedPassword,
|
||||||
|
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)');
|
||||||
|
}
|
||||||
|
|
||||||
|
main()
|
||||||
|
.catch(e => {
|
||||||
|
console.error('Error seeding database:', e);
|
||||||
|
process.exit(1);
|
||||||
|
})
|
||||||
|
.finally(async () => {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
});
|
||||||
39
prisma/seed.mjs
Normal file
39
prisma/seed.mjs
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// Seed script for creating initial data
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
// 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
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an admin user
|
||||||
|
const hashedPassword = await bcrypt.hash("admin123", 10);
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: "admin@demo.com",
|
||||||
|
password: hashedPassword,
|
||||||
|
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)");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error seeding database:", error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
39
prisma/seed.ts
Normal file
39
prisma/seed.ts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
// seed.ts - Create initial admin user and company
|
||||||
|
import { PrismaClient } from "@prisma/client";
|
||||||
|
import bcrypt from "bcryptjs";
|
||||||
|
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
// 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
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create an admin user
|
||||||
|
const hashedPassword = await bcrypt.hash("admin123", 10);
|
||||||
|
await prisma.user.create({
|
||||||
|
data: {
|
||||||
|
email: "admin@demo.com",
|
||||||
|
password: hashedPassword,
|
||||||
|
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)");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error seeding database:", error);
|
||||||
|
process.exit(1);
|
||||||
|
} finally {
|
||||||
|
await prisma.$disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@ -1,9 +1,9 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
content: [
|
content: [
|
||||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {},
|
extend: {},
|
||||||
|
|||||||
@ -1,14 +1,10 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es5",
|
"target": "es5",
|
||||||
"lib": [
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
"dom",
|
|
||||||
"dom.iterable",
|
|
||||||
"esnext"
|
|
||||||
],
|
|
||||||
"allowJs": true,
|
"allowJs": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"strict": false, // Temporarily disabled strict mode
|
"strict": true,
|
||||||
"noImplicitAny": false, // Allow implicit any types
|
"noImplicitAny": false, // Allow implicit any types
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
@ -25,19 +21,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": ["./*"]
|
||||||
"./*"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"strictNullChecks": true
|
"strictNullChecks": true
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
"next-env.d.ts",
|
"exclude": ["node_modules"]
|
||||||
"**/*.ts",
|
|
||||||
"**/*.tsx",
|
|
||||||
".next/types/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"node_modules"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user