source: petify-frontend/src/components/OwnerAppointmentsCalendar.vue

Last change on this file was 92e7c7a, checked in by veronika-ils <ilioskaveronika@…>, 7 hours ago

Petify fullstack project

  • Property mode set to 100644
File size: 7.0 KB
RevLine 
[92e7c7a]1<template>
2 <div class="appointments-calendar">
3 <div class="calendar-header">
4 <button class="btn btn-outline-secondary btn-sm" type="button" @click="goPrevMonth">
5 Prev
6 </button>
7
8 <div
9 v-if="!isEditingMonth"
10 class="calendar-title"
11 title="Double-click to change month and year"
12 @dblclick="startEditingMonth"
13 >
14 {{ monthLabel }}
15 </div>
16
17 <div v-else class="month-year-editor">
18 <select v-model.number="editMonth" class="form-control form-control-sm month-select">
19 <option v-for="m in months" :key="m.value" :value="m.value">
20 {{ m.label }}
21 </option>
22 </select>
23
24 <input
25 ref="yearInputRef"
26 v-model.number="editYear"
27 type="number"
28 class="form-control form-control-sm year-input"
29 min="1900"
30 max="2100"
31 @keyup.enter="applyMonthYearInput"
32 @keyup.esc="cancelEditingMonth"
33 />
34
35 <button class="btn btn-sm btn-primary" type="button" @click="applyMonthYearInput">
36 Go
37 </button>
38
39 <button class="btn btn-sm btn-outline-secondary" type="button" @click="cancelEditingMonth">
40 Cancel
41 </button>
42 </div>
43
44 <button class="btn btn-outline-secondary btn-sm" type="button" @click="goNextMonth">
45 Next
46 </button>
47 </div>
48
49 <div class="calendar-grid">
50 <div v-for="dayName in dayNames" :key="dayName" class="calendar-cell calendar-day-name">
51 {{ dayName }}
52 </div>
53
54 <button
55 v-for="day in calendarDays"
56 :key="day.key"
57 class="calendar-cell calendar-day"
58 :class="{
59 'is-empty': !day.date,
60 'is-selected': day.dateKey === selectedDate,
61 'has-appointments': day.hasAppointments,
62 }"
63 type="button"
64 :disabled="!day.date"
65 @click="selectDay(day.dateKey)"
66 >
67 <span v-if="day.date" class="day-number">{{ day.dayNumber }}</span>
68 <span v-if="day.hasAppointments" class="day-dot"></span>
69 </button>
70 </div>
71 </div>
72</template>
73
74<script setup lang="ts">
75import { computed, nextTick, ref } from 'vue'
76
77type AppointmentDay = {
78 appointmentId: number
79 dateTime: string
80}
81
82const props = defineProps<{
83 appointments: AppointmentDay[]
84 selectedDate: string
85}>()
86
87const emit = defineEmits<{
88 (e: 'select', dateKey: string): void
89}>()
90
91const dayNames = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
92const monthCursor = ref(new Date())
93
94const isEditingMonth = ref(false)
95const editMonth = ref(new Date().getMonth())
96const editYear = ref(new Date().getFullYear())
97const yearInputRef = ref<HTMLInputElement | null>(null)
98
99const months = [
100 { value: 0, label: 'January' },
101 { value: 1, label: 'February' },
102 { value: 2, label: 'March' },
103 { value: 3, label: 'April' },
104 { value: 4, label: 'May' },
105 { value: 5, label: 'June' },
106 { value: 6, label: 'July' },
107 { value: 7, label: 'August' },
108 { value: 8, label: 'September' },
109 { value: 9, label: 'October' },
110 { value: 10, label: 'November' },
111 { value: 11, label: 'December' },
112]
113
114const appointmentDateKeys = computed(() => {
115 const keys = new Set<string>()
116
117 props.appointments.forEach((appt) => {
118 keys.add(toDateKey(new Date(appt.dateTime)))
119 })
120
121 return keys
122})
123
124const monthLabel = computed(() => {
125 return monthCursor.value.toLocaleDateString('en-US', {
126 month: 'long',
127 year: 'numeric',
128 })
129})
130
131const calendarDays = computed(() => {
132 const year = monthCursor.value.getFullYear()
133 const month = monthCursor.value.getMonth()
134
135 const firstDay = new Date(year, month, 1)
136 const lastDay = new Date(year, month + 1, 0)
137
138 const startOffset = firstDay.getDay()
139 const totalDays = lastDay.getDate()
140
141 const cells: Array<{
142 key: string
143 date?: Date
144 dateKey?: string
145 dayNumber?: number
146 hasAppointments: boolean
147 }> = []
148
149 for (let i = 0; i < startOffset; i++) {
150 cells.push({
151 key: `empty-${i}`,
152 hasAppointments: false,
153 })
154 }
155
156 for (let day = 1; day <= totalDays; day++) {
157 const date = new Date(year, month, day)
158 const dateKey = toDateKey(date)
159
160 cells.push({
161 key: dateKey,
162 date,
163 dateKey,
164 dayNumber: day,
165 hasAppointments: appointmentDateKeys.value.has(dateKey),
166 })
167 }
168
169 const remainder = cells.length % 7
170 if (remainder !== 0) {
171 for (let i = 0; i < 7 - remainder; i++) {
172 cells.push({
173 key: `tail-${i}`,
174 hasAppointments: false,
175 })
176 }
177 }
178
179 return cells
180})
181
182function startEditingMonth() {
183 editMonth.value = monthCursor.value.getMonth()
184 editYear.value = monthCursor.value.getFullYear()
185 isEditingMonth.value = true
186
187 nextTick(() => {
188 yearInputRef.value?.focus()
189 })
190}
191
192function applyMonthYearInput() {
193 if (!editYear.value || editYear.value < 1900 || editYear.value > 2100) {
194 return
195 }
196
197 monthCursor.value = new Date(editYear.value, editMonth.value, 1)
198 isEditingMonth.value = false
199}
200
201function cancelEditingMonth() {
202 isEditingMonth.value = false
203}
204
205function toDateKey(date: Date): string {
206 const year = date.getFullYear()
207 const month = String(date.getMonth() + 1).padStart(2, '0')
208 const day = String(date.getDate()).padStart(2, '0')
209
210 return `${year}-${month}-${day}`
211}
212
213function selectDay(dateKey?: string) {
214 if (!dateKey) return
215 emit('select', dateKey)
216}
217
218function goPrevMonth() {
219 const date = new Date(monthCursor.value)
220 date.setMonth(date.getMonth() - 1)
221 monthCursor.value = date
222}
223
224function goNextMonth() {
225 const date = new Date(monthCursor.value)
226 date.setMonth(date.getMonth() + 1)
227 monthCursor.value = date
228}
229</script>
230
231<style scoped>
232.appointments-calendar {
233 border: 1px solid #e2e8f0;
234 border-radius: 12px;
235 padding: 16px;
236 background: #fff;
237}
238
239.calendar-header {
240 display: flex;
241 align-items: center;
242 justify-content: space-between;
243 gap: 12px;
244 margin-bottom: 12px;
245}
246
247.calendar-title {
248 font-weight: 700;
249 color: #1a202c;
250 cursor: pointer;
251 min-width: 180px;
252 text-align: center;
253}
254
255.month-year-editor {
256 display: flex;
257 align-items: center;
258 gap: 8px;
259 flex: 1;
260 justify-content: center;
261}
262
263.month-select {
264 max-width: 150px;
265}
266
267.year-input {
268 max-width: 100px;
269}
270
271.calendar-grid {
272 display: grid;
273 grid-template-columns: repeat(7, 1fr);
274 gap: 6px;
275}
276
277.calendar-cell {
278 border: 1px solid #e2e8f0;
279 border-radius: 8px;
280 min-height: 44px;
281 background: #f8fafc;
282 display: flex;
283 align-items: center;
284 justify-content: center;
285 position: relative;
286}
287
288.calendar-day-name {
289 background: transparent;
290 border: none;
291 font-weight: 600;
292 color: #4a5568;
293 min-height: 24px;
294}
295
296.calendar-day {
297 cursor: pointer;
298 transition: all 0.2s ease;
299}
300
301.calendar-day:disabled {
302 cursor: default;
303 background: transparent;
304 border: none;
305}
306
307.calendar-day.has-appointments {
308 border-color: #f97316;
309}
310
311.calendar-day.is-selected {
312 background: #f97316;
313 color: #fff;
314 border-color: #f97316;
315}
316
317.day-number {
318 font-weight: 600;
319}
320
321.day-dot {
322 width: 6px;
323 height: 6px;
324 border-radius: 50%;
325 background: #f97316;
326 position: absolute;
327 bottom: 6px;
328}
329
330.calendar-day.is-selected .day-dot {
331 background: #fff;
332}
333</style>
Note: See TracBrowser for help on using the repository browser.