diff --git a/frontend/src/i18n/locales/de/personal.json b/frontend/src/i18n/locales/de/personal.json
index f04d480..7d6bec9 100644
--- a/frontend/src/i18n/locales/de/personal.json
+++ b/frontend/src/i18n/locales/de/personal.json
@@ -3,6 +3,11 @@
"calendar": {
"title": "Kalender",
"today": "Heute",
+ "newEntry": "Neuer Eintrag",
+ "editEntry": "Eintrag bearbeiten",
+ "selectedDays": "{count} Tage ausgewählt",
+ "createEventForSelection": "Termin erstellen",
+ "clearSelection": "Auswahl aufheben",
"views": {
"month": "Monat",
"week": "Woche",
@@ -40,6 +45,31 @@
"oct": "Oktober",
"nov": "November",
"dec": "Dezember"
+ },
+ "categories": {
+ "personal": "Persönlich",
+ "work": "Arbeit",
+ "family": "Familie",
+ "health": "Gesundheit",
+ "birthday": "Geburtstag",
+ "holiday": "Urlaub",
+ "reminder": "Erinnerung",
+ "other": "Sonstiges"
+ },
+ "form": {
+ "title": "Titel",
+ "titlePlaceholder": "Titel eingeben...",
+ "category": "Kategorie",
+ "startDate": "Startdatum",
+ "startTime": "Startzeit",
+ "endDate": "Enddatum",
+ "endTime": "Endzeit",
+ "allDay": "Ganztägig",
+ "description": "Beschreibung",
+ "descriptionPlaceholder": "Optionale Beschreibung...",
+ "save": "Speichern",
+ "cancel": "Abbrechen",
+ "delete": "Löschen"
}
}
}
diff --git a/frontend/src/i18n/locales/en/personal.json b/frontend/src/i18n/locales/en/personal.json
index 80a58cb..e8451c1 100644
--- a/frontend/src/i18n/locales/en/personal.json
+++ b/frontend/src/i18n/locales/en/personal.json
@@ -3,6 +3,11 @@
"calendar": {
"title": "Calendar",
"today": "Today",
+ "newEntry": "New Entry",
+ "editEntry": "Edit Entry",
+ "selectedDays": "{count} days selected",
+ "createEventForSelection": "Create Event",
+ "clearSelection": "Clear Selection",
"views": {
"month": "Month",
"week": "Week",
@@ -40,6 +45,31 @@
"oct": "October",
"nov": "November",
"dec": "December"
+ },
+ "categories": {
+ "personal": "Personal",
+ "work": "Work",
+ "family": "Family",
+ "health": "Health",
+ "birthday": "Birthday",
+ "holiday": "Holiday",
+ "reminder": "Reminder",
+ "other": "Other"
+ },
+ "form": {
+ "title": "Title",
+ "titlePlaceholder": "Enter title...",
+ "category": "Category",
+ "startDate": "Start Date",
+ "startTime": "Start Time",
+ "endDate": "End Date",
+ "endTime": "End Time",
+ "allDay": "All Day",
+ "description": "Description",
+ "descriptionPlaceholder": "Optional description...",
+ "save": "Save",
+ "cancel": "Cancel",
+ "delete": "Delete"
}
}
}
diff --git a/frontend/src/views/personal/CalendarView.vue b/frontend/src/views/personal/CalendarView.vue
index 4700dbf..f55ffb5 100644
--- a/frontend/src/views/personal/CalendarView.vue
+++ b/frontend/src/views/personal/CalendarView.vue
@@ -5,6 +5,9 @@
+
+
+ {{ $t('personal.calendar.selectedDays', { count: selectedDates.length }) }}
+
+
+
+
@@ -78,8 +99,18 @@
v-for="hour in hours"
:key="hour"
:class="['time-slot', { 'current-hour': isCurrentHour(day.date, hour) }]"
- @click="createEventAt(day.date, hour)"
- >
+ @click="openNewEventDialog(day.date, hour)"
+ >
+
+ {{ event.title }}
+
+
@@ -108,8 +139,18 @@
v-for="hour in workHours"
:key="hour"
:class="['time-slot', { 'current-hour': isCurrentHour(day.date, hour) }]"
- @click="createEventAt(day.date, hour)"
- >
+ @click="openNewEventDialog(day.date, hour)"
+ >
+
+ {{ event.title }}
+
+
@@ -119,7 +160,7 @@
@@ -134,13 +175,114 @@
+ :class="['time-slot', { 'current-hour': isCurrentHour(currentDateStr, hour) }]"
+ @click="openNewEventDialog(currentDateStr, hour)"
+ >
+
+ {{ formatEventTime(event) }}
+ {{ event.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -159,8 +301,42 @@ export default {
],
weekDays: ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'],
hours: Array.from({ length: 24 }, (_, i) => i),
- workHours: Array.from({ length: 12 }, (_, i) => i + 7), // 7:00 - 18:00
- events: [] // Placeholder for events
+ workHours: Array.from({ length: 12 }, (_, i) => i + 7),
+
+ // Categories with colors
+ categories: [
+ { id: 'personal', color: '#4CAF50' }, // Green
+ { id: 'work', color: '#2196F3' }, // Blue
+ { id: 'family', color: '#9C27B0' }, // Purple
+ { id: 'health', color: '#F44336' }, // Red
+ { id: 'birthday', color: '#FF9800' }, // Orange
+ { id: 'holiday', color: '#00BCD4' }, // Cyan
+ { id: 'reminder', color: '#795548' }, // Brown
+ { id: 'other', color: '#607D8B' } // Gray-Blue
+ ],
+
+ // Events storage
+ events: [],
+ nextEventId: 1,
+
+ // Selection
+ selectedDates: [],
+ isDragging: false,
+ dragStartDate: null,
+
+ // Dialog
+ showEventDialog: false,
+ editingEvent: null,
+ eventForm: {
+ title: '',
+ categoryId: 'personal',
+ startDate: '',
+ startTime: '09:00',
+ endDate: '',
+ endTime: '10:00',
+ allDay: false,
+ description: ''
+ }
};
},
computed: {
@@ -171,13 +347,14 @@ export default {
}
return this.currentDate.toLocaleDateString(this.$i18n.locale, options);
},
+ currentDateStr() {
+ return this.currentDate.toISOString().split('T')[0];
+ },
monthDays() {
const year = this.currentDate.getFullYear();
const month = this.currentDate.getMonth();
const firstDay = new Date(year, month, 1);
- const lastDay = new Date(year, month + 1, 0);
- // Find Monday of the first week
let startDate = new Date(firstDay);
const dayOfWeek = firstDay.getDay();
const diff = dayOfWeek === 0 ? -6 : 1 - dayOfWeek;
@@ -187,7 +364,6 @@ export default {
const today = new Date();
today.setHours(0, 0, 0, 0);
- // Generate 6 weeks (42 days)
for (let i = 0; i < 42; i++) {
const date = new Date(startDate);
date.setDate(startDate.getDate() + i);
@@ -209,7 +385,6 @@ export default {
const today = new Date();
today.setHours(0, 0, 0, 0);
- // Find Monday of current week
const monday = this.getMonday(this.currentDate);
for (let i = 0; i < 7; i++) {
@@ -229,13 +404,11 @@ export default {
return days;
},
workWeekDaysData() {
- // Only Monday to Friday
return this.weekDaysData.slice(0, 5);
},
currentDayData() {
const date = this.currentDate;
const dayIndex = date.getDay();
- // Convert Sunday (0) to index 6, Monday (1) to 0, etc.
const weekdayIndex = dayIndex === 0 ? 6 : dayIndex - 1;
return {
@@ -251,6 +424,13 @@ export default {
return this.currentDate.toDateString() === today.toDateString();
}
},
+ mounted() {
+ this.loadEvents();
+ document.addEventListener('mouseup', this.endDragSelection);
+ },
+ beforeUnmount() {
+ document.removeEventListener('mouseup', this.endDragSelection);
+ },
methods: {
getMonday(date) {
const d = new Date(date);
@@ -305,16 +485,210 @@ export default {
const date = new Date(dateStr);
return date.toDateString() === now.toDateString() && now.getHours() === hour;
},
- selectDate(dateStr) {
- this.currentDate = new Date(dateStr);
- this.currentView = 'day';
+
+ // Selection methods
+ isDateSelected(dateStr) {
+ return this.selectedDates.includes(dateStr);
},
- createEventAt(dateStr, hour) {
- // Placeholder for event creation
- console.log(`Create event at ${dateStr} ${hour}:00`);
+ handleDayClick(dateStr, event) {
+ if (event.shiftKey && this.selectedDates.length > 0) {
+ // Shift-click: select range
+ const lastSelected = this.selectedDates[this.selectedDates.length - 1];
+ this.selectDateRange(lastSelected, dateStr);
+ } else if (event.ctrlKey || event.metaKey) {
+ // Ctrl/Cmd-click: toggle selection
+ const index = this.selectedDates.indexOf(dateStr);
+ if (index > -1) {
+ this.selectedDates.splice(index, 1);
+ } else {
+ this.selectedDates.push(dateStr);
+ }
+ } else if (!this.isDragging) {
+ // Normal click: open event dialog for this day
+ this.openNewEventDialog(dateStr);
+ }
+ },
+ startDragSelection(dateStr) {
+ this.isDragging = true;
+ this.dragStartDate = dateStr;
+ this.selectedDates = [dateStr];
+ },
+ continueDragSelection(dateStr) {
+ if (this.isDragging && this.dragStartDate) {
+ this.selectDateRange(this.dragStartDate, dateStr);
+ }
+ },
+ endDragSelection() {
+ if (this.isDragging && this.selectedDates.length === 1) {
+ // Single day was clicked, not dragged
+ this.selectedDates = [];
+ }
+ this.isDragging = false;
+ this.dragStartDate = null;
+ },
+ selectDateRange(startStr, endStr) {
+ const start = new Date(startStr);
+ const end = new Date(endStr);
+
+ if (start > end) {
+ [start, end] = [end, start];
+ }
+
+ this.selectedDates = [];
+ const current = new Date(start);
+ while (current <= end) {
+ this.selectedDates.push(current.toISOString().split('T')[0]);
+ current.setDate(current.getDate() + 1);
+ }
+ },
+ clearSelection() {
+ this.selectedDates = [];
+ },
+ createEventFromSelection() {
+ if (this.selectedDates.length > 0) {
+ const sorted = [...this.selectedDates].sort();
+ this.openNewEventDialog(sorted[0], null, sorted[sorted.length - 1]);
+ }
+ },
+
+ // Event methods
+ getCategoryColor(categoryId) {
+ const cat = this.categories.find(c => c.id === categoryId);
+ return cat ? cat.color : '#607D8B';
+ },
+ getContrastColor(hexColor) {
+ const r = parseInt(hexColor.slice(1, 3), 16);
+ const g = parseInt(hexColor.slice(3, 5), 16);
+ const b = parseInt(hexColor.slice(5, 7), 16);
+ const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255;
+ return luminance > 0.5 ? '#000000' : '#ffffff';
},
getEventsForDate(dateStr) {
- return this.events.filter(e => e.date === dateStr);
+ return this.events.filter(event => {
+ const eventStart = event.startDate;
+ const eventEnd = event.endDate || event.startDate;
+ return dateStr >= eventStart && dateStr <= eventEnd;
+ });
+ },
+ getEventsForDateTime(dateStr, hour) {
+ return this.events.filter(event => {
+ if (event.allDay) return false;
+ const eventStart = event.startDate;
+ const eventEnd = event.endDate || event.startDate;
+ if (dateStr < eventStart || dateStr > eventEnd) return false;
+
+ const startHour = parseInt(event.startTime?.split(':')[0] || '0');
+ const endHour = parseInt(event.endTime?.split(':')[0] || '23');
+
+ if (dateStr === eventStart && dateStr === eventEnd) {
+ return hour >= startHour && hour < endHour;
+ } else if (dateStr === eventStart) {
+ return hour >= startHour;
+ } else if (dateStr === eventEnd) {
+ return hour < endHour;
+ }
+ return true;
+ });
+ },
+ formatEventTime(event) {
+ if (event.allDay) return '';
+ return event.startTime || '';
+ },
+
+ // Dialog methods
+ openNewEventDialog(dateStr = null, hour = null, endDateStr = null) {
+ this.editingEvent = null;
+ const today = new Date().toISOString().split('T')[0];
+
+ this.eventForm = {
+ title: '',
+ categoryId: 'personal',
+ startDate: dateStr || today,
+ startTime: hour !== null ? `${hour.toString().padStart(2, '0')}:00` : '09:00',
+ endDate: endDateStr || dateStr || today,
+ endTime: hour !== null ? `${(hour + 1).toString().padStart(2, '0')}:00` : '10:00',
+ allDay: endDateStr !== null && endDateStr !== dateStr,
+ description: ''
+ };
+
+ this.showEventDialog = true;
+ this.selectedDates = [];
+
+ this.$nextTick(() => {
+ this.$refs.eventTitleInput?.focus();
+ });
+ },
+ editEvent(event) {
+ this.editingEvent = event;
+ this.eventForm = {
+ title: event.title,
+ categoryId: event.categoryId,
+ startDate: event.startDate,
+ startTime: event.startTime || '09:00',
+ endDate: event.endDate || event.startDate,
+ endTime: event.endTime || '10:00',
+ allDay: event.allDay || false,
+ description: event.description || ''
+ };
+ this.showEventDialog = true;
+ },
+ closeEventDialog() {
+ this.showEventDialog = false;
+ this.editingEvent = null;
+ },
+ saveEvent() {
+ if (!this.eventForm.title) return;
+
+ const eventData = {
+ title: this.eventForm.title,
+ categoryId: this.eventForm.categoryId,
+ startDate: this.eventForm.startDate,
+ startTime: this.eventForm.allDay ? null : this.eventForm.startTime,
+ endDate: this.eventForm.endDate,
+ endTime: this.eventForm.allDay ? null : this.eventForm.endTime,
+ allDay: this.eventForm.allDay,
+ description: this.eventForm.description
+ };
+
+ if (this.editingEvent) {
+ // Update existing event
+ const index = this.events.findIndex(e => e.id === this.editingEvent.id);
+ if (index > -1) {
+ this.events[index] = { ...eventData, id: this.editingEvent.id };
+ }
+ } else {
+ // Create new event
+ this.events.push({ ...eventData, id: this.nextEventId++ });
+ }
+
+ this.saveEvents();
+ this.closeEventDialog();
+ },
+ deleteEvent() {
+ if (this.editingEvent) {
+ const index = this.events.findIndex(e => e.id === this.editingEvent.id);
+ if (index > -1) {
+ this.events.splice(index, 1);
+ }
+ this.saveEvents();
+ this.closeEventDialog();
+ }
+ },
+
+ // Persistence (localStorage for now, later backend)
+ saveEvents() {
+ localStorage.setItem('calendarEvents', JSON.stringify(this.events));
+ localStorage.setItem('calendarNextId', this.nextEventId.toString());
+ },
+ loadEvents() {
+ const saved = localStorage.getItem('calendarEvents');
+ if (saved) {
+ this.events = JSON.parse(saved);
+ }
+ const nextId = localStorage.getItem('calendarNextId');
+ if (nextId) {
+ this.nextEventId = parseInt(nextId);
+ }
}
}
};
@@ -348,6 +722,20 @@ h2 {
gap: 8px;
}
+.btn-new-event {
+ padding: 8px 16px;
+ background: var(--color-primary-green);
+ color: #000;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-weight: 600;
+
+ &:hover {
+ background: var(--color-primary-green-hover);
+ }
+}
+
.btn-today {
padding: 8px 16px;
background: var(--color-primary-orange);
@@ -410,12 +798,53 @@ h2 {
}
}
+// Selection info bar
+.selection-info {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 10px 16px;
+ background: #e3f2fd;
+ border: 1px solid #90caf9;
+ border-radius: 6px;
+ margin-bottom: 16px;
+}
+
+.btn-create-from-selection {
+ padding: 6px 12px;
+ background: var(--color-primary-green);
+ color: #000;
+ border: none;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9em;
+
+ &:hover {
+ background: var(--color-primary-green-hover);
+ }
+}
+
+.btn-clear-selection {
+ padding: 6px 12px;
+ background: #fff;
+ color: #666;
+ border: 1px solid #ddd;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 0.9em;
+
+ &:hover {
+ background: #f5f5f5;
+ }
+}
+
// Month View
.month-view {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
overflow: hidden;
+ user-select: none;
}
.weekday-headers {
@@ -439,7 +868,7 @@ h2 {
}
.day-cell {
- min-height: 80px;
+ min-height: 90px;
padding: 8px;
border-right: 1px solid #eee;
border-bottom: 1px solid #eee;
@@ -480,6 +909,14 @@ h2 {
&.weekend {
background: #f9f9f9;
}
+
+ &.selected {
+ background: #bbdefb !important;
+
+ &:hover {
+ background: #90caf9 !important;
+ }
+ }
}
.day-number {
@@ -490,15 +927,25 @@ h2 {
.day-events {
display: flex;
- flex-wrap: wrap;
+ flex-direction: column;
gap: 2px;
margin-top: 4px;
+ overflow: hidden;
}
-.event-dot {
- width: 8px;
- height: 8px;
- border-radius: 50%;
+.event-item {
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 0.75em;
+ color: #fff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ cursor: pointer;
+
+ &:hover {
+ opacity: 0.9;
+ }
}
// Week & Day View
@@ -608,6 +1055,7 @@ h2 {
border-bottom: 1px solid #f0f0f0;
cursor: pointer;
transition: background 0.2s;
+ position: relative;
&:hover {
background: var(--color-primary-orange-light);
@@ -619,6 +1067,35 @@ h2 {
}
}
+.time-event {
+ position: absolute;
+ left: 2px;
+ right: 2px;
+ top: 2px;
+ padding: 2px 6px;
+ border-radius: 3px;
+ font-size: 0.75em;
+ color: #fff;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ cursor: pointer;
+ z-index: 1;
+
+ &:hover {
+ opacity: 0.9;
+ }
+
+ &.full-width {
+ right: 2px;
+ }
+}
+
+.event-time {
+ font-weight: 600;
+ margin-right: 4px;
+}
+
// Day View specifics
.day-header {
padding: 20px;
@@ -636,6 +1113,202 @@ h2 {
}
}
+// Dialog
+.dialog-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.5);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+}
+
+.event-dialog {
+ background: #fff;
+ border-radius: 12px;
+ width: 90%;
+ max-width: 500px;
+ max-height: 90vh;
+ overflow: hidden;
+ display: flex;
+ flex-direction: column;
+ box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
+}
+
+.dialog-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 16px 20px;
+ border-bottom: 1px solid #eee;
+ background: var(--color-primary-orange);
+
+ h3 {
+ margin: 0;
+ font-size: 1.2em;
+ color: #000;
+ }
+}
+
+.btn-close {
+ background: transparent;
+ border: none;
+ font-size: 1.5em;
+ cursor: pointer;
+ color: #000;
+ padding: 0;
+ line-height: 1;
+
+ &:hover {
+ opacity: 0.7;
+ }
+}
+
+.dialog-body {
+ padding: 20px;
+ overflow-y: auto;
+ flex: 1;
+}
+
+.form-group {
+ margin-bottom: 16px;
+
+ label {
+ display: block;
+ margin-bottom: 6px;
+ font-weight: 500;
+ color: var(--color-text-primary);
+ }
+
+ input[type="text"],
+ input[type="date"],
+ input[type="time"],
+ textarea {
+ width: 100%;
+ padding: 10px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 1em;
+
+ &:focus {
+ outline: none;
+ border-color: var(--color-primary-orange);
+ }
+ }
+
+ textarea {
+ resize: vertical;
+ }
+}
+
+.form-row {
+ display: flex;
+ gap: 16px;
+
+ .form-group {
+ flex: 1;
+ }
+}
+
+.checkbox-label {
+ display: flex !important;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+
+ input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ }
+}
+
+.category-selector {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.category-btn {
+ padding: 6px 12px;
+ border: 2px solid;
+ border-radius: 20px;
+ cursor: pointer;
+ font-size: 0.85em;
+ font-weight: 500;
+ transition: all 0.2s;
+
+ &:hover {
+ opacity: 0.8;
+ }
+
+ &.selected {
+ font-weight: 600;
+ }
+}
+
+.dialog-footer {
+ display: flex;
+ align-items: center;
+ padding: 16px 20px;
+ border-top: 1px solid #eee;
+ gap: 10px;
+}
+
+.spacer {
+ flex: 1;
+}
+
+.btn-delete {
+ padding: 10px 16px;
+ background: #f44336;
+ color: #fff;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-weight: 500;
+
+ &:hover {
+ background: #d32f2f;
+ }
+}
+
+.btn-cancel {
+ padding: 10px 16px;
+ background: #f5f5f5;
+ color: #666;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ cursor: pointer;
+
+ &:hover {
+ background: #e0e0e0;
+ }
+}
+
+.btn-save {
+ padding: 10px 20px;
+ background: var(--color-primary-green);
+ color: #000;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-weight: 600;
+
+ &:hover:not(:disabled) {
+ background: var(--color-primary-green-hover);
+ }
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: not-allowed;
+ }
+}
+
// Responsive
@media (max-width: 768px) {
.calendar-toolbar {
@@ -645,6 +1318,7 @@ h2 {
.nav-buttons {
justify-content: center;
+ flex-wrap: wrap;
}
.view-tabs {
@@ -660,5 +1334,15 @@ h2 {
padding: 8px 4px;
font-size: 0.8em;
}
+
+ .form-row {
+ flex-direction: column;
+ gap: 0;
+ }
+
+ .selection-info {
+ flex-direction: column;
+ text-align: center;
+ }
}