Refactor member activity display to group participations by date

This commit updates the `MemberActivityStatsDialog` component to group member participations by date, enhancing the presentation of activity data. The logic is introduced to aggregate activities under their respective dates, ensuring a clearer and more organized display. Additionally, CSS styles are added to improve the visual hierarchy and user experience when viewing recent participations.
This commit is contained in:
Torsten Schulz (local)
2025-11-14 23:27:46 +01:00
parent 5dda346fd7
commit ce2bda37ac
2 changed files with 206 additions and 121 deletions

View File

@@ -99,74 +99,73 @@ export const getMemberActivities = async (req, res) => {
// 2. Get all group activities for groups the member belongs to
const groupActivities = [];
if (memberGroupIds.size > 0) {
const groupActivitiesData = await DiaryDateActivity.findAll({
// Suche direkt nach GroupActivity-Einträgen für die Gruppen des Members
const groupActivitiesData = await GroupActivity.findAll({
where: {
groupId: {
[Op.in]: Array.from(memberGroupIds)
}
},
include: [
{
model: DiaryDates,
as: 'diaryDate',
where: startDate ? {
date: {
[Op.gte]: startDate
model: DiaryDateActivity,
as: 'activityGroupActivity',
include: [
{
model: DiaryDates,
as: 'diaryDate',
where: startDate ? {
date: {
[Op.gte]: startDate
}
} : {}
},
{
model: PredefinedActivity,
as: 'predefinedActivity',
required: false
}
} : {}
]
},
{
model: PredefinedActivity,
as: 'predefinedActivity',
required: false // Kann null sein für Gruppen-Aktivitäten
},
{
model: GroupActivity,
as: 'groupActivities',
where: {
groupId: {
[Op.in]: Array.from(memberGroupIds)
}
},
required: true,
include: [
{
model: PredefinedActivity,
as: 'groupPredefinedActivity',
required: false // Kann null sein
}
]
as: 'groupPredefinedActivity',
required: false
}
]
});
// Erstelle virtuelle DiaryMemberActivity-Objekte für Gruppen-Aktivitäten
for (const activity of groupActivitiesData) {
// Finde den entsprechenden Participant für diese Aktivität
for (const groupActivity of groupActivitiesData) {
if (!groupActivity.activityGroupActivity || !groupActivity.activityGroupActivity.diaryDate) {
continue; // Überspringe, wenn keine DiaryDateActivity oder kein DiaryDate vorhanden
}
const activity = groupActivity.activityGroupActivity;
const diaryDateId = activity.diaryDateId;
const relevantParticipants = participants.filter(p => p.diaryDateId === diaryDateId);
// Finde alle relevanten Participants für dieses DiaryDate
const relevantParticipants = participants.filter(p =>
p.diaryDateId === diaryDateId &&
p.groupId === groupActivity.groupId
);
for (const participant of relevantParticipants) {
// Prüfe, ob die Aktivität für die Gruppe des Participants ist
const activityGroupIds = activity.groupActivities?.map(ga => ga.groupId) || [];
if (participant.groupId !== null && activityGroupIds.includes(participant.groupId)) {
// Für Gruppen-Aktivitäten: Verwende die PredefinedActivity aus GroupActivity
// Falls vorhanden, sonst die aus DiaryDateActivity
const groupActivity = activity.groupActivities?.find(ga => ga.groupId === participant.groupId);
if (groupActivity && groupActivity.groupPredefinedActivity) {
// Erstelle ein modifiziertes Activity-Objekt mit der PredefinedActivity aus GroupActivity
const modifiedActivity = {
...activity.toJSON(),
predefinedActivity: groupActivity.groupPredefinedActivity
};
groupActivities.push({
activity: modifiedActivity,
participant: participant,
id: null // Virtuell, nicht in DB
});
} else if (activity.predefinedActivity) {
// Fallback: Verwende die PredefinedActivity aus DiaryDateActivity
groupActivities.push({
activity: activity,
participant: participant,
id: null // Virtuell, nicht in DB
});
}
// Verwende die PredefinedActivity aus GroupActivity, falls vorhanden
// Sonst die aus DiaryDateActivity
const predefinedActivity = groupActivity.groupPredefinedActivity || activity.predefinedActivity;
if (predefinedActivity) {
// Erstelle ein modifiziertes Activity-Objekt
const modifiedActivity = {
...activity.toJSON(),
predefinedActivity: predefinedActivity
};
groupActivities.push({
activity: modifiedActivity,
participant: participant,
id: null // Virtuell, nicht in DB
});
}
}
}
@@ -299,71 +298,70 @@ export const getMemberLastParticipations = async (req, res) => {
// 2. Get all group activities for groups the member belongs to
const groupActivities = [];
if (memberGroupIds.size > 0) {
const groupActivitiesData = await DiaryDateActivity.findAll({
// Suche direkt nach GroupActivity-Einträgen für die Gruppen des Members
const groupActivitiesData = await GroupActivity.findAll({
where: {
groupId: {
[Op.in]: Array.from(memberGroupIds)
}
},
include: [
{
model: DiaryDates,
as: 'diaryDate'
model: DiaryDateActivity,
as: 'activityGroupActivity',
include: [
{
model: DiaryDates,
as: 'diaryDate'
},
{
model: PredefinedActivity,
as: 'predefinedActivity',
required: false
}
]
},
{
model: PredefinedActivity,
as: 'predefinedActivity',
required: false // Kann null sein für Gruppen-Aktivitäten
},
{
model: GroupActivity,
as: 'groupActivities',
where: {
groupId: {
[Op.in]: Array.from(memberGroupIds)
}
},
required: true,
include: [
{
model: PredefinedActivity,
as: 'groupPredefinedActivity',
required: false // Kann null sein
}
]
as: 'groupPredefinedActivity',
required: false
}
],
order: [[{ model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
order: [[{ model: DiaryDateActivity, as: 'activityGroupActivity' }, { model: DiaryDates, as: 'diaryDate' }, 'date', 'DESC']],
limit: parseInt(limit) * 10 // Get more to filter
});
// Erstelle virtuelle DiaryMemberActivity-Objekte für Gruppen-Aktivitäten
for (const activity of groupActivitiesData) {
// Finde den entsprechenden Participant für diese Aktivität
for (const groupActivity of groupActivitiesData) {
if (!groupActivity.activityGroupActivity || !groupActivity.activityGroupActivity.diaryDate) {
continue; // Überspringe, wenn keine DiaryDateActivity oder kein DiaryDate vorhanden
}
const activity = groupActivity.activityGroupActivity;
const diaryDateId = activity.diaryDateId;
const relevantParticipants = participants.filter(p => p.diaryDateId === diaryDateId);
// Finde alle relevanten Participants für dieses DiaryDate
const relevantParticipants = participants.filter(p =>
p.diaryDateId === diaryDateId &&
p.groupId === groupActivity.groupId
);
for (const participant of relevantParticipants) {
// Prüfe, ob die Aktivität für die Gruppe des Participants ist
const activityGroupIds = activity.groupActivities?.map(ga => ga.groupId) || [];
if (participant.groupId !== null && activityGroupIds.includes(participant.groupId)) {
// Für Gruppen-Aktivitäten: Verwende die PredefinedActivity aus GroupActivity
// Falls vorhanden, sonst die aus DiaryDateActivity
const groupActivity = activity.groupActivities?.find(ga => ga.groupId === participant.groupId);
if (groupActivity && groupActivity.groupPredefinedActivity) {
// Erstelle ein modifiziertes Activity-Objekt mit der PredefinedActivity aus GroupActivity
const modifiedActivity = {
...activity.toJSON(),
predefinedActivity: groupActivity.groupPredefinedActivity
};
groupActivities.push({
activity: modifiedActivity,
participant: participant,
id: null // Virtuell, nicht in DB
});
} else if (activity.predefinedActivity) {
// Fallback: Verwende die PredefinedActivity aus DiaryDateActivity
groupActivities.push({
activity: activity,
participant: participant,
id: null // Virtuell, nicht in DB
});
}
// Verwende die PredefinedActivity aus GroupActivity, falls vorhanden
// Sonst die aus DiaryDateActivity
const predefinedActivity = groupActivity.groupPredefinedActivity || activity.predefinedActivity;
if (predefinedActivity) {
// Erstelle ein modifiziertes Activity-Objekt
const modifiedActivity = {
...activity.toJSON(),
predefinedActivity: predefinedActivity
};
groupActivities.push({
activity: modifiedActivity,
participant: participant,
id: null // Virtuell, nicht in DB
});
}
}
}
@@ -392,27 +390,57 @@ export const getMemberLastParticipations = async (req, res) => {
// Kombiniere beide Listen
const allActivities = [...memberActivities, ...uniqueGroupActivities];
// Format the results
const participations = allActivities
// Gruppiere nach Datum
const participationsByDate = new Map();
allActivities
.filter(ma => {
if (!ma.activity || !ma.activity.predefinedActivity || !ma.activity.diaryDate || !ma.participant) {
return false;
}
return true;
})
.forEach(ma => {
const date = ma.activity.diaryDate.date;
const diaryDateId = ma.activity.diaryDate.id;
const activityName = ma.activity.predefinedActivity.name;
if (!participationsByDate.has(date)) {
participationsByDate.set(date, {
date: date,
diaryDateId: diaryDateId,
activities: []
});
}
const dateEntry = participationsByDate.get(date);
// Füge Aktivität nur hinzu, wenn sie noch nicht vorhanden ist (vermeide Duplikate)
if (!dateEntry.activities.find(a => a === activityName)) {
dateEntry.activities.push(activityName);
}
});
// Sortiere nach Datum (neueste zuerst) und nehme die letzten N Daten
const sortedDates = Array.from(participationsByDate.values())
.sort((a, b) => {
// Sortiere nach Datum (neueste zuerst)
const dateA = new Date(a.activity.diaryDate.date);
const dateB = new Date(b.activity.diaryDate.date);
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB - dateA;
})
.slice(0, parseInt(limit)) // Limit after filtering
.map(ma => ({
id: ma.id,
activityName: ma.activity.predefinedActivity.name,
date: ma.activity.diaryDate.date,
diaryDateId: ma.activity.diaryDate.id
}));
.slice(0, parseInt(limit));
// Formatiere für das Frontend: Flache Liste mit Datum und Aktivität
const participations = [];
sortedDates.forEach(dateEntry => {
dateEntry.activities.forEach(activityName => {
participations.push({
id: null, // Virtuell
activityName: activityName,
date: dateEntry.date,
diaryDateId: dateEntry.diaryDateId
});
});
});
return res.status(200).json(participations);

View File

@@ -10,10 +10,14 @@
<!-- Letzte 3 Teilnahmen -->
<div class="section">
<h3 class="section-title">Letzte 3 Teilnahmen</h3>
<div v-if="lastParticipations && lastParticipations.length" class="participations-list">
<div v-for="participation in lastParticipations" :key="participation.id" class="participation-item">
<div class="participation-name">{{ participation.activityName }}</div>
<div class="participation-date">{{ formatDate(participation.date) }}</div>
<div v-if="groupedParticipations && groupedParticipations.length" class="participations-list">
<div v-for="dateGroup in groupedParticipations" :key="dateGroup.date" class="participation-date-group">
<div class="participation-date-header">{{ formatDate(dateGroup.date) }}</div>
<div class="participation-activities">
<div v-for="(activity, index) in dateGroup.activities" :key="index" class="participation-item">
<div class="participation-name">{{ activity }}</div>
</div>
</div>
</div>
</div>
<div v-else class="no-data">
@@ -65,6 +69,39 @@ export default {
}
},
emits: ['update:modelValue', 'close'],
computed: {
groupedParticipations() {
if (!this.lastParticipations || this.lastParticipations.length === 0) {
return [];
}
// Gruppiere nach Datum
const grouped = new Map();
this.lastParticipations.forEach(participation => {
const date = participation.date;
if (!grouped.has(date)) {
grouped.set(date, {
date: date,
activities: []
});
}
const dateGroup = grouped.get(date);
// Füge Aktivität nur hinzu, wenn sie noch nicht vorhanden ist
if (!dateGroup.activities.includes(participation.activityName)) {
dateGroup.activities.push(participation.activityName);
}
});
// Sortiere nach Datum (neueste zuerst) und nehme die ersten 3
return Array.from(grouped.values())
.sort((a, b) => {
const dateA = new Date(a.date);
const dateB = new Date(b.date);
return dateB - dateA;
})
.slice(0, 3);
}
},
methods: {
handleClose() {
this.$emit('update:modelValue', false);
@@ -108,6 +145,26 @@ export default {
gap: 0.75rem;
}
.participation-date-group {
margin-bottom: 1rem;
}
.participation-date-header {
font-weight: 600;
color: var(--primary-color);
font-size: 0.95rem;
margin-bottom: 0.5rem;
padding-bottom: 0.25rem;
border-bottom: 1px solid var(--background-light);
}
.participation-activities {
display: flex;
flex-direction: column;
gap: 0.5rem;
margin-left: 0.5rem;
}
.participation-item,
.stat-item {
display: flex;