Adds in dashboard page showing basic Mantis statistics
This commit is contained in:
parent
7564937faa
commit
92230f8a07
13 changed files with 595 additions and 19 deletions
|
@ -20,6 +20,7 @@
|
||||||
"@quixo3/prisma-session-store": "^3.1.13",
|
"@quixo3/prisma-session-store": "^3.1.13",
|
||||||
"@simplewebauthn/browser": "^13.1.0",
|
"@simplewebauthn/browser": "^13.1.0",
|
||||||
"@simplewebauthn/server": "^13.1.1",
|
"@simplewebauthn/server": "^13.1.1",
|
||||||
|
"apexcharts": "^4.7.0",
|
||||||
"axios": "^1.8.4",
|
"axios": "^1.8.4",
|
||||||
"better-sqlite3": "^11.9.1",
|
"better-sqlite3": "^11.9.1",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
|
@ -38,9 +39,11 @@
|
||||||
"pino-http": "^10.4.0",
|
"pino-http": "^10.4.0",
|
||||||
"pino-pretty": "^13.0.0",
|
"pino-pretty": "^13.0.0",
|
||||||
"quasar": "^2.16.0",
|
"quasar": "^2.16.0",
|
||||||
|
"superjson": "^2.2.2",
|
||||||
"uuid": "^11.1.0",
|
"uuid": "^11.1.0",
|
||||||
"vue": "^3.4.18",
|
"vue": "^3.4.18",
|
||||||
"vue-router": "^4.0.0"
|
"vue-router": "^4.0.0",
|
||||||
|
"vue3-apexcharts": "^1.8.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.25.1",
|
"@eslint/js": "^9.25.1",
|
||||||
|
|
81
pnpm-lock.yaml
generated
81
pnpm-lock.yaml
generated
|
@ -29,6 +29,9 @@ importers:
|
||||||
'@simplewebauthn/server':
|
'@simplewebauthn/server':
|
||||||
specifier: ^13.1.1
|
specifier: ^13.1.1
|
||||||
version: 13.1.1
|
version: 13.1.1
|
||||||
|
apexcharts:
|
||||||
|
specifier: ^4.7.0
|
||||||
|
version: 4.7.0
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.8.4
|
specifier: ^1.8.4
|
||||||
version: 1.8.4
|
version: 1.8.4
|
||||||
|
@ -83,6 +86,9 @@ importers:
|
||||||
quasar:
|
quasar:
|
||||||
specifier: ^2.16.0
|
specifier: ^2.16.0
|
||||||
version: 2.18.1
|
version: 2.18.1
|
||||||
|
superjson:
|
||||||
|
specifier: ^2.2.2
|
||||||
|
version: 2.2.2
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^11.1.0
|
specifier: ^11.1.0
|
||||||
version: 11.1.0
|
version: 11.1.0
|
||||||
|
@ -92,6 +98,9 @@ importers:
|
||||||
vue-router:
|
vue-router:
|
||||||
specifier: ^4.0.0
|
specifier: ^4.0.0
|
||||||
version: 4.5.0(vue@3.5.13(typescript@5.8.3))
|
version: 4.5.0(vue@3.5.13(typescript@5.8.3))
|
||||||
|
vue3-apexcharts:
|
||||||
|
specifier: ^1.8.0
|
||||||
|
version: 1.8.0(apexcharts@4.7.0)(vue@3.5.13(typescript@5.8.3))
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.25.1
|
specifier: ^9.25.1
|
||||||
|
@ -677,6 +686,31 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
eslint: '>=9.0.0'
|
eslint: '>=9.0.0'
|
||||||
|
|
||||||
|
'@svgdotjs/svg.draggable.js@3.0.6':
|
||||||
|
resolution: {integrity: sha512-7iJFm9lL3C40HQcqzEfezK2l+dW2CpoVY3b77KQGqc8GXWa6LhhmX5Ckv7alQfUXBuZbjpICZ+Dvq1czlGx7gA==}
|
||||||
|
peerDependencies:
|
||||||
|
'@svgdotjs/svg.js': ^3.2.4
|
||||||
|
|
||||||
|
'@svgdotjs/svg.filter.js@3.0.9':
|
||||||
|
resolution: {integrity: sha512-/69XMRCDoam2HgC4ldHIaDgeQf1ViHIsa0Ld4uWgiXtZ+E24DWHe/9Ib6kbNiZ7WRIdlVokUDR1Fg0kjIpkfbw==}
|
||||||
|
engines: {node: '>= 0.8.0'}
|
||||||
|
|
||||||
|
'@svgdotjs/svg.js@3.2.4':
|
||||||
|
resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==}
|
||||||
|
|
||||||
|
'@svgdotjs/svg.resize.js@2.0.5':
|
||||||
|
resolution: {integrity: sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==}
|
||||||
|
engines: {node: '>= 14.18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@svgdotjs/svg.js': ^3.2.4
|
||||||
|
'@svgdotjs/svg.select.js': ^4.0.1
|
||||||
|
|
||||||
|
'@svgdotjs/svg.select.js@4.0.2':
|
||||||
|
resolution: {integrity: sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g==}
|
||||||
|
engines: {node: '>= 14.18'}
|
||||||
|
peerDependencies:
|
||||||
|
'@svgdotjs/svg.js': ^3.2.4
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
|
||||||
|
|
||||||
|
@ -831,6 +865,9 @@ packages:
|
||||||
'@vue/shared@3.5.13':
|
'@vue/shared@3.5.13':
|
||||||
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
|
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
|
||||||
|
|
||||||
|
'@yr/monotone-cubic-spline@1.0.3':
|
||||||
|
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0:
|
||||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||||
engines: {node: '>=6.5'}
|
engines: {node: '>=6.5'}
|
||||||
|
@ -880,6 +917,9 @@ packages:
|
||||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
apexcharts@4.7.0:
|
||||||
|
resolution: {integrity: sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==}
|
||||||
|
|
||||||
archiver-utils@5.0.2:
|
archiver-utils@5.0.2:
|
||||||
resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==}
|
resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==}
|
||||||
engines: {node: '>= 14'}
|
engines: {node: '>= 14'}
|
||||||
|
@ -3075,6 +3115,12 @@ packages:
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
vue: ^3.2.0
|
vue: ^3.2.0
|
||||||
|
|
||||||
|
vue3-apexcharts@1.8.0:
|
||||||
|
resolution: {integrity: sha512-5tSD4mXTBbIJ9ir+58qHE6oNtIe0RNgqIRYMKpcsIaxkKtwUww4JhvPkpUFlmiW4OJbbdklgjleXq1lfcM4gdA==}
|
||||||
|
peerDependencies:
|
||||||
|
apexcharts: '>=4.0.0'
|
||||||
|
vue: '>=3.0.0'
|
||||||
|
|
||||||
vue@3.5.13:
|
vue@3.5.13:
|
||||||
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
|
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -3676,6 +3722,25 @@ snapshots:
|
||||||
- supports-color
|
- supports-color
|
||||||
- typescript
|
- typescript
|
||||||
|
|
||||||
|
'@svgdotjs/svg.draggable.js@3.0.6(@svgdotjs/svg.js@3.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@svgdotjs/svg.js': 3.2.4
|
||||||
|
|
||||||
|
'@svgdotjs/svg.filter.js@3.0.9':
|
||||||
|
dependencies:
|
||||||
|
'@svgdotjs/svg.js': 3.2.4
|
||||||
|
|
||||||
|
'@svgdotjs/svg.js@3.2.4': {}
|
||||||
|
|
||||||
|
'@svgdotjs/svg.resize.js@2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4))':
|
||||||
|
dependencies:
|
||||||
|
'@svgdotjs/svg.js': 3.2.4
|
||||||
|
'@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4)
|
||||||
|
|
||||||
|
'@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4)':
|
||||||
|
dependencies:
|
||||||
|
'@svgdotjs/svg.js': 3.2.4
|
||||||
|
|
||||||
'@swc/helpers@0.5.17':
|
'@swc/helpers@0.5.17':
|
||||||
dependencies:
|
dependencies:
|
||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
|
@ -3893,6 +3958,8 @@ snapshots:
|
||||||
|
|
||||||
'@vue/shared@3.5.13': {}
|
'@vue/shared@3.5.13': {}
|
||||||
|
|
||||||
|
'@yr/monotone-cubic-spline@1.0.3': {}
|
||||||
|
|
||||||
abort-controller@3.0.0:
|
abort-controller@3.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
event-target-shim: 5.0.1
|
event-target-shim: 5.0.1
|
||||||
|
@ -3936,6 +4003,15 @@ snapshots:
|
||||||
normalize-path: 3.0.0
|
normalize-path: 3.0.0
|
||||||
picomatch: 2.3.1
|
picomatch: 2.3.1
|
||||||
|
|
||||||
|
apexcharts@4.7.0:
|
||||||
|
dependencies:
|
||||||
|
'@svgdotjs/svg.draggable.js': 3.0.6(@svgdotjs/svg.js@3.2.4)
|
||||||
|
'@svgdotjs/svg.filter.js': 3.0.9
|
||||||
|
'@svgdotjs/svg.js': 3.2.4
|
||||||
|
'@svgdotjs/svg.resize.js': 2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4))
|
||||||
|
'@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4)
|
||||||
|
'@yr/monotone-cubic-spline': 1.0.3
|
||||||
|
|
||||||
archiver-utils@5.0.2:
|
archiver-utils@5.0.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
glob: 10.4.5
|
glob: 10.4.5
|
||||||
|
@ -6181,6 +6257,11 @@ snapshots:
|
||||||
'@vue/devtools-api': 6.6.4
|
'@vue/devtools-api': 6.6.4
|
||||||
vue: 3.5.13(typescript@5.8.3)
|
vue: 3.5.13(typescript@5.8.3)
|
||||||
|
|
||||||
|
vue3-apexcharts@1.8.0(apexcharts@4.7.0)(vue@3.5.13(typescript@5.8.3)):
|
||||||
|
dependencies:
|
||||||
|
apexcharts: 4.7.0
|
||||||
|
vue: 3.5.13(typescript@5.8.3)
|
||||||
|
|
||||||
vue@3.5.13(typescript@5.8.3):
|
vue@3.5.13(typescript@5.8.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vue/compiler-dom': 3.5.13
|
'@vue/compiler-dom': 3.5.13
|
||||||
|
|
|
@ -13,6 +13,7 @@ export default defineConfig((/* ctx */) =>
|
||||||
// --> boot files are part of "main.js"
|
// --> boot files are part of "main.js"
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
// https://v2.quasar.dev/quasar-cli-vite/boot-files
|
||||||
boot: [
|
boot: [
|
||||||
|
'apexcharts'
|
||||||
],
|
],
|
||||||
|
|
||||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
|
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
|
||||||
|
|
|
@ -3,6 +3,7 @@ import { PrismaClient } from '@prisma/client'; // Import Prisma Client
|
||||||
import { getMantisSettings, saveTicketToDatabase } from '../services/mantisDownloader.js';
|
import { getMantisSettings, saveTicketToDatabase } from '../services/mantisDownloader.js';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import reader from '@kenjiuno/msgreader';
|
import reader from '@kenjiuno/msgreader';
|
||||||
|
import SuperJSON from 'superjson';
|
||||||
const MsgReader = reader.default;
|
const MsgReader = reader.default;
|
||||||
|
|
||||||
const prisma = new PrismaClient(); // Instantiate Prisma Client
|
const prisma = new PrismaClient(); // Instantiate Prisma Client
|
||||||
|
@ -281,4 +282,78 @@ router.get('/msg-extract/:ticketId/:attachmentId/:innerAttachmentId', async(req,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /mantis/stats/issues - Get daily count of new Mantis issues (last 7 days)
|
||||||
|
router.get('/stats/issues', async(req, res) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Calculate the date range (last 7 days)
|
||||||
|
const endDate = new Date();
|
||||||
|
endDate.setHours(23, 59, 59, 999); // End of today
|
||||||
|
|
||||||
|
const startDate = new Date();
|
||||||
|
startDate.setDate(startDate.getDate() - 6); // 7 days ago
|
||||||
|
startDate.setHours(0, 0, 0, 0); // Start of the day
|
||||||
|
|
||||||
|
// Query for daily counts of issues created in the last 7 days
|
||||||
|
const dailyIssues = await prisma.$queryRaw`
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) as date,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM
|
||||||
|
"MantisIssue"
|
||||||
|
WHERE
|
||||||
|
created_at >= ${startDate} AND created_at <= ${endDate}
|
||||||
|
GROUP BY
|
||||||
|
DATE(created_at)
|
||||||
|
ORDER BY
|
||||||
|
date ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.status(200).json(dailyIssues);
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Error fetching Mantis issue statistics:', error.message);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch Mantis issue statistics' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// GET /mantis/stats/comments - Get daily count of new Mantis comments (last 7 days)
|
||||||
|
router.get('/stats/comments', async(req, res) =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Calculate the date range (last 7 days)
|
||||||
|
const endDate = new Date();
|
||||||
|
endDate.setHours(23, 59, 59, 999); // End of today
|
||||||
|
|
||||||
|
const startDate = new Date();
|
||||||
|
startDate.setDate(startDate.getDate() - 6); // 7 days ago
|
||||||
|
startDate.setHours(0, 0, 0, 0); // Start of the day
|
||||||
|
|
||||||
|
// Query for daily counts of comments created in the last 7 days
|
||||||
|
const dailyComments = await prisma.$queryRaw`
|
||||||
|
SELECT
|
||||||
|
DATE(created_at) as date,
|
||||||
|
COUNT(*) as count
|
||||||
|
FROM
|
||||||
|
"MantisComment"
|
||||||
|
WHERE
|
||||||
|
created_at >= ${startDate} AND created_at <= ${endDate}
|
||||||
|
GROUP BY
|
||||||
|
DATE(created_at)
|
||||||
|
ORDER BY
|
||||||
|
date ASC
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.status(200).json(dailyComments);
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Error fetching Mantis comment statistics:', error.message);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch Mantis comment statistics' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export default router;
|
export default router;
|
|
@ -22,7 +22,7 @@ import authRoutes from './routes/auth.js';
|
||||||
import chatRoutes from './routes/chat.js';
|
import chatRoutes from './routes/chat.js';
|
||||||
import settingsRoutes from './routes/settings.js';
|
import settingsRoutes from './routes/settings.js';
|
||||||
import userPreferencesRoutes from './routes/userPreferences.js';
|
import userPreferencesRoutes from './routes/userPreferences.js';
|
||||||
import mantisRoutes from './routes/mantis.js'; // Import Mantis routes
|
import mantisRoutes from './routes/mantis.js';
|
||||||
import cron from 'node-cron';
|
import cron from 'node-cron';
|
||||||
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
import { generateAndStoreMantisSummary } from './services/mantisSummarizer.js';
|
||||||
import { requireAuth } from './middlewares/authMiddleware.js';
|
import { requireAuth } from './middlewares/authMiddleware.js';
|
||||||
|
@ -30,6 +30,7 @@ import { requireAuth } from './middlewares/authMiddleware.js';
|
||||||
import { setup as setupMantisDownloader } from './services/mantisDownloader.js';
|
import { setup as setupMantisDownloader } from './services/mantisDownloader.js';
|
||||||
|
|
||||||
import { logger } from './utils/logging.js';
|
import { logger } from './utils/logging.js';
|
||||||
|
import SuperJSON from 'superjson';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
|
@ -100,12 +101,26 @@ app.disable('x-powered-by');
|
||||||
// Add JSON body parsing middleware
|
// Add JSON body parsing middleware
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
|
|
||||||
|
app.use((req, res, next) =>
|
||||||
|
{
|
||||||
|
res.json = (data) =>
|
||||||
|
{
|
||||||
|
if (res.headersSent)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.setHeader('Content-Type', 'application/json');
|
||||||
|
res.send(SuperJSON.stringify(data));
|
||||||
|
};
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
// Add API routes
|
// Add API routes
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/chat', requireAuth, chatRoutes);
|
app.use('/api/chat', requireAuth, chatRoutes);
|
||||||
app.use('/api/user-preferences', requireAuth, userPreferencesRoutes);
|
app.use('/api/user-preferences', requireAuth, userPreferencesRoutes);
|
||||||
app.use('/api/settings', requireAuth, settingsRoutes);
|
app.use('/api/settings', requireAuth, settingsRoutes);
|
||||||
app.use('/api/mantis', requireAuth, mantisRoutes); // Register Mantis routes
|
app.use('/api/mantis', requireAuth, mantisRoutes);
|
||||||
app.use('/api', requireAuth, apiRoutes);
|
app.use('/api', requireAuth, apiRoutes);
|
||||||
|
|
||||||
if (process.env.PROD)
|
if (process.env.PROD)
|
||||||
|
|
8
src/boot/apexcharts.js
Normal file
8
src/boot/apexcharts.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { defineBoot } from '#q-app/wrappers';
|
||||||
|
|
||||||
|
import VueApexCharts from 'vue3-apexcharts';
|
||||||
|
|
||||||
|
export default defineBoot(async({app}) =>
|
||||||
|
{
|
||||||
|
app.use(VueApexCharts);
|
||||||
|
});
|
|
@ -1,14 +1,30 @@
|
||||||
import { boot } from 'quasar/wrappers';
|
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
// Be careful when using SSR for cross-request state pollution
|
import SuperJSON from 'superjson';
|
||||||
// due to creating a Singleton instance here;
|
|
||||||
// If any client changes this (global) instance, it might be a
|
|
||||||
// good idea to move this instance creation inside of the
|
|
||||||
// "export default () => {}" function below (which runs individually
|
|
||||||
// for each client)
|
|
||||||
|
|
||||||
axios.defaults.withCredentials = true; // Enable sending cookies with requests
|
axios.defaults.withCredentials = true; // Enable sending cookies with requests
|
||||||
|
|
||||||
// Export the API instance so you can import it easily elsewhere, e.g. stores
|
axios.interceptors.response.use(
|
||||||
|
(response) =>
|
||||||
|
{
|
||||||
|
//If the response content type is application/json, we want to parse it with SuperJSON
|
||||||
|
if (response.headers['content-type'] && response.headers['content-type'].includes('application/json'))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
response.data = SuperJSON.parse(JSON.stringify(response.data));
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Error deserializing response data:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return response;
|
||||||
|
},
|
||||||
|
(error) =>
|
||||||
|
{
|
||||||
|
// Handle errors here if needed
|
||||||
|
return Promise.reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
export default axios;
|
export default axios;
|
|
@ -21,4 +21,8 @@ body {
|
||||||
.bg-blurred {
|
.bg-blurred {
|
||||||
background: rgba(0, 0, 0, 0.5);
|
background: rgba(0, 0, 0, 0.5);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-shadow {
|
||||||
|
filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
|
||||||
}
|
}
|
366
src/pages/DashboardPage.vue
Normal file
366
src/pages/DashboardPage.vue
Normal file
|
@ -0,0 +1,366 @@
|
||||||
|
<template>
|
||||||
|
<q-page padding>
|
||||||
|
<div class="q-pa-md">
|
||||||
|
<div class="text-h4 q-mb-md">
|
||||||
|
Dashboard
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row q-col-gutter-md">
|
||||||
|
<!-- Mantis Issues Chart -->
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-card class="dashboard-card">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">
|
||||||
|
New Mantis Issues
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle2">
|
||||||
|
Last 7 days
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<div
|
||||||
|
v-if="loadingIssues"
|
||||||
|
class="flex flex-center q-pa-xl"
|
||||||
|
>
|
||||||
|
<q-spinner
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="issueChartData.series[0].data.length === 0"
|
||||||
|
class="text-center q-pa-md"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="info"
|
||||||
|
color="grey"
|
||||||
|
size="3em"
|
||||||
|
/>
|
||||||
|
<div class="text-grey q-mt-md">
|
||||||
|
No data available for this period
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<apexchart
|
||||||
|
type="bar"
|
||||||
|
height="300"
|
||||||
|
:options="issueChartOptions"
|
||||||
|
:series="issueChartData.series"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Mantis Comments Chart -->
|
||||||
|
<div class="col-12 col-md-6">
|
||||||
|
<q-card class="dashboard-card">
|
||||||
|
<q-card-section>
|
||||||
|
<div class="text-h6">
|
||||||
|
New Mantis Comments
|
||||||
|
</div>
|
||||||
|
<div class="text-subtitle2">
|
||||||
|
Last 7 days
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
|
||||||
|
<q-card-section>
|
||||||
|
<div
|
||||||
|
v-if="loadingComments"
|
||||||
|
class="flex flex-center q-pa-xl"
|
||||||
|
>
|
||||||
|
<q-spinner
|
||||||
|
color="primary"
|
||||||
|
size="3em"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-else-if="commentChartData.series[0].data.length === 0"
|
||||||
|
class="text-center q-pa-md"
|
||||||
|
>
|
||||||
|
<q-icon
|
||||||
|
name="info"
|
||||||
|
color="grey"
|
||||||
|
size="3em"
|
||||||
|
/>
|
||||||
|
<div class="text-grey q-mt-md">
|
||||||
|
No data available for this period
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<apexchart
|
||||||
|
type="bar"
|
||||||
|
height="300"
|
||||||
|
:options="commentChartOptions"
|
||||||
|
:series="commentChartData.series"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</q-card-section>
|
||||||
|
</q-card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</q-page>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
|
||||||
|
import { useQuasar } from 'quasar';
|
||||||
|
import { date } from 'quasar';
|
||||||
|
import axios from 'boot/axios';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'DashboardPage',
|
||||||
|
|
||||||
|
setup()
|
||||||
|
{
|
||||||
|
const $q = useQuasar();
|
||||||
|
|
||||||
|
const loadingIssues = ref(true);
|
||||||
|
const loadingComments = ref(true);
|
||||||
|
const issuesData = ref([]);
|
||||||
|
const commentsData = ref([]);
|
||||||
|
|
||||||
|
// Format date as "Monday 25th"
|
||||||
|
const formatDate = (dateStr) =>
|
||||||
|
{
|
||||||
|
const dateObj = new Date(dateStr);
|
||||||
|
const day = date.formatDate(dateObj, 'dddd');
|
||||||
|
const dayNum = dateObj.getDate();
|
||||||
|
|
||||||
|
// Add ordinal suffix (st, nd, rd, th)
|
||||||
|
let suffix = 'th';
|
||||||
|
if (dayNum % 10 === 1 && dayNum !== 11) suffix = 'st';
|
||||||
|
else if (dayNum % 10 === 2 && dayNum !== 12) suffix = 'nd';
|
||||||
|
else if (dayNum % 10 === 3 && dayNum !== 13) suffix = 'rd';
|
||||||
|
|
||||||
|
return `${day} ${dayNum}${suffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Generate an array of dates for the last 7 days
|
||||||
|
const getLast7Days = () =>
|
||||||
|
{
|
||||||
|
const result = [];
|
||||||
|
for (let i = 6; i >= 0; i--)
|
||||||
|
{
|
||||||
|
const d = new Date();
|
||||||
|
d.setDate(d.getDate() - i);
|
||||||
|
result.push(date.formatDate(d, 'YYYY-MM-DD'));
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prepare data with 0 counts for days with no data
|
||||||
|
const prepareChartData = (data) =>
|
||||||
|
{
|
||||||
|
const last7Days = getLast7Days();
|
||||||
|
const countMap = {};
|
||||||
|
|
||||||
|
// Initialize with 0 counts
|
||||||
|
last7Days.forEach(day =>
|
||||||
|
{
|
||||||
|
countMap[day] = 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fill in actual counts
|
||||||
|
data.forEach(item =>
|
||||||
|
{
|
||||||
|
console.log(item);
|
||||||
|
const dateStr = item.date.toISOString().split('T')[0];
|
||||||
|
if (countMap[dateStr] !== undefined)
|
||||||
|
{
|
||||||
|
countMap[dateStr] = Number(item.count);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Convert to arrays for chart
|
||||||
|
const categories = Object.keys(countMap).map(formatDate);
|
||||||
|
const counts = Object.values(countMap);
|
||||||
|
|
||||||
|
return {
|
||||||
|
categories,
|
||||||
|
series: [{
|
||||||
|
name: 'Count',
|
||||||
|
data: counts
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computed properties for chart data
|
||||||
|
const issueChartData = computed(() =>
|
||||||
|
{
|
||||||
|
return prepareChartData(issuesData.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
const commentChartData = computed(() =>
|
||||||
|
{
|
||||||
|
return prepareChartData(commentsData.value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Chart options
|
||||||
|
const getChartOptions = (title, isDark) =>
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
chart: {
|
||||||
|
type: 'bar',
|
||||||
|
height: 300,
|
||||||
|
toolbar: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
foreColor: isDark ? '#ccc' : '#333'
|
||||||
|
},
|
||||||
|
plotOptions: {
|
||||||
|
bar: {
|
||||||
|
borderRadius: 6,
|
||||||
|
columnWidth: '50%',
|
||||||
|
dataLabels: {
|
||||||
|
position: 'top'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors: [isDark ? '#3498db' : '#1976D2'],
|
||||||
|
dataLabels: {
|
||||||
|
enabled: true,
|
||||||
|
offsetY: -20,
|
||||||
|
style: {
|
||||||
|
fontSize: '12px',
|
||||||
|
colors: [isDark ? '#fff' : '#000']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: {
|
||||||
|
borderColor: isDark ? '#555' : '#e0e0e0'
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
categories: issueChartData.value.categories,
|
||||||
|
position: 'bottom',
|
||||||
|
axisBorder: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTicks: {
|
||||||
|
show: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
axisBorder: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
axisTicks: {
|
||||||
|
show: false
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
show: true
|
||||||
|
},
|
||||||
|
min: 0,
|
||||||
|
forceNiceScale: true
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
text: title,
|
||||||
|
align: 'center',
|
||||||
|
style: {
|
||||||
|
fontSize: '16px',
|
||||||
|
color: isDark ? '#fff' : '#333'
|
||||||
|
},
|
||||||
|
floating: false
|
||||||
|
},
|
||||||
|
theme: {
|
||||||
|
mode: isDark ? 'dark' : 'light'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Computed options that change with dark mode
|
||||||
|
const issueChartOptions = computed(() =>
|
||||||
|
{
|
||||||
|
return getChartOptions('New Mantis Issues', $q.dark.isActive);
|
||||||
|
});
|
||||||
|
|
||||||
|
const commentChartOptions = computed(() =>
|
||||||
|
{
|
||||||
|
return getChartOptions('New Mantis Comments', $q.dark.isActive);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch data from API
|
||||||
|
const fetchIssuesData = async() =>
|
||||||
|
{
|
||||||
|
loadingIssues.value = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const response = await axios.get('/api/mantis/stats/issues');
|
||||||
|
issuesData.value = response.data;
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Error fetching issues data:', error);
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Failed to load issues data',
|
||||||
|
icon: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
loadingIssues.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchCommentsData = async() =>
|
||||||
|
{
|
||||||
|
loadingComments.value = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const response = await axios.get('/api/mantis/stats/comments');
|
||||||
|
console.log(response.data);
|
||||||
|
commentsData.value = response.data;
|
||||||
|
}
|
||||||
|
catch (error)
|
||||||
|
{
|
||||||
|
console.error('Error fetching comments data:', error);
|
||||||
|
$q.notify({
|
||||||
|
color: 'negative',
|
||||||
|
message: 'Failed to load comments data',
|
||||||
|
icon: 'warning'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
loadingComments.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Watch for dark mode changes
|
||||||
|
const refreshCharts = () =>
|
||||||
|
{
|
||||||
|
issueChartOptions.value = getChartOptions('New Mantis Issues', $q.dark.isActive);
|
||||||
|
commentChartOptions.value = getChartOptions('New Mantis Comments', $q.dark.isActive);
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => $q.dark.isActive,
|
||||||
|
refreshCharts
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fetch data on component mount
|
||||||
|
onMounted(() =>
|
||||||
|
{
|
||||||
|
fetchIssuesData();
|
||||||
|
fetchCommentsData();
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
loadingIssues,
|
||||||
|
loadingComments,
|
||||||
|
issueChartData,
|
||||||
|
commentChartData,
|
||||||
|
issueChartOptions,
|
||||||
|
commentChartOptions
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.dashboard-card {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -4,7 +4,7 @@
|
||||||
<img
|
<img
|
||||||
src="/stylepoint.png"
|
src="/stylepoint.png"
|
||||||
alt="StylePoint Logo"
|
alt="StylePoint Logo"
|
||||||
class="logo q-mb-md"
|
class="drop-shadow q-mb-md"
|
||||||
style="max-width: 300px; width: 100%;"
|
style="max-width: 300px; width: 100%;"
|
||||||
>
|
>
|
||||||
<h1 class="text-h3 text-weight-bold text-yellow q-mb-sm">
|
<h1 class="text-h3 text-weight-bold text-yellow q-mb-sm">
|
||||||
|
@ -63,10 +63,6 @@ const features = ref([
|
||||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.75);
|
text-shadow: 0 0 10px rgba(0, 0, 0, 0.75);
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo {
|
|
||||||
filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
|
|
||||||
}
|
|
||||||
|
|
||||||
.features {
|
.features {
|
||||||
background-color: rgba(0, 0, 0, 0.25);
|
background-color: rgba(0, 0, 0, 0.25);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<img
|
<img
|
||||||
src="/stylepoint.png"
|
src="/stylepoint.png"
|
||||||
alt="StylePoint Logo"
|
alt="StylePoint Logo"
|
||||||
class="logo q-mb-md absolute"
|
class="drop-shadow q-mb-md absolute"
|
||||||
style="max-width: 300px; width: 100%; top: 75px;"
|
style="max-width: 300px; width: 100%; top: 75px;"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
<img
|
<img
|
||||||
src="/stylepoint.png"
|
src="/stylepoint.png"
|
||||||
alt="StylePoint Logo"
|
alt="StylePoint Logo"
|
||||||
class="logo q-mb-md absolute"
|
class="drop-shadow q-mb-md absolute"
|
||||||
style="max-width: 300px; width: 100%; top: 75px;"
|
style="max-width: 300px; width: 100%; top: 75px;"
|
||||||
>
|
>
|
||||||
<q-card
|
<q-card
|
||||||
|
|
|
@ -33,7 +33,18 @@ const routes = [
|
||||||
caption: 'Create an account'
|
caption: 'Create an account'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
// Add a new route specifically for managing passkeys when logged in
|
{
|
||||||
|
path: '/dashboard',
|
||||||
|
name: 'dashboard',
|
||||||
|
component: () => import('pages/DashboardPage.vue'),
|
||||||
|
meta: {
|
||||||
|
requiresAuth: true,
|
||||||
|
navGroup: 'auth',
|
||||||
|
icon: 'dashboard',
|
||||||
|
title: 'Dashboard',
|
||||||
|
caption: 'Overview and stats'
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/passkeys',
|
path: '/passkeys',
|
||||||
name: 'passkeys',
|
name: 'passkeys',
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue