| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328 |
- <template>
- <div class="avail-calendar">
- <div class="avail-cal__nav row items-center justify-between q-px-md q-pt-md q-pb-sm q-gutter-x-sm">
- <q-btn flat round dense icon="mdi-chevron-left" size="sm" color="text" @click="prevMonth" />
- <span class="avail-cal__nav-label month-label">{{ monthLabel }}</span>
- <q-btn flat round dense icon="mdi-chevron-right" size="sm" color="text" @click="nextMonth" />
- <q-btn flat round dense icon="mdi-chevron-left" size="xs" color="text" @click="prevYear" />
- <span class="avail-cal__nav-label year-label">{{ currentYear }}</span>
- <q-btn flat round dense icon="mdi-chevron-right" size="xs" color="text" @click="nextYear" />
- </div>
- <div class="avail-cal__weekdays row q-px-sm q-pb-xs">
- <div
- v-for="wd in weekdayLabels"
- :key="wd"
- class="avail-cal__weekday col text-center text-weight-bold"
- >
- {{ wd }}
- </div>
- </div>
- <div class="avail-cal__grid q-px-sm q-pb-md">
- <div
- v-for="n in firstDayOffset"
- :key="`empty-${n}`"
- class="avail-cal__cell"
- />
- <div
- v-for="day in daysInMonth"
- :key="day"
- class="avail-cal__cell column items-center justify-start"
- >
- <button
- class="avail-cal__day-btn"
- :class="{
- 'avail-cal__day--today': isToday(day),
- 'avail-cal__day--past': isPast(day),
- }"
- @click="onDayClick(day)"
- >
- {{ day }}
- </button>
- <div v-if="hasAnyBlock(day)" class="avail-cal__pill" @click="onDayClick(day)">
- <div
- class="avail-cal__pill-half avail-cal__pill-left"
- :class="morningState(day)"
- />
- <div
- class="avail-cal__pill-half avail-cal__pill-right"
- :class="afternoonState(day)"
- />
- </div>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, computed } from 'vue'
- import { useI18n } from 'vue-i18n'
- const props = defineProps({
- blockedDays: {
- type: Array,
- default: () => [],
- },
- })
- const emit = defineEmits(['date-click', 'navigation'])
- const { locale } = useI18n()
- const today = new Date()
- const currentYear = ref(today.getFullYear())
- const currentMonth = ref(today.getMonth() + 1) // 1-12
- const weekdayLabels = computed(() => {
- const base = new Date(2023, 0, 1)
- return Array.from({ length: 7 }, (_, i) => {
- const d = new Date(base)
- d.setDate(d.getDate() + i)
- return d.toLocaleDateString(locale.value || 'pt-BR', { weekday: 'narrow' })
- .replace('.', '')
- .slice(0, 3)
- .toUpperCase()
- })
- })
- const monthLabel = computed(() => {
- const d = new Date(currentYear.value, currentMonth.value - 1, 1)
- return d.toLocaleDateString(locale.value || 'pt-BR', { month: 'long' })
- })
- const daysInMonth = computed(() => {
- return new Date(currentYear.value, currentMonth.value, 0).getDate()
- })
- const firstDayOffset = computed(() => {
- return new Date(currentYear.value, currentMonth.value - 1, 1).getDay()
- })
- const prevMonth = () => {
- if (currentMonth.value === 1) {
- currentMonth.value = 12
- currentYear.value--
- } else {
- currentMonth.value--
- }
- emitNavigation()
- }
- const nextMonth = () => {
- if (currentMonth.value === 12) {
- currentMonth.value = 1
- currentYear.value++
- } else {
- currentMonth.value++
- }
- emitNavigation()
- }
- const prevYear = () => {
- currentYear.value--
- emitNavigation()
- }
- const nextYear = () => {
- currentYear.value++
- emitNavigation()
- }
- const emitNavigation = () => {
- emit('navigation', { year: currentYear.value, month: currentMonth.value })
- }
- const pad = (n) => String(n).padStart(2, '0')
- const dateStrForDay = (day) =>
- `${currentYear.value}-${pad(currentMonth.value)}-${pad(day)}`
- const isToday = (day) => {
- return (
- day === today.getDate() &&
- currentMonth.value === today.getMonth() + 1 &&
- currentYear.value === today.getFullYear()
- )
- }
- const isPast = (day) => {
- const d = new Date(currentYear.value, currentMonth.value - 1, day)
- const todayStart = new Date(today.getFullYear(), today.getMonth(), today.getDate())
- return d < todayStart
- }
- const blockedPeriodsForDay = (day) => {
- const dateStr = dateStrForDay(day)
- return props.blockedDays
- .filter((bd) => bd.date === dateStr)
- .map((bd) => bd.period)
- }
- const hasAnyBlock = (day) => blockedPeriodsForDay(day).length > 0
- const isMorningBlocked = (day) => {
- const periods = blockedPeriodsForDay(day)
- return periods.includes('morning') || periods.includes('all')
- }
- const isAfternoonBlocked = (day) => {
- const periods = blockedPeriodsForDay(day)
- return periods.includes('afternoon') || periods.includes('all')
- }
- const morningState = (day) => ({
- 'pill-half--blocked': isMorningBlocked(day),
- 'pill-half--free': !isMorningBlocked(day),
- })
- const afternoonState = (day) => ({
- 'pill-half--blocked': isAfternoonBlocked(day),
- 'pill-half--free': !isAfternoonBlocked(day),
- })
- const onDayClick = (day) => {
- const dateStr = `${currentYear.value}/${pad(currentMonth.value)}/${pad(day)}`
- emit('date-click', dateStr)
- }
- </script>
- <style scoped lang="scss">
- $morning-color: #f916f9;
- $afternoon-color: #6366F1;
- $blocked-opacity: 1;
- $free-opacity: 0.05;
- .avail-calendar {
- background: #fff;
- border-radius: 20px;
- overflow: hidden;
- user-select: none;
- }
- /* ── Navegação ───────────────────────────────────────── */
- .avail-cal__nav {
- background: #fff;
- }
- .avail-cal__nav-label {
- font-weight: 700;
- color: #1E293B;
- &.month-label {
- color: #6366F1;
- font-size: 15px;
- text-transform: capitalize;
- }
- &.year-label {
- color: #6366F1;
- font-size: 15px;
- }
- }
- /* ── Cabeçalho weekdays ──────────────────────────────── */
- .avail-cal__weekdays {
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- }
- .avail-cal__weekday {
- font-size: 11px;
- color: #6366F1;
- opacity: 0.8;
- padding: 4px 0;
- }
- /* ── Grid de dias ────────────────────────────────────── */
- .avail-cal__grid {
- display: grid;
- grid-template-columns: repeat(7, 1fr);
- gap: 0;
- }
- .avail-cal__cell {
- display: flex;
- flex-direction: column;
- align-items: center;
- padding: 2px 0 6px;
- }
- /* ── Botão do número ─────────────────────────────────── */
- .avail-cal__day-btn {
- width: 32px;
- height: 32px;
- border: none;
- background: transparent;
- border-radius: 50%;
- font-family: 'Inter', sans-serif;
- font-size: 13px;
- font-weight: 500;
- color: #1E293B;
- cursor: pointer;
- display: flex;
- align-items: center;
- justify-content: center;
- transition: background 0.15s;
- &:hover {
- background: #f1f5f9;
- }
- &.avail-cal__day--today {
- color: #7c4dff;
- background: rgba(124, 77, 255, 0.08);
- }
- &.avail-cal__day--past {
- color: #CBD5E1;
- cursor: default;
- pointer-events: none;
- }
- }
- /* ── Pílula ──────────────────────────────────────────── */
- .avail-cal__pill {
- display: flex;
- width: 28px;
- height: 9px;
- border-radius: 999px;
- overflow: hidden;
- cursor: pointer;
- margin-top: 2px;
- }
- .avail-cal__pill-half {
- flex: 1;
- transition: opacity 0.2s, background-color 0.2s;
- }
- .avail-cal__pill-left {
- background-color: $morning-color;
- border-radius: 999px 0 0 999px;
- &.pill-half--blocked {
- opacity: $blocked-opacity;
- }
- &.pill-half--free {
- opacity: $free-opacity;
- }
- }
- .avail-cal__pill-right {
- background-color: $afternoon-color;
- border-radius: 0 999px 999px 0;
- &.pill-half--blocked {
- opacity: $blocked-opacity;
- }
- &.pill-half--free {
- opacity: $free-opacity;
- }
- }
- </style>
|