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:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user