Some icons changed, first implementation of contact edit
@@ -42,3 +42,13 @@ export const changeTranslation = async (req, res) => {
|
|||||||
res.status(403).json({ error: error.message });
|
res.status(403).json({ error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getOpenContacts = async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { userid: userId } = req.headers;
|
||||||
|
const openContacts = await AdminService.getOpenContacts(userId);
|
||||||
|
res.status(200).json(openContacts);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(403).json({ error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,11 @@ const ContactMessage = sequelize.define('contact_message', {
|
|||||||
type: DataTypes.BOOLEAN,
|
type: DataTypes.BOOLEAN,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
|
isFinished: {
|
||||||
|
type: DataTypes.BOOLEAN,
|
||||||
|
allowNull: false,
|
||||||
|
defaultValue: false
|
||||||
|
}
|
||||||
}, {
|
}, {
|
||||||
tableName: 'contact_message',
|
tableName: 'contact_message',
|
||||||
timestamps: true,
|
timestamps: true,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import { authenticate } from '../middleware/authMiddleware.js';
|
import { authenticate } from '../middleware/authMiddleware.js';
|
||||||
import { getOpenInterests, changeInterest, deleteInterest, changeTranslation } from '../controllers/adminController.js';
|
import { getOpenInterests, changeInterest, deleteInterest, changeTranslation, getOpenContacts } from '../controllers/adminController.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
@@ -9,4 +9,6 @@ router.post('/interest', authenticate, changeInterest);
|
|||||||
router.post('/interest/translation', authenticate, changeTranslation);
|
router.post('/interest/translation', authenticate, changeTranslation);
|
||||||
router.delete('/interest/:id', authenticate, deleteInterest);
|
router.delete('/interest/:id', authenticate, deleteInterest);
|
||||||
|
|
||||||
|
router.get('/opencontacts', authenticate, getOpenContacts);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -4,13 +4,11 @@ import InterestType from "../models/type/interest.js"
|
|||||||
import InterestTranslationType from "../models/type/interest_translation.js"
|
import InterestTranslationType from "../models/type/interest_translation.js"
|
||||||
import User from "../models/community/user.js";
|
import User from "../models/community/user.js";
|
||||||
import UserParamValue from "../models/type/user_param_value.js";
|
import UserParamValue from "../models/type/user_param_value.js";
|
||||||
|
import ContactMessage from "../models/service/contactmessage.js";
|
||||||
|
|
||||||
class AdminService {
|
class AdminService {
|
||||||
async hasUserAccess(userId, section) {
|
async hasUserAccess(userId, section) {
|
||||||
const userRights = await UserRight.findAll({
|
const userRights = await UserRight.findAll({
|
||||||
/* where: {
|
|
||||||
userId: userId,
|
|
||||||
},*/
|
|
||||||
include: [{
|
include: [{
|
||||||
model: UserRightType,
|
model: UserRightType,
|
||||||
as: 'rightType',
|
as: 'rightType',
|
||||||
@@ -119,6 +117,18 @@ class AdminService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getOpenContacts(userId) {
|
||||||
|
if (!this.hasUserAccess(userId, 'contacts')) {
|
||||||
|
throw new Error('noaccess');
|
||||||
|
}
|
||||||
|
const openContacts = await ContactMessage.findAll({
|
||||||
|
where: {
|
||||||
|
isFinished: false,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return openContacts;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new AdminService();
|
export default new AdminService();
|
||||||
10
frontend/package-lock.json
generated
@@ -9,6 +9,7 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"vue": "~3.4.31",
|
"vue": "~3.4.31",
|
||||||
"vue-i18n": "^10.0.0-beta.2",
|
"vue-i18n": "^10.0.0-beta.2",
|
||||||
@@ -2513,6 +2514,15 @@
|
|||||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/date-fns": {
|
||||||
|
"version": "3.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz",
|
||||||
|
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
|
||||||
|
"funding": {
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/debounce": {
|
"node_modules/debounce": {
|
||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz",
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^1.7.2",
|
"axios": "^1.7.2",
|
||||||
|
"date-fns": "^3.6.0",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"vue": "~3.4.31",
|
"vue": "~3.4.31",
|
||||||
"vue-i18n": "^10.0.0-beta.2",
|
"vue-i18n": "^10.0.0-beta.2",
|
||||||
|
|||||||
BIN
frontend/public/images/icons/cam.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
frontend/public/images/icons/message.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 380 B After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 493 B After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/public/images/icons/minigames.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 7.0 KiB |
@@ -11,6 +11,14 @@
|
|||||||
"isadult": "Nur für Erwachsene",
|
"isadult": "Nur für Erwachsene",
|
||||||
"delete": "Löschen"
|
"delete": "Löschen"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"contacts": {
|
||||||
|
"title": "[Admin] - Kontaktanfragen",
|
||||||
|
"date": "Datum",
|
||||||
|
"from": "Absender",
|
||||||
|
"actions": "Aktionen",
|
||||||
|
"open": "Bearbeiten",
|
||||||
|
"finished": "Abschließen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -24,5 +24,8 @@
|
|||||||
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner Email-Adresse zu.",
|
"acceptdatasave": "Ich stimme der vorübergehenden Speicherung meiner Email-Adresse zu.",
|
||||||
"accept2": "Ohne diese Zustimmung können wir Dir leider nicht antworten."
|
"accept2": "Ohne diese Zustimmung können wir Dir leider nicht antworten."
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"general": {
|
||||||
|
"datetimelong": "dd.MM.yyyy HH:mm:ss"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import SexualitySettingsView from '../views/settings/SexualityView.vue';
|
|||||||
import AccountSettingsView from '../views/settings/AccountView.vue';
|
import AccountSettingsView from '../views/settings/AccountView.vue';
|
||||||
import InterestsView from '../views/settings/InterestsView.vue';
|
import InterestsView from '../views/settings/InterestsView.vue';
|
||||||
import AdminInterestsView from '../views/admin/InterestsView.vue';
|
import AdminInterestsView from '../views/admin/InterestsView.vue';
|
||||||
|
import AdminContactsView from '../views/admin/ContactsView.vue';
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
{
|
{
|
||||||
@@ -55,7 +56,13 @@ const routes = [
|
|||||||
name: 'AdminInterests',
|
name: 'AdminInterests',
|
||||||
component: AdminInterestsView,
|
component: AdminInterestsView,
|
||||||
meta: { requiresAuth: true }
|
meta: { requiresAuth: true }
|
||||||
}
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/contacts',
|
||||||
|
name: 'AdminContacts',
|
||||||
|
component: AdminContactsView,
|
||||||
|
meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
frontend/src/utils/datetime.js
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import { format } from 'date-fns';
|
||||||
|
import { useI18n } from 'vue-i18n';
|
||||||
|
|
||||||
|
export function formatDateTimeLong(timestamp) {
|
||||||
|
const { t } = useI18n();
|
||||||
|
const dateFormat = t('general.datetimelong', 'yyyy-MM-dd HH:mm:ss');
|
||||||
|
const date = new Date(timestamp);
|
||||||
|
return format(date, dateFormat);
|
||||||
|
}
|
||||||
64
frontend/src/views/admin/ContactsView.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<h2>{{ $t('admin.contacts.title') }}</h2>
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ $t('admin.contacts.date') }}</th>
|
||||||
|
<th>{{ $t('admin.contacts.from') }}</th>
|
||||||
|
<th>{{ $t('admin.contacts.actions') }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="contact in contacts">
|
||||||
|
<td>{{ formatDateTimeLong(contact.createdAt) }}</td>
|
||||||
|
<td>{{ contact.email }}</td>
|
||||||
|
<td>
|
||||||
|
<button @clicked="openRequest(contact)">{{ $t('admin.contacts.open') }}</button>
|
||||||
|
<button @clicked="finishRequest(contact)">{{ $t('admin.contacts.finished') }}</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<ErrorDialog ref="errorDialog" />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import apiClient from '@/utils/axios.js';
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import ErrorDialog from '@/dialogues/standard/ErrorDialog.vue';
|
||||||
|
import { formatDateTimeLong } from '@/utils/datetime.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'AdminContactsView',
|
||||||
|
components: {
|
||||||
|
ErrorDialog
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
contacts: []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getContacts()
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatDateTimeLong,
|
||||||
|
async getContacts() {
|
||||||
|
try {
|
||||||
|
const openContactRequests = await apiClient.get('/api/admin/opencontacts');
|
||||||
|
this.contacts = openContactRequests.data;
|
||||||
|
} catch (error) {
|
||||||
|
this.$refs.errorDialog.open(`tr:error.${error.response.data.error}`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async openRequest(contact) {
|
||||||
|
|
||||||
|
},
|
||||||
|
async finishRequest(contact) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||