Fügt Unterstützung für offizielle Turniere und Wettbewerbe hinzu. Aktualisiert die Datenbankmodelle, um Geschlecht für Mitglieder zu erfassen, und implementiert neue Routen sowie Frontend-Komponenten zur Anzeige und Verwaltung dieser Daten. Verbessert die Benutzeroberfläche zur Eingabe von Mitgliederdaten und aktualisiert die Abhängigkeiten im Projekt.
This commit is contained in:
@@ -52,6 +52,10 @@
|
||||
<span class="nav-icon">🏆</span>
|
||||
Turniere
|
||||
</a>
|
||||
<a href="/official-tournaments" class="nav-link">
|
||||
<span class="nav-icon">📄</span>
|
||||
Offizielle Turniere
|
||||
</a>
|
||||
<a href="/predefined-activities" class="nav-link">
|
||||
<span class="nav-icon">⚙️</span>
|
||||
Vordefinierte Aktivitäten
|
||||
|
||||
@@ -12,6 +12,7 @@ import ScheduleView from './views/ScheduleView.vue';
|
||||
import TournamentsView from './views/TournamentsView.vue';
|
||||
import TrainingStatsView from './views/TrainingStatsView.vue';
|
||||
import PredefinedActivities from './views/PredefinedActivities.vue';
|
||||
import OfficialTournaments from './views/OfficialTournaments.vue';
|
||||
|
||||
const routes = [
|
||||
{ path: '/register', component: Register },
|
||||
@@ -27,6 +28,7 @@ const routes = [
|
||||
{ path: '/tournaments', component: TournamentsView },
|
||||
{ path: '/training-stats', component: TrainingStatsView },
|
||||
{ path: '/predefined-activities', component: PredefinedActivities },
|
||||
{ path: '/official-tournaments', component: OfficialTournaments },
|
||||
];
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -20,6 +20,14 @@
|
||||
<label><span>Geburtsdatum:</span> <input type="date" v-model="newBirthdate"></label>
|
||||
<label><span>Telefon-Nr.:</span> <input type="text" v-model="newPhone"></label>
|
||||
<label><span>Email-Adresse:</span> <input type="email" v-model="newEmail"></label>
|
||||
<label><span>Geschlecht:</span>
|
||||
<select v-model="newGender">
|
||||
<option value="unknown">Unbekannt</option>
|
||||
<option value="male">Männlich</option>
|
||||
<option value="female">Weiblich</option>
|
||||
<option value="diverse">Divers</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="checkbox-item"><span>Aktiv:</span> <input type="checkbox" v-model="newActive"></label>
|
||||
<label class="checkbox-item"><span>Pics in Internet erlaubt:</span> <input type="checkbox" v-model="newPicsInInternetAllowed"></label>
|
||||
<label class="checkbox-item"><span>Testmitgliedschaft:</span> <input type="checkbox" v-model="testMembership"></label>
|
||||
@@ -52,6 +60,7 @@
|
||||
<th>Geburtsdatum</th>
|
||||
<th>Telefon-Nr.</th>
|
||||
<th>Email-Adresse</th>
|
||||
<th>Geschlecht</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -70,6 +79,7 @@
|
||||
<td>{{ getFormattedBirthdate(member.birthDate) }}</td>
|
||||
<td>{{ member.phone }}</td>
|
||||
<td>{{ member.email }}</td>
|
||||
<td>{{ member.gender || 'unknown' }}</td>
|
||||
<td>
|
||||
<button @click.stop="openNotesModal(member)">Notizen</button>
|
||||
</td>
|
||||
@@ -123,9 +133,10 @@ export default {
|
||||
newLastname: '',
|
||||
newStreet: '',
|
||||
newCity: '',
|
||||
newBirthdate: '01.01.2010',
|
||||
newBirthdate: '',
|
||||
newPhone: '',
|
||||
newEmail: '',
|
||||
newGender: 'unknown',
|
||||
newActive: true,
|
||||
memberToEdit: null,
|
||||
memberImage: null,
|
||||
@@ -174,12 +185,13 @@ export default {
|
||||
this.newLastname = '';
|
||||
this.newStreet = '';
|
||||
this.newCity = '';
|
||||
this.newBirthdate = '01.01.2010';
|
||||
this.newBirthdate = '';
|
||||
this.newPhone = '';
|
||||
this.newEmail = '';
|
||||
this.newActive = true;
|
||||
this.newPicsInInternetAllowed = false;
|
||||
this.testMembership = true;
|
||||
this.newGender = 'unknown';
|
||||
this.memberImage = null;
|
||||
this.memberImagePreview = null;
|
||||
},
|
||||
@@ -203,6 +215,7 @@ export default {
|
||||
birthdate: this.newBirthdate,
|
||||
phone: this.newPhone,
|
||||
email: this.newEmail,
|
||||
gender: this.newGender,
|
||||
active: this.newActive,
|
||||
id: this.memberToEdit ? this.memberToEdit.id : null,
|
||||
testMembership: this.testMembership,
|
||||
@@ -246,8 +259,9 @@ export default {
|
||||
this.newCity = member.city;
|
||||
this.newPhone = member.phone;
|
||||
this.newEmail = member.email;
|
||||
this.newGender = member.gender || 'unknown';
|
||||
this.newActive = member.active;
|
||||
this.newBirthdate = date.toISOString().split('T')[0];
|
||||
this.newBirthdate = this.formatDateForInput(birthDate);
|
||||
this.testMembership = member.testMembership;
|
||||
this.newPicsInInternetAllowed = member.picsInInternetAllowed;
|
||||
try {
|
||||
@@ -260,6 +274,22 @@ export default {
|
||||
this.memberImagePreview = null;
|
||||
}
|
||||
},
|
||||
formatDateForInput(value) {
|
||||
if (!value || typeof value !== 'string') return '';
|
||||
const v = value.trim();
|
||||
// dd.mm.yyyy
|
||||
const m1 = v.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||
if (m1) {
|
||||
const yyyy = m1[3];
|
||||
const mm = String(Number(m1[2])).padStart(2, '0');
|
||||
const dd = String(Number(m1[1])).padStart(2, '0');
|
||||
return `${yyyy}-${mm}-${dd}`;
|
||||
}
|
||||
// ISO/yyy-mm-dd
|
||||
const d = new Date(v);
|
||||
if (!isNaN(d.getTime())) return d.toISOString().split('T')[0];
|
||||
return '';
|
||||
},
|
||||
resetToNewMember() {
|
||||
this.memberToEdit = null;
|
||||
this.resetNewMember();
|
||||
|
||||
344
frontend/src/views/OfficialTournaments.vue
Normal file
344
frontend/src/views/OfficialTournaments.vue
Normal file
@@ -0,0 +1,344 @@
|
||||
<template>
|
||||
<div class="official-tournaments">
|
||||
<h2>Offizielle Turniere</h2>
|
||||
<div v-if="list && list.length" class="list">
|
||||
<h3>Gespeicherte Veranstaltungen</h3>
|
||||
<ul>
|
||||
<li v-for="t in list" :key="t.id" style="display:flex; align-items:center; gap:.5rem;">
|
||||
<a href="#" @click.prevent="uploadedId = String(t.id); reload();" style="flex:1;">
|
||||
{{ t.title || ('Turnier #' + t.id) }}
|
||||
</a>
|
||||
<span v-if="t.termin || t.eventDate"> — {{ t.termin || t.eventDate }}</span>
|
||||
<button class="btn-secondary" @click.prevent="removeTournament(t)" title="Löschen">🗑️</button>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="uploader">
|
||||
<input type="file" accept="application/pdf" @change="onFile" />
|
||||
<button class="btn-primary" :disabled="!selectedFile" @click="upload">PDF hochladen</button>
|
||||
</div>
|
||||
<div v-if="parsed">
|
||||
<div class="meta">
|
||||
<div><strong>Titel:</strong> {{ parsed.parsedData.title || '–' }}</div>
|
||||
<div><strong>Termin:</strong> {{ parsed.parsedData.termin || '–' }}</div>
|
||||
<div><strong>Austragungsorte:</strong>
|
||||
<ul>
|
||||
<li v-for="(o,i) in parsed.parsedData.austragungsorte" :key="i">{{ o }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div><strong>Konkurrenztypen:</strong> {{ (parsed.parsedData.konkurrenztypen||[]).join(', ') || '–' }}</div>
|
||||
<div><strong>Meldeschlüsse:</strong> {{ (parsed.parsedData.meldeschluesse||[]).join(' | ') || '–' }}</div>
|
||||
<div><strong>Altersklassen:</strong> {{ (parsed.parsedData.altersklassen||[]).join(', ') || '–' }}</div>
|
||||
<div><strong>Startzeiten:</strong>
|
||||
<span v-for="(t,ak) in parsed.parsedData.startzeiten" :key="ak" style="margin-right:.5rem;">{{ ak }}: {{ t }}</span>
|
||||
</div>
|
||||
<div v-if="parsed.parsedData.meldeschluesseByAk && Object.keys(parsed.parsedData.meldeschluesseByAk).length">
|
||||
<strong>Meldeschlüsse je AK:</strong>
|
||||
<span v-for="(arr,ak) in parsed.parsedData.meldeschluesseByAk" :key="ak" style="margin-right:.5rem;">
|
||||
{{ ak }}: {{ arr.join(', ') }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<!-- ehemals 'Erkannte Einträge' entfernt -->
|
||||
</div>
|
||||
|
||||
<div v-if="parsed && parsed.parsedData.competitions && parsed.parsedData.competitions.length">
|
||||
<h3>Konkurrenzen</h3>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>Altersklasse/Wettbewerb</th>
|
||||
<th>Startzeit</th>
|
||||
<th>Startgeld</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(c,idx) in parsed.parsedData.competitions" :key="idx">
|
||||
<tr>
|
||||
<td style="width:2.5rem;">
|
||||
<button class="btn-secondary" @click.prevent="toggleRow(c, idx)" :aria-expanded="isExpanded(c, idx)">
|
||||
{{ isExpanded(c, idx) ? '▾' : '▸' }}
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ c.ageClassCompetition || c.altersklasseWettbewerb }}</td>
|
||||
<td>{{ c.startTime || c.startzeit || '–' }}</td>
|
||||
<td>{{ c.entryFee || c.startgeld || '–' }}</td>
|
||||
</tr>
|
||||
<tr v-if="isExpanded(c, idx)" class="comp-details">
|
||||
<td :colspan="4">
|
||||
<div class="details">
|
||||
<div class="detail-item"><strong>Meldeschluss (Datum):</strong> {{ c.registrationDeadlineDate || c.meldeschlussDatum || '–' }}</div>
|
||||
<div class="detail-item"><strong>Meldeschluss (Online):</strong> {{ c.registrationDeadlineOnline || c.meldeschlussOnline || '–' }}</div>
|
||||
<div class="detail-item"><strong>Stichtag:</strong> {{ c.cutoffDate || c.stichtag || '–' }}</div>
|
||||
<div class="detail-item"><strong>Offen für:</strong> {{ c.openTo || c.offenFuer || '–' }}</div>
|
||||
<div class="detail-item"><strong>Vorrunde:</strong> {{ c.preliminaryRound || c.vorrunde || '–' }}</div>
|
||||
<div class="detail-item"><strong>Endrunde:</strong> {{ c.finalRound || c.endrunde || '–' }}</div>
|
||||
<div class="detail-item">
|
||||
<strong>Teilnahmeberechtigt ({{ eligibleMembers(c).length }}):</strong>
|
||||
<table class="eligible-table" v-if="eligibleMembers(c).length">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Name</th>
|
||||
<th>Geburtsdatum</th>
|
||||
<th>Alter</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="m in eligibleMembers(c)" :key="m.id">
|
||||
<td>{{ m.firstName }} {{ m.lastName }}</td>
|
||||
<td>{{ formatDateStr(m.birthDate) }}</td>
|
||||
<td>{{ ageOnRef(m, c) ?? '–' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import apiClient from '../apiClient.js';
|
||||
import { mapGetters } from 'vuex';
|
||||
|
||||
export default {
|
||||
name: 'OfficialTournaments',
|
||||
data() {
|
||||
return {
|
||||
selectedFile: null,
|
||||
uploadedId: null,
|
||||
parsed: null,
|
||||
members: [],
|
||||
matches: {},
|
||||
list: [],
|
||||
expanded: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentClub'])
|
||||
},
|
||||
methods: {
|
||||
onFile(e) {
|
||||
this.selectedFile = e.target.files && e.target.files[0] ? e.target.files[0] : null;
|
||||
},
|
||||
isExpanded(c, idx) {
|
||||
return !!this.expanded[String(idx)];
|
||||
},
|
||||
toggleRow(c, idx) {
|
||||
const k = String(idx);
|
||||
this.$set ? this.$set(this.expanded, k, !this.expanded[k]) : (this.expanded[k] = !this.expanded[k]);
|
||||
},
|
||||
async upload() {
|
||||
if (!this.selectedFile) return;
|
||||
const fd = new FormData();
|
||||
fd.append('pdf', this.selectedFile);
|
||||
const r = await apiClient.post(`/official-tournaments/${this.currentClub}/upload`, fd, {
|
||||
headers: { 'Content-Type': 'multipart/form-data' }
|
||||
});
|
||||
this.uploadedId = r.data.id;
|
||||
await this.reload();
|
||||
await this.loadList();
|
||||
},
|
||||
async reload() {
|
||||
if (!this.uploadedId) return;
|
||||
const t = await apiClient.get(`/official-tournaments/${this.currentClub}/${this.uploadedId}`);
|
||||
this.parsed = t.data;
|
||||
// Mitglieder laden (alle aktiv)
|
||||
const m = await apiClient.get(`/clubmembers/get/${this.currentClub}/true`);
|
||||
this.members = m.data;
|
||||
},
|
||||
async loadList() {
|
||||
const r = await apiClient.get(`/official-tournaments/${this.currentClub}`);
|
||||
this.list = r.data;
|
||||
},
|
||||
// Eligibility: Datum parsen (dd.mm.yyyy | yyyy-mm-dd | ISO)
|
||||
parseDateFlexible(s) {
|
||||
if (!s || typeof s !== 'string') return null;
|
||||
const t = s.trim();
|
||||
let m = t.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||
if (m) {
|
||||
const d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
m = t.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (m) {
|
||||
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
const d = new Date(t);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
},
|
||||
getCutoffDate(c) {
|
||||
return this.parseDateFlexible(c.cutoffDate || c.stichtag || '');
|
||||
},
|
||||
getMemberBirthDate(member) {
|
||||
return this.parseDateFlexible(member.birthDate || '');
|
||||
},
|
||||
// Referenzdatum für Altersberechnung: Startzeit-Datum > Turnier-Termin
|
||||
getReferenceDate(c) {
|
||||
const fromStart = (c.startTime || c.startzeit || '').match(/(\d{1,2}\.\d{1,2}\.\d{4})/);
|
||||
if (fromStart) return this.parseDateFlexible(fromStart[1]);
|
||||
return this.parseDateFlexible(this.parsed?.parsedData?.termin || '');
|
||||
},
|
||||
// Uxx oder AK xx erkennen -> Maximalalter in Jahren
|
||||
getAgeLimitFromText(text) {
|
||||
if (!text) return null;
|
||||
const t = String(text).toUpperCase();
|
||||
let m = t.match(/\bU\s*(\d{1,2})\b/);
|
||||
if (m) return Number(m[1]);
|
||||
m = t.match(/\bAK\s*(\d{1,2})\b/);
|
||||
if (m) return Number(m[1]);
|
||||
return null;
|
||||
},
|
||||
calculateAgeOnDate(birthDate, refDate) {
|
||||
if (!birthDate || !refDate) return null;
|
||||
let age = refDate.getFullYear() - birthDate.getFullYear();
|
||||
const m = refDate.getMonth() - birthDate.getMonth();
|
||||
if (m < 0 || (m === 0 && refDate.getDate() < birthDate.getDate())) age--;
|
||||
return age;
|
||||
},
|
||||
getGenderRule(c) {
|
||||
const txt = `${c.ageClassCompetition || c.altersklasseWettbewerb || ''} ${c.openTo || c.offenFuer || ''}`.toLowerCase();
|
||||
if (/(mädchen|weiblich|\bw\b)/.test(txt)) return 'female';
|
||||
if (/(jungen|männlich|\bm\b)/.test(txt)) return 'male';
|
||||
if (/jugend/.test(txt)) return 'both';
|
||||
return 'both';
|
||||
},
|
||||
isEligibleByAge(member, c) {
|
||||
const cutoff = this.getCutoffDate(c);
|
||||
const bd = this.getMemberBirthDate(member);
|
||||
if (cutoff) {
|
||||
if (!bd || !(bd.getTime() > cutoff.getTime())) return false; // jünger als Stichtag
|
||||
}
|
||||
const limit = this.getAgeLimitFromText((c.ageClassCompetition || c.altersklasseWettbewerb || ''));
|
||||
if (limit != null) {
|
||||
const ref = this.getReferenceDate(c);
|
||||
const age = this.calculateAgeOnDate(bd, ref);
|
||||
if (age == null || !(age < limit)) return false; // Uxx => Alter < xx
|
||||
}
|
||||
return true;
|
||||
},
|
||||
isEligibleForCompetition(member, c) {
|
||||
// Geschlecht
|
||||
const rule = this.getGenderRule(c);
|
||||
const g = member.gender || 'unknown';
|
||||
if (rule === 'female' && g !== 'female') return false;
|
||||
if (rule === 'male' && g !== 'male') return false;
|
||||
// Alter (Stichtag UND/ODER U/AK-Regel; beide greifen, wenn vorhanden)
|
||||
if (!this.isEligibleByAge(member, c)) return false;
|
||||
return true;
|
||||
},
|
||||
eligibleMembers(c) {
|
||||
return (this.members || []).filter(m => this.isEligibleForCompetition(m, c));
|
||||
},
|
||||
ageOnRef(member, c) {
|
||||
const bd = this.getMemberBirthDate(member);
|
||||
const ref = this.getReferenceDate(c);
|
||||
return this.calculateAgeOnDate(bd, ref);
|
||||
},
|
||||
formatDateStr(s) {
|
||||
const d = this.parseDateFlexible(s);
|
||||
if (!d) return '–';
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const yyyy = d.getFullYear();
|
||||
return `${dd}.${mm}.${yyyy}`;
|
||||
},
|
||||
// Eligibility helpers
|
||||
parseDateFlexible(s) {
|
||||
if (!s || typeof s !== 'string') return null;
|
||||
const t = s.trim();
|
||||
let m = t.match(/^(\d{1,2})\.(\d{1,2})\.(\d{4})$/);
|
||||
if (m) {
|
||||
const d = new Date(Number(m[3]), Number(m[2]) - 1, Number(m[1]));
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
m = t.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (m) {
|
||||
const d = new Date(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
}
|
||||
const d = new Date(t);
|
||||
return isNaN(d.getTime()) ? null : d;
|
||||
},
|
||||
getCutoffDate(c) {
|
||||
return this.parseDateFlexible(c.cutoffDate || c.stichtag || '');
|
||||
},
|
||||
getMemberBirthDate(member) {
|
||||
return this.parseDateFlexible(member.birthDate || '');
|
||||
},
|
||||
getGenderRule(c) {
|
||||
const txt = `${c.ageClassCompetition || c.altersklasseWettbewerb || ''} ${c.openTo || c.offenFuer || ''}`.toLowerCase();
|
||||
if (/(mädchen|weiblich|\bw\b)/.test(txt)) return 'female';
|
||||
if (/(jungen|männlich|\bm\b)/.test(txt)) return 'male';
|
||||
if (/jugend/.test(txt)) return 'both';
|
||||
return 'both';
|
||||
},
|
||||
isEligibleForCompetition(member, c) {
|
||||
const rule = this.getGenderRule(c);
|
||||
const g = member.gender || 'unknown';
|
||||
if (rule === 'female' && g !== 'female') return false;
|
||||
if (rule === 'male' && g !== 'male') return false;
|
||||
const cutoff = this.getCutoffDate(c);
|
||||
if (cutoff) {
|
||||
const bd = this.getMemberBirthDate(member);
|
||||
if (!bd) return false;
|
||||
if (!(bd.getTime() > cutoff.getTime())) return false; // jünger als Stichtag => geboren nach Stichtag
|
||||
}
|
||||
return true;
|
||||
},
|
||||
eligibleMembers(c) {
|
||||
return (this.members || []).filter(m => this.isEligibleForCompetition(m, c));
|
||||
},
|
||||
async removeTournament(t) {
|
||||
if (!confirm(`Turnier wirklich löschen?\n${t.title || 'Ohne Titel'} (ID ${t.id})`)) return;
|
||||
await apiClient.delete(`/official-tournaments/${this.currentClub}/${t.id}`);
|
||||
if (String(this.uploadedId) === String(t.id)) {
|
||||
this.parsed = null;
|
||||
this.uploadedId = null;
|
||||
}
|
||||
await this.loadList();
|
||||
},
|
||||
suggestMembers(entry) {
|
||||
const name = (entry.name || '').toLowerCase();
|
||||
const parts = name.split(/\s+/).filter(Boolean);
|
||||
return this.members.filter(m => {
|
||||
const fn = (m.firstName || '').toLowerCase();
|
||||
const ln = (m.lastName || '').toLowerCase();
|
||||
const match = parts.some(p => fn.includes(p) || ln.includes(p));
|
||||
if (!match) return false;
|
||||
if (!entry.genderHint) return true;
|
||||
if (entry.genderHint === 'm') return (m.gender || 'unknown') === 'male';
|
||||
if (entry.genderHint === 'w') return (m.gender || 'unknown') === 'female';
|
||||
return true;
|
||||
}).slice(0, 10);
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
await this.loadList();
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.official-tournaments { display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
.uploader { display: flex; gap: 0.5rem; align-items: center; }
|
||||
table { width: 100%; border-collapse: collapse; }
|
||||
th, td { border-bottom: 1px solid var(--border-color); padding: 0.5rem; text-align: left; }
|
||||
.comp-details td { background: var(--background-light, #fafafa); }
|
||||
.details { display: grid; grid-template-columns: 1fr; gap: .4rem 0; padding: .5rem 0; }
|
||||
.detail-item { font-size: .95rem; }
|
||||
.eligible-list { margin-top: .25rem; display: flex; flex-wrap: wrap; gap: .25rem .5rem; }
|
||||
.eligible-name { background: var(--background, #f1f1f1); border: 1px solid var(--border-color, #ddd); border-radius: 4px; padding: 2px 6px; }
|
||||
.eligible-table { width: 100%; border-collapse: collapse; margin-top: .25rem; }
|
||||
.eligible-table th, .eligible-table td { border-bottom: 1px solid var(--border-color); padding: .25rem .4rem; text-align: left; }
|
||||
</style>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user