more club funtions, fix for team edit

This commit is contained in:
Torsten Schulz (local)
2026-06-21 17:15:04 +02:00
parent 542fae089c
commit 302ed20a9b
18 changed files with 2032 additions and 16 deletions

View File

@@ -16,6 +16,10 @@ ALTER TABLE `clubs`
ADD COLUMN IF NOT EXISTS `billing_email` varchar(255) NULL,
ADD COLUMN IF NOT EXISTS `iban` varchar(34) NULL,
ADD COLUMN IF NOT EXISTS `bic` varchar(11) NULL,
ADD COLUMN IF NOT EXISTS `outgoing_invoice_prefix` varchar(24) NOT NULL DEFAULT 'RE',
ADD COLUMN IF NOT EXISTS `outgoing_invoice_next_number` int NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS `incoming_invoice_prefix` varchar(24) NOT NULL DEFAULT 'EI',
ADD COLUMN IF NOT EXISTS `incoming_invoice_next_number` int NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS `is_archived` tinyint(1) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS `archived_at` datetime NULL;
@@ -200,3 +204,67 @@ CREATE TABLE IF NOT EXISTS `club_accounts` (
KEY `idx_club_accounts_club_status` (`club_id`, `status`, `account_type`),
KEY `idx_club_accounts_default` (`club_id`, `is_default`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `club_invoice_parties` (
`id` bigint NOT NULL AUTO_INCREMENT,
`club_id` bigint NOT NULL,
`party_type` varchar(32) NOT NULL DEFAULT 'customer',
`name` varchar(255) NOT NULL,
`contact_name` varchar(255) NULL,
`email` varchar(255) NULL,
`phone` varchar(80) NULL,
`street` varchar(255) NULL,
`postal_code` varchar(24) NULL,
`city` varchar(120) NULL,
`country_code` varchar(2) NOT NULL DEFAULT 'DE',
`iban` varchar(34) NULL,
`bic` varchar(11) NULL,
`tax_identifier` varchar(64) NULL,
`notes` text NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_club_invoice_parties_club_type` (`club_id`, `party_type`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `club_invoices` (
`id` bigint NOT NULL AUTO_INCREMENT,
`club_id` bigint NOT NULL,
`invoice_direction` varchar(16) NOT NULL,
`invoice_type` varchar(32) NOT NULL,
`status` varchar(32) NOT NULL DEFAULT 'draft',
`invoice_number` varchar(64) NULL,
`external_reference` varchar(255) NULL,
`party_id` bigint NULL,
`account_id` bigint NULL,
`issued_on` date NULL,
`due_on` date NULL,
`paid_on` date NULL,
`net_amount_cents` bigint NOT NULL DEFAULT 0,
`tax_amount_cents` bigint NOT NULL DEFAULT 0,
`gross_amount_cents` bigint NOT NULL DEFAULT 0,
`currency_code` varchar(3) NOT NULL DEFAULT 'EUR',
`description` text NULL,
`document_id` bigint NULL,
`created_by_user_id` bigint NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`archived_at` datetime NULL,
PRIMARY KEY (`id`),
KEY `idx_club_invoices_club_direction_status` (`club_id`, `invoice_direction`, `status`, `due_on`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
CREATE TABLE IF NOT EXISTS `club_invoice_items` (
`id` bigint NOT NULL AUTO_INCREMENT,
`invoice_id` bigint NOT NULL,
`line_no` int NOT NULL DEFAULT 1,
`description` text NOT NULL,
`quantity` decimal(12,2) NOT NULL DEFAULT 1.00,
`unit_price_cents` bigint NOT NULL DEFAULT 0,
`tax_rate` decimal(5,2) NOT NULL DEFAULT 0.00,
`total_cents` bigint NOT NULL DEFAULT 0,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uq_club_invoice_items_line` (`invoice_id`, `line_no`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

View File

@@ -21,6 +21,10 @@ ALTER TABLE IF EXISTS clubs
ADD COLUMN IF NOT EXISTS billing_email varchar(255),
ADD COLUMN IF NOT EXISTS iban varchar(34),
ADD COLUMN IF NOT EXISTS bic varchar(11),
ADD COLUMN IF NOT EXISTS outgoing_invoice_prefix varchar(24) NOT NULL DEFAULT 'RE',
ADD COLUMN IF NOT EXISTS outgoing_invoice_next_number integer NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS incoming_invoice_prefix varchar(24) NOT NULL DEFAULT 'EI',
ADD COLUMN IF NOT EXISTS incoming_invoice_next_number integer NOT NULL DEFAULT 1,
ADD COLUMN IF NOT EXISTS is_archived boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS archived_at timestamptz;
@@ -507,6 +511,7 @@ CREATE TABLE IF NOT EXISTS club_invoices (
invoice_number varchar(64),
external_reference varchar(255),
party_id bigint,
account_id bigint,
issued_on date,
due_on date,
paid_on date,

View File

@@ -299,6 +299,7 @@ export const CLUB_DATA_MODELS = {
'invoice_number',
'external_reference',
'party_id',
'account_id',
'issued_on',
'due_on',
'paid_on',

View File

@@ -42,6 +42,7 @@ const ClubHistoryView = () => import('./views/ClubHistoryView.vue');
const ClubStatisticsView = () => import('./views/ClubStatisticsView.vue');
const ClubArchiveView = () => import('./views/ClubArchiveView.vue');
const ClubAccountsView = () => import('./views/ClubAccountsView.vue');
const ClubInvoicesView = () => import('./views/ClubInvoicesView.vue');
const ClubConceptModuleView = () => import('./views/ClubConceptModuleView.vue');
const Impressum = () => import('./views/Impressum.vue');
const Datenschutz = () => import('./views/Datenschutz.vue');
@@ -140,6 +141,7 @@ const conceptRoutes = CLUB_CONCEPT_ROUTES
.filter((route) => route.path !== '/club-statistics')
.filter((route) => route.path !== '/club-archive')
.filter((route) => route.path !== '/club-accounts')
.filter((route) => route.path !== '/club-invoices')
.map((route) => ({
path: route.path,
name: route.name,
@@ -189,6 +191,7 @@ const routes = [
{ path: '/club-statistics', name: 'club-statistics', component: ClubStatisticsView, meta: withMeta({ products: clubOnly, permission: ['statistics', 'read'] }) },
{ path: '/club-archive', name: 'club-archive', component: ClubArchiveView, meta: withMeta({ products: clubOnly, permission: ['settings', 'read'] }) },
{ path: '/club-accounts', name: 'club-accounts', component: ClubAccountsView, meta: withMeta({ products: clubOnly, permission: ['members', 'read'] }) },
{ path: '/club-invoices', name: 'club-invoices', component: ClubInvoicesView, meta: withMeta({ products: clubOnly, permission: ['members', 'read'] }) },
...conceptRoutes,
{ path: '/impressum', name: 'impressum', component: Impressum, meta: withMeta({ public: true, products: allProducts }) },
{ path: '/datenschutz', name: 'datenschutz', component: Datenschutz, meta: withMeta({ public: true, products: allProducts }) },

File diff suppressed because it is too large Load Diff

View File

@@ -124,6 +124,30 @@
</div>
</section>
<section v-if="currentClub && !loading" class="card">
<h2>Rechnungsnummern</h2>
<p class="hint">Automatische Nummernvergabe für Ausgangs- und Eingangsrechnungen.</p>
<div class="field-grid">
<div class="field-group">
<label>Präfix Ausgangsrechnungen</label>
<input v-model="outgoingInvoicePrefix" class="text-input" maxlength="24" placeholder="RE" />
</div>
<div class="field-group">
<label>Nächste Nummer Ausgang</label>
<input v-model.number="outgoingInvoiceNextNumber" class="text-input" type="number" min="1" step="1" />
</div>
<div class="field-group">
<label>Präfix Eingangsrechnungen</label>
<input v-model="incomingInvoicePrefix" class="text-input" maxlength="24" placeholder="EI" />
</div>
<div class="field-group">
<label>Nächste Nummer Eingang</label>
<input v-model.number="incomingInvoiceNextNumber" class="text-input" type="number" min="1" step="1" />
</div>
</div>
<p class="hint">Beispiel heute: {{ invoiceNumberPreview('outgoing') }} / {{ invoiceNumberPreview('incoming') }}</p>
</section>
<section v-if="currentClub && !loading" class="card actions-card">
<div class="actions">
<button class="btn btn-primary" @click="save">{{ $t('clubSettings.save') }}</button>
@@ -249,6 +273,10 @@ export default {
myTischtennisFedNickname: '',
autoFetchRankings: false,
memberDataQualityRequirements: defaultMemberDataQualityRequirements(),
outgoingInvoicePrefix: 'RE',
outgoingInvoiceNextNumber: 1,
incomingInvoicePrefix: 'EI',
incomingInvoiceNextNumber: 1,
saved: false,
loading: false,
loadError: null,
@@ -315,6 +343,10 @@ export default {
this.myTischtennisFedNickname = '';
this.autoFetchRankings = false;
this.memberDataQualityRequirements = defaultMemberDataQualityRequirements();
this.outgoingInvoicePrefix = 'RE';
this.outgoingInvoiceNextNumber = 1;
this.incomingInvoicePrefix = 'EI';
this.incomingInvoiceNextNumber = 1;
this.loadError = null;
return;
}
@@ -330,6 +362,10 @@ export default {
this.myTischtennisFedNickname = club?.myTischtennisFedNickname ?? '';
this.autoFetchRankings = !!club?.autoFetchRankings;
this.memberDataQualityRequirements = this.normalizeMemberDataQualityRequirements(club?.memberDataQualityRequirements);
this.outgoingInvoicePrefix = club?.outgoingInvoicePrefix ?? 'RE';
this.outgoingInvoiceNextNumber = Number(club?.outgoingInvoiceNextNumber ?? 1) || 1;
this.incomingInvoicePrefix = club?.incomingInvoicePrefix ?? 'EI';
this.incomingInvoiceNextNumber = Number(club?.incomingInvoiceNextNumber ?? 1) || 1;
} catch (e) {
this.loadError = this.$t('clubSettings.loadFailed');
this.greeting = '';
@@ -339,6 +375,10 @@ export default {
this.myTischtennisFedNickname = '';
this.autoFetchRankings = false;
this.memberDataQualityRequirements = defaultMemberDataQualityRequirements();
this.outgoingInvoicePrefix = 'RE';
this.outgoingInvoiceNextNumber = 1;
this.incomingInvoicePrefix = 'EI';
this.incomingInvoiceNextNumber = 1;
} finally {
this.loading = false;
}
@@ -363,6 +403,26 @@ export default {
return null;
}
},
normalizeInvoicePrefix(value, fallback) {
const normalized = String(value || fallback || '').trim().toUpperCase().slice(0, 24);
return normalized || fallback;
},
normalizeInvoiceNextNumber(value) {
const numeric = Number.parseInt(value, 10);
return Number.isInteger(numeric) && numeric > 0 ? numeric : 1;
},
invoiceNumberPreview(direction) {
const year = new Date().getFullYear();
const isIncoming = direction === 'incoming';
const prefix = isIncoming
? this.normalizeInvoicePrefix(this.incomingInvoicePrefix, 'EI')
: this.normalizeInvoicePrefix(this.outgoingInvoicePrefix, 'RE');
const nextNumber = isIncoming
? this.normalizeInvoiceNextNumber(this.incomingInvoiceNextNumber)
: this.normalizeInvoiceNextNumber(this.outgoingInvoiceNextNumber);
const padded = String(nextNumber).padStart(4, '0');
return `${prefix}-${year}-${padded}`;
},
emptyVenueForm() {
return { id: null, name: '', address: '', zip: '', city: '' };
},
@@ -444,6 +504,10 @@ export default {
myTischtennisFedNickname: this.myTischtennisFedNickname || null,
autoFetchRankings: this.autoFetchRankings,
memberDataQualityRequirements: this.normalizeMemberDataQualityRequirements(this.memberDataQualityRequirements),
outgoingInvoicePrefix: this.normalizeInvoicePrefix(this.outgoingInvoicePrefix, 'RE'),
outgoingInvoiceNextNumber: this.normalizeInvoiceNextNumber(this.outgoingInvoiceNextNumber),
incomingInvoicePrefix: this.normalizeInvoicePrefix(this.incomingInvoicePrefix, 'EI'),
incomingInvoiceNextNumber: this.normalizeInvoiceNextNumber(this.incomingInvoiceNextNumber),
});
this.saved = true;
setTimeout(() => (this.saved = false), 1500);