diff --git a/package.json b/package.json
index 85c8836..2d09c3f 100644
--- a/package.json
+++ b/package.json
@@ -20,6 +20,7 @@
"@quixo3/prisma-session-store": "^3.1.13",
"@simplewebauthn/browser": "^13.1.0",
"@simplewebauthn/server": "^13.1.1",
+ "apexcharts": "^4.7.0",
"axios": "^1.8.4",
"better-sqlite3": "^11.9.1",
"date-fns": "^4.1.0",
@@ -38,9 +39,11 @@
"pino-http": "^10.4.0",
"pino-pretty": "^13.0.0",
"quasar": "^2.16.0",
+ "superjson": "^2.2.2",
"uuid": "^11.1.0",
"vue": "^3.4.18",
- "vue-router": "^4.0.0"
+ "vue-router": "^4.0.0",
+ "vue3-apexcharts": "^1.8.0"
},
"devDependencies": {
"@eslint/js": "^9.25.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8b64008..e8e3e93 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -29,6 +29,9 @@ importers:
'@simplewebauthn/server':
specifier: ^13.1.1
version: 13.1.1
+ apexcharts:
+ specifier: ^4.7.0
+ version: 4.7.0
axios:
specifier: ^1.8.4
version: 1.8.4
@@ -83,6 +86,9 @@ importers:
quasar:
specifier: ^2.16.0
version: 2.18.1
+ superjson:
+ specifier: ^2.2.2
+ version: 2.2.2
uuid:
specifier: ^11.1.0
version: 11.1.0
@@ -92,6 +98,9 @@ importers:
vue-router:
specifier: ^4.0.0
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:
'@eslint/js':
specifier: ^9.25.1
@@ -677,6 +686,31 @@ packages:
peerDependencies:
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':
resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
@@ -831,6 +865,9 @@ packages:
'@vue/shared@3.5.13':
resolution: {integrity: sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==}
+ '@yr/monotone-cubic-spline@1.0.3':
+ resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
+
abort-controller@3.0.0:
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
engines: {node: '>=6.5'}
@@ -880,6 +917,9 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
+ apexcharts@4.7.0:
+ resolution: {integrity: sha512-iZSrrBGvVlL+nt2B1NpqfDuBZ9jX61X9I2+XV0hlYXHtTwhwLTHDKGXjNXAgFBDLuvSYCB/rq2nPWVPRv2DrGA==}
+
archiver-utils@5.0.2:
resolution: {integrity: sha512-wuLJMmIBQYCsGZgYLTy5FIB2pF6Lfb6cXMSF8Qywwk3t20zWnAi7zLcQFdKQmIB8wyZpY5ER38x08GbwtR2cLA==}
engines: {node: '>= 14'}
@@ -3075,6 +3115,12 @@ packages:
peerDependencies:
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:
resolution: {integrity: sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==}
peerDependencies:
@@ -3676,6 +3722,25 @@ snapshots:
- supports-color
- 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':
dependencies:
tslib: 2.8.1
@@ -3893,6 +3958,8 @@ snapshots:
'@vue/shared@3.5.13': {}
+ '@yr/monotone-cubic-spline@1.0.3': {}
+
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
@@ -3936,6 +4003,15 @@ snapshots:
normalize-path: 3.0.0
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:
dependencies:
glob: 10.4.5
@@ -6181,6 +6257,11 @@ snapshots:
'@vue/devtools-api': 6.6.4
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):
dependencies:
'@vue/compiler-dom': 3.5.13
diff --git a/quasar.config.js b/quasar.config.js
index 3003be1..ec3f55e 100644
--- a/quasar.config.js
+++ b/quasar.config.js
@@ -13,6 +13,7 @@ export default defineConfig((/* ctx */) =>
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli-vite/boot-files
boot: [
+ 'apexcharts'
],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#css
diff --git a/src-server/routes/mantis.js b/src-server/routes/mantis.js
index 1d96572..479a0b3 100644
--- a/src-server/routes/mantis.js
+++ b/src-server/routes/mantis.js
@@ -3,6 +3,7 @@ import { PrismaClient } from '@prisma/client'; // Import Prisma Client
import { getMantisSettings, saveTicketToDatabase } from '../services/mantisDownloader.js';
import axios from 'axios';
import reader from '@kenjiuno/msgreader';
+import SuperJSON from 'superjson';
const MsgReader = reader.default;
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;
\ No newline at end of file
diff --git a/src-server/server.js b/src-server/server.js
index a7518f8..cc803e9 100644
--- a/src-server/server.js
+++ b/src-server/server.js
@@ -22,7 +22,7 @@ import authRoutes from './routes/auth.js';
import chatRoutes from './routes/chat.js';
import settingsRoutes from './routes/settings.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 { generateAndStoreMantisSummary } from './services/mantisSummarizer.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 { logger } from './utils/logging.js';
+import SuperJSON from 'superjson';
dotenv.config();
@@ -100,12 +101,26 @@ app.disable('x-powered-by');
// Add JSON body parsing middleware
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
app.use('/api/auth', authRoutes);
app.use('/api/chat', requireAuth, chatRoutes);
app.use('/api/user-preferences', requireAuth, userPreferencesRoutes);
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);
if (process.env.PROD)
diff --git a/src/boot/apexcharts.js b/src/boot/apexcharts.js
new file mode 100644
index 0000000..592fe40
--- /dev/null
+++ b/src/boot/apexcharts.js
@@ -0,0 +1,8 @@
+import { defineBoot } from '#q-app/wrappers';
+
+import VueApexCharts from 'vue3-apexcharts';
+
+export default defineBoot(async({app}) =>
+{
+ app.use(VueApexCharts);
+});
\ No newline at end of file
diff --git a/src/boot/axios.js b/src/boot/axios.js
index e0e6099..afbae11 100644
--- a/src/boot/axios.js
+++ b/src/boot/axios.js
@@ -1,14 +1,30 @@
-import { boot } from 'quasar/wrappers';
import axios from 'axios';
-// Be careful when using SSR for cross-request state pollution
-// 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)
+import SuperJSON from 'superjson';
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;
\ No newline at end of file
diff --git a/src/css/app.scss b/src/css/app.scss
index 09428ab..ed0bd1b 100644
--- a/src/css/app.scss
+++ b/src/css/app.scss
@@ -21,4 +21,8 @@ body {
.bg-blurred {
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(10px);
+}
+
+.drop-shadow {
+ filter: drop-shadow(0 0 25px rgba(0, 0, 0, 0.5));
}
\ No newline at end of file
diff --git a/src/pages/DashboardPage.vue b/src/pages/DashboardPage.vue
new file mode 100644
index 0000000..6db126e
--- /dev/null
+++ b/src/pages/DashboardPage.vue
@@ -0,0 +1,366 @@
+
+