feat: Enhance club payment claims and task automation
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 1m0s

- Added `paid_amount_cents` column to `club_payment_claims` for better tracking of payments.
- Implemented compatibility checks for the new column in `clubPaymentClaimService` and `clubTaskAutomationService`.
- Updated various views to handle read-only states when editing is not allowed.
- Refactored forms in `ClubAccountsView`, `ClubInvoicesView`, `ClubTasksView`, and `ClubCommunicationView` to use factory functions for cleaner code.
- Introduced a new migration script to add the `paid_amount_cents` column if it doesn't exist and initialize it for existing paid claims.
- Created a detailed plan for enhancing club features and ensuring stability in existing modules.
This commit is contained in:
Torsten Schulz (local)
2026-07-13 17:01:13 +02:00
parent 90e6c2f9f6
commit ac97332e6f
17 changed files with 628 additions and 307 deletions

View File

@@ -13,6 +13,7 @@ import {
TrainingGroup,
} from '../models/index.js';
import clubArchiveService from '../services/clubArchiveService.js';
import { hasClubPaymentClaimPaidAmountCentsColumn } from '../services/clubPaymentClaimCompatibility.js';
import { getSafeErrorMessage } from '../utils/errorUtils.js';
function formatRequestWorkflowStage(stage) {
@@ -249,6 +250,7 @@ export const getClubDashboard = async (req, res) => {
today.setHours(0, 0, 0, 0);
const todayIso = today.toISOString().slice(0, 10);
const availableTables = await loadAvailableTables();
const hasPaidAmountCentsColumn = await hasClubPaymentClaimPaidAmountCentsColumn();
const [
requests,
@@ -293,14 +295,16 @@ export const getClubDashboard = async (req, res) => {
},
attributes: ['memberId'],
})),
loadOptionalTableData(availableTables, 'club_payment_claims', () => ClubPaymentClaim.findAll({
where: {
clubId,
status: { [Op.in]: ['open', 'partially_paid'] },
archivedAt: null,
},
order: [['dueOn', 'ASC']],
})),
hasPaidAmountCentsColumn
? loadOptionalTableData(availableTables, 'club_payment_claims', () => ClubPaymentClaim.findAll({
where: {
clubId,
status: { [Op.in]: ['open', 'partially_paid'] },
archivedAt: null,
},
order: [['dueOn', 'ASC']],
}))
: Promise.resolve([]),
loadOptionalTableData(availableTables, 'calendar_events', () => CalendarEvent.findAll({
where: {
clubId,

View File

@@ -10,6 +10,7 @@ import Season from '../models/Season.js';
import User from '../models/User.js';
import HttpError from '../exceptions/HttpError.js';
import { devLog } from '../utils/logger.js';
import { hasUserClubAccess } from '../utils/userUtils.js';
import { randomUUID } from 'crypto';
const teamDataFetchJobs = new Map();
@@ -635,15 +636,34 @@ class MyTischtennisUrlController {
/**
* Configure league from myTischtennis table URL
* POST /api/mytischtennis/configure-league
* Body: { url: string, createSeason?: boolean }
* Body: { url: string, clubId: number, createSeason?: boolean }
*/
async configureLeague(req, res, next) {
try {
const { url, createSeason } = req.body;
const { url, createSeason, clubId } = req.body;
const userIdOrEmail = req.headers.userid;
if (!url) {
throw new HttpError('URL is required', 400);
if (!url || !clubId) {
throw new HttpError('URL and clubId are required', 400);
}
let userId = userIdOrEmail;
if (isNaN(userIdOrEmail)) {
const user = await User.findOne({ where: { email: userIdOrEmail } });
if (!user) {
throw new HttpError('User not found', 404);
}
userId = user.id;
}
const normalizedClubId = Number.parseInt(clubId, 10);
if (!Number.isInteger(normalizedClubId) || normalizedClubId <= 0) {
throw new HttpError('clubId must be a valid number', 400);
}
const hasAccess = await hasUserClubAccess(userId, normalizedClubId);
if (!hasAccess) {
throw new HttpError('Keine Berechtigung für diesen Verein', 403);
}
// Parse URL
@@ -669,6 +689,7 @@ class MyTischtennisUrlController {
// Find or create league
let league = await League.findOne({
where: {
clubId: normalizedClubId,
myTischtennisGroupId: parsedData.groupId,
association: parsedData.association
}
@@ -677,6 +698,7 @@ class MyTischtennisUrlController {
if (!league) {
league = await League.create({
name: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version
clubId: normalizedClubId,
myTischtennisGroupId: parsedData.groupId,
association: parsedData.association,
groupname: parsedData.groupnameOriginal, // Verwende die originale URL-kodierte Version