1515 lines
55 KiB
Vue
1515 lines
55 KiB
Vue
<script setup lang="ts">
|
|
usePageTitle('Calendar')
|
|
|
|
type CalEventType = 'meeting' | 'renewal' | 'claim' | 'payment' | 'reminder' | 'threshold'
|
|
|
|
interface CalEvent {
|
|
id: string
|
|
title: string
|
|
time: string
|
|
type: CalEventType
|
|
source?: 'gmail' | 'system' | 'manual'
|
|
customer?: string
|
|
detail?: string
|
|
urgent?: boolean
|
|
/** Day of month (1-based) for month view placement */
|
|
dayOfMonth?: number
|
|
}
|
|
|
|
/* ── Layer system ── */
|
|
type CalLayer = 'renewals' | 'claims' | 'payments' | 'meetings' | 'reminders' | 'alerts'
|
|
|
|
const layerMeta: Record<CalLayer, { label: string; types: CalEventType[]; color: string; icon: string }> = {
|
|
meetings: { label: 'Meetings & calls', types: ['meeting'], color: '#01696f', icon: 'i-heroicons-calendar-days' },
|
|
renewals: { label: 'Renewals', types: ['renewal'], color: '#7c3aed', icon: 'i-heroicons-arrow-path' },
|
|
claims: { label: 'Claims', types: ['claim'], color: '#c13838', icon: 'i-heroicons-shield-exclamation' },
|
|
payments: { label: 'Payments & AR', types: ['payment'], color: '#c27b1a', icon: 'i-heroicons-banknotes' },
|
|
reminders: { label: 'Reminders', types: ['reminder'], color: '#0f7b5f', icon: 'i-heroicons-bell' },
|
|
alerts: { label: 'Threshold alerts', types: ['threshold'], color: '#be185d', icon: 'i-heroicons-exclamation-triangle' },
|
|
}
|
|
|
|
const LAYER_ORDER: CalLayer[] = ['meetings', 'renewals', 'claims', 'payments', 'reminders', 'alerts']
|
|
|
|
const activeLayers = reactive<Record<CalLayer, boolean>>({
|
|
meetings: true,
|
|
renewals: true,
|
|
claims: true,
|
|
payments: true,
|
|
reminders: true,
|
|
alerts: true,
|
|
})
|
|
|
|
const layerPanelOpen = ref(false)
|
|
|
|
const activeLayerCount = computed(() =>
|
|
LAYER_ORDER.filter(l => activeLayers[l]).length
|
|
)
|
|
|
|
/** Which event types are enabled by active layers */
|
|
const enabledTypes = computed(() => {
|
|
const types = new Set<CalEventType>()
|
|
for (const layer of LAYER_ORDER) {
|
|
if (activeLayers[layer]) {
|
|
for (const t of layerMeta[layer].types) types.add(t)
|
|
}
|
|
}
|
|
return types
|
|
})
|
|
|
|
function toggleLayer(layer: CalLayer) { activeLayers[layer] = !activeLayers[layer] }
|
|
function enableAllLayers() { LAYER_ORDER.forEach(l => { activeLayers[l] = true }) }
|
|
function disableAllLayers() { LAYER_ORDER.forEach(l => { activeLayers[l] = false }) }
|
|
|
|
const calView = ref<'month' | 'week' | 'day'>('month')
|
|
|
|
const typeMeta: Record<CalEventType, { label: string; color: string; icon: string }> = {
|
|
meeting: { label: 'Meetings', color: '#01696f', icon: 'i-heroicons-calendar-days' },
|
|
renewal: { label: 'Renewals', color: '#7c3aed', icon: 'i-heroicons-arrow-path' },
|
|
claim: { label: 'Claims', color: '#c13838', icon: 'i-heroicons-shield-exclamation' },
|
|
payment: { label: 'Payments', color: '#c27b1a', icon: 'i-heroicons-banknotes' },
|
|
reminder: { label: 'Reminders', color: '#0f7b5f', icon: 'i-heroicons-bell' },
|
|
threshold: { label: 'Alerts', color: '#be185d', icon: 'i-heroicons-exclamation-triangle' },
|
|
}
|
|
|
|
/* ── Today's events ── */
|
|
const todayEventsSeed: CalEvent[] = [
|
|
{ id: 'c1', title: 'Renewal review — Constructora Delta', time: '9:00 AM', type: 'renewal', source: 'system', customer: 'Constructora Delta S.A.', detail: 'Policy expires Jul 15. Premium $55K, 3 yr client.', urgent: true },
|
|
{ id: 'c2', title: 'Call with María Pérez', time: '10:30 AM', type: 'meeting', source: 'gmail', customer: 'María Pérez', detail: 'Auto quote follow-up. Synced from Gmail.' },
|
|
{ id: 'c3', title: 'Unpaid premium — Farmacia Salud', time: '11:00 AM', type: 'payment', source: 'system', customer: 'Farmacia Salud', detail: '45 days overdue. $12,800 group life.', urgent: true },
|
|
{ id: 'c4', title: 'Claim status update — Hotel Pacífico', time: '1:00 PM', type: 'claim', source: 'system', customer: 'Hotel Pacífico', detail: 'Fire claim #CL-2891. Adjuster report due.' },
|
|
{ id: 'c5', title: 'Client crossed $100K premium', time: '2:00 PM', type: 'threshold', source: 'system', customer: 'Banco Regional', detail: 'Total book now $186K. VIP follow-up recommended.' },
|
|
{ id: 'c6', title: 'Prepare renewal proposal', time: '3:30 PM', type: 'reminder', source: 'manual', detail: 'Transportes del Sur fleet policy. Get 3 comparative quotes.' },
|
|
{ id: 'c7', title: 'Team sync — weekly pipeline', time: '4:00 PM', type: 'meeting', source: 'gmail', detail: 'Google Meet link attached. All agents.' },
|
|
{ id: 'c8', title: '5 renewals due next 7 days', time: '—', type: 'renewal', source: 'system', detail: 'Batch alert: review upcoming expirations.', urgent: true },
|
|
]
|
|
|
|
/* ── Month-spread mock events (scattered across the month) ── */
|
|
const monthMockEventsSeed: CalEvent[] = [
|
|
{ id: 'm1', title: 'Renewal — Transportes Delta fleet', time: '9:00', type: 'renewal', dayOfMonth: 2, urgent: true },
|
|
{ id: 'm2', title: 'Payment due — Retail Plaza', time: '—', type: 'payment', dayOfMonth: 3 },
|
|
{ id: 'm3', title: 'Client meeting — Banco Regional', time: '11:00', type: 'meeting', dayOfMonth: 5, customer: 'Banco Regional' },
|
|
{ id: 'm4', title: 'Claim follow-up — Hotel Pacífico', time: '2:00', type: 'claim', dayOfMonth: 7 },
|
|
{ id: 'm5', title: 'Renewal — Clínica Norte group health', time: '—', type: 'renewal', dayOfMonth: 8, urgent: true },
|
|
{ id: 'm6', title: 'Premium threshold — Solís Calderón', time: '—', type: 'threshold', dayOfMonth: 10, customer: 'Luis Andrés Solís' },
|
|
{ id: 'm7', title: 'Quarterly review prep', time: '3:00', type: 'reminder', dayOfMonth: 12 },
|
|
{ id: 'm8', title: 'Team sync', time: '4:00', type: 'meeting', dayOfMonth: 14 },
|
|
{ id: 'm9', title: 'Collections call — 3 accounts', time: '10:00', type: 'payment', dayOfMonth: 15, urgent: true },
|
|
{ id: 'm10', title: 'New business onboarding — Startup Labs', time: '1:00', type: 'meeting', dayOfMonth: 17, customer: 'Startup Labs' },
|
|
{ id: 'm11', title: 'Renewal batch — 4 policies', time: '—', type: 'renewal', dayOfMonth: 19 },
|
|
{ id: 'm12', title: 'Claim deadline — CL-3020', time: '—', type: 'claim', dayOfMonth: 21, urgent: true },
|
|
{ id: 'm13', title: 'Reminder: quote comparison due', time: '—', type: 'reminder', dayOfMonth: 22 },
|
|
{ id: 'm14', title: 'Payment reconciliation', time: '9:00', type: 'payment', dayOfMonth: 24 },
|
|
{ id: 'm15', title: 'Board presentation prep', time: '2:00', type: 'meeting', dayOfMonth: 26 },
|
|
{ id: 'm16', title: 'Month-end close', time: '—', type: 'reminder', dayOfMonth: 28 },
|
|
{ id: 'm17', title: 'Renewal — María Pérez auto', time: '—', type: 'renewal', dayOfMonth: 30 },
|
|
]
|
|
|
|
/* ── User-created appointments (reactive, add/remove) ── */
|
|
const userAppointments = ref<CalEvent[]>([])
|
|
let nextApptId = 1
|
|
|
|
/* ── Add appointment modal ── */
|
|
const addModalOpen = ref(false)
|
|
const addForm = reactive({
|
|
title: '',
|
|
date: new Date().toISOString().slice(0, 10),
|
|
startTime: '09:00',
|
|
endTime: '10:00',
|
|
allDay: false,
|
|
type: 'meeting' as CalEventType,
|
|
customer: '',
|
|
dayOfMonth: new Date().getDate(),
|
|
location: '',
|
|
videoCall: false,
|
|
repeat: 'none' as 'none' | 'daily' | 'weekly' | 'monthly' | 'yearly',
|
|
guests: '',
|
|
notification: '15' as string,
|
|
color: '' as string,
|
|
detail: '',
|
|
})
|
|
|
|
function resetAddForm() {
|
|
addForm.title = ''
|
|
addForm.date = new Date().toISOString().slice(0, 10)
|
|
addForm.startTime = '09:00'
|
|
addForm.endTime = '10:00'
|
|
addForm.allDay = false
|
|
addForm.type = 'meeting'
|
|
addForm.customer = ''
|
|
addForm.dayOfMonth = new Date().getDate()
|
|
addForm.location = ''
|
|
addForm.videoCall = false
|
|
addForm.repeat = 'none'
|
|
addForm.guests = ''
|
|
addForm.notification = '15'
|
|
addForm.color = ''
|
|
addForm.detail = ''
|
|
}
|
|
|
|
const eventColors = [
|
|
{ label: 'Teal', value: '#01696f' },
|
|
{ label: 'Purple', value: '#7c3aed' },
|
|
{ label: 'Red', value: '#c13838' },
|
|
{ label: 'Amber', value: '#c27b1a' },
|
|
{ label: 'Green', value: '#0f7b5f' },
|
|
{ label: 'Pink', value: '#be185d' },
|
|
{ label: 'Blue', value: '#2563eb' },
|
|
]
|
|
|
|
function openAddModal(presetDay?: number) {
|
|
resetAddForm()
|
|
if (presetDay) {
|
|
addForm.dayOfMonth = presetDay
|
|
const y = currentMonth.value.getFullYear()
|
|
const m = currentMonth.value.getMonth()
|
|
const d = new Date(y, m, presetDay)
|
|
addForm.date = d.toISOString().slice(0, 10)
|
|
}
|
|
addModalOpen.value = true
|
|
}
|
|
|
|
function submitAppointment() {
|
|
if (!addForm.title.trim()) return
|
|
const parsedDay = addForm.date ? new Date(addForm.date + 'T12:00:00').getDate() : addForm.dayOfMonth
|
|
const timeLabel = addForm.allDay ? 'All day' : addForm.startTime ? formatTime12(addForm.startTime) : '—'
|
|
const appt: CalEvent = {
|
|
id: `user-${nextApptId++}`,
|
|
title: addForm.title.trim(),
|
|
time: timeLabel,
|
|
type: addForm.type,
|
|
source: 'manual',
|
|
customer: addForm.customer.trim() || undefined,
|
|
detail: [
|
|
addForm.detail.trim(),
|
|
addForm.location ? `Location: ${addForm.location}` : '',
|
|
addForm.guests ? `Guests: ${addForm.guests}` : '',
|
|
].filter(Boolean).join(' · ') || undefined,
|
|
dayOfMonth: parsedDay,
|
|
}
|
|
userAppointments.value = [...userAppointments.value, appt]
|
|
addModalOpen.value = false
|
|
}
|
|
|
|
function formatTime12(t: string) {
|
|
const [h, m] = t.split(':').map(Number)
|
|
const ampm = h >= 12 ? 'PM' : 'AM'
|
|
const h12 = h % 12 || 12
|
|
return `${h12}:${String(m).padStart(2, '0')} ${ampm}`
|
|
}
|
|
|
|
function removeAppointment(id: string) {
|
|
userAppointments.value = userAppointments.value.filter(a => a.id !== id)
|
|
}
|
|
|
|
/* ── Merged events ── */
|
|
const todayEvents = computed(() => {
|
|
const today = new Date().getDate()
|
|
const userToday = userAppointments.value.filter(a => a.dayOfMonth === today)
|
|
return [...todayEventsSeed, ...userToday]
|
|
})
|
|
|
|
const monthMockEvents = computed(() => {
|
|
return [...monthMockEventsSeed, ...userAppointments.value]
|
|
})
|
|
|
|
const filteredEvents = computed(() => todayEvents.value.filter(e => enabledTypes.value.has(e.type)))
|
|
|
|
/* ── Month calendar grid ── */
|
|
const currentMonth = ref(new Date())
|
|
|
|
const monthLabel = computed(() =>
|
|
currentMonth.value.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
|
|
)
|
|
|
|
function prevMonth() {
|
|
const d = new Date(currentMonth.value)
|
|
d.setMonth(d.getMonth() - 1)
|
|
currentMonth.value = d
|
|
}
|
|
function nextMonth() {
|
|
const d = new Date(currentMonth.value)
|
|
d.setMonth(d.getMonth() + 1)
|
|
currentMonth.value = d
|
|
}
|
|
function goToday() {
|
|
currentMonth.value = new Date()
|
|
}
|
|
|
|
const monthGrid = computed(() => {
|
|
const year = currentMonth.value.getFullYear()
|
|
const month = currentMonth.value.getMonth()
|
|
const firstDay = new Date(year, month, 1)
|
|
const lastDay = new Date(year, month + 1, 0)
|
|
const daysInMonth = lastDay.getDate()
|
|
|
|
// Monday = 0 start
|
|
let startDow = firstDay.getDay() - 1
|
|
if (startDow < 0) startDow = 6
|
|
|
|
const today = new Date()
|
|
const isCurrentMonth = today.getFullYear() === year && today.getMonth() === month
|
|
const todayDate = today.getDate()
|
|
|
|
const cells: {
|
|
date: number
|
|
isCurrentMonth: boolean
|
|
isToday: boolean
|
|
events: CalEvent[]
|
|
}[] = []
|
|
|
|
// Previous month trailing days
|
|
const prevLastDay = new Date(year, month, 0).getDate()
|
|
for (let i = startDow - 1; i >= 0; i--) {
|
|
cells.push({ date: prevLastDay - i, isCurrentMonth: false, isToday: false, events: [] })
|
|
}
|
|
|
|
// Current month days
|
|
for (let d = 1; d <= daysInMonth; d++) {
|
|
const dayEvents = monthMockEvents.value.filter(e => e.dayOfMonth === d && enabledTypes.value.has(e.type))
|
|
// Add today's events to today's date
|
|
if (isCurrentMonth && d === todayDate) {
|
|
const todayFiltered = todayEventsSeed.filter(e => enabledTypes.value.has(e.type))
|
|
dayEvents.push(...todayFiltered)
|
|
}
|
|
cells.push({
|
|
date: d,
|
|
isCurrentMonth: true,
|
|
isToday: isCurrentMonth && d === todayDate,
|
|
events: dayEvents
|
|
})
|
|
}
|
|
|
|
// Next month leading days (fill to 6 rows)
|
|
const remaining = (7 - (cells.length % 7)) % 7
|
|
for (let i = 1; i <= remaining; i++) {
|
|
cells.push({ date: i, isCurrentMonth: false, isToday: false, events: [] })
|
|
}
|
|
|
|
return cells
|
|
})
|
|
|
|
const weekDays = computed(() => {
|
|
const today = new Date()
|
|
const dow = today.getDay()
|
|
const monday = new Date(today)
|
|
monday.setDate(today.getDate() - (dow === 0 ? 6 : dow - 1))
|
|
return Array.from({ length: 7 }, (_, i) => {
|
|
const d = new Date(monday)
|
|
d.setDate(monday.getDate() + i)
|
|
return {
|
|
label: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][i],
|
|
date: d.getDate(),
|
|
isToday: d.toDateString() === today.toDateString(),
|
|
events: i === (dow === 0 ? 6 : dow - 1) ? todayEvents.value.length : Math.floor(Math.random() * 4)
|
|
}
|
|
})
|
|
})
|
|
|
|
function dotStyle(type: CalEventType) { return `background: ${typeMeta[type].color}` }
|
|
|
|
/* ── Click outside to close panels ── */
|
|
function onClickOutside(e: MouseEvent) {
|
|
const target = e.target as HTMLElement
|
|
if (layerPanelOpen.value && !target.closest('.cal-layers-wrap')) layerPanelOpen.value = false
|
|
}
|
|
onMounted(() => document.addEventListener('click', onClickOutside))
|
|
onUnmounted(() => document.removeEventListener('click', onClickOutside))
|
|
|
|
/* ── Google Calendar link (placeholder) ── */
|
|
function openGoogleCalendar() {
|
|
window.open('https://calendar.google.com', '_blank')
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="cal-page">
|
|
<!-- Header row -->
|
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
<div>
|
|
<h1 class="mt-1 text-2xl font-semibold tracking-tight text-[var(--text-primary)]">Calendar</h1>
|
|
<p class="mt-2 text-[14px] leading-relaxed text-[var(--text-muted)]">
|
|
Your schedule, system alerts, renewal deadlines, and follow-ups in one view.
|
|
</p>
|
|
</div>
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<!-- Add appointment -->
|
|
<button type="button" class="cal-action-btn cal-action-primary" @click="openAddModal()">
|
|
<UIcon name="i-heroicons-plus" style="width: 14px; height: 14px;" />
|
|
New event
|
|
</button>
|
|
<!-- Google Calendar link -->
|
|
<button type="button" class="cal-action-btn" @click="openGoogleCalendar">
|
|
<UIcon name="i-heroicons-arrow-top-right-on-square" style="width: 13px; height: 13px;" />
|
|
Google Calendar
|
|
</button>
|
|
<!-- View toggle -->
|
|
<div class="cal-toggle">
|
|
<button type="button" class="cal-toggle-btn" :class="calView === 'month' ? 'cal-toggle-on' : 'cal-toggle-off'" @click="calView = 'month'">Month</button>
|
|
<button type="button" class="cal-toggle-btn" :class="calView === 'week' ? 'cal-toggle-on' : 'cal-toggle-off'" @click="calView = 'week'">Week</button>
|
|
<button type="button" class="cal-toggle-btn" :class="calView === 'day' ? 'cal-toggle-on' : 'cal-toggle-off'" @click="calView = 'day'">Day</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Layer strip + integration badges -->
|
|
<div class="cal-layers-row">
|
|
<!-- Active layer pills -->
|
|
<div class="flex items-center gap-2 flex-wrap flex-1 min-w-0">
|
|
<TransitionGroup
|
|
enter-active-class="transition-all duration-200 ease-out"
|
|
enter-from-class="opacity-0 scale-90"
|
|
enter-to-class="opacity-100 scale-100"
|
|
leave-active-class="transition-all duration-150 ease-in"
|
|
leave-from-class="opacity-100 scale-100"
|
|
leave-to-class="opacity-0 scale-90"
|
|
>
|
|
<button
|
|
v-for="layer in LAYER_ORDER"
|
|
v-show="activeLayers[layer]"
|
|
:key="layer"
|
|
type="button"
|
|
class="cal-layer-pill"
|
|
:style="`--lc: ${layerMeta[layer].color}`"
|
|
@click="toggleLayer(layer)"
|
|
:title="`Click to hide ${layerMeta[layer].label}`"
|
|
>
|
|
<span class="cal-layer-dot" :style="`background: ${layerMeta[layer].color}`" />
|
|
{{ layerMeta[layer].label }}
|
|
<UIcon name="i-heroicons-x-mark" style="width: 10px; height: 10px; opacity: 0.5;" />
|
|
</button>
|
|
</TransitionGroup>
|
|
<span v-if="activeLayerCount === 0" class="text-[12px] text-[var(--text-muted)] italic">No layers active — click Layers to add some</span>
|
|
</div>
|
|
|
|
<!-- Layers config button -->
|
|
<div class="cal-layers-wrap relative">
|
|
<button type="button" class="cal-ctrl-btn" @click.stop="layerPanelOpen = !layerPanelOpen">
|
|
<UIcon name="i-heroicons-adjustments-horizontal" style="width: 14px; height: 14px;" />
|
|
Layers
|
|
<span v-if="activeLayerCount < 6" class="cal-ctrl-count">{{ activeLayerCount }}/6</span>
|
|
</button>
|
|
<!-- Layer config dropdown -->
|
|
<Transition
|
|
enter-active-class="transition duration-150 ease-out"
|
|
enter-from-class="opacity-0 scale-95 -translate-y-1"
|
|
enter-to-class="opacity-100 scale-100 translate-y-0"
|
|
leave-active-class="transition duration-100 ease-in"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0 scale-95"
|
|
>
|
|
<div v-if="layerPanelOpen" class="cal-layer-dropdown">
|
|
<div class="cal-layer-dropdown-head">
|
|
<span>Calendar layers</span>
|
|
<div class="flex gap-1">
|
|
<button type="button" class="cal-layer-action" @click="enableAllLayers">All on</button>
|
|
<button type="button" class="cal-layer-action" @click="disableAllLayers">All off</button>
|
|
</div>
|
|
</div>
|
|
<p class="cal-layer-dropdown-hint">Toggle information layers on/off to customize your view.</p>
|
|
<ul class="cal-layer-list">
|
|
<li
|
|
v-for="layer in LAYER_ORDER"
|
|
:key="layer"
|
|
class="cal-layer-item"
|
|
:class="activeLayers[layer] ? 'cal-layer-item-on' : ''"
|
|
@click="toggleLayer(layer)"
|
|
>
|
|
<div class="cal-layer-check" :style="activeLayers[layer] ? `background: ${layerMeta[layer].color}; border-color: ${layerMeta[layer].color}` : ''">
|
|
<UIcon v-if="activeLayers[layer]" name="i-heroicons-check" style="width: 11px; height: 11px; color: #fff;" />
|
|
</div>
|
|
<UIcon :name="layerMeta[layer].icon" style="width: 14px; height: 14px;" :style="`color: ${layerMeta[layer].color}; opacity: 0.7;`" />
|
|
<span class="text-[12px] font-medium text-[var(--text-primary)]">{{ layerMeta[layer].label }}</span>
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
|
|
<!-- Integration badges -->
|
|
<div class="flex items-center gap-1.5 ml-1">
|
|
<span class="cal-badge cal-badge-gmail"><UIcon name="i-heroicons-envelope" style="width: 11px; height: 11px;" /> Gmail</span>
|
|
<span class="cal-badge cal-badge-system"><UIcon name="i-heroicons-cog-6-tooth" style="width: 11px; height: 11px;" /> System</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Legend -->
|
|
<div class="cal-legend">
|
|
<div class="flex items-center justify-between">
|
|
<p class="text-[11px] font-semibold uppercase tracking-[0.04em] text-[#8a8a86]">Event types</p>
|
|
<p class="text-[10px] text-[var(--text-muted)]">Double-click a day to add an event</p>
|
|
</div>
|
|
<div class="flex flex-wrap gap-3 mt-2">
|
|
<div v-for="(meta, type) in typeMeta" :key="type" class="flex items-center gap-1.5">
|
|
<span class="cal-dot" :style="`background: ${meta.color}`" />
|
|
<span class="text-[12px] text-[var(--text-muted)]">{{ meta.label }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Main card -->
|
|
<div class="cal-card">
|
|
|
|
<!-- ═══ MONTH VIEW ═══ -->
|
|
<template v-if="calView === 'month'">
|
|
<!-- Month navigation -->
|
|
<div class="cal-month-nav">
|
|
<button type="button" class="cal-month-arrow" @click="prevMonth">
|
|
<UIcon name="i-heroicons-chevron-left" style="width: 16px; height: 16px;" />
|
|
</button>
|
|
<div class="flex items-center gap-2">
|
|
<h2 class="text-[14px] font-semibold text-[var(--text-primary)]">{{ monthLabel }}</h2>
|
|
<button type="button" class="cal-today-btn" @click="goToday">Today</button>
|
|
</div>
|
|
<button type="button" class="cal-month-arrow" @click="nextMonth">
|
|
<UIcon name="i-heroicons-chevron-right" style="width: 16px; height: 16px;" />
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Day-of-week headers -->
|
|
<div class="cal-month-header">
|
|
<div v-for="day in ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']" :key="day" class="cal-month-dow">
|
|
{{ day }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Calendar grid -->
|
|
<div class="cal-month-grid">
|
|
<div
|
|
v-for="(cell, ci) in monthGrid"
|
|
:key="ci"
|
|
class="cal-month-cell"
|
|
:class="{
|
|
'cal-month-cell-other': !cell.isCurrentMonth,
|
|
'cal-month-cell-today': cell.isToday,
|
|
}"
|
|
@dblclick="cell.isCurrentMonth && openAddModal(cell.date)"
|
|
>
|
|
<div class="cal-month-cell-head">
|
|
<span class="cal-month-date" :class="cell.isToday ? 'cal-month-date-today' : ''">
|
|
{{ cell.date }}
|
|
</span>
|
|
<!-- Add button on hover for current month cells -->
|
|
<button
|
|
v-if="cell.isCurrentMonth"
|
|
type="button"
|
|
class="cal-cell-add"
|
|
title="Add event"
|
|
@click.stop="openAddModal(cell.date)"
|
|
>
|
|
<UIcon name="i-heroicons-plus" style="width: 10px; height: 10px;" />
|
|
</button>
|
|
</div>
|
|
<div v-if="cell.events.length > 0" class="cal-month-events">
|
|
<div
|
|
v-for="ev in cell.events.slice(0, 3)"
|
|
:key="ev.id"
|
|
class="cal-month-ev"
|
|
:class="ev.urgent ? 'cal-month-ev-urgent' : ''"
|
|
:title="ev.title"
|
|
>
|
|
<span class="cal-month-ev-dot" :style="dotStyle(ev.type)" />
|
|
<span class="cal-month-ev-text">{{ ev.title }}</span>
|
|
<!-- Remove button for user-created events -->
|
|
<button
|
|
v-if="ev.id.startsWith('user-')"
|
|
type="button"
|
|
class="cal-ev-remove-mini"
|
|
title="Remove"
|
|
@click.stop="removeAppointment(ev.id)"
|
|
>
|
|
<UIcon name="i-heroicons-x-mark" style="width: 8px; height: 8px;" />
|
|
</button>
|
|
</div>
|
|
<p v-if="cell.events.length > 3" class="cal-month-ev-more">
|
|
+{{ cell.events.length - 3 }} more
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- ═══ WEEK VIEW ═══ -->
|
|
<template v-if="calView === 'week'">
|
|
<div class="cal-week">
|
|
<div v-for="day in weekDays" :key="day.label" class="cal-week-cell" :class="day.isToday ? 'cal-week-active' : ''">
|
|
<span class="text-[10px] uppercase tracking-wider" :class="day.isToday ? 'font-semibold text-[#01696f]' : 'text-[var(--text-muted)]'">{{ day.label }}</span>
|
|
<span class="cal-week-num" :class="day.isToday ? 'cal-week-num-today' : ''">{{ day.date }}</span>
|
|
<div v-if="day.events > 0" class="flex justify-center gap-0.5 mt-1">
|
|
<span v-for="n in Math.min(day.events, 4)" :key="n" class="cal-week-pip" :class="day.isToday ? 'bg-[#01696f]' : 'bg-[rgba(0,0,0,0.12)]'" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="cal-events">
|
|
<div
|
|
v-for="event in filteredEvents"
|
|
:key="event.id"
|
|
class="cal-ev"
|
|
:class="event.urgent ? 'cal-ev-urgent' : ''"
|
|
>
|
|
<div class="cal-ev-time">{{ event.time }}</div>
|
|
<div class="cal-ev-dot" :style="dotStyle(event.type)" />
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<p class="text-[13px] font-medium text-[var(--text-primary)]">{{ event.title }}</p>
|
|
<span v-if="event.urgent" class="cal-ev-urgent-tag">Urgent</span>
|
|
<span v-if="event.id.startsWith('user-')" class="cal-ev-user-tag">You</span>
|
|
</div>
|
|
<p v-if="event.detail" class="mt-0.5 text-[12px] leading-snug text-[var(--text-muted)]">{{ event.detail }}</p>
|
|
</div>
|
|
<button
|
|
v-if="event.id.startsWith('user-')"
|
|
type="button"
|
|
class="cal-ev-remove"
|
|
title="Remove event"
|
|
@click="removeAppointment(event.id)"
|
|
>
|
|
<UIcon name="i-heroicons-trash" style="width: 14px; height: 14px;" />
|
|
</button>
|
|
<UIcon v-else :name="typeMeta[event.type].icon" style="width: 16px; height: 16px; flex-shrink: 0; opacity: 0.4;" :style="`color: ${typeMeta[event.type].color}`" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<!-- ═══ DAY VIEW ═══ -->
|
|
<template v-if="calView === 'day'">
|
|
<div class="cal-week">
|
|
<div v-for="day in weekDays" :key="day.label" class="cal-week-cell" :class="day.isToday ? 'cal-week-active' : ''">
|
|
<span class="text-[10px] uppercase tracking-wider" :class="day.isToday ? 'font-semibold text-[#01696f]' : 'text-[var(--text-muted)]'">{{ day.label }}</span>
|
|
<span class="cal-week-num" :class="day.isToday ? 'cal-week-num-today' : ''">{{ day.date }}</span>
|
|
<div v-if="day.events > 0" class="flex justify-center gap-0.5 mt-1">
|
|
<span v-for="n in Math.min(day.events, 4)" :key="n" class="cal-week-pip" :class="day.isToday ? 'bg-[#01696f]' : 'bg-[rgba(0,0,0,0.12)]'" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div v-if="filteredEvents.length === 0" class="px-6 py-12 text-center">
|
|
<p class="text-[13px] text-[var(--text-muted)]">No events match your current layers.</p>
|
|
</div>
|
|
<div v-else class="cal-events">
|
|
<div
|
|
v-for="event in filteredEvents"
|
|
:key="event.id"
|
|
class="cal-ev"
|
|
:class="event.urgent ? 'cal-ev-urgent' : ''"
|
|
>
|
|
<div class="cal-ev-time">{{ event.time }}</div>
|
|
<div class="cal-ev-dot" :style="dotStyle(event.type)" />
|
|
<div class="min-w-0 flex-1">
|
|
<div class="flex items-center gap-2 flex-wrap">
|
|
<p class="text-[13px] font-medium text-[var(--text-primary)]">{{ event.title }}</p>
|
|
<span v-if="event.urgent" class="cal-ev-urgent-tag">Urgent</span>
|
|
<span v-if="event.id.startsWith('user-')" class="cal-ev-user-tag">You</span>
|
|
</div>
|
|
<p v-if="event.detail" class="mt-0.5 text-[12px] leading-snug text-[var(--text-muted)]">{{ event.detail }}</p>
|
|
<div class="mt-1.5 flex items-center gap-2 flex-wrap">
|
|
<span class="cal-ev-src">{{ event.source === 'gmail' ? 'Gmail' : event.source === 'system' ? 'System' : 'Manual' }}</span>
|
|
<span v-if="event.customer" class="cal-ev-cust">{{ event.customer }}</span>
|
|
</div>
|
|
</div>
|
|
<button
|
|
v-if="event.id.startsWith('user-')"
|
|
type="button"
|
|
class="cal-ev-remove"
|
|
title="Remove event"
|
|
@click="removeAppointment(event.id)"
|
|
>
|
|
<UIcon name="i-heroicons-trash" style="width: 14px; height: 14px;" />
|
|
</button>
|
|
<UIcon v-else :name="typeMeta[event.type].icon" style="width: 16px; height: 16px; flex-shrink: 0; opacity: 0.4;" :style="`color: ${typeMeta[event.type].color}`" />
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- ═══ ADD EVENT MODAL ═══ -->
|
|
<Teleport to="body">
|
|
<Transition
|
|
enter-active-class="transition duration-200 ease-out"
|
|
enter-from-class="opacity-0"
|
|
enter-to-class="opacity-100"
|
|
leave-active-class="transition duration-150 ease-in"
|
|
leave-from-class="opacity-100"
|
|
leave-to-class="opacity-0"
|
|
>
|
|
<div v-if="addModalOpen" class="cal-modal-overlay" @click.self="addModalOpen = false">
|
|
<Transition
|
|
enter-active-class="transition duration-200 ease-out"
|
|
enter-from-class="opacity-0 scale-95 translate-y-2"
|
|
enter-to-class="opacity-100 scale-100 translate-y-0"
|
|
leave-active-class="transition duration-150 ease-in"
|
|
leave-from-class="opacity-100 scale-100"
|
|
leave-to-class="opacity-0 scale-95"
|
|
>
|
|
<div v-if="addModalOpen" class="cal-modal">
|
|
<!-- Title bar -->
|
|
<div class="cal-modal-head">
|
|
<div class="cal-modal-head-left">
|
|
<h3>New event</h3>
|
|
</div>
|
|
<button type="button" class="cal-modal-close" @click="addModalOpen = false">
|
|
<UIcon name="i-heroicons-x-mark" style="width: 16px; height: 16px;" />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="cal-modal-body">
|
|
<!-- Title (large input, Gmail-style) -->
|
|
<input
|
|
v-model="addForm.title"
|
|
type="text"
|
|
class="cal-title-input"
|
|
placeholder="Add title"
|
|
@keydown.enter="submitAppointment"
|
|
/>
|
|
|
|
<!-- Event type chips -->
|
|
<div class="cal-type-chips">
|
|
<button
|
|
v-for="(meta, type) in typeMeta"
|
|
:key="type"
|
|
type="button"
|
|
class="cal-type-chip"
|
|
:class="{ 'cal-type-chip--active': addForm.type === type }"
|
|
:style="addForm.type === type ? `--chip-c: ${meta.color}` : ''"
|
|
@click="addForm.type = type"
|
|
>
|
|
<UIcon :name="meta.icon" style="width: 13px; height: 13px;" />
|
|
{{ meta.label }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Date & time row -->
|
|
<div class="cal-row cal-row-icon">
|
|
<UIcon name="i-heroicons-clock" class="cal-row-icon-el" />
|
|
<div class="cal-row-content">
|
|
<div class="cal-datetime-row">
|
|
<input v-model="addForm.date" type="date" class="cal-input cal-input-date" />
|
|
<template v-if="!addForm.allDay">
|
|
<input v-model="addForm.startTime" type="time" class="cal-input cal-input-time" />
|
|
<span class="cal-time-sep">—</span>
|
|
<input v-model="addForm.endTime" type="time" class="cal-input cal-input-time" />
|
|
</template>
|
|
</div>
|
|
<label class="cal-checkbox-label">
|
|
<input v-model="addForm.allDay" type="checkbox" class="cal-checkbox" />
|
|
All day
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Repeat -->
|
|
<div class="cal-row cal-row-icon">
|
|
<UIcon name="i-heroicons-arrow-path" class="cal-row-icon-el" />
|
|
<select v-model="addForm.repeat" class="cal-select cal-select-full">
|
|
<option value="none">Does not repeat</option>
|
|
<option value="daily">Daily</option>
|
|
<option value="weekly">Weekly</option>
|
|
<option value="monthly">Monthly</option>
|
|
<option value="yearly">Annually</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Guests -->
|
|
<div class="cal-row cal-row-icon">
|
|
<UIcon name="i-heroicons-user-plus" class="cal-row-icon-el" />
|
|
<input v-model="addForm.guests" type="text" class="cal-input cal-input-full" placeholder="Add guests (emails, comma separated)" />
|
|
</div>
|
|
|
|
<!-- Video call toggle -->
|
|
<div class="cal-row cal-row-icon">
|
|
<UIcon name="i-heroicons-video-camera" class="cal-row-icon-el" />
|
|
<button
|
|
type="button"
|
|
class="cal-video-btn"
|
|
:class="{ 'cal-video-btn--active': addForm.videoCall }"
|
|
@click="addForm.videoCall = !addForm.videoCall"
|
|
>
|
|
<UIcon :name="addForm.videoCall ? 'i-heroicons-check-circle' : 'i-heroicons-plus-circle'" style="width: 15px; height: 15px;" />
|
|
{{ addForm.videoCall ? 'Google Meet link added' : 'Add Google Meet video conferencing' }}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Location -->
|
|
<div class="cal-row cal-row-icon">
|
|
<UIcon name="i-heroicons-map-pin" class="cal-row-icon-el" />
|
|
<input v-model="addForm.location" type="text" class="cal-input cal-input-full" placeholder="Add location" />
|
|
</div>
|
|
|
|
<!-- Notification -->
|
|
<div class="cal-row cal-row-icon">
|
|
<UIcon name="i-heroicons-bell" class="cal-row-icon-el" />
|
|
<select v-model="addForm.notification" class="cal-select cal-select-full">
|
|
<option value="0">At time of event</option>
|
|
<option value="5">5 minutes before</option>
|
|
<option value="10">10 minutes before</option>
|
|
<option value="15">15 minutes before</option>
|
|
<option value="30">30 minutes before</option>
|
|
<option value="60">1 hour before</option>
|
|
<option value="1440">1 day before</option>
|
|
</select>
|
|
</div>
|
|
|
|
<!-- Customer (insurance-specific) -->
|
|
<div class="cal-row cal-row-icon">
|
|
<UIcon name="i-heroicons-building-office" class="cal-row-icon-el" />
|
|
<input v-model="addForm.customer" type="text" class="cal-input cal-input-full" placeholder="Link to customer or policy (optional)" />
|
|
</div>
|
|
|
|
<!-- Color -->
|
|
<div class="cal-row cal-row-icon">
|
|
<UIcon name="i-heroicons-swatch" class="cal-row-icon-el" />
|
|
<div class="cal-color-row">
|
|
<button
|
|
v-for="c in eventColors"
|
|
:key="c.value"
|
|
type="button"
|
|
class="cal-color-dot"
|
|
:class="{ 'cal-color-dot--active': addForm.color === c.value }"
|
|
:style="`background: ${c.value}`"
|
|
:title="c.label"
|
|
@click="addForm.color = addForm.color === c.value ? '' : c.value"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Description -->
|
|
<div class="cal-row cal-row-icon cal-row-icon-top">
|
|
<UIcon name="i-heroicons-bars-3-bottom-left" class="cal-row-icon-el" />
|
|
<textarea v-model="addForm.detail" class="cal-textarea cal-textarea-full" rows="3" placeholder="Add description or notes" />
|
|
</div>
|
|
</div>
|
|
|
|
<div class="cal-modal-foot">
|
|
<div class="cal-gcal-hint">
|
|
<UIcon name="i-heroicons-arrow-top-right-on-square" style="width: 12px; height: 12px;" />
|
|
<span>Sync with <button type="button" class="cal-gcal-link" @click="openGoogleCalendar">Google Calendar</button></span>
|
|
</div>
|
|
<div class="cal-modal-foot-actions">
|
|
<button type="button" class="cal-btn-cancel" @click="addModalOpen = false">Cancel</button>
|
|
<button
|
|
type="button"
|
|
class="cal-btn-submit"
|
|
:disabled="!addForm.title.trim()"
|
|
@click="submitAppointment"
|
|
>
|
|
Save
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
</Transition>
|
|
</Teleport>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.cal-page {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
padding-bottom: 3rem;
|
|
}
|
|
|
|
/* ── Action buttons ── */
|
|
.cal-action-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 6px 12px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
background: #fff;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cal-action-btn:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
|
|
.cal-action-primary {
|
|
background: #01696f;
|
|
color: #fff;
|
|
border-color: #01696f;
|
|
}
|
|
.cal-action-primary:hover {
|
|
background: #015a5f;
|
|
color: #fff;
|
|
border-color: #015a5f;
|
|
}
|
|
|
|
/* ── Controls ── */
|
|
.cal-ctrl-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 5px 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
background: transparent;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cal-ctrl-btn:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
|
|
.cal-ctrl-count {
|
|
font-size: 10px; font-weight: 600; padding: 0 5px;
|
|
border-radius: 9999px; background: rgba(1,105,111,0.1); color: #01696f;
|
|
}
|
|
|
|
.cal-toggle {
|
|
display: inline-flex; gap: 1px; padding: 2px; border-radius: 8px; background: rgba(0,0,0,0.04);
|
|
}
|
|
.cal-toggle-btn {
|
|
padding: 4px 10px; border-radius: 6px; font-size: 12px; font-weight: 500;
|
|
border: none; cursor: pointer; transition: all 150ms ease;
|
|
}
|
|
.cal-toggle-on { background: #fff; color: var(--text-primary); box-shadow: 0 1px 2px rgba(0,0,0,0.06); }
|
|
.cal-toggle-off { background: transparent; color: var(--text-muted); }
|
|
|
|
/* ── Layers row ── */
|
|
.cal-layers-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cal-layer-pill {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 3px 10px 3px 8px;
|
|
border-radius: 9999px;
|
|
border: 1px solid color-mix(in srgb, var(--lc) 20%, transparent);
|
|
background: color-mix(in srgb, var(--lc) 5%, transparent);
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
color: var(--lc);
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
white-space: nowrap;
|
|
}
|
|
.cal-layer-pill:hover {
|
|
background: color-mix(in srgb, var(--lc) 10%, transparent);
|
|
border-color: color-mix(in srgb, var(--lc) 30%, transparent);
|
|
}
|
|
.cal-layer-dot {
|
|
width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0;
|
|
}
|
|
|
|
/* ── Layer dropdown ── */
|
|
.cal-layer-dropdown {
|
|
position: absolute;
|
|
right: 0;
|
|
top: calc(100% + 6px);
|
|
z-index: 30;
|
|
width: 260px;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
background: #fff;
|
|
box-shadow: 0 8px 30px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06);
|
|
overflow: hidden;
|
|
}
|
|
.cal-layer-dropdown-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 12px 14px 0;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
.cal-layer-action {
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
color: #01696f;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
padding: 2px 6px;
|
|
border-radius: 4px;
|
|
transition: background 150ms ease;
|
|
}
|
|
.cal-layer-action:hover { background: rgba(1,105,111,0.06); }
|
|
.cal-layer-dropdown-hint {
|
|
padding: 4px 14px 8px;
|
|
font-size: 11px;
|
|
color: #8a8a86;
|
|
}
|
|
.cal-layer-list {
|
|
list-style: none;
|
|
margin: 0;
|
|
padding: 0;
|
|
}
|
|
.cal-layer-item {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
padding: 9px 14px;
|
|
cursor: pointer;
|
|
transition: background 100ms ease;
|
|
border-top: 1px solid rgba(0,0,0,0.03);
|
|
}
|
|
.cal-layer-item:hover { background: rgba(0,0,0,0.02); }
|
|
.cal-layer-item-on { background: rgba(1,105,111,0.02); }
|
|
.cal-layer-item-on:hover { background: rgba(1,105,111,0.04); }
|
|
.cal-layer-check {
|
|
width: 16px; height: 16px;
|
|
border-radius: 4px;
|
|
border: 1.5px solid rgba(0,0,0,0.15);
|
|
display: flex; align-items: center; justify-content: center;
|
|
flex-shrink: 0;
|
|
transition: all 150ms ease;
|
|
}
|
|
|
|
/* ── Badges ── */
|
|
.cal-badge {
|
|
display: inline-flex; align-items: center; gap: 3px;
|
|
padding: 2px 7px; border-radius: 6px; font-size: 10px; font-weight: 500;
|
|
}
|
|
.cal-badge-gmail { background: rgba(219,68,55,0.06); color: #b33a2e; }
|
|
.cal-badge-system { background: rgba(1,105,111,0.06); color: #01696f; }
|
|
|
|
/* ── Card ── */
|
|
.cal-card {
|
|
border-radius: 12px; border: 1px solid rgba(0,0,0,0.06);
|
|
background: #fff; box-shadow: 0 1px 3px rgba(0,0,0,0.03); overflow: hidden;
|
|
}
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
MONTH VIEW
|
|
══════════════════════════════════════════════════════ */
|
|
.cal-month-nav {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 14px 20px;
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.cal-month-arrow {
|
|
display: flex; align-items: center; justify-content: center;
|
|
width: 28px; height: 28px; border-radius: 6px;
|
|
border: none; background: transparent; color: #8a8a86;
|
|
cursor: pointer; transition: all 150ms ease;
|
|
}
|
|
.cal-month-arrow:hover { background: rgba(0,0,0,0.04); color: var(--text-primary); }
|
|
.cal-today-btn {
|
|
font-size: 11px; font-weight: 500; color: #01696f;
|
|
padding: 2px 8px; border-radius: 4px; border: none;
|
|
background: rgba(1,105,111,0.06); cursor: pointer;
|
|
transition: background 150ms ease;
|
|
}
|
|
.cal-today-btn:hover { background: rgba(1,105,111,0.1); }
|
|
|
|
.cal-month-header {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.cal-month-dow {
|
|
padding: 8px 0;
|
|
text-align: center;
|
|
font-size: 10px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.06em;
|
|
color: #8a8a86;
|
|
}
|
|
|
|
.cal-month-grid {
|
|
display: grid;
|
|
grid-template-columns: repeat(7, 1fr);
|
|
}
|
|
.cal-month-cell {
|
|
min-height: 90px;
|
|
padding: 6px 8px;
|
|
border-right: 1px solid rgba(0,0,0,0.04);
|
|
border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
transition: background 100ms ease;
|
|
position: relative;
|
|
overflow: hidden;
|
|
}
|
|
.cal-month-cell:nth-child(7n) { border-right: none; }
|
|
.cal-month-cell:hover { background: rgba(0,0,0,0.01); }
|
|
.cal-month-cell-other { opacity: 0.35; }
|
|
.cal-month-cell-today { background: rgba(1,105,111,0.02); }
|
|
.cal-month-cell-today:hover { background: rgba(1,105,111,0.04); }
|
|
|
|
.cal-month-cell-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
}
|
|
.cal-cell-add {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 16px; height: 16px;
|
|
border-radius: 4px;
|
|
border: none;
|
|
background: transparent;
|
|
color: #8a8a86;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cal-month-cell:hover .cal-cell-add { opacity: 1; }
|
|
.cal-cell-add:hover { background: rgba(1,105,111,0.1); color: #01696f; }
|
|
|
|
.cal-month-date {
|
|
display: inline-flex; align-items: center; justify-content: center;
|
|
width: 24px; height: 24px; border-radius: 50%;
|
|
font-size: 12px; font-weight: 500; color: var(--text-primary);
|
|
}
|
|
.cal-month-date-today {
|
|
background: #01696f; color: #ffffff; font-weight: 600;
|
|
}
|
|
|
|
.cal-month-events {
|
|
display: flex; flex-direction: column; gap: 2px; margin-top: 4px;
|
|
}
|
|
.cal-month-ev {
|
|
display: flex; align-items: center; gap: 4px;
|
|
padding: 2px 4px; border-radius: 4px;
|
|
transition: background 100ms ease;
|
|
cursor: default;
|
|
}
|
|
.cal-month-ev:hover { background: rgba(0,0,0,0.03); }
|
|
.cal-month-ev-urgent { background: rgba(193,56,56,0.03); }
|
|
.cal-month-ev-dot {
|
|
width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0;
|
|
}
|
|
.cal-month-ev-text {
|
|
font-size: 10px; font-weight: 500; color: var(--text-primary);
|
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
min-width: 0; flex: 1;
|
|
}
|
|
.cal-month-ev-more {
|
|
font-size: 9px; font-weight: 600; color: #01696f;
|
|
padding: 1px 4px;
|
|
}
|
|
.cal-ev-remove-mini {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 14px; height: 14px;
|
|
border-radius: 3px;
|
|
border: none;
|
|
background: transparent;
|
|
color: #c13838;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
flex-shrink: 0;
|
|
transition: all 120ms ease;
|
|
}
|
|
.cal-month-ev:hover .cal-ev-remove-mini { opacity: 0.6; }
|
|
.cal-ev-remove-mini:hover { opacity: 1 !important; background: rgba(193,56,56,0.08); }
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
WEEK STRIP
|
|
══════════════════════════════════════════════════════ */
|
|
.cal-week {
|
|
display: grid; grid-template-columns: repeat(7, 1fr);
|
|
border-bottom: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.cal-week-cell {
|
|
display: flex; flex-direction: column; align-items: center;
|
|
padding: 12px 4px 10px; transition: background 100ms ease;
|
|
}
|
|
.cal-week-active { background: rgba(1,105,111,0.03); }
|
|
.cal-week-num {
|
|
font-size: 16px; font-weight: 600; margin-top: 2px; color: var(--text-primary);
|
|
width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border-radius: 50%;
|
|
}
|
|
.cal-week-num-today { background: #01696f; color: #fff; }
|
|
.cal-week-pip { width: 4px; height: 4px; border-radius: 50%; }
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
DAY EVENT LIST
|
|
══════════════════════════════════════════════════════ */
|
|
.cal-events { display: flex; flex-direction: column; }
|
|
.cal-ev {
|
|
display: flex; align-items: flex-start; gap: 10px;
|
|
padding: 16px 20px; border-bottom: 1px solid rgba(0,0,0,0.04);
|
|
transition: background 100ms ease;
|
|
}
|
|
.cal-ev:last-child { border-bottom: none; }
|
|
.cal-ev:hover { background: rgba(0,0,0,0.015); }
|
|
.cal-ev-urgent { background: rgba(193,56,56,0.02); }
|
|
.cal-ev-urgent:hover { background: rgba(193,56,56,0.04); }
|
|
|
|
.cal-ev-time {
|
|
width: 68px; flex-shrink: 0; font-size: 12px; font-weight: 500;
|
|
color: var(--text-muted); padding-top: 1px; font-variant-numeric: tabular-nums;
|
|
}
|
|
.cal-ev-dot {
|
|
width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; margin-top: 5px;
|
|
}
|
|
.cal-ev-urgent-tag {
|
|
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px;
|
|
background: rgba(193,56,56,0.08); color: #c13838; flex-shrink: 0;
|
|
}
|
|
.cal-ev-user-tag {
|
|
font-size: 10px; font-weight: 600; padding: 1px 6px; border-radius: 9999px;
|
|
background: rgba(1,105,111,0.08); color: #01696f; flex-shrink: 0;
|
|
}
|
|
.cal-ev-src {
|
|
font-size: 10px; font-weight: 500; padding: 1px 5px; border-radius: 4px;
|
|
background: rgba(0,0,0,0.04); color: var(--text-muted);
|
|
}
|
|
.cal-ev-cust {
|
|
font-size: 10px; font-weight: 500; padding: 1px 5px; border-radius: 4px;
|
|
background: rgba(1,105,111,0.06); color: #01696f;
|
|
}
|
|
.cal-ev-remove {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px; height: 28px;
|
|
border-radius: 6px;
|
|
border: none;
|
|
background: transparent;
|
|
color: #c13838;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
flex-shrink: 0;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cal-ev:hover .cal-ev-remove { opacity: 0.5; }
|
|
.cal-ev-remove:hover { opacity: 1 !important; background: rgba(193,56,56,0.06); }
|
|
|
|
/* ── Legend ── */
|
|
.cal-legend {
|
|
padding: 16px 20px; border-radius: 10px;
|
|
border: 1px solid rgba(0,0,0,0.06); background: var(--surface);
|
|
}
|
|
.cal-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
|
|
|
|
/* ══════════════════════════════════════════════════════
|
|
ADD EVENT MODAL
|
|
══════════════════════════════════════════════════════ */
|
|
.cal-modal-overlay {
|
|
position: fixed;
|
|
inset: 0;
|
|
z-index: 100;
|
|
background: rgba(0,0,0,0.25);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 20px;
|
|
backdrop-filter: blur(2px);
|
|
}
|
|
.cal-modal {
|
|
width: 100%;
|
|
max-width: 560px;
|
|
border-radius: 12px;
|
|
background: #fff;
|
|
box-shadow: 0 20px 60px rgba(0,0,0,0.15), 0 4px 16px rgba(0,0,0,0.08);
|
|
overflow: hidden;
|
|
max-height: calc(100vh - 40px);
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.cal-modal-head {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 18px 20px 0;
|
|
flex-shrink: 0;
|
|
}
|
|
.cal-modal-head h3 {
|
|
font-size: 16px;
|
|
font-weight: 600;
|
|
color: var(--text-primary);
|
|
}
|
|
.cal-modal-close {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
width: 28px; height: 28px;
|
|
border-radius: 6px;
|
|
border: none;
|
|
background: transparent;
|
|
color: #8a8a86;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cal-modal-close:hover { background: rgba(0,0,0,0.05); color: var(--text-primary); }
|
|
|
|
.cal-modal-body {
|
|
padding: 16px 20px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 12px;
|
|
overflow-y: auto;
|
|
flex: 1;
|
|
min-height: 0;
|
|
}
|
|
|
|
/* ── Gmail-style title input ── */
|
|
.cal-title-input {
|
|
width: 100%;
|
|
border: none;
|
|
outline: none;
|
|
font-size: 20px;
|
|
font-weight: 500;
|
|
color: var(--text-primary);
|
|
padding: 4px 0 8px;
|
|
border-bottom: 2px solid rgba(0,0,0,0.06);
|
|
background: transparent;
|
|
transition: border-color 200ms ease;
|
|
}
|
|
.cal-title-input::placeholder { color: #b0b0ac; font-weight: 400; }
|
|
.cal-title-input:focus { border-bottom-color: #01696f; }
|
|
|
|
/* ── Event type chips ── */
|
|
.cal-type-chips {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
}
|
|
.cal-type-chip {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 5px;
|
|
padding: 5px 11px;
|
|
border-radius: 9999px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
background: #fff;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cal-type-chip:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
|
|
.cal-type-chip--active {
|
|
background: color-mix(in srgb, var(--chip-c, #01696f) 8%, transparent);
|
|
border-color: color-mix(in srgb, var(--chip-c, #01696f) 25%, transparent);
|
|
color: var(--chip-c, #01696f);
|
|
}
|
|
|
|
/* ── Form rows with icon ── */
|
|
.cal-row { display: flex; align-items: center; gap: 10px; }
|
|
.cal-row-icon { display: flex; align-items: center; gap: 10px; }
|
|
.cal-row-icon-el {
|
|
width: 18px; height: 18px;
|
|
color: #8a8a86;
|
|
flex-shrink: 0;
|
|
}
|
|
.cal-row-icon-top { align-items: flex-start; }
|
|
.cal-row-icon-top .cal-row-icon-el { margin-top: 8px; }
|
|
.cal-row-content {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 6px;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
|
|
/* ── Date & time row ── */
|
|
.cal-datetime-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-wrap: wrap;
|
|
}
|
|
.cal-input-date { min-width: 140px; flex: 1; }
|
|
.cal-input-time { width: 110px; }
|
|
.cal-time-sep {
|
|
font-size: 13px;
|
|
color: #8a8a86;
|
|
flex-shrink: 0;
|
|
}
|
|
.cal-input-full { flex: 1; min-width: 0; }
|
|
.cal-select-full { flex: 1; min-width: 0; }
|
|
.cal-textarea-full { flex: 1; min-width: 0; }
|
|
|
|
/* ── Checkbox ── */
|
|
.cal-checkbox-label {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
}
|
|
.cal-checkbox {
|
|
width: 14px; height: 14px;
|
|
border-radius: 3px;
|
|
accent-color: #01696f;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* ── Video call button ── */
|
|
.cal-video-btn {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 7px 12px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
background: #fff;
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
flex: 1;
|
|
}
|
|
.cal-video-btn:hover { border-color: rgba(0,0,0,0.15); }
|
|
.cal-video-btn--active {
|
|
background: rgba(1,105,111,0.04);
|
|
border-color: rgba(1,105,111,0.2);
|
|
color: #01696f;
|
|
}
|
|
|
|
/* ── Color dots ── */
|
|
.cal-color-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.cal-color-dot {
|
|
width: 20px; height: 20px;
|
|
border-radius: 50%;
|
|
border: 2px solid transparent;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
position: relative;
|
|
}
|
|
.cal-color-dot:hover { transform: scale(1.15); }
|
|
.cal-color-dot--active {
|
|
border-color: var(--text-primary);
|
|
box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px currentColor;
|
|
}
|
|
|
|
/* ── Modal footer ── */
|
|
.cal-modal-foot-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
|
|
.cal-field {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 4px;
|
|
}
|
|
.cal-label {
|
|
font-size: 11px;
|
|
font-weight: 600;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.04em;
|
|
color: #8a8a86;
|
|
}
|
|
.cal-input {
|
|
padding: 7px 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
background: #fff;
|
|
outline: none;
|
|
transition: border-color 150ms ease;
|
|
}
|
|
.cal-input:focus { border-color: #01696f; }
|
|
.cal-select {
|
|
padding: 7px 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
background: #fff;
|
|
outline: none;
|
|
transition: border-color 150ms ease;
|
|
appearance: none;
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6' fill='none'%3E%3Cpath d='M1 1l4 4 4-4' stroke='%238a8a86' stroke-width='1.2' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
background-repeat: no-repeat;
|
|
background-position: right 10px center;
|
|
padding-right: 28px;
|
|
}
|
|
.cal-select:focus { border-color: #01696f; }
|
|
.cal-textarea {
|
|
padding: 7px 10px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.1);
|
|
font-size: 13px;
|
|
color: var(--text-primary);
|
|
background: #fff;
|
|
outline: none;
|
|
resize: vertical;
|
|
font-family: inherit;
|
|
transition: border-color 150ms ease;
|
|
}
|
|
.cal-textarea:focus { border-color: #01696f; }
|
|
|
|
.cal-gcal-hint {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
padding: 8px 10px;
|
|
border-radius: 8px;
|
|
background: rgba(0,0,0,0.02);
|
|
font-size: 11px;
|
|
color: #8a8a86;
|
|
}
|
|
.cal-gcal-link {
|
|
color: #01696f;
|
|
font-weight: 500;
|
|
background: none;
|
|
border: none;
|
|
cursor: pointer;
|
|
text-decoration: underline;
|
|
text-decoration-color: rgba(1,105,111,0.3);
|
|
text-underline-offset: 2px;
|
|
transition: color 150ms ease;
|
|
}
|
|
.cal-gcal-link:hover { color: #015a5f; }
|
|
|
|
.cal-modal-foot {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 8px;
|
|
padding: 12px 20px 16px;
|
|
border-top: 1px solid rgba(0,0,0,0.06);
|
|
}
|
|
.cal-btn-cancel {
|
|
padding: 7px 14px;
|
|
border-radius: 8px;
|
|
border: 1px solid rgba(0,0,0,0.08);
|
|
background: #fff;
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cal-btn-cancel:hover { border-color: rgba(0,0,0,0.15); color: var(--text-primary); }
|
|
.cal-btn-submit {
|
|
padding: 7px 16px;
|
|
border-radius: 8px;
|
|
border: none;
|
|
background: #01696f;
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #fff;
|
|
cursor: pointer;
|
|
transition: all 150ms ease;
|
|
}
|
|
.cal-btn-submit:hover { background: #015a5f; }
|
|
.cal-btn-submit:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
/* ── Responsive ── */
|
|
@media (max-width: 640px) {
|
|
.cal-month-cell { min-height: 60px; padding: 4px; }
|
|
.cal-month-ev-text { font-size: 9px; }
|
|
.cal-month-date { width: 20px; height: 20px; font-size: 11px; }
|
|
.cal-layers-row { flex-direction: column; align-items: flex-start; }
|
|
}
|
|
</style>
|