Added accidents to diary

This commit is contained in:
Torsten Schulz
2025-03-10 16:46:43 +01:00
parent 9442e3683b
commit c294dd7b2a
10 changed files with 2176 additions and 1369 deletions

View File

@@ -0,0 +1,23 @@
import AccidentService from '../services/accidentService.js';
export const addAccident = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, memberId, diaryDateId, accident } = req.body;
await AccidentService.createAccident(userToken, clubId, memberId, diaryDateId, accident);
res.status(201).json({ message: 'Accident added successfully' });
} catch (error) {
res.status(500).json({ message: 'Error adding accident', error: error.message });
}
};
export const getAccidents = async (req, res) => {
try {
const { authcode: userToken } = req.headers;
const { clubId, dateId } = req.params;
const accidents = await AccidentService.getAccidents(userToken, clubId, dateId);
res.status(200).json(accidents);
} catch (error) {
res.status(500).json({ message: 'Error getting activities', error: error.message });
}
}

View File

@@ -0,0 +1,43 @@
import { DataTypes } from 'sequelize';
import sequelize from '../database.js';
import bcrypt from 'bcrypt';
import { encryptData, decryptData } from '../utils/encrypt.js';
const Accident = sequelize.define('Accident', {
memberId: {
type: DataTypes.INTEGER,
allowNull: false,
},
diaryDateId: {
type: DataTypes.INTEGER,
allowNull: false,
},
accident: {
type: DataTypes.STRING,
allowNull: false,
set(value) {
const encryptedValue = encryptData(value);
this.setDataValue('accident', encryptedValue);
},
get() {
const encryptedValue = this.getDataValue('accident');
return decryptData(encryptedValue);
}
},
salt: {
type: DataTypes.STRING,
allowNull: true,
},
}, {
tableName: 'accident',
underscored: true,
timestamps: true,
hooks: {
beforeCreate: async (user) => {
const salt = await bcrypt.genSalt(10);
user.salt = salt;
},
}
});
export default Accident;

View File

@@ -4,7 +4,7 @@ import sequelize from '../database.js';
import Club from './Club.js';
import { encryptData, decryptData } from '../utils/encrypt.js';
const Member = sequelize.define('User', {
const Member = sequelize.define('Member', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,

View File

@@ -21,6 +21,7 @@ import Season from './Season.js';
import Location from './Location.js';
import Group from './Group.js';
import GroupActivity from './GroupActivity.js';
import Accident from './Accident.js';
User.hasMany(Log, { foreignKey: 'userId' });
Log.belongsTo(User, { foreignKey: 'userId' });
@@ -114,6 +115,12 @@ DiaryDateTag.belongsTo(DiaryTag, { foreignKey: 'tagId', as: 'tag' });
DiaryMemberTag.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(DiaryMemberTag, { foreignKey: 'diaryDateId', as: 'diaryMemberTags' });
Accident.belongsTo(Member, { foreignKey: 'memberId', as: 'members' });
Member.hasMany(Accident, { foreignKey: 'memberId', as: 'accidents' });
Accident.belongsTo(DiaryDate, { foreignKey: 'diaryDateId', as: 'diaryDates' });
DiaryDate.hasMany(Accident, { foreignKey: 'diaryDateId', as: 'accidents' });
export {
User,
Log,
@@ -137,4 +144,5 @@ export {
Team,
Group,
GroupActivity,
Accident,
};

View File

@@ -0,0 +1,10 @@
import express from 'express';
import { addAccident, getAccidents } from '../controllers/accidentController.js';
import { authenticate } from '../middleware/authMiddleware.js';
const router = express.Router();
router.post('/', authenticate, addAccident);
router.get('/:clubId/:dateId', authenticate, getAccidents);
export default router;

View File

@@ -7,7 +7,8 @@ import {
User, Log, Club, UserClub, Member, DiaryDate, Participant, Activity, MemberNote,
DiaryNote, DiaryTag, MemberDiaryTag, DiaryDateTag, DiaryMemberNote, DiaryMemberTag,
PredefinedActivity, DiaryDateActivity, Match, League, Team, Group,
GroupActivity
GroupActivity,
Accident
} from './models/index.js';
import authRoutes from './routes/authRoutes.js';
import clubRoutes from './routes/clubRoutes.js';
@@ -27,6 +28,7 @@ import Location from './models/Location.js';
import groupRoutes from './routes/groupRoutes.js';
import diaryDateTagRoutes from './routes/diaryDateTagRoutes.js';
import sessionRoutes from './routes/sessionRoutes.js';
import accidentRoutes from './routes/accidentRoutes.js';
const app = express();
const port = process.env.PORT || 3000;
@@ -53,6 +55,7 @@ app.use('/api/matches', matchRoutes);
app.use('/api/group', groupRoutes);
app.use('/api/diarydatetags', diaryDateTagRoutes);
app.use('/api/session', sessionRoutes);
app.use('/api/accident', accidentRoutes);
app.use(express.static(path.join(__dirname, '../frontend/dist')));
@@ -88,6 +91,7 @@ app.get('*', (req, res) => {
await Match.sync({ alter: true });
await Group.sync({ alter: true });
await GroupActivity.sync({ alter: true });
await Accident.sync({ alter: true });
app.listen(port, () => {
console.log(`Server is running on http://localhost:${port}`);

View File

@@ -0,0 +1,62 @@
import Accident from '../models/Accident.js';
import DiaryDate from '../models/DiaryDates.js';
import Member from '../models/Member.js';
import { checkAccess, getUserByToken} from '../utils/userUtils.js';
class AccidentService {
async createAccident(userToken, clubId, memberId, diaryDateId, accident) {
await checkAccess(userToken, clubId);
const user = await getUserByToken(userToken);
if (!user) {
console.log('---------------');
throw new Error('User not found');
}
const member = await Member.findByPk(memberId);
if (!member || member.clubId != clubId) {
throw new Error('Member not found');
}
console.log(diaryDateId);
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (!diaryDate || diaryDate.clubId != clubId) {
throw new Error('Diary date not found');
}
const newAccident = await Accident.create({
memberId,
diaryDateId: diaryDate.id,
accident,
});
return newAccident;
}
async getAccidents(userToken, clubId, diaryDateId) {
await checkAccess(userToken, clubId);
const user = await getUserByToken(userToken);
if (!user) {
throw new Error('User not found');
}
const diaryDate = await DiaryDate.findByPk(diaryDateId);
if (!diaryDate || diaryDate.clubId != clubId) {
throw new Error('Diary date not found');
}
const accidents = await Accident.findAll({
where: {
diaryDateId,
},
include: [{
model: Member,
as: 'members'
}]
});
const cleanupedAccidents = accidents.map(accident => {
return {
accident: accident.accident,
firstName: accident.members.firstName,
lastName: accident.members.lastName,
}
})
return cleanupedAccidents;
}
}
export default new AccidentService();

View File

@@ -1,6 +1,7 @@
import User from '../models/User.js'
import UserClub from '../models/UserClub.js';
import HttpError from '../exceptions/HttpError.js';
import { config } from 'dotenv';
export const getUserByToken = async(token) => {
try {
@@ -32,6 +33,7 @@ export const hasUserClubAccess = async(userId, clubId) => {
console.log(error);
throw new HttpError('notfound', 500);
}
console.log('---- no user found');
}
export const checkAccess = async(userToken, clubId) => {

View File

@@ -158,6 +158,11 @@
</div>
</div>
<div class="column">
<div>
<button @click="addAccident">Unfall buchen</button>
<div v-if="accidents.length > 0">
</div>
</div>
<h3>Aktivitäten</h3>
<textarea v-model="newActivity"></textarea>
<button @click="addActivity">Aktivität hinzufügen</button>
@@ -176,9 +181,9 @@
:checked="isParticipant(member.id)">
<span class="clickable" @click="selectMember(member)"
:class="{ highlighted: selectedMember && selectedMember.id === member.id }">{{
member.firstName
member ? member.firstName : ''
}} {{
member.lastName }}</span>
member ? member.lastName : '' }}</span>
<span v-if="false" @click="openNotesModal(member)" class="clickable">📝</span>
<span @click="showPic(member)" class="img-icon" v-if="member.hasImage">&#x1F5BC;</span>
<span class="pointer" @click="openTagInfos(member)"></span>
@@ -241,7 +246,27 @@
<img :src="imageUrl" @click="closeImage" />
</div>
</div>
<img v-if="showImage" class="img" :src="imageUrl" @click="closeImage()" />
<div v-if="showAccidentForm" class="accidentForm">
<form @submit.prevent="submitAccident">
<div>
<label for="memberId">Mitglied:</label>
<select id="memberId" v-model="accident.memberId">
<template v-for="member in members" :key="member.id" :value="member.id">
<option v-if="participants.indexOf(member.id) >= 0" :value="member.id">{{ member.firstName + ' ' + member.lastName }}</option>
</template>
</select>
</div>
<div>
<label for="accident">Unfall:</label>
<textarea id="accident" v-model="accident.accident" required ></textarea>
</div>
<button type="button" @click="saveAccident">Eintragen</button>
<button type="button" @click="closeAccidentForm">Schießen</button>
<ul>
<li v-for="accident in accidents" :key="accident.id">{{ accident.firstName + ' ' + accident.lastName + ': ' + accident.accident}}</li>
</ul>
</form>
</div>
</template>
<script>
@@ -307,6 +332,13 @@ export default {
bellSound: new Audio('/sound/bell-123742.mp3'),
thumbSound: new Audio('/sound/thump-105302.mp3'),
timeChecker: null,
showAccidentForm: false,
accident: {
memberId: '',
diaryDateId: '',
accident: '',
},
accidents: [],
};
},
watch: {
@@ -327,6 +359,7 @@ export default {
const lastItem = this.trainingPlan[this.trainingPlan.length - 1];
return lastItem.endTime;
},
filteredPredefinedActivities() {
const input = this.newPlanItem.activity.toLowerCase();
return this.predefinedActivities.filter(activity =>
@@ -343,10 +376,12 @@ export default {
await this.loadPredefinedActivities();
}
},
setCurrentDate() {
const today = new Date().toISOString().split('T')[0];
this.newDate = today;
},
async handleDateChange() {
this.showForm = this.date === 'new';
if (this.date && this.date !== 'new') {
@@ -378,6 +413,7 @@ export default {
this.participants = [];
}
},
initializeSortable() {
const el = this.$refs.sortableList;
Sortable.create(el, {
@@ -385,6 +421,7 @@ export default {
onEnd: this.onDragEnd,
});
},
async createDate() {
try {
const response = await apiClient.post(`/diary/${this.currentClub}`, {
@@ -402,6 +439,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async updateTrainingTimes() {
try {
const dateId = this.date.id;
@@ -416,22 +454,27 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async loadMembers() {
const response = await apiClient.get(`/clubmembers/get/${this.currentClub}/false`);
this.members = response.data;
},
async loadParticipants(dateId) {
const response = await apiClient.get(`/participants/${dateId}`);
this.participants = response.data.map(participant => participant.memberId);
},
async loadActivities(dateId) {
const response = await apiClient.get(`/activities/${dateId}`);
this.activities = response.data;
},
async loadTags() {
const response = await apiClient.get('/tags');
this.availableTags = response.data;
},
async loadPredefinedActivities() {
try {
const response = await apiClient.get('/predefined-activities');
@@ -440,6 +483,7 @@ export default {
console.error('Fehler beim Laden der vordefinierten Aktivitäten:', error);
}
},
async loadGroups() {
try {
const response = await apiClient.get(`/group/${this.currentClub}/${this.date.id}`);
@@ -448,9 +492,11 @@ export default {
console.log(error);
}
},
isParticipant(memberId) {
return this.participants.includes(memberId);
},
async toggleParticipant(memberId) {
const isParticipant = this.isParticipant(memberId);
const dateId = this.date.id;
@@ -468,6 +514,7 @@ export default {
this.participants.push(memberId);
}
},
async addActivity() {
const dateId = this.date.id;
if (this.newActivity) {
@@ -481,14 +528,17 @@ export default {
this.selectedActivityTags = [];
}
},
async selectMember(member) {
this.selectedMember = member;
},
async openNotesModal(member) {
this.noteMember = member;
this.loadMemberNotesAndTags(this.date.id, member.id);
this.showNotesModal = true;
},
async loadMemberNotesAndTags(diaryDateId, memberId) {
this.doMemberTagUpdates = false;
try {
@@ -509,6 +559,7 @@ export default {
}
this.doMemberTagUpdates = true;
},
async addMemberNote() {
if (this.newNoteContent && this.selectedMember) {
const response = await apiClient.post(`/diarymember/${this.currentClub}/note`, {
@@ -523,21 +574,25 @@ export default {
alert('Bitte wählen Sie einen Teilnehmer aus und geben Sie einen Notiztext ein.');
}
},
async deleteNote(noteId) {
const response = await apiClient.delete(`/diarymember/${this.currentClub}/note/${noteId}`, {
clubId: this.currentClub
});
this.notes = response.data;
},
closeNotesModal() {
this.showNotesModal = false;
},
async addNewTagFromInput(event) {
const inputValue = event.target.value.trim();
if (inputValue) {
await this.addNewTag(inputValue);
}
},
async addNewTag(newTagName) {
try {
const response = await apiClient.post('/tags', { name: newTagName });
@@ -549,12 +604,14 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async addNewTagForMemberFromInput(event) {
const inputValue = event.target.value.trim();
if (inputValue) {
await this.addNewTagForMember(inputValue);
}
},
async addNewTagForMember(newTagName) {
try {
const response = await apiClient.post('/tags', { name: newTagName });
@@ -567,6 +624,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async linkTagToDiaryDate(tag) {
if (!tag || !tag.id) {
console.warn("Ungültiges Tag-Objekt:", tag);
@@ -583,6 +641,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async linkTagToMemberAndDate(tag) {
try {
const tagId = tag.id;
@@ -596,6 +655,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async updateActivityTags() {
try {
const selectedTags = this.selectedActivityTags;
@@ -613,6 +673,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async updateMemberTags() {
if (!this.doMemberTagUpdates || !this.selectedMember) {
return;
@@ -629,6 +690,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeMemberTag(tagId) {
try {
await apiClient.post(`/diarymember/${this.currentClub}/tag/remove`, {
@@ -642,6 +704,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeMemberNote(noteContent) {
try {
await apiClient.post(`/diarymember/${this.currentClub}/note/remove`, {
@@ -655,6 +718,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async removeActivityTag(tag) {
try {
const tagId = tag.id;
@@ -667,6 +731,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async handleActivityTagInput(tags) {
const newTags = tags.filter(tag => !this.previousActivityTags.some(prevTag => prevTag.id === tag.id));
for (const tag of newTags) {
@@ -674,12 +739,14 @@ export default {
}
this.previousActivityTags = [...tags];
},
selectPredefinedActivity(activity) {
this.newPlanItem.activity = activity.name;
this.newPlanItem.durationText = activity.durationText;
this.newPlanItem.duration = activity.duration || '';
this.showDropdown = false;
},
async addPlanItem() {
try {
if (this.addNewItem || this.addNewTimeblock) {
@@ -710,6 +777,7 @@ export default {
alert('Ein Fehler ist aufgetreten. Bitte versuchen Sie es erneut.');
}
},
async updatePlanItemGroup(planItemId, groupId) {
try {
await apiClient.put(`/diary-date-activities/${this.currentClub}/${planItemId}/group`, {
@@ -720,6 +788,7 @@ export default {
console.error('Fehler beim Aktualisieren der Planungs-Item-Gruppe:', error);
}
},
async removePlanItem(planItemId) {
try {
await apiClient.delete(`/diary-date-activities/${this.currentClub}/${planItemId}`);
@@ -1040,6 +1109,29 @@ export default {
time !== this.trainingStart && time !== this.trainingEnd
);
},
async addAccident() {
this.accidents = [];
this.showAccidentForm = !this.showAccidentForm;
await this.getAccidents();
},
async saveAccident() {
const accident = this.accident;
accident['clubId'] = this.currentClub;
accident['diaryDateId'] = this.date.id;
const response = await apiClient.post('/accident', this.accident);
this.getAccidents();
},
async getAccidents() {
const response = await apiClient.get(`/accident/${this.currentClub}/${this.date.id}`);
this.accidents = response.data;
},
closeAccidentForm() {
this.showAccidentForm = false;
},
},
async mounted() {
await this.init();
@@ -1337,4 +1429,18 @@ img {
.tag-list {
margin: 0 0 1em 1.5em;
}
.accidentForm {
display: block;
position: fixed;
width: 70em;
height: 40em;
top: calc(50% - 20em);
left: calc(50% - 35em);
border: 2px solid #555;
box-shadow: 4px 4px 3px #aaa;
overflow: hidden;
padding: 3px;
background-color: #fff;
}
</style>

3277
package-lock.json generated

File diff suppressed because it is too large Load Diff