Changeset f6ed6e4 for petify-frontend/src/views
- Timestamp:
- 08/22/26 19:00:38 (13 hours ago)
- Branches:
- master
- Parents:
- ae83647
- Location:
- petify-frontend/src/views
- Files:
-
- 2 edited
-
ClinicDashboardView.vue (modified) (19 diffs)
-
OwnerProfileView.vue (modified) (7 diffs)
Legend:
- Unmodified
- Added
- Removed
-
petify-frontend/src/views/ClinicDashboardView.vue
rae83647 rf6ed6e4 8 8 <p v-if="clinic" class="clinic-subtitle">{{ clinic.name }} - {{ clinic.city }}, {{ clinic.address }}</p> 9 9 </div> 10 <div class="toolbar">10 <div v-if="canUseSchedule" class="toolbar"> 11 11 <button class="btn btn-outline-secondary btn-sm" type="button" @click="goToPreviousDay"> 12 12 Previous day … … 34 34 35 35 <template v-else> 36 <section v-if="clinic && !canUseSchedule" class="setup-panel"> 37 <div> 38 <p class="eyebrow">Required setup</p> 39 <h2>Add your working schedule</h2> 40 <p class="setup-copy"> 41 Users can book appointment slots only after your clinic adds working days and opening hours. 42 </p> 43 </div> 44 45 <form class="schedule-form" @submit.prevent="saveSchedule"> 46 <fieldset class="days-fieldset"> 47 <legend>Working days</legend> 48 <label v-for="day in workDayOptions" :key="day.value" class="day-check"> 49 <input 50 v-model="scheduleForm.workDays" 51 type="checkbox" 52 :value="day.value" 53 /> 54 <span>{{ day.label }}</span> 55 </label> 56 </fieldset> 57 58 <div class="time-grid"> 59 <div class="form-group"> 60 <label class="form-label" for="startTime">Start time</label> 61 <input id="startTime" v-model="scheduleForm.startTime" class="form-control" type="time" required /> 62 </div> 63 <div class="form-group"> 64 <label class="form-label" for="endTime">End time</label> 65 <input id="endTime" v-model="scheduleForm.endTime" class="form-control" type="time" required /> 66 </div> 67 </div> 68 69 <div v-if="scheduleSetupError" class="alert alert-danger">{{ scheduleSetupError }}</div> 70 <button class="btn btn-primary" type="submit" :disabled="isSavingSchedule"> 71 {{ isSavingSchedule ? 'Saving...' : 'Save schedule' }} 72 </button> 73 </form> 74 </section> 75 76 <template v-else> 36 77 <div class="summary-strip"> 37 78 <div class="summary-item"> … … 59 100 60 101 <div class="slot-grid"> 102 <div v-if="daySlots.length === 0" class="panel-empty full-width"> 103 This is not a working day for your clinic. 104 </div> 61 105 <div 62 106 v-for="slot in daySlots" … … 131 175 </aside> 132 176 </div> 177 </template> 133 178 </template> 134 179 </section> … … 147 192 getMyNotifications, 148 193 markMyClinicAppointmentNoShow, 194 updateMyClinicSchedule, 149 195 type AppNotification, 150 196 type AppointmentSlot, … … 174 220 const notifications = ref<AppNotification[]>([]) 175 221 const isLoading = ref(false) 222 const isSavingSchedule = ref(false) 176 223 const updatingAppointmentId = ref<number | null>(null) 177 224 const accessError = ref('') 178 225 const scheduleError = ref('') 226 const scheduleSetupError = ref('') 179 227 const notificationsError = ref('') 180 228 const NON_BLOCKING_STATUSES = new Set(['CANCELLED', 'CANCELED', 'NO_SHOW']) … … 182 230 183 231 const canUseDashboard = computed(() => auth.isAuthenticated && auth.user?.userType === 'CLINIC') 232 const canUseSchedule = computed(() => Boolean(clinic.value?.scheduleComplete)) 233 const scheduleForm = ref({ 234 workDays: [] as string[], 235 startTime: '09:00', 236 endTime: '17:00', 237 }) 238 const workDayOptions = [ 239 { value: 'MONDAY', label: 'Mon' }, 240 { value: 'TUESDAY', label: 'Tue' }, 241 { value: 'WEDNESDAY', label: 'Wed' }, 242 { value: 'THURSDAY', label: 'Thu' }, 243 { value: 'FRIDAY', label: 'Fri' }, 244 { value: 'SATURDAY', label: 'Sat' }, 245 { value: 'SUNDAY', label: 'Sun' }, 246 ] 184 247 185 248 const appointmentsByDateTime = computed(() => { … … 205 268 const now = new Date() 206 269 207 for (let hour = 9; hour < 17; hour += 1) { 208 for (const minute of [0, 30]) { 209 const dateTime = `${selectedDate.value}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}` 210 const label = `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}` 270 for (const time of getClinicTimesForDate(selectedDate.value)) { 271 const dateTime = `${selectedDate.value}T${time}` 272 const label = time 211 273 const key = normalizeDateTime(dateTime) 212 274 const appointment = appointmentsByDateTime.value.get(key) … … 252 314 }) 253 315 } 254 }255 316 } 256 317 … … 274 335 const slots: AppointmentSlot[] = [] 275 336 276 for (let hour = 9; hour < 17; hour += 1) { 277 for (const minute of [0, 30]) { 278 const dateTime = `${date}T${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}` 337 for (const time of getClinicTimesForDate(date)) { 338 const dateTime = `${date}T${time}` 279 339 const key = normalizeDateTime(dateTime) 280 340 if (new Date(dateTime).getTime() < now.getTime()) continue … … 282 342 slots.push({ 283 343 dateTime, 284 label: `${String(hour).padStart(2, '0')}:${String(minute).padStart(2, '0')}`,344 label: time, 285 345 }) 286 }287 346 } 288 347 … … 291 350 292 351 async function loadSchedule() { 293 if (!auth.user?.userId || !canUseDashboard.value || ! selectedDate.value) return352 if (!auth.user?.userId || !canUseDashboard.value || !canUseSchedule.value || !selectedDate.value) return 294 353 295 354 const requestId = ++latestScheduleRequest … … 319 378 } 320 379 380 async function saveSchedule() { 381 if (!auth.user?.userId) return 382 if (scheduleForm.value.workDays.length === 0) { 383 scheduleSetupError.value = 'Choose at least one working day' 384 return 385 } 386 387 if (!scheduleForm.value.startTime || !scheduleForm.value.endTime) { 388 scheduleSetupError.value = 'Start and end time are required' 389 return 390 } 391 392 if (scheduleForm.value.startTime >= scheduleForm.value.endTime) { 393 scheduleSetupError.value = 'Start time must be before end time' 394 return 395 } 396 397 try { 398 isSavingSchedule.value = true 399 scheduleSetupError.value = '' 400 clinic.value = await updateMyClinicSchedule(auth.user.userId, { 401 workDays: scheduleForm.value.workDays.join(','), 402 startTime: scheduleForm.value.startTime, 403 endTime: scheduleForm.value.endTime, 404 }) 405 hydrateScheduleForm() 406 await loadSchedule() 407 } catch (error) { 408 scheduleSetupError.value = error instanceof Error ? error.message : 'Failed to save clinic schedule' 409 } finally { 410 isSavingSchedule.value = false 411 } 412 } 413 321 414 async function loadNotifications() { 322 415 if (!auth.user?.userId || !canUseDashboard.value) return … … 417 510 } 418 511 512 function hydrateScheduleForm() { 513 scheduleForm.value = { 514 workDays: clinic.value?.workDays ? clinic.value.workDays.split(',').map((day) => day.trim()).filter(Boolean) : [], 515 startTime: clinic.value?.startTime?.slice(0, 5) || '09:00', 516 endTime: clinic.value?.endTime?.slice(0, 5) || '17:00', 517 } 518 } 519 520 function getClinicTimesForDate(date: string): string[] { 521 if (!clinic.value?.workDays || !clinic.value.startTime || !clinic.value.endTime) return [] 522 if (!isClinicWorkingDay(date)) return [] 523 524 const times: string[] = [] 525 const start = timeToMinutes(clinic.value.startTime) 526 const end = timeToMinutes(clinic.value.endTime) 527 528 for (let minutes = start; minutes < end; minutes += 30) { 529 times.push(minutesToTime(minutes)) 530 } 531 532 return times 533 } 534 535 function isClinicWorkingDay(date: string): boolean { 536 const day = new Date(`${date}T00:00:00`).toLocaleDateString('en-US', { weekday: 'long' }).toUpperCase() 537 return clinic.value?.workDays?.split(',').map((item) => item.trim().toUpperCase()).includes(day) ?? false 538 } 539 540 function timeToMinutes(time: string): number { 541 const parts = time.slice(0, 5).split(':').map(Number) 542 const hours = parts[0] ?? 0 543 const minutes = parts[1] ?? 0 544 return hours * 60 + minutes 545 } 546 547 function minutesToTime(total: number): string { 548 const hours = Math.floor(total / 60) 549 const minutes = total % 60 550 return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}` 551 } 552 419 553 function formatDateTime(value: string): string { 420 554 return new Date(value).toLocaleString('en-US', { … … 439 573 try { 440 574 clinic.value = await getMyClinic(auth.user.userId) 575 hydrateScheduleForm() 441 576 accessError.value = '' 442 577 } catch (error) { … … 445 580 } 446 581 447 await Promise.all([loadSchedule(), loadNotifications()]) 582 if (canUseSchedule.value) { 583 await Promise.all([loadSchedule(), loadNotifications()]) 584 } else { 585 await loadNotifications() 586 } 448 587 }) 449 588 … … 510 649 .summary-strip, 511 650 .schedule-section, 512 .appointments-panel { 651 .appointments-panel, 652 .setup-panel { 513 653 background: white; 514 654 border: 1px solid #e2e8f0; … … 529 669 color: #718096; 530 670 margin: 0; 671 } 672 673 .setup-panel { 674 display: grid; 675 gap: 24px; 676 padding: 28px; 677 } 678 679 .setup-panel h2 { 680 color: #1a202c; 681 font-size: 1.45rem; 682 margin: 0 0 8px; 683 } 684 685 .setup-copy { 686 color: #718096; 687 margin: 0; 688 } 689 690 .schedule-form { 691 display: grid; 692 gap: 20px; 693 max-width: 720px; 694 } 695 696 .days-fieldset { 697 border: 0; 698 display: flex; 699 flex-wrap: wrap; 700 gap: 10px; 701 margin: 0; 702 padding: 0; 703 } 704 705 .days-fieldset legend { 706 color: #2d3748; 707 font-weight: 700; 708 margin-bottom: 8px; 709 width: 100%; 710 } 711 712 .day-check { 713 align-items: center; 714 border: 1px solid #cbd5e0; 715 border-radius: 8px; 716 cursor: pointer; 717 display: inline-flex; 718 gap: 8px; 719 padding: 9px 12px; 720 } 721 722 .day-check:has(input:checked) { 723 background: #fff7ed; 724 border-color: #f97316; 725 color: #9a3412; 726 } 727 728 .time-grid { 729 display: grid; 730 grid-template-columns: repeat(2, minmax(0, 180px)); 731 gap: 16px; 732 } 733 734 .form-group { 735 display: grid; 736 gap: 8px; 737 } 738 739 .form-label { 740 color: #2d3748; 741 font-weight: 700; 531 742 } 532 743 … … 625 836 } 626 837 838 .full-width { 839 grid-column: 1 / -1; 840 } 841 627 842 .slot-card { 628 843 border: 1px solid #e2e8f0; -
petify-frontend/src/views/OwnerProfileView.vue
rae83647 rf6ed6e4 208 208 209 209 <div class="form-group"> 210 <label class="form-label" for="interactionType">How did you interact?</label> 211 <select 212 id="interactionType" 213 v-model="newReview.interactionType" 214 class="form-control" 215 required 216 > 217 <option value="" disabled>Select an option</option> 218 <option 219 v-for="option in interactionTypeOptions" 220 :key="option.value" 221 :value="option.value" 222 > 223 {{ option.label }} 224 </option> 225 </select> 226 </div> 227 228 <div class="form-group"> 210 229 <label class="form-label" for="comment">Comment</label> 211 230 <textarea … … 226 245 type="submit" 227 246 class="btn btn-primary" 228 :disabled="isSubmittingReview || newReview.rating === 0 "247 :disabled="isSubmittingReview || newReview.rating === 0 || !newReview.interactionType" 229 248 > 230 249 <span v-if="isSubmittingReview">Submitting...</span> … … 287 306 import { useRoute, RouterLink } from 'vue-router' 288 307 import { getUserProfile, getUserListings, getUserPets, loadUserVerificationStatus } from '../api/profile' 289 import { createReview, getReviewsByOwner, deleteReview as deleteReviewAPI } from '../api/reviews' 308 import { 309 createReview, 310 getReviewsByOwner, 311 deleteReview as deleteReviewAPI, 312 type UserReviewInteractionType, 313 } from '../api/reviews' 290 314 import { useAuthStore } from '../stores/auth' 291 315 … … 305 329 const isSubmittingReview = ref(false) 306 330 const reviewError = ref<string | null>(null) 307 const newReview = ref({ 331 const interactionTypeOptions: Array<{ value: UserReviewInteractionType; label: string }> = [ 332 { value: 'EVENT', label: 'Met at an event' }, 333 { value: 'PERSONAL_INTERACTION', label: 'Personal interaction' }, 334 { value: 'ONLINE', label: 'Online interaction' }, 335 { value: 'PHONE_CALL', label: 'Phone call' }, 336 { value: 'OTHER', label: 'Other' }, 337 ] 338 339 const newReview = ref<{ 340 rating: number 341 comment: string 342 interactionType: UserReviewInteractionType | '' 343 }>({ 308 344 rating: 0, 309 345 comment: '', 346 interactionType: '', 310 347 }) 311 348 … … 445 482 } 446 483 484 if (!newReview.value.interactionType) { 485 reviewError.value = 'Please choose how you interacted with this user' 486 return 487 } 488 447 489 isSubmittingReview.value = true 448 490 reviewError.value = null … … 453 495 auth.user.userId, 454 496 newReview.value.rating, 455 newReview.value.comment 497 newReview.value.comment, 498 newReview.value.interactionType 456 499 ) 457 500 … … 459 502 newReview.value.rating = 0 460 503 newReview.value.comment = '' 504 newReview.value.interactionType = '' 461 505 await loadReviews() 462 506 } catch (err) {
Note:
See TracChangeset
for help on using the changeset viewer.
