more club funtions, fix for team edit
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -299,6 +299,7 @@ export const CLUB_DATA_MODELS = {
|
||||
'invoice_number',
|
||||
'external_reference',
|
||||
'party_id',
|
||||
'account_id',
|
||||
'issued_on',
|
||||
'due_on',
|
||||
'paid_on',
|
||||
|
||||
@@ -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 }) },
|
||||
|
||||
1061
frontend/src/views/ClubInvoicesView.vue
Normal file
1061
frontend/src/views/ClubInvoicesView.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user