Add last scheduler executions endpoint and member quick update functions
Implemented a new endpoint in ApiLogController to retrieve the last execution information for scheduler jobs. Added quick update functions in MemberService and corresponding routes for updating test membership status and marking member forms as handed over. Enhanced the MembersView to support quick actions for managing test memberships and form statuses, improving user experience and operational efficiency.
This commit is contained in:
@@ -62,6 +62,23 @@ class ApiLogController {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/logs/scheduler/last-executions
|
||||
* Get last execution info for scheduler jobs
|
||||
*/
|
||||
async getLastSchedulerExecutions(req, res, next) {
|
||||
try {
|
||||
const { clubId } = req.query;
|
||||
const results = await apiLogService.getLastSchedulerExecutions(clubId ? parseInt(clubId) : null);
|
||||
res.json({
|
||||
success: true,
|
||||
data: results
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ApiLogController();
|
||||
|
||||
@@ -100,6 +100,30 @@ const rotateMemberImage = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
const quickUpdateTestMembership = async (req, res) => {
|
||||
try {
|
||||
const { clubId, memberId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const result = await MemberService.quickUpdateTestMembership(userToken, clubId, memberId);
|
||||
res.status(result.status).json(result.response);
|
||||
} catch (error) {
|
||||
console.error('[quickUpdateTestMembership] - Error:', error);
|
||||
res.status(500).json({ error: 'Failed to update test membership' });
|
||||
}
|
||||
};
|
||||
|
||||
const quickUpdateMemberFormHandedOver = async (req, res) => {
|
||||
try {
|
||||
const { clubId, memberId } = req.params;
|
||||
const { authcode: userToken } = req.headers;
|
||||
const result = await MemberService.quickUpdateMemberFormHandedOver(userToken, clubId, memberId);
|
||||
res.status(result.status).json(result.response);
|
||||
} catch (error) {
|
||||
console.error('[quickUpdateMemberFormHandedOver] - Error:', error);
|
||||
res.status(500).json({ error: 'Failed to update member form status' });
|
||||
}
|
||||
};
|
||||
|
||||
const transferMembers = async (req, res) => {
|
||||
try {
|
||||
const { id: clubId } = req.params;
|
||||
@@ -132,4 +156,4 @@ const transferMembers = async (req, res) => {
|
||||
}
|
||||
};
|
||||
|
||||
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage, transferMembers };
|
||||
export { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage, transferMembers, quickUpdateTestMembership, quickUpdateMemberFormHandedOver };
|
||||
@@ -11,6 +11,9 @@ router.use(authenticate);
|
||||
// Get logs - requires permissions or admin
|
||||
router.get('/', apiLogController.getLogs);
|
||||
|
||||
// Get last scheduler executions
|
||||
router.get('/scheduler/last-executions', apiLogController.getLastSchedulerExecutions);
|
||||
|
||||
// Get single log by ID
|
||||
router.get('/:id', apiLogController.getLogById);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage, transferMembers } from '../controllers/memberController.js';
|
||||
import { getClubMembers, getWaitingApprovals, setClubMembers, uploadMemberImage, getMemberImage, updateRatingsFromMyTischtennis, rotateMemberImage, transferMembers, quickUpdateTestMembership, quickUpdateMemberFormHandedOver } from '../controllers/memberController.js';
|
||||
import express from 'express';
|
||||
import { authenticate } from '../middleware/authMiddleware.js';
|
||||
import { authorize } from '../middleware/authorizationMiddleware.js';
|
||||
@@ -17,5 +17,7 @@ router.get('/notapproved/:id', authenticate, authorize('members', 'read'), getWa
|
||||
router.post('/update-ratings/:id', authenticate, authorize('mytischtennis', 'write'), updateRatingsFromMyTischtennis);
|
||||
router.post('/rotate-image/:clubId/:memberId', authenticate, authorize('members', 'write'), rotateMemberImage);
|
||||
router.post('/transfer/:id', authenticate, authorize('members', 'write'), transferMembers);
|
||||
router.post('/quick-update-test-membership/:clubId/:memberId', authenticate, authorize('members', 'write'), quickUpdateTestMembership);
|
||||
router.post('/quick-update-member-form/:clubId/:memberId', authenticate, authorize('members', 'write'), quickUpdateMemberFormHandedOver);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -184,6 +184,130 @@ class ApiLogService {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last execution info for scheduler jobs
|
||||
*/
|
||||
async getLastSchedulerExecutions(clubId = null) {
|
||||
try {
|
||||
const jobTypes = ['rating_updates', 'match_results'];
|
||||
const results = {};
|
||||
|
||||
for (const jobType of jobTypes) {
|
||||
const lastExecution = await ApiLog.findOne({
|
||||
where: {
|
||||
logType: 'scheduler',
|
||||
schedulerJobType: jobType
|
||||
},
|
||||
order: [['createdAt', 'DESC']],
|
||||
attributes: ['id', 'createdAt', 'statusCode', 'responseBody', 'executionTime', 'errorMessage']
|
||||
});
|
||||
|
||||
if (lastExecution) {
|
||||
let parsedResponse = null;
|
||||
let updatedCount = null;
|
||||
let fetchedCount = null;
|
||||
let teamDetails = [];
|
||||
|
||||
// Parse responseBody to extract counts
|
||||
try {
|
||||
if (lastExecution.responseBody) {
|
||||
parsedResponse = JSON.parse(lastExecution.responseBody);
|
||||
// Extract counts from response
|
||||
if (parsedResponse.totalUpdated !== undefined) {
|
||||
updatedCount = parsedResponse.totalUpdated;
|
||||
} else if (parsedResponse.updatedCount !== undefined) {
|
||||
updatedCount = parsedResponse.updatedCount;
|
||||
}
|
||||
if (parsedResponse.totalFetched !== undefined) {
|
||||
fetchedCount = parsedResponse.totalFetched;
|
||||
} else if (parsedResponse.fetchedCount !== undefined) {
|
||||
fetchedCount = parsedResponse.fetchedCount;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors
|
||||
}
|
||||
|
||||
// For match_results, try to get team-specific details from API logs
|
||||
if (jobType === 'match_results') {
|
||||
try {
|
||||
// Find all API logs created around the same time as the scheduler execution
|
||||
// (within 5 minutes before and after)
|
||||
const timeWindowStart = new Date(lastExecution.createdAt);
|
||||
timeWindowStart.setMinutes(timeWindowStart.getMinutes() - 5);
|
||||
const timeWindowEnd = new Date(lastExecution.createdAt);
|
||||
timeWindowEnd.setMinutes(timeWindowEnd.getMinutes() + 5);
|
||||
|
||||
const teamLogs = await ApiLog.findAll({
|
||||
where: {
|
||||
logType: 'api_request',
|
||||
schedulerJobType: 'mytischtennis_fetch',
|
||||
createdAt: {
|
||||
[Op.between]: [timeWindowStart, timeWindowEnd]
|
||||
}
|
||||
},
|
||||
order: [['createdAt', 'DESC']],
|
||||
attributes: ['id', 'requestBody', 'statusCode', 'createdAt']
|
||||
});
|
||||
|
||||
// Extract team information from requestBody
|
||||
const teamMap = new Map();
|
||||
for (const log of teamLogs) {
|
||||
try {
|
||||
if (log.requestBody) {
|
||||
const requestData = JSON.parse(log.requestBody);
|
||||
if (requestData.clubTeamId && requestData.teamName) {
|
||||
const teamId = requestData.clubTeamId;
|
||||
if (!teamMap.has(teamId)) {
|
||||
teamMap.set(teamId, {
|
||||
clubTeamId: teamId,
|
||||
teamName: requestData.teamName,
|
||||
success: log.statusCode === 200,
|
||||
lastRun: log.createdAt
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors for individual logs
|
||||
}
|
||||
}
|
||||
|
||||
teamDetails = Array.from(teamMap.values());
|
||||
} catch (e) {
|
||||
console.error('Error extracting team details:', e);
|
||||
}
|
||||
}
|
||||
|
||||
results[jobType] = {
|
||||
lastRun: lastExecution.createdAt,
|
||||
success: lastExecution.statusCode === 200,
|
||||
executionTime: lastExecution.executionTime,
|
||||
updatedCount: updatedCount,
|
||||
fetchedCount: fetchedCount,
|
||||
errorMessage: lastExecution.errorMessage,
|
||||
teamDetails: teamDetails
|
||||
};
|
||||
} else {
|
||||
results[jobType] = {
|
||||
lastRun: null,
|
||||
success: null,
|
||||
executionTime: null,
|
||||
updatedCount: null,
|
||||
fetchedCount: null,
|
||||
errorMessage: null,
|
||||
teamDetails: []
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
} catch (error) {
|
||||
console.error('Error getting last scheduler executions:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new ApiLogService();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import UserClub from "../models/UserClub.js";
|
||||
import { checkAccess, getUserByToken, hasUserClubAccess } from "../utils/userUtils.js";
|
||||
import Member from "../models/Member.js";
|
||||
import Participant from "../models/Participant.js";
|
||||
import DiaryDate from "../models/DiaryDates.js";
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import sharp from 'sharp';
|
||||
@@ -492,6 +494,56 @@ class MemberService {
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
async quickUpdateTestMembership(userToken, clubId, memberId) {
|
||||
try {
|
||||
await checkAccess(userToken, clubId);
|
||||
const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
|
||||
if (!member) {
|
||||
return { status: 404, response: { error: 'Member not found in this club' } };
|
||||
}
|
||||
|
||||
if (!member.testMembership) {
|
||||
return { status: 400, response: { error: 'Member is not a test member' } };
|
||||
}
|
||||
|
||||
member.testMembership = false;
|
||||
await member.save();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: { success: true, message: 'Testmitgliedschaft entfernt' }
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[quickUpdateTestMembership] - Error:', error);
|
||||
return { status: 500, response: { error: 'Failed to update test membership' } };
|
||||
}
|
||||
}
|
||||
|
||||
async quickUpdateMemberFormHandedOver(userToken, clubId, memberId) {
|
||||
try {
|
||||
await checkAccess(userToken, clubId);
|
||||
const member = await Member.findOne({ where: { id: memberId, clubId: clubId } });
|
||||
if (!member) {
|
||||
return { status: 404, response: { error: 'Member not found in this club' } };
|
||||
}
|
||||
|
||||
if (member.memberFormHandedOver) {
|
||||
return { status: 400, response: { error: 'Member form already handed over' } };
|
||||
}
|
||||
|
||||
member.memberFormHandedOver = true;
|
||||
await member.save();
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: { success: true, message: 'Mitgliedsformular als ausgehändigt markiert' }
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('[quickUpdateMemberFormHandedOver] - Error:', error);
|
||||
return { status: 500, response: { error: 'Failed to update member form status' } };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default new MemberService();
|
||||
@@ -121,7 +121,8 @@
|
||||
<th>Geburtsdatum</th>
|
||||
<th>Telefon-Nr.</th>
|
||||
<th>Email-Adresse</th>
|
||||
<th></th>
|
||||
<th v-if="hasTestMembers">Trainingsteilnahmen</th>
|
||||
<th>Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -137,6 +138,7 @@
|
||||
<td>
|
||||
<span class="gender-symbol" :class="['gender-' + (member.gender || 'unknown'), { 'is-inactive': !member.active }]" :title="labelGender(member.gender)">{{ genderSymbol(member.gender) }}</span>
|
||||
<span class="gender-name" :class="['gender-' + (member.gender || 'unknown'), { 'is-inactive': !member.active }]">
|
||||
<span v-if="member.testMembership && member.trainingParticipations >= 5" class="warning-icon" title="5 oder mehr Trainingsteilnahmen">⚠️</span>
|
||||
{{ member.lastName }}, {{ member.firstName }}
|
||||
<span v-if="!member.active && showInactiveMembers" class="inactive-badge">inaktiv</span>
|
||||
</span>
|
||||
@@ -154,9 +156,21 @@
|
||||
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
|
||||
<td>{{ member.phone }}</td>
|
||||
<td>{{ member.email }}</td>
|
||||
<td v-if="hasTestMembers">
|
||||
<span v-if="member.testMembership">{{ member.trainingParticipations || 0 }}</span>
|
||||
<span v-else>-</span>
|
||||
</td>
|
||||
<td>
|
||||
<button @click.stop="openNotesModal(member)">Notizen</button>
|
||||
<button @click.stop="openActivitiesModal(member)" class="btn-activities">Übungen</button>
|
||||
<div class="action-buttons-row">
|
||||
<button v-if="member.testMembership" @click.stop="quickRemoveTestMembership(member)" class="btn-quick-action" title="Keine Testmitgliedschaft mehr">
|
||||
Keine Testmitgliedschaft
|
||||
</button>
|
||||
<button v-if="!member.memberFormHandedOver" @click.stop="quickMarkFormHandedOver(member)" class="btn-quick-action" title="Mitgliedsformular ausgehändigt">
|
||||
Formular ausgehändigt
|
||||
</button>
|
||||
<button @click.stop="openNotesModal(member)">Notizen</button>
|
||||
<button @click.stop="openActivitiesModal(member)" class="btn-activities">Übungen</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
@@ -286,6 +300,10 @@ export default {
|
||||
|
||||
return true;
|
||||
});
|
||||
},
|
||||
|
||||
hasTestMembers() {
|
||||
return this.members.some(member => member.testMembership);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
@@ -396,6 +414,87 @@ export default {
|
||||
this.members.forEach(member => {
|
||||
this.loadMemberImage(member);
|
||||
});
|
||||
|
||||
// Lade Trainingsteilnahmen für alle Testmitglieder auf einmal über /training-stats
|
||||
await this.loadTrainingParticipations();
|
||||
},
|
||||
|
||||
async loadTrainingParticipations() {
|
||||
try {
|
||||
const response = await apiClient.get(`/training-stats/${this.currentClub}`);
|
||||
const trainingStats = response.data.members || [];
|
||||
|
||||
console.log('[loadTrainingParticipations] Training Stats geladen:', trainingStats.length, 'Mitglieder');
|
||||
|
||||
// Erstelle eine Map für schnellen Zugriff: memberId -> participationTotal
|
||||
// Verwende parseInt, um sicherzustellen, dass IDs als Zahlen verglichen werden
|
||||
const participationMap = new Map();
|
||||
trainingStats.forEach(stat => {
|
||||
const memberId = parseInt(stat.id, 10);
|
||||
participationMap.set(memberId, stat.participationTotal || 0);
|
||||
console.log(`[loadTrainingParticipations] Map gesetzt: ID ${memberId} -> ${stat.participationTotal || 0}`);
|
||||
});
|
||||
|
||||
// Setze Trainingsteilnahmen für alle Testmitglieder
|
||||
const testMembers = this.members.filter(m => m.testMembership);
|
||||
console.log('[loadTrainingParticipations] Testmitglieder gefunden:', testMembers.length);
|
||||
|
||||
testMembers.forEach(member => {
|
||||
const memberId = parseInt(member.id, 10);
|
||||
const count = participationMap.get(memberId);
|
||||
console.log(`[loadTrainingParticipations] Mitglied ${memberId} (${member.firstName} ${member.lastName}): count=${count}`);
|
||||
// Nur setzen, wenn ein Wert gefunden wurde (nicht undefined)
|
||||
if (count !== undefined) {
|
||||
this.$set(member, 'trainingParticipations', count);
|
||||
console.log(`[loadTrainingParticipations] Trainingsteilnahmen gesetzt: ${count}`);
|
||||
} else {
|
||||
// Fallback: 0 setzen, wenn kein Wert gefunden wurde
|
||||
this.$set(member, 'trainingParticipations', 0);
|
||||
console.log(`[loadTrainingParticipations] Kein Wert gefunden, setze 0`);
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Trainingsteilnahmen:', error);
|
||||
// Bei Fehler setze 0 für alle Testmitglieder
|
||||
this.members.forEach(member => {
|
||||
if (member.testMembership) {
|
||||
this.$set(member, 'trainingParticipations', 0);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async quickRemoveTestMembership(member) {
|
||||
try {
|
||||
const response = await apiClient.post(`/clubmembers/quick-update-test-membership/${this.currentClub}/${member.id}`);
|
||||
if (response.data.success) {
|
||||
member.testMembership = false;
|
||||
member.trainingParticipations = undefined; // Entferne die Anzeige
|
||||
this.showInfo('Erfolg', response.data.message || 'Testmitgliedschaft entfernt', '', 'success');
|
||||
} else {
|
||||
this.showInfo('Fehler', response.data.error || 'Fehler beim Entfernen der Testmitgliedschaft', '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Entfernen der Testmitgliedschaft:', error);
|
||||
const errorMessage = error.response?.data?.error || error.message || 'Fehler beim Entfernen der Testmitgliedschaft';
|
||||
this.showInfo('Fehler', errorMessage, '', 'error');
|
||||
}
|
||||
},
|
||||
|
||||
async quickMarkFormHandedOver(member) {
|
||||
try {
|
||||
const response = await apiClient.post(`/clubmembers/quick-update-member-form/${this.currentClub}/${member.id}`);
|
||||
if (response.data.success) {
|
||||
member.memberFormHandedOver = true;
|
||||
this.showInfo('Erfolg', response.data.message || 'Mitgliedsformular als ausgehändigt markiert', '', 'success');
|
||||
} else {
|
||||
this.showInfo('Fehler', response.data.error || 'Fehler beim Markieren des Formulars', '', 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Markieren des Formulars:', error);
|
||||
const errorMessage = error.response?.data?.error || error.message || 'Fehler beim Markieren des Formulars';
|
||||
this.showInfo('Fehler', errorMessage, '', 'error');
|
||||
}
|
||||
},
|
||||
toggleNewMember() {
|
||||
this.memberFormIsOpen = !this.memberFormIsOpen;
|
||||
@@ -1172,4 +1271,33 @@ table td {
|
||||
.btn-transfer:hover {
|
||||
background-color: #138496;
|
||||
}
|
||||
|
||||
.action-buttons-row {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.btn-quick-action {
|
||||
background-color: #ffc107;
|
||||
color: #000;
|
||||
border: none;
|
||||
padding: 0.4rem 0.8rem;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
font-size: 0.85em;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.2s ease;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.btn-quick-action:hover {
|
||||
background-color: #e0a800;
|
||||
}
|
||||
|
||||
.warning-icon {
|
||||
margin-right: 0.25rem;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,39 @@
|
||||
:show-current-season="true"
|
||||
/>
|
||||
|
||||
<!-- Automatische Jobs Info -->
|
||||
<div v-if="schedulerJobs.rating_updates || schedulerJobs.match_results" class="scheduler-jobs-info">
|
||||
<div class="job-info" v-if="schedulerJobs.rating_updates?.lastRun">
|
||||
<span class="job-label">🔄 Rating-Updates:</span>
|
||||
<span class="job-details">
|
||||
Zuletzt: {{ formatJobDate(schedulerJobs.rating_updates.lastRun) }}
|
||||
<span v-if="schedulerJobs.rating_updates.updatedCount !== null" class="job-count">
|
||||
({{ schedulerJobs.rating_updates.updatedCount }} aktualisiert)
|
||||
</span>
|
||||
<span v-if="!schedulerJobs.rating_updates.success" class="job-error">⚠️ Fehler</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="job-info" v-if="schedulerJobs.match_results?.lastRun">
|
||||
<div class="job-header">
|
||||
<span class="job-label">📊 Spielergebnisse:</span>
|
||||
<span class="job-details">
|
||||
Zuletzt: {{ formatJobDate(schedulerJobs.match_results.lastRun) }}
|
||||
<span v-if="schedulerJobs.match_results.fetchedCount !== null" class="job-count">
|
||||
({{ schedulerJobs.match_results.fetchedCount }} abgerufen)
|
||||
</span>
|
||||
<span v-if="!schedulerJobs.match_results.success" class="job-error">⚠️ Fehler</span>
|
||||
</span>
|
||||
</div>
|
||||
<div v-if="schedulerJobs.match_results.teamDetails && schedulerJobs.match_results.teamDetails.length > 0" class="team-details">
|
||||
<div v-for="team in getFilteredTeamDetails(schedulerJobs.match_results.teamDetails)" :key="team.clubTeamId" class="team-detail-item">
|
||||
<span class="team-name">{{ team.teamName }}</span>
|
||||
<span v-if="team.success" class="team-status success">✓</span>
|
||||
<span v-else class="team-status error">✗</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="newteam">
|
||||
<div class="toggle-new-team">
|
||||
<span @click="toggleNewTeam">
|
||||
@@ -167,6 +200,25 @@
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Automatische Jobs Info für dieses Team -->
|
||||
<div v-if="getTeamJobInfo(teamToEdit)" class="team-job-info compact">
|
||||
<span class="section-title">🔄 Automatische Jobs</span>
|
||||
<div v-if="getTeamJobInfo(teamToEdit).lastRun" class="team-job-details">
|
||||
<div class="team-job-item">
|
||||
<span class="team-job-label">Zuletzt aktualisiert:</span>
|
||||
<span class="team-job-value">{{ formatJobDate(getTeamJobInfo(teamToEdit).lastRun) }}</span>
|
||||
</div>
|
||||
<div v-if="getTeamJobInfo(teamToEdit).success !== null" class="team-job-item">
|
||||
<span class="team-job-label">Status:</span>
|
||||
<span v-if="getTeamJobInfo(teamToEdit).success" class="team-job-status success">✓ Erfolgreich</span>
|
||||
<span v-else class="team-job-status error">✗ Fehler</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="team-job-details">
|
||||
<span class="team-job-no-data">Noch keine automatische Aktualisierung</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- MyTischtennis URL Konfiguration -->
|
||||
<div class="mytischtennis-config compact">
|
||||
<div class="mytischtennis-header-compact">
|
||||
@@ -261,6 +313,20 @@
|
||||
✗ Nicht konfiguriert
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Automatische Jobs Info für dieses Team -->
|
||||
<div class="info-row team-job-status-row">
|
||||
<span class="label">🔄 Automatische Jobs:</span>
|
||||
<span class="value">
|
||||
<span v-if="getTeamJobInfo(team) && getTeamJobInfo(team).lastRun" class="team-job-status-value">
|
||||
{{ formatJobDate(getTeamJobInfo(team).lastRun) }}
|
||||
<span v-if="getTeamJobInfo(team).success !== null" class="team-job-status-icon" :class="getTeamJobInfo(team).success ? 'success' : 'error'">
|
||||
{{ getTeamJobInfo(team).success ? '✓' : '✗' }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else class="team-job-no-data">Nie</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PDF-Dokumente Icons -->
|
||||
@@ -401,6 +467,12 @@ export default {
|
||||
const loadingStats = ref(false);
|
||||
const memberById = ref({});
|
||||
|
||||
// Scheduler Jobs Info
|
||||
const schedulerJobs = ref({
|
||||
rating_updates: null,
|
||||
match_results: null
|
||||
});
|
||||
|
||||
// Computed
|
||||
const selectedClub = computed(() => store.state.currentClub);
|
||||
const authToken = computed(() => store.state.token);
|
||||
@@ -442,6 +514,9 @@ export default {
|
||||
|
||||
// Lade alle Team-Dokumente nach dem Laden der Teams
|
||||
await loadAllTeamDocuments();
|
||||
|
||||
// Aktualisiere Job-Informationen, damit Team-Filterung korrekt funktioniert
|
||||
await loadSchedulerJobsInfo();
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Club-Teams:', error);
|
||||
}
|
||||
@@ -494,12 +569,14 @@ export default {
|
||||
}
|
||||
};
|
||||
|
||||
const editTeam = (team) => {
|
||||
const editTeam = async (team) => {
|
||||
teamToEdit.value = team;
|
||||
newTeamName.value = team.name;
|
||||
newLeagueId.value = team.leagueId || '';
|
||||
teamFormIsOpen.value = true;
|
||||
loadTeamDocuments();
|
||||
await loadTeamDocuments();
|
||||
// Aktualisiere Job-Informationen, damit Team-spezifische Daten aktuell sind
|
||||
await loadSchedulerJobsInfo();
|
||||
};
|
||||
|
||||
const deleteTeam = async (team) => {
|
||||
@@ -568,6 +645,31 @@ export default {
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
};
|
||||
|
||||
const formatJobDate = (dateString) => {
|
||||
if (!dateString) return 'Nie';
|
||||
const date = new Date(dateString);
|
||||
const now = new Date();
|
||||
const diffMs = now - date;
|
||||
const diffMins = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
const diffDays = Math.floor(diffMs / 86400000);
|
||||
|
||||
if (diffMins < 1) return 'gerade eben';
|
||||
if (diffMins < 60) return `vor ${diffMins} Min.`;
|
||||
if (diffHours < 24) return `vor ${diffHours} Std.`;
|
||||
if (diffDays === 1) return 'gestern';
|
||||
if (diffDays < 7) return `vor ${diffDays} Tagen`;
|
||||
|
||||
// Format: DD.MM.YYYY HH:MM
|
||||
return date.toLocaleDateString('de-DE', {
|
||||
day: '2-digit',
|
||||
month: '2-digit',
|
||||
year: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
const loadTeamDocuments = async () => {
|
||||
if (!teamToEdit.value) return;
|
||||
@@ -739,10 +841,54 @@ export default {
|
||||
loadLeagues();
|
||||
};
|
||||
|
||||
// Load scheduler jobs info
|
||||
const loadSchedulerJobsInfo = async () => {
|
||||
try {
|
||||
const clubIdParam = selectedClub.value ? `?clubId=${selectedClub.value}` : '';
|
||||
const response = await apiClient.get(`/logs/scheduler/last-executions${clubIdParam}`);
|
||||
if (response.data.success) {
|
||||
schedulerJobs.value = response.data.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Job-Informationen:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Filter team details to only show teams from current club
|
||||
const getFilteredTeamDetails = (teamDetails) => {
|
||||
if (!teamDetails || !selectedClub.value) return teamDetails;
|
||||
// Filter by checking if team exists in current club's teams
|
||||
return teamDetails.filter(team => {
|
||||
return teams.value.some(t => t.id === team.clubTeamId);
|
||||
});
|
||||
};
|
||||
|
||||
// Get job info for a specific team
|
||||
const getTeamJobInfo = (team) => {
|
||||
if (!team || !team.id || !schedulerJobs.value.match_results?.teamDetails) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const teamDetail = schedulerJobs.value.match_results.teamDetails.find(
|
||||
td => td.clubTeamId === team.id
|
||||
);
|
||||
|
||||
if (!teamDetail) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
lastRun: teamDetail.lastRun,
|
||||
success: teamDetail.success
|
||||
};
|
||||
};
|
||||
|
||||
// Lifecycle
|
||||
onMounted(() => {
|
||||
// Lade Ligen beim ersten Laden der Seite (ohne Saison-Filter)
|
||||
loadLeagues();
|
||||
// Lade Job-Informationen
|
||||
loadSchedulerJobsInfo();
|
||||
});
|
||||
|
||||
// PDF-Dialog Funktionen
|
||||
@@ -1188,7 +1334,12 @@ export default {
|
||||
getMyTischtennisStatus,
|
||||
fetchTeamDataManually,
|
||||
refreshPlayerStats
|
||||
,memberById
|
||||
,memberById,
|
||||
schedulerJobs,
|
||||
formatJobDate,
|
||||
loadSchedulerJobsInfo,
|
||||
getFilteredTeamDetails,
|
||||
getTeamJobInfo
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -2259,4 +2410,170 @@ export default {
|
||||
color: #2e7d32;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Scheduler Jobs Info */
|
||||
.scheduler-jobs-info {
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: var(--background-light);
|
||||
border-radius: var(--border-radius);
|
||||
border: 1px solid var(--border-color);
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.job-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.job-info:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.job-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-color);
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.job-details {
|
||||
color: var(--text-muted);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.job-count {
|
||||
color: var(--primary-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.job-error {
|
||||
color: #dc3545;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.job-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.team-details {
|
||||
margin-top: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.team-detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.8rem;
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: white;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.team-name {
|
||||
flex: 1;
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.team-status {
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.team-status.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.team-status.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
/* Team Job Info */
|
||||
.team-job-info.compact {
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0.75rem;
|
||||
background: var(--background-light);
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.team-job-details {
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.team-job-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
.team-job-label {
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.team-job-value {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.team-job-status {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.team-job-status.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.team-job-status.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
|
||||
.team-job-no-data {
|
||||
color: var(--text-muted);
|
||||
font-size: 0.85rem;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.team-job-status-row {
|
||||
margin-top: 0.3rem;
|
||||
padding-top: 0.3rem;
|
||||
border-top: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.team-job-status-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.3rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.team-job-status-icon {
|
||||
font-weight: bold;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.team-job-status-icon.success {
|
||||
color: #28a745;
|
||||
}
|
||||
|
||||
.team-job-status-icon.error {
|
||||
color: #dc3545;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user