feat(ParticipantController, ParticipantModel, ParticipantRoutes, DiaryParticipantsPanel, i18n): implement participant status management and UI updates
- Added functionality to update participant attendance status, allowing for 'excused' and 'cancelled' states. - Enhanced the participant model to include a default attendance status and validation for status values. - Updated participant routes to support status updates and integrated new status handling in the participant controller. - Modified the DiaryParticipantsPanel to visually indicate participant status and added a toggle for changing status. - Expanded localization files to include new keys for participant status, improving accessibility for users in both English and German.
This commit is contained in:
@@ -38,13 +38,16 @@
|
||||
type="checkbox"
|
||||
:value="member.id"
|
||||
:checked="participants.includes(member.id)"
|
||||
:disabled="participantStatusMap[member.id] === 'excused'"
|
||||
@change="$emit('toggle-participant', member.id)"
|
||||
>
|
||||
</label>
|
||||
<span class="clickable participant-name" @click.stop="$emit('open-notes', member)">
|
||||
<span v-if="member.testMembership && member.trainingParticipations >= 6" class="warning-icon warning-icon-severe" :title="$t('members.sixOrMoreParticipations')">🛑</span>
|
||||
<span v-else-if="member.testMembership && member.trainingParticipations >= 3" class="warning-icon" :title="$t('members.threeOrMoreParticipations')">⚠️</span>
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
<span :class="{ 'participant-name-excused': participantStatusMap[member.id] === 'excused' }">
|
||||
{{ member.firstName }} {{ member.lastName }}
|
||||
</span>
|
||||
</span>
|
||||
<div class="participant-actions">
|
||||
<select
|
||||
@@ -58,6 +61,15 @@
|
||||
{{ group.name }}
|
||||
</option>
|
||||
</select>
|
||||
<button
|
||||
type="button"
|
||||
class="participant-status-toggle"
|
||||
:class="{ active: participantStatusMap[member.id] === 'excused' }"
|
||||
:title="$t('diary.participantStatusExcused')"
|
||||
@click="$emit('toggle-member-status', member.id)"
|
||||
>
|
||||
📴
|
||||
</button>
|
||||
<span @click="$emit('show-pic', member)" class="img-icon" v-if="member.hasImage">🖼</span>
|
||||
<span
|
||||
v-if="member.testMembership === true && member.memberFormHandedOver !== true"
|
||||
@@ -108,12 +120,17 @@ export default {
|
||||
memberGroupsMap: {
|
||||
type: Object,
|
||||
required: true
|
||||
},
|
||||
participantStatusMap: {
|
||||
type: Object,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
emits: [
|
||||
'update:participantSearchQuery',
|
||||
'update:participantFilter',
|
||||
'toggle-participant',
|
||||
'toggle-member-status',
|
||||
'update-member-group',
|
||||
'open-notes',
|
||||
'show-pic',
|
||||
@@ -206,4 +223,29 @@ export default {
|
||||
background-color: var(--surface-color);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.participant-status-toggle {
|
||||
margin-left: 10px;
|
||||
padding: 0.28rem 0.5rem;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 999px;
|
||||
background: var(--surface-color);
|
||||
color: var(--text-color);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.participant-status-toggle.active {
|
||||
background: rgba(217, 164, 65, 0.14);
|
||||
color: #8b6113;
|
||||
border-color: rgba(217, 164, 65, 0.3);
|
||||
}
|
||||
|
||||
.participant-name-excused {
|
||||
color: #7a6a48;
|
||||
font-style: italic;
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@@ -466,6 +466,9 @@
|
||||
"filterPresent": "Anwesend",
|
||||
"filterAbsent": "Abwesend",
|
||||
"filterTest": "Probe",
|
||||
"participantStatusNone": "Kein Status",
|
||||
"participantStatusExcused": "Entschuldigt",
|
||||
"participantStatusCancelled": "Abgesagt",
|
||||
"quickAdd": "+ Schnell hinzufügen",
|
||||
"selectTags": "Tags auswählen",
|
||||
"createDrawing": "Übungszeichnung erstellen",
|
||||
|
||||
@@ -133,7 +133,10 @@
|
||||
"standardActivities": "Standard activities",
|
||||
"standardDurationShort": "Min",
|
||||
"standardActivityAddError": "Standard activity could not be added.",
|
||||
"statusReady": "Times and training plan are set."
|
||||
"statusReady": "Times and training plan are set.",
|
||||
"participantStatusNone": "No status",
|
||||
"participantStatusExcused": "Excused",
|
||||
"participantStatusCancelled": "Cancelled"
|
||||
},
|
||||
"predefinedActivities": {
|
||||
"excludeFromStats": "Exclude from statistics",
|
||||
|
||||
@@ -642,9 +642,11 @@
|
||||
:participant-filter="participantFilter"
|
||||
:groups="groups"
|
||||
:member-groups-map="memberGroupsMap"
|
||||
:participant-status-map="participantStatusMap"
|
||||
@update:participant-search-query="participantSearchQuery = $event"
|
||||
@update:participant-filter="participantFilter = $event"
|
||||
@toggle-participant="toggleParticipant"
|
||||
@toggle-member-status="toggleParticipantStatus"
|
||||
@update-member-group="updateMemberGroup($event.memberId, $event.groupId)"
|
||||
@open-notes="openNotesModal"
|
||||
@show-pic="showPic"
|
||||
@@ -973,7 +975,9 @@ export default {
|
||||
activityMembersOpenId: null,
|
||||
activityMembersMap: {}, // key: activityId, value: Set(participantIds)
|
||||
activityGroupsMap: {}, // key: activityId, value: groupId
|
||||
participantMapByMemberId: {}, // key: memberId, value: participantId
|
||||
memberGroupsMap: {}, // key: memberId, value: groupId
|
||||
participantStatusMap: {}, // key: memberId, value: attendanceStatus
|
||||
groupActivityMembersOpenId: null,
|
||||
groupActivityMembersMap: {}, // key: groupActivityId, value: Set(participantIds)
|
||||
editingGroupActivity: null, // Gruppenaktivität, die gerade bearbeitet wird
|
||||
@@ -1647,16 +1651,24 @@ export default {
|
||||
|
||||
async loadParticipants(dateId) {
|
||||
const response = await apiClient.get(`/participants/${dateId}`);
|
||||
this.participants = response.data.map(participant => participant.memberId);
|
||||
// Map für memberId -> participantId speichern
|
||||
this.participantMapByMemberId = response.data.reduce((map, p) => { map[p.memberId] = p.id; return map; }, {});
|
||||
// Map für memberId -> groupId speichern und mit Reaktivität initialisieren
|
||||
response.data.forEach(p => {
|
||||
const participantRows = response.data || [];
|
||||
this.participants = participantRows
|
||||
.filter(participant => !participant.attendanceStatus || participant.attendanceStatus === 'present')
|
||||
.map(participant => participant.memberId);
|
||||
this.participantMapByMemberId = participantRows.reduce((map, p) => {
|
||||
map[p.memberId] = p.id;
|
||||
return map;
|
||||
}, {});
|
||||
this.participantStatusMap = participantRows.reduce((map, p) => {
|
||||
map[p.memberId] = p.attendanceStatus || 'present';
|
||||
return map;
|
||||
}, {});
|
||||
this.memberGroupsMap = {};
|
||||
participantRows.forEach(p => {
|
||||
const groupValue = (p.groupId !== null && p.groupId !== undefined) ? String(p.groupId) : '';
|
||||
if (this.$set) {
|
||||
this.$set(this.memberGroupsMap, p.memberId, groupValue);
|
||||
} else {
|
||||
// Vue 3: Reaktivität wird automatisch gewährleistet
|
||||
this.memberGroupsMap = {
|
||||
...this.memberGroupsMap,
|
||||
[p.memberId]: groupValue
|
||||
@@ -1710,6 +1722,10 @@ export default {
|
||||
return this.participants.includes(memberId);
|
||||
},
|
||||
|
||||
getParticipantStatus(memberId) {
|
||||
return this.participantStatusMap[memberId] || '';
|
||||
},
|
||||
|
||||
async toggleParticipant(memberId) {
|
||||
const isParticipant = this.isParticipant(memberId);
|
||||
const dateId = this.date.id;
|
||||
@@ -1719,15 +1735,89 @@ export default {
|
||||
memberId
|
||||
});
|
||||
this.participants = this.participants.filter(id => id !== memberId);
|
||||
const nextStatusMap = { ...this.participantStatusMap };
|
||||
const nextParticipantMap = { ...this.participantMapByMemberId };
|
||||
const nextGroupMap = { ...this.memberGroupsMap };
|
||||
delete nextStatusMap[memberId];
|
||||
delete nextParticipantMap[memberId];
|
||||
delete nextGroupMap[memberId];
|
||||
this.participantStatusMap = nextStatusMap;
|
||||
this.participantMapByMemberId = nextParticipantMap;
|
||||
this.memberGroupsMap = nextGroupMap;
|
||||
} else {
|
||||
await apiClient.post('/participants/add', {
|
||||
const response = await apiClient.post('/participants/add', {
|
||||
diaryDateId: dateId,
|
||||
memberId
|
||||
});
|
||||
this.participants.push(memberId);
|
||||
this.participants = [...new Set([...this.participants, memberId])];
|
||||
this.participantStatusMap = {
|
||||
...this.participantStatusMap,
|
||||
[memberId]: 'present'
|
||||
};
|
||||
this.participantMapByMemberId = {
|
||||
...this.participantMapByMemberId,
|
||||
[memberId]: response.data.id
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
async updateParticipantStatus(memberId, status) {
|
||||
const dateId = this.date.id;
|
||||
const response = await apiClient.put(`/participants/${dateId}/${memberId}/status`, {
|
||||
attendanceStatus: status
|
||||
});
|
||||
const participant = response.data;
|
||||
|
||||
this.participants = this.participants.filter(id => id !== memberId);
|
||||
this.participantStatusMap = {
|
||||
...this.participantStatusMap,
|
||||
[memberId]: participant.attendanceStatus
|
||||
};
|
||||
this.participantMapByMemberId = {
|
||||
...this.participantMapByMemberId,
|
||||
[memberId]: participant.id
|
||||
};
|
||||
this.memberGroupsMap = {
|
||||
...this.memberGroupsMap,
|
||||
[memberId]: ''
|
||||
};
|
||||
},
|
||||
|
||||
async clearParticipantStatus(memberId) {
|
||||
const dateId = this.date.id;
|
||||
|
||||
if (this.isParticipant(memberId)) {
|
||||
this.participantStatusMap = {
|
||||
...this.participantStatusMap,
|
||||
[memberId]: 'present'
|
||||
};
|
||||
return;
|
||||
}
|
||||
|
||||
await apiClient.post('/participants/remove', {
|
||||
diaryDateId: dateId,
|
||||
memberId
|
||||
});
|
||||
const nextStatusMap = { ...this.participantStatusMap };
|
||||
const nextParticipantMap = { ...this.participantMapByMemberId };
|
||||
const nextGroupMap = { ...this.memberGroupsMap };
|
||||
delete nextStatusMap[memberId];
|
||||
delete nextParticipantMap[memberId];
|
||||
delete nextGroupMap[memberId];
|
||||
this.participantStatusMap = nextStatusMap;
|
||||
this.participantMapByMemberId = nextParticipantMap;
|
||||
this.memberGroupsMap = nextGroupMap;
|
||||
},
|
||||
|
||||
async toggleParticipantStatus(memberId) {
|
||||
if (this.getParticipantStatus(memberId) === 'excused') {
|
||||
await this.clearParticipantStatus(memberId);
|
||||
return;
|
||||
}
|
||||
|
||||
await this.updateParticipantStatus(memberId, 'excused');
|
||||
},
|
||||
|
||||
async markFormHandedOver(member) {
|
||||
try {
|
||||
const memberData = {
|
||||
@@ -3569,35 +3659,52 @@ export default {
|
||||
if (this.date && this.date.id === data.dateId) {
|
||||
// Entferne aus participants-Array
|
||||
this.participants = this.participants.filter(memberId => memberId !== data.participantId);
|
||||
// Entferne aus Maps
|
||||
delete this.participantMapByMemberId[data.participantId];
|
||||
delete this.memberGroupsMap[data.participantId];
|
||||
const nextParticipantMap = { ...this.participantMapByMemberId };
|
||||
const nextGroupMap = { ...this.memberGroupsMap };
|
||||
const nextStatusMap = { ...this.participantStatusMap };
|
||||
delete nextParticipantMap[data.participantId];
|
||||
delete nextGroupMap[data.participantId];
|
||||
delete nextStatusMap[data.participantId];
|
||||
this.participantMapByMemberId = nextParticipantMap;
|
||||
this.memberGroupsMap = nextGroupMap;
|
||||
this.participantStatusMap = nextStatusMap;
|
||||
}
|
||||
},
|
||||
|
||||
async handleParticipantUpdated(data) {
|
||||
// Nur aktualisieren, wenn das aktuelle Datum betroffen ist
|
||||
if (this.date && this.date !== 'new' && String(this.date.id) === String(data.dateId)) {
|
||||
// Aktualisiere groupId in memberGroupsMap
|
||||
const status = data.participant.attendanceStatus || 'present';
|
||||
const groupValue = (data.participant.groupId !== null && data.participant.groupId !== undefined)
|
||||
? String(data.participant.groupId)
|
||||
: '';
|
||||
|
||||
// Verwende $set für Vue 2 - das ist wichtig für Reaktivität
|
||||
|
||||
if (this.$set) {
|
||||
this.$set(this.memberGroupsMap, data.participant.memberId, groupValue);
|
||||
this.$set(this.participantStatusMap, data.participant.memberId, status);
|
||||
this.$set(this.participantMapByMemberId, data.participant.memberId, data.participant.id);
|
||||
} else {
|
||||
// Vue 3: Erstelle neues Objekt für Reaktivität
|
||||
this.memberGroupsMap = {
|
||||
...this.memberGroupsMap,
|
||||
[data.participant.memberId]: groupValue
|
||||
};
|
||||
this.participantStatusMap = {
|
||||
...this.participantStatusMap,
|
||||
[data.participant.memberId]: status
|
||||
};
|
||||
this.participantMapByMemberId = {
|
||||
...this.participantMapByMemberId,
|
||||
[data.participant.memberId]: data.participant.id
|
||||
};
|
||||
}
|
||||
|
||||
// Warte auf Vue-Update und force dann ein Re-Render
|
||||
|
||||
if (status === 'present') {
|
||||
this.participants = [...new Set([...this.participants, data.participant.memberId])];
|
||||
} else {
|
||||
this.participants = this.participants.filter(memberId => memberId !== data.participant.memberId);
|
||||
}
|
||||
|
||||
await this.$nextTick();
|
||||
|
||||
// Force Vue update um sicherzustellen, dass die UI aktualisiert wird
|
||||
this.$forceUpdate();
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user