feat(Calendar): integrate CalendarEvent model and enhance calendar functionality
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
All checks were successful
Deploy tt-tagebuch / deploy (push) Successful in 43s
- Added CalendarEvent model to the backend, establishing relationships with the Club model for better event management. - Updated server.js to include calendarEventRoutes, enabling API access for calendar events. - Enhanced CalendarView.vue to support custom event creation and management, improving user interaction with the calendar. - Refactored various components to streamline event handling and improve overall user experience in the calendar interface. - Updated TODO and DEVELOPMENT documentation to reflect new calendar features and architectural decisions.
This commit is contained in:
@@ -29,39 +29,78 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section v-if="currentClub" class="training-cancellation-panel">
|
||||
<div>
|
||||
<h3>Training fällt aus</h3>
|
||||
<p>Hier eingetragene Tage blenden die regelmäßigen Trainingszeiten aus.</p>
|
||||
<details v-if="currentClub" class="calendar-options" :open="optionsOpen">
|
||||
<summary class="calendar-options-summary" @click.prevent="optionsOpen = !optionsOpen">
|
||||
<span>Optionen</span>
|
||||
<small>Ausfälle & eigene Termine</small>
|
||||
</summary>
|
||||
<div class="calendar-options-body">
|
||||
<section class="training-cancellation-panel">
|
||||
<div>
|
||||
<h3>Training fällt aus</h3>
|
||||
<p>Blendet regelmäßige Trainingszeiten aus.</p>
|
||||
</div>
|
||||
<form class="training-cancellation-form" @submit.prevent="saveTrainingCancellation">
|
||||
<label>
|
||||
<span>Datum</span>
|
||||
<input v-model="cancellationForm.startDate" type="date" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis optional</span>
|
||||
<input v-model="cancellationForm.endDate" type="date" />
|
||||
</label>
|
||||
<input v-model="cancellationForm.reason" type="text" placeholder="Grund (optional)" />
|
||||
<button type="submit" :disabled="cancellationSaving">
|
||||
{{ cancellationSaving ? 'Speichern...' : 'Eintragen' }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="visibleTrainingCancellations.length" class="training-cancellation-list">
|
||||
<button
|
||||
v-for="cancellation in visibleTrainingCancellations"
|
||||
:key="`cancel-${cancellation.cancellationId}`"
|
||||
type="button"
|
||||
class="training-cancellation-item"
|
||||
@click="deleteTrainingCancellation(cancellation)"
|
||||
title="Löschen"
|
||||
>
|
||||
<strong>{{ formatShortDate(cancellation.date) }}</strong>
|
||||
<span>{{ cancellation.title }}</span>
|
||||
<small>Löschen</small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="custom-event-panel">
|
||||
<div>
|
||||
<h3>Eigene Termine</h3>
|
||||
<p>Kreistage, Sitzungen, interne Meetings, ...</p>
|
||||
</div>
|
||||
<form class="custom-event-form" @submit.prevent="saveCustomEvent">
|
||||
<input v-model="customEventForm.title" type="text" placeholder="Titel" required />
|
||||
<input v-model="customEventForm.startDate" type="date" required />
|
||||
<input v-model="customEventForm.endDate" type="date" />
|
||||
<input v-model="customEventForm.category" type="text" placeholder="Kategorie (optional)" />
|
||||
<button type="submit" :disabled="customEventSaving">
|
||||
{{ customEventSaving ? 'Speichern...' : 'Anlegen' }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="visibleCustomEvents.length" class="custom-event-list">
|
||||
<button
|
||||
v-for="event in visibleCustomEvents"
|
||||
:key="`ce-${event.customEventId}`"
|
||||
type="button"
|
||||
class="custom-event-item"
|
||||
@click="deleteCustomEvent(event)"
|
||||
title="Löschen"
|
||||
>
|
||||
<strong>{{ formatEventDate(event) }}</strong>
|
||||
<span>{{ event.title }}</span>
|
||||
<small>Löschen</small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<form class="training-cancellation-form" @submit.prevent="saveTrainingCancellation">
|
||||
<label>
|
||||
<span>Datum</span>
|
||||
<input v-model="cancellationForm.startDate" type="date" required />
|
||||
</label>
|
||||
<label>
|
||||
<span>Bis optional</span>
|
||||
<input v-model="cancellationForm.endDate" type="date" />
|
||||
</label>
|
||||
<input v-model="cancellationForm.reason" type="text" placeholder="Grund, z.B. Halle gesperrt" />
|
||||
<button type="submit" :disabled="cancellationSaving">
|
||||
{{ cancellationSaving ? 'Speichern...' : 'Eintragen' }}
|
||||
</button>
|
||||
</form>
|
||||
<div v-if="visibleTrainingCancellations.length" class="training-cancellation-list">
|
||||
<button
|
||||
v-for="cancellation in visibleTrainingCancellations"
|
||||
:key="`cancel-${cancellation.cancellationId}`"
|
||||
type="button"
|
||||
class="training-cancellation-item"
|
||||
@click="deleteTrainingCancellation(cancellation)"
|
||||
>
|
||||
<strong>{{ formatShortDate(cancellation.date) }}</strong>
|
||||
<span>{{ cancellation.title }}</span>
|
||||
<small>Löschen</small>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
</details>
|
||||
|
||||
<section v-if="sourceWarnings.length" class="calendar-state calendar-state-warning">
|
||||
{{ sourceWarnings.join(' · ') }}
|
||||
@@ -116,17 +155,34 @@
|
||||
<span class="agenda-time">{{ event.time }}</span>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<ConfirmDialog
|
||||
v-model="confirmDialog.isOpen"
|
||||
:title="confirmDialog.title"
|
||||
:message="confirmDialog.message"
|
||||
:details="confirmDialog.details"
|
||||
:type="confirmDialog.type"
|
||||
:confirm-text="confirmDialog.confirmText"
|
||||
:cancel-text="confirmDialog.cancelText"
|
||||
:show-cancel="confirmDialog.showCancel"
|
||||
@confirm="handleConfirmResult(true)"
|
||||
@cancel="handleConfirmResult(false)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import apiClient from '../apiClient';
|
||||
import ConfirmDialog from '../components/ConfirmDialog.vue';
|
||||
|
||||
const WEEKDAYS = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'];
|
||||
|
||||
export default {
|
||||
name: 'CalendarView',
|
||||
components: {
|
||||
ConfirmDialog,
|
||||
},
|
||||
data() {
|
||||
const today = new Date();
|
||||
return {
|
||||
@@ -142,18 +198,39 @@ export default {
|
||||
match: true,
|
||||
holiday: true,
|
||||
schoolHoliday: true,
|
||||
trainingCancellation: true
|
||||
trainingCancellation: true,
|
||||
customEvent: true
|
||||
},
|
||||
cancellationSaving: false,
|
||||
cancellationForm: {
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
reason: ''
|
||||
}
|
||||
},
|
||||
customEventSaving: false,
|
||||
customEventForm: {
|
||||
title: '',
|
||||
startDate: '',
|
||||
endDate: '',
|
||||
category: ''
|
||||
},
|
||||
optionsOpen: false,
|
||||
confirmDialog: {
|
||||
isOpen: false,
|
||||
title: '',
|
||||
message: '',
|
||||
details: '',
|
||||
type: 'info',
|
||||
confirmText: '',
|
||||
cancelText: '',
|
||||
showCancel: true,
|
||||
resolveCallback: null,
|
||||
},
|
||||
plannedTrainingByDateKey: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapGetters(['currentClub']),
|
||||
...mapGetters(['currentClub', 'currentClubName']),
|
||||
weekdays() {
|
||||
return WEEKDAYS;
|
||||
},
|
||||
@@ -165,7 +242,8 @@ export default {
|
||||
{ key: 'match', label: 'Punktspiel' },
|
||||
{ key: 'holiday', label: 'Feiertag' },
|
||||
{ key: 'schoolHoliday', label: 'Ferien' },
|
||||
{ key: 'trainingCancellation', label: 'Ausfall' }
|
||||
{ key: 'trainingCancellation', label: 'Ausfall' },
|
||||
{ key: 'customEvent', label: 'Termin' }
|
||||
];
|
||||
},
|
||||
monthLabel() {
|
||||
@@ -184,13 +262,14 @@ export default {
|
||||
const monthEnd = new Date(year, month + 1, 0);
|
||||
return this.visibleEventPool
|
||||
.filter(event => this.isEventInRange(event, monthStart, monthEnd))
|
||||
.filter(event => this.shouldShowEventOnDate(event, this.toDateKey(event.date)))
|
||||
.sort((a, b) => a.startsAt - b.startsAt);
|
||||
},
|
||||
eventCounts() {
|
||||
return this.events.reduce((counts, event) => {
|
||||
counts[event.type] = (counts[event.type] || 0) + 1;
|
||||
return counts;
|
||||
}, { training: 0, tournament: 0, officialTournament: 0, match: 0, holiday: 0, schoolHoliday: 0, trainingCancellation: 0 });
|
||||
}, { training: 0, tournament: 0, officialTournament: 0, match: 0, holiday: 0, schoolHoliday: 0, trainingCancellation: 0, customEvent: 0 });
|
||||
},
|
||||
visibleTrainingCancellations() {
|
||||
const month = this.cursor.getMonth();
|
||||
@@ -200,6 +279,14 @@ export default {
|
||||
.filter(event => event.date.getFullYear() === year && event.date.getMonth() === month)
|
||||
.sort((a, b) => a.startsAt - b.startsAt);
|
||||
},
|
||||
visibleCustomEvents() {
|
||||
const month = this.cursor.getMonth();
|
||||
const year = this.cursor.getFullYear();
|
||||
return this.events
|
||||
.filter(event => event.type === 'customEvent')
|
||||
.filter(event => event.date.getFullYear() === year && event.date.getMonth() === month)
|
||||
.sort((a, b) => a.startsAt - b.startsAt);
|
||||
},
|
||||
sourceWarnings() {
|
||||
return this.sourceErrors.map(source => `${source} konnte nicht geladen werden`);
|
||||
},
|
||||
@@ -223,6 +310,7 @@ export default {
|
||||
isToday: key === todayKey,
|
||||
events: this.visibleEventPool
|
||||
.filter(event => this.isEventOnDate(event, date))
|
||||
.filter(event => this.shouldShowEventOnDate(event, key))
|
||||
.sort((a, b) => a.startsAt - b.startsAt)
|
||||
};
|
||||
});
|
||||
@@ -240,6 +328,32 @@ export default {
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
async showConfirm(title, message, details = '', type = 'info', options = {}) {
|
||||
return new Promise((resolve) => {
|
||||
this.confirmDialog = {
|
||||
isOpen: true,
|
||||
title,
|
||||
message,
|
||||
details,
|
||||
type,
|
||||
confirmText: options.confirmText || '',
|
||||
cancelText: options.cancelText || '',
|
||||
showCancel: options.showCancel !== undefined ? !!options.showCancel : true,
|
||||
resolveCallback: resolve,
|
||||
};
|
||||
});
|
||||
},
|
||||
handleConfirmResult(confirmed) {
|
||||
if (this.confirmDialog.resolveCallback) {
|
||||
this.confirmDialog.resolveCallback(confirmed);
|
||||
}
|
||||
this.confirmDialog.isOpen = false;
|
||||
this.confirmDialog.resolveCallback = null;
|
||||
},
|
||||
isOurTeam(teamName) {
|
||||
if (!teamName || !this.currentClubName) return false;
|
||||
return String(teamName).startsWith(this.currentClubName);
|
||||
},
|
||||
async loadCalendarEvents() {
|
||||
if (!this.currentClub) {
|
||||
this.events = [];
|
||||
@@ -254,6 +368,7 @@ export default {
|
||||
this.loadSource('Trainingstage', () => this.loadTrainingEvents()),
|
||||
this.loadSource('Trainingszeiten', () => this.loadRecurringTrainingEvents()),
|
||||
this.loadSource('Trainingsausfälle', () => this.loadTrainingCancellationEvents()),
|
||||
this.loadSource('Eigene Termine', () => this.loadCustomEvents()),
|
||||
this.loadSource('Turniere', () => this.loadTournamentEvents()),
|
||||
this.loadSource('Turnierteilnahmen', () => this.loadOfficialTournamentEvents()),
|
||||
this.loadSource('Punktspiele', () => this.loadMatchEvents()),
|
||||
@@ -264,6 +379,14 @@ export default {
|
||||
.filter(result => result.status === 'fulfilled')
|
||||
.flatMap(result => result.value.events)
|
||||
.filter(event => event.date && !Number.isNaN(event.date.getTime()));
|
||||
|
||||
this.plannedTrainingByDateKey = loadedEvents
|
||||
.filter(event => event.type === 'training')
|
||||
.reduce((map, event) => {
|
||||
map[this.toDateKey(event.date)] = true;
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
const cancellationDates = new Set(
|
||||
loadedEvents
|
||||
.filter(event => event.type === 'trainingCancellation')
|
||||
@@ -470,6 +593,50 @@ export default {
|
||||
this.ensureSuccess(response, 'Trainingsausfälle');
|
||||
await this.loadCalendarEvents();
|
||||
},
|
||||
async loadCustomEvents() {
|
||||
const response = await apiClient.get(`/calendar-events/${this.currentClub}`, {
|
||||
params: { year: this.displayedYear },
|
||||
});
|
||||
this.ensureSuccess(response, 'Eigene Termine');
|
||||
return (response.data || []).map(event => {
|
||||
const date = this.parseDate(event.startDate);
|
||||
const endDate = this.parseDate(event.endDate || event.startDate);
|
||||
return {
|
||||
id: `custom-event-${event.id}`,
|
||||
customEventId: event.id,
|
||||
type: 'customEvent',
|
||||
date,
|
||||
endDate,
|
||||
startsAt: this.combineDateTime(date),
|
||||
time: '',
|
||||
title: event.title,
|
||||
subtitle: event.category || 'Eigener Termin',
|
||||
};
|
||||
});
|
||||
},
|
||||
async saveCustomEvent() {
|
||||
if (!this.currentClub || !this.customEventForm.title || !this.customEventForm.startDate) return;
|
||||
this.customEventSaving = true;
|
||||
try {
|
||||
const response = await apiClient.post(`/calendar-events/${this.currentClub}`, {
|
||||
title: this.customEventForm.title,
|
||||
startDate: this.customEventForm.startDate,
|
||||
endDate: this.customEventForm.endDate || this.customEventForm.startDate,
|
||||
category: this.customEventForm.category || null,
|
||||
});
|
||||
this.ensureSuccess(response, 'Eigene Termine');
|
||||
this.customEventForm = { title: '', startDate: '', endDate: '', category: '' };
|
||||
await this.loadCalendarEvents();
|
||||
} finally {
|
||||
this.customEventSaving = false;
|
||||
}
|
||||
},
|
||||
async deleteCustomEvent(event) {
|
||||
if (!this.currentClub || !event?.customEventId) return;
|
||||
const response = await apiClient.delete(`/calendar-events/${this.currentClub}/${event.customEventId}`);
|
||||
this.ensureSuccess(response, 'Eigene Termine');
|
||||
await this.loadCalendarEvents();
|
||||
},
|
||||
async loadTournamentEvents() {
|
||||
const response = await apiClient.get(`/tournament/${this.currentClub}`);
|
||||
this.ensureSuccess(response, 'Turniere');
|
||||
@@ -490,7 +657,9 @@ export default {
|
||||
async loadMatchEvents() {
|
||||
const response = await apiClient.get(`/matches/leagues/${this.currentClub}/matches`);
|
||||
this.ensureSuccess(response, 'Punktspiele');
|
||||
return (response.data || []).map(match => {
|
||||
return (response.data || [])
|
||||
.filter(match => this.isOurTeam(match.homeTeam?.name) || this.isOurTeam(match.guestTeam?.name))
|
||||
.map(match => {
|
||||
const date = this.parseDate(match.date);
|
||||
const home = match.homeTeam?.name || 'Heim';
|
||||
const guest = match.guestTeam?.name || 'Gast';
|
||||
@@ -603,6 +772,10 @@ export default {
|
||||
const eventEnd = this.startOfDay(event.endDate || event.date);
|
||||
return eventStart <= this.startOfDay(rangeEnd) && eventEnd >= this.startOfDay(rangeStart);
|
||||
},
|
||||
shouldShowEventOnDate(event, dateKey) {
|
||||
if (event.type !== 'trainingCancellation') return true;
|
||||
return Boolean(this.plannedTrainingByDateKey[dateKey]);
|
||||
},
|
||||
startOfDay(date) {
|
||||
const result = new Date(date);
|
||||
result.setHours(0, 0, 0, 0);
|
||||
@@ -644,9 +817,51 @@ export default {
|
||||
this.cursor = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
},
|
||||
openEvent(event) {
|
||||
if (event?.type === 'holiday' || event?.type === 'schoolHoliday') {
|
||||
this.offerTrainingCancellationForHoliday(event);
|
||||
return;
|
||||
}
|
||||
if (event.route) {
|
||||
this.$router.push(event.route);
|
||||
}
|
||||
},
|
||||
async offerTrainingCancellationForHoliday(event) {
|
||||
if (!this.currentClub) return;
|
||||
const startDateKey = this.toDateKey(event.date);
|
||||
const endDateKey = this.toDateKey(event.endDate || event.date);
|
||||
const isRange = startDateKey !== endDateKey;
|
||||
|
||||
const shouldCreate = await this.showConfirm(
|
||||
'Trainingsausfall',
|
||||
'Training als Ausfall markieren?',
|
||||
'',
|
||||
'warning',
|
||||
{ confirmText: 'Ja', cancelText: 'Nein' }
|
||||
);
|
||||
if (!shouldCreate) return;
|
||||
|
||||
let useRange = false;
|
||||
if (isRange) {
|
||||
useRange = await this.showConfirm(
|
||||
'Zeitraum',
|
||||
`Gilt der Ausfall für den gesamten Zeitraum (${startDateKey} bis ${endDateKey})?`,
|
||||
'',
|
||||
'info',
|
||||
{ confirmText: 'Zeitraum', cancelText: 'Nur Tag' }
|
||||
);
|
||||
}
|
||||
|
||||
const reason = `${event.title}${event.subtitle ? ` (${event.subtitle})` : ''}`.trim();
|
||||
await this.createTrainingCancellation(startDateKey, useRange ? endDateKey : startDateKey, reason);
|
||||
await this.loadCalendarEvents();
|
||||
},
|
||||
async createTrainingCancellation(startDate, endDate, reason) {
|
||||
const response = await apiClient.post(`/training-cancellations/${this.currentClub}`, {
|
||||
startDate,
|
||||
endDate,
|
||||
reason
|
||||
});
|
||||
this.ensureSuccess(response, 'Trainingsausfälle');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -662,6 +877,7 @@ export default {
|
||||
.calendar-header,
|
||||
.calendar-toolbar,
|
||||
.training-cancellation-panel,
|
||||
.custom-event-panel,
|
||||
.calendar-agenda {
|
||||
border: 1px solid #dfe7e2;
|
||||
border-radius: 10px;
|
||||
@@ -731,7 +947,7 @@ export default {
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
padding: 0.85rem 1rem;
|
||||
padding: 0.65rem 1rem;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
@@ -741,13 +957,50 @@ export default {
|
||||
border: 1px solid #d6e2dc;
|
||||
border-radius: 999px;
|
||||
background: #f8fbf9;
|
||||
font-size: 0.84rem;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
color: #40524b;
|
||||
cursor: pointer;
|
||||
padding: 0.35rem 0.65rem;
|
||||
}
|
||||
|
||||
.calendar-options {
|
||||
border: 1px solid #dfe7e2;
|
||||
border-radius: 10px;
|
||||
background: #fff;
|
||||
box-shadow: 0 6px 18px rgba(15, 23, 42, 0.05);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-options-summary {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
padding: 0.75rem 1rem;
|
||||
background: #f8fbf9;
|
||||
border-bottom: 1px solid #edf1ee;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.calendar-options-summary span {
|
||||
font-weight: 900;
|
||||
color: #14251f;
|
||||
}
|
||||
|
||||
.calendar-options-summary small {
|
||||
color: #607169;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.calendar-options-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.legend-item.inactive {
|
||||
opacity: 0.45;
|
||||
}
|
||||
@@ -766,12 +1019,76 @@ export default {
|
||||
.legend-holiday::before { background: #dc2626; }
|
||||
.legend-schoolHoliday::before { background: #7c3aed; }
|
||||
.legend-trainingCancellation::before { background: #64748b; }
|
||||
.legend-customEvent::before { background: #0ea5e9; }
|
||||
|
||||
.custom-event-panel {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.custom-event-panel h3 {
|
||||
margin: 0;
|
||||
color: #14251f;
|
||||
}
|
||||
|
||||
.custom-event-panel p {
|
||||
margin: 0.25rem 0 0;
|
||||
color: #607169;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.custom-event-form {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 1fr) 10rem 10rem 12rem auto;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-event-form input,
|
||||
.custom-event-form button {
|
||||
border: 1px solid #cfdad4;
|
||||
border-radius: 8px;
|
||||
min-height: 2.4rem;
|
||||
padding: 0 0.75rem;
|
||||
}
|
||||
|
||||
.custom-event-form button {
|
||||
background: #0ea5e9;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.custom-event-list {
|
||||
display: flex;
|
||||
grid-column: 1 / -1;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.custom-event-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 999px;
|
||||
background: #f0f9ff;
|
||||
color: #0c4a6e;
|
||||
cursor: pointer;
|
||||
padding: 0.35rem 0.65rem;
|
||||
}
|
||||
|
||||
.custom-event-item small {
|
||||
color: #991b1b;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.training-cancellation-panel {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(14rem, 1fr) minmax(18rem, 2fr);
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.training-cancellation-panel h3 {
|
||||
@@ -931,6 +1248,7 @@ export default {
|
||||
.event-holiday { background: #fee2e2; border-left: 4px solid #dc2626; }
|
||||
.event-schoolHoliday { background: #f3e8ff; border-left: 4px solid #7c3aed; }
|
||||
.event-trainingCancellation { background: #f1f5f9; border-left: 4px solid #64748b; }
|
||||
.event-customEvent { background: #f0f9ff; border-left: 4px solid #0ea5e9; }
|
||||
|
||||
.event-time {
|
||||
font-size: 0.72rem;
|
||||
@@ -1008,16 +1326,25 @@ export default {
|
||||
@media (max-width: 900px) {
|
||||
.calendar-header,
|
||||
.calendar-toolbar,
|
||||
.training-cancellation-panel {
|
||||
.training-cancellation-panel,
|
||||
.custom-event-panel {
|
||||
align-items: stretch;
|
||||
grid-template-columns: 1fr;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.calendar-options-body {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.training-cancellation-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.custom-event-form {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
Reference in New Issue
Block a user