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 }) }} + + +
+
@@ -37,19 +51,26 @@ :class="['day-cell', { 'other-month': !day.currentMonth, 'today': day.isToday, - 'weekend': day.isWeekend + 'weekend': day.isWeekend, + 'selected': isDateSelected(day.date) }]" - @click="selectDate(day.date)" + @click="handleDayClick(day.date, $event)" + @mousedown="startDragSelection(day.date)" + @mouseenter="continueDragSelection(day.date)" + @mouseup="endDragSelection" > {{ day.dayNumber }}
+ @click.stop="editEvent(event)" + > + {{ event.title }} +
@@ -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 @@
- {{ $t(`personal.calendar.weekdays.${currentDayData.weekday}`) }}, + {{ $t(`personal.calendar.weekdaysFull.${currentDayData.weekday}`) }}, {{ currentDayData.dayNumber }}. {{ $t(`personal.calendar.months.${currentDayData.month}`) }} {{ currentDayData.year }}
@@ -134,13 +175,114 @@
+ :class="['time-slot', { 'current-hour': isCurrentHour(currentDateStr, hour) }]" + @click="openNewEventDialog(currentDateStr, hour)" + > +
+ {{ formatEventTime(event) }} + {{ event.title }} +
+
+ + +
+
+
+

{{ editingEvent ? $t('personal.calendar.editEntry') : $t('personal.calendar.newEntry') }}

+ +
+
+
+ + +
+ +
+ +
+ +
+
+ +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ +
+ +
+ +
+ + +
+
+ +
+
@@ -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; + } }