1004 lines
30 KiB
Vue
1004 lines
30 KiB
Vue
<template>
|
|
<div class="p-4 md:p-6">
|
|
<!-- Header -->
|
|
<div class="mb-4 flex items-center justify-between">
|
|
<div class="min-w-0">
|
|
<h1 class="text-xl md:text-2xl font-semibold text-[var(--text-color)] leading-tight">
|
|
Adicionar Atividade
|
|
</h1>
|
|
<p class="text-sm text-[var(--text-color-secondary)]">
|
|
Cadastre um compromisso e visualize como ele ficará na agenda.
|
|
</p>
|
|
</div>
|
|
|
|
<Button
|
|
icon="pi pi-times"
|
|
severity="secondary"
|
|
text
|
|
rounded
|
|
aria-label="Fechar"
|
|
@click="onClose"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Main surface -->
|
|
<div class="activity-shell">
|
|
<!-- LEFT -->
|
|
<div class="left-pane">
|
|
<Card class="h-full">
|
|
<template #content>
|
|
<!-- Tabs fake (somente “Atividade”, como no layout) -->
|
|
<div class="fake-tabs">
|
|
<div class="fake-tab is-active">Atividade</div>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<div class="field">
|
|
<label class="field-label">Título:</label>
|
|
|
|
<!-- Tipo de atividade -->
|
|
<div class="activity-types">
|
|
<div
|
|
v-for="t in activityTypes"
|
|
:key="t.value"
|
|
class="activity-type"
|
|
:class="{ 'is-selected': form.type === t.value }"
|
|
@click="form.type = t.value"
|
|
role="button"
|
|
tabindex="0"
|
|
>
|
|
<RadioButton
|
|
:inputId="`type-${t.value}`"
|
|
name="activityType"
|
|
:value="t.value"
|
|
v-model="form.type"
|
|
/>
|
|
<label :for="`type-${t.value}`" class="activity-type__label">
|
|
<span class="dot" :style="{ background: t.dot }" />
|
|
<span class="name">{{ t.label }}</span>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Título custom -->
|
|
<div class="mt-3">
|
|
<FloatLabel>
|
|
<InputText
|
|
id="customTitle"
|
|
v-model="form.customTitle"
|
|
class="w-full"
|
|
autocomplete="off"
|
|
/>
|
|
<label for="customTitle">Informe o título se necessário</label>
|
|
</FloatLabel>
|
|
<small class="hint">
|
|
Se vazio, usaremos o título do tipo selecionado.
|
|
</small>
|
|
</div>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
<!-- Paciente -->
|
|
<div class="field">
|
|
<div class="flex items-center justify-between gap-3">
|
|
<label class="field-label m-0">Paciente:</label>
|
|
<Button
|
|
icon="pi pi-plus"
|
|
label="Adicionar paciente"
|
|
severity="secondary"
|
|
text
|
|
@click="onAddPatient"
|
|
/>
|
|
</div>
|
|
|
|
<div class="patient-row mt-2">
|
|
<Avatar
|
|
:image="selectedPatient?.avatar"
|
|
shape="circle"
|
|
class="patient-avatar"
|
|
/>
|
|
<div class="min-w-0">
|
|
<div class="flex items-center gap-2 min-w-0">
|
|
<div class="patient-name truncate">
|
|
{{ selectedPatient?.name || 'Selecione um paciente' }}
|
|
</div>
|
|
<Tag
|
|
v-if="selectedPatient?.recorrente"
|
|
value="RECORRENTE"
|
|
severity="danger"
|
|
class="patient-tag"
|
|
/>
|
|
</div>
|
|
<div class="patient-meta">
|
|
<span class="pi pi-phone" />
|
|
<span>{{ selectedPatient?.phone || '—' }}</span>
|
|
<span class="sep">•</span>
|
|
<span class="pi pi-envelope" />
|
|
<span class="truncate">{{ selectedPatient?.email || '—' }}</span>
|
|
</div>
|
|
<div class="patient-meta">
|
|
<span class="pi pi-map-marker" />
|
|
<span class="truncate">{{ selectedPatient?.address || '—' }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="min-w-[240px] hidden md:block">
|
|
<!-- Select (PrimeVue v4) -->
|
|
<FloatLabel>
|
|
<Select
|
|
inputId="patientSelect"
|
|
v-model="form.patientId"
|
|
:options="patients"
|
|
optionLabel="name"
|
|
optionValue="id"
|
|
filter
|
|
class="w-full"
|
|
/>
|
|
<label for="patientSelect">Selecionar paciente</label>
|
|
</FloatLabel>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Select mobile -->
|
|
<div class="mt-3 md:hidden">
|
|
<FloatLabel>
|
|
<Select
|
|
inputId="patientSelectMobile"
|
|
v-model="form.patientId"
|
|
:options="patients"
|
|
optionLabel="name"
|
|
optionValue="id"
|
|
filter
|
|
class="w-full"
|
|
/>
|
|
<label for="patientSelectMobile">Selecionar paciente</label>
|
|
</FloatLabel>
|
|
</div>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
<!-- Data e horário -->
|
|
<div class="field">
|
|
<div class="field-label">Data e Horário <span class="muted">(Fuso: Brasil - São Paulo, +3H)</span></div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mt-2">
|
|
<FloatLabel>
|
|
<Calendar
|
|
inputId="date"
|
|
v-model="form.date"
|
|
dateFormat="dd/mm/yy"
|
|
showIcon
|
|
class="w-full"
|
|
/>
|
|
<label for="date">Data</label>
|
|
</FloatLabel>
|
|
|
|
<FloatLabel>
|
|
<Select
|
|
inputId="startTime"
|
|
v-model="form.startTime"
|
|
:options="timeOptions"
|
|
class="w-full"
|
|
/>
|
|
<label for="startTime">Início</label>
|
|
</FloatLabel>
|
|
|
|
<FloatLabel>
|
|
<InputText
|
|
inputId="endTime"
|
|
:value="computedEndTime"
|
|
class="w-full"
|
|
disabled
|
|
/>
|
|
<label for="endTime">Fim</label>
|
|
</FloatLabel>
|
|
</div>
|
|
|
|
<div class="mt-3">
|
|
<div class="field-label">Selecione o horário:</div>
|
|
|
|
<div class="slots-block mt-2">
|
|
<div class="slots-row">
|
|
<div class="slot-group-title">MANHÃ</div>
|
|
<div class="slot-chips">
|
|
<Button
|
|
v-for="s in slotGroups.manha"
|
|
:key="`m-${s}`"
|
|
:label="s"
|
|
size="small"
|
|
:severity="slotSeverity(s)"
|
|
:outlined="!isSelectedSlot(s)"
|
|
class="slot-chip"
|
|
@click="selectSlot(s)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="slots-row">
|
|
<div class="slot-group-title">
|
|
TARDE <span class="muted">(intervalo: 12:00 - 13:00)</span>
|
|
</div>
|
|
<div class="slot-chips">
|
|
<Button
|
|
v-for="s in slotGroups.tarde"
|
|
:key="`t-${s}`"
|
|
:label="s"
|
|
size="small"
|
|
:severity="slotSeverity(s)"
|
|
:outlined="!isSelectedSlot(s)"
|
|
class="slot-chip"
|
|
@click="selectSlot(s)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="slots-row">
|
|
<div class="slot-group-title">NOITE</div>
|
|
<div class="slot-chips">
|
|
<Button
|
|
v-for="s in slotGroups.noite"
|
|
:key="`n-${s}`"
|
|
:label="s"
|
|
size="small"
|
|
:severity="slotSeverity(s)"
|
|
:outlined="!isSelectedSlot(s)"
|
|
class="slot-chip"
|
|
@click="selectSlot(s)"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="legend mt-2">
|
|
<span class="legend-item">
|
|
<span class="legend-dot is-blocked" /> Slot bloqueado há paciente
|
|
</span>
|
|
<span class="legend-item">
|
|
<span class="legend-dot is-free" /> Disponível
|
|
</span>
|
|
<span class="legend-item">
|
|
<span class="legend-dot is-selected" /> Selecionado
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Divider />
|
|
|
|
<!-- Observações -->
|
|
<div class="field">
|
|
<FloatLabel>
|
|
<InputText
|
|
id="notes"
|
|
v-model="form.notes"
|
|
class="w-full"
|
|
autocomplete="off"
|
|
/>
|
|
<label for="notes">Observações (opcional)</label>
|
|
</FloatLabel>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
|
|
<!-- RIGHT -->
|
|
<div class="right-pane">
|
|
<Card class="h-full">
|
|
<template #content>
|
|
<div class="preview-top">
|
|
<div class="flex items-center gap-2">
|
|
<Tag value="Hoje" severity="secondary" />
|
|
<div class="preview-date">
|
|
{{ previewDateLabel }}
|
|
</div>
|
|
</div>
|
|
<div class="preview-badge">PRÉ-VISUALIZAÇÃO</div>
|
|
</div>
|
|
|
|
<Divider class="my-3" />
|
|
|
|
<!-- Timeline -->
|
|
<div class="timeline">
|
|
<div class="timeline-left">
|
|
<div
|
|
v-for="h in timelineHours"
|
|
:key="`h-${h}`"
|
|
class="hour"
|
|
>
|
|
<div class="hour-label">{{ pad2(h) }}00</div>
|
|
<div class="half-label">30</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="timeline-grid">
|
|
<div
|
|
v-for="h in timelineHours"
|
|
:key="`g-${h}`"
|
|
class="grid-hour"
|
|
>
|
|
<div class="grid-line" />
|
|
<div class="grid-half" />
|
|
</div>
|
|
|
|
<!-- “evento” preview -->
|
|
<div
|
|
class="event-chip"
|
|
:style="eventStyle"
|
|
>
|
|
<div class="event-title">{{ computedTitle }}</div>
|
|
<div class="event-sub">{{ selectedPatient?.name || '—' }}</div>
|
|
</div>
|
|
|
|
<!-- marcador de “agora” (estático, só estética) -->
|
|
<div class="now-marker" :style="{ top: nowMarkerTop + 'px' }">
|
|
<div class="now-pill">10:06</div>
|
|
<div class="now-line" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- conflito -->
|
|
<div v-if="hasConflict" class="conflict mt-3">
|
|
<i class="pi pi-exclamation-triangle" />
|
|
<div class="conflict-text">
|
|
<b>Conflito detectado:</b> Já possui um evento neste horário!
|
|
</div>
|
|
</div>
|
|
|
|
<Divider class="my-4" />
|
|
|
|
<!-- Repetição -->
|
|
<div class="section">
|
|
<div class="section-title">Repetição:</div>
|
|
|
|
<div class="repeat-row mt-2">
|
|
<div class="repeat-options">
|
|
<div
|
|
v-for="r in repeatOptions"
|
|
:key="r.value"
|
|
class="repeat-pill"
|
|
:class="{ 'is-on': form.repeat === r.value }"
|
|
@click="form.repeat = r.value"
|
|
role="button"
|
|
tabindex="0"
|
|
>
|
|
<RadioButton
|
|
:inputId="`rep-${r.value}`"
|
|
name="repeat"
|
|
:value="r.value"
|
|
v-model="form.repeat"
|
|
/>
|
|
<label :for="`rep-${r.value}`">{{ r.label }}</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mt-3">
|
|
<div>
|
|
<FloatLabel>
|
|
<Select
|
|
inputId="repeatUntil"
|
|
v-model="form.repeatUntil"
|
|
:options="repeatUntilOptions"
|
|
class="w-full"
|
|
:disabled="form.repeat === 'none'"
|
|
/>
|
|
<label for="repeatUntil">Termine em</label>
|
|
</FloatLabel>
|
|
</div>
|
|
|
|
<div>
|
|
<FloatLabel>
|
|
<InputText
|
|
id="repeatCount"
|
|
v-model="form.repeatCount"
|
|
class="w-full"
|
|
:disabled="form.repeat === 'none'"
|
|
/>
|
|
<label for="repeatCount">Data específica</label>
|
|
</FloatLabel>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Divider class="my-4" />
|
|
|
|
<!-- Lembrete -->
|
|
<div class="section">
|
|
<div class="section-title">Lembrete:</div>
|
|
<div class="mt-2">
|
|
<FloatLabel>
|
|
<Select
|
|
inputId="reminder"
|
|
v-model="form.reminder"
|
|
:options="reminderOptions"
|
|
class="w-full"
|
|
/>
|
|
<label for="reminder">Quando avisar</label>
|
|
</FloatLabel>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Atenção recorrente -->
|
|
<div v-if="selectedPatient?.recorrente" class="attention mt-4">
|
|
<div class="attention-title">Atenção :</div>
|
|
<ul class="attention-list">
|
|
<li>Este é um paciente recorrente, com horários já pré-estabelecidos na sua agenda.</li>
|
|
<li>Se continuar o sistema adicionará uma sessão extra.</li>
|
|
<li class="linklike" @click="onManageRecurring">Se deseja alterar os horários pré-estabelecidos, clique aqui</li>
|
|
</ul>
|
|
</div>
|
|
|
|
<!-- Footer buttons -->
|
|
<div class="footer-actions mt-5">
|
|
<Button
|
|
label="Cancelar"
|
|
severity="secondary"
|
|
outlined
|
|
class="w-full md:w-auto"
|
|
@click="onClose"
|
|
/>
|
|
<Button
|
|
label="Salvar"
|
|
class="w-full md:w-auto"
|
|
@click="onSave"
|
|
/>
|
|
</div>
|
|
</template>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, reactive } from 'vue'
|
|
|
|
// PrimeVue
|
|
import Card from 'primevue/card'
|
|
import Button from 'primevue/button'
|
|
import Divider from 'primevue/divider'
|
|
import FloatLabel from 'primevue/floatlabel'
|
|
import InputText from 'primevue/inputtext'
|
|
import Calendar from 'primevue/calendar'
|
|
import RadioButton from 'primevue/radiobutton'
|
|
import Tag from 'primevue/tag'
|
|
import Avatar from 'primevue/avatar'
|
|
import Select from 'primevue/select'
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Mock de dados (substitua por Supabase depois)
|
|
// ---------------------------------------------------------------------
|
|
const patients = [
|
|
{
|
|
id: 'p1',
|
|
name: 'Leonardo Nohama',
|
|
recorrente: true,
|
|
phone: '(16) 98828-8038',
|
|
email: 'leonardo@gmail.com',
|
|
address: 'Rua Dr. X, 123, Centro, Campinas, SP — 13010-001',
|
|
avatar: 'https://i.pravatar.cc/80?img=12'
|
|
},
|
|
{
|
|
id: 'p2',
|
|
name: 'Mariana S.',
|
|
recorrente: false,
|
|
phone: '(11) 99999-0000',
|
|
email: 'mariana@email.com',
|
|
address: 'São Carlos, SP',
|
|
avatar: 'https://i.pravatar.cc/80?img=47'
|
|
},
|
|
{
|
|
id: 'p3',
|
|
name: 'Rafael A.',
|
|
recorrente: false,
|
|
phone: '(19) 98888-1212',
|
|
email: 'rafa@exemplo.com',
|
|
address: 'Araraquara, SP',
|
|
avatar: 'https://i.pravatar.cc/80?img=33'
|
|
}
|
|
]
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Form state
|
|
// ---------------------------------------------------------------------
|
|
const form = reactive({
|
|
type: 'atendimento',
|
|
customTitle: '',
|
|
patientId: 'p1',
|
|
date: new Date(),
|
|
startTime: '09:00',
|
|
durationMin: 60,
|
|
notes: '',
|
|
repeat: 'none', // none | daily | weekly | biweekly | monthly
|
|
repeatUntil: 'never',
|
|
repeatCount: 'Após 10 ocorrências',
|
|
reminder: '4h'
|
|
})
|
|
|
|
// ---------------------------------------------------------------------
|
|
// UI options
|
|
// ---------------------------------------------------------------------
|
|
const activityTypes = [
|
|
{ value: 'atendimento', label: 'Atendimento', dot: 'rgba(59,130,246,.85)' },
|
|
{ value: 'supervisao', label: 'Supervisão', dot: 'rgba(99,102,241,.85)' },
|
|
{ value: 'analise', label: 'Análise pessoal', dot: 'rgba(245,158,11,.85)' },
|
|
{ value: 'estudo', label: 'Estudo', dot: 'rgba(16,185,129,.85)' }
|
|
]
|
|
|
|
const timeOptions = [
|
|
'07:00','07:30','08:00','08:30','09:00','09:30','10:00','10:30','11:00','11:30',
|
|
'12:00','12:30','13:00','13:30','14:00','14:30','15:00','15:30','16:00','16:30',
|
|
'17:00','17:30','18:00','18:30','19:00','19:30','20:00','20:30','21:00','21:30','22:00'
|
|
]
|
|
|
|
const repeatOptions = [
|
|
{ value: 'none', label: 'Não repete' },
|
|
{ value: 'daily', label: 'Diário' },
|
|
{ value: 'weekly', label: 'Semanal' },
|
|
{ value: 'biweekly', label: 'Quinzenal' },
|
|
{ value: 'monthly', label: 'Mensal' }
|
|
]
|
|
|
|
const repeatUntilOptions = ['Nunca', 'Data específica', 'Após X ocorrências'].map((v) => ({
|
|
label: v,
|
|
value: v === 'Nunca' ? 'never' : v === 'Data específica' ? 'date' : 'count'
|
|
}))
|
|
|
|
const reminderOptions = [
|
|
{ label: 'Sem lembrete', value: 'none' },
|
|
{ label: '10 minutos antes', value: '10m' },
|
|
{ label: '30 minutos antes', value: '30m' },
|
|
{ label: '1 hora antes', value: '1h' },
|
|
{ label: '4 horas antes', value: '4h' },
|
|
{ label: '1 dia antes', value: '1d' }
|
|
]
|
|
|
|
// Slots (exemplo)
|
|
const slotGroups = {
|
|
manha: ['08:00','09:00','10:00','11:00'],
|
|
tarde: ['13:00','15:00','16:00','17:00'],
|
|
noite: ['18:00','19:00','20:00','21:00','22:00']
|
|
}
|
|
|
|
// Slots bloqueados “pelo paciente” (mock)
|
|
const blockedSlots = new Set(['16:00', '21:00', '09:00']) // exemplo
|
|
const timelineStart = 7
|
|
const timelineEnd = 12 // só pra caber bonito; você pode aumentar conforme sua agenda
|
|
const timelineHours = Array.from({ length: timelineEnd - timelineStart }, (_, i) => timelineStart + i)
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Computeds
|
|
// ---------------------------------------------------------------------
|
|
const selectedPatient = computed(() => patients.find(p => p.id === form.patientId) || null)
|
|
|
|
const computedTitle = computed(() => {
|
|
const typed = (form.customTitle || '').trim()
|
|
if (typed) return typed
|
|
const found = activityTypes.find(t => t.value === form.type)
|
|
return found?.label || 'Atividade'
|
|
})
|
|
|
|
const computedEndTime = computed(() => addMinutesToTime(form.startTime, form.durationMin))
|
|
|
|
// conflito mock: se startTime estiver em “09:00” dispara
|
|
const hasConflict = computed(() => form.startTime === '09:00')
|
|
|
|
// label de data no topo
|
|
const previewDateLabel = computed(() => {
|
|
const d = form.date instanceof Date ? form.date : new Date()
|
|
const day = d.getDate()
|
|
const month = d.toLocaleDateString('pt-BR', { month: 'long' })
|
|
const weekday = d.toLocaleDateString('pt-BR', { weekday: 'long' })
|
|
return `${day} ${month}, ${weekday}`
|
|
})
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Preview positioning (timeline)
|
|
// ---------------------------------------------------------------------
|
|
const pxPerHour = 84 // ajuste fino do grid
|
|
const nowMarkerTop = 3.1 * pxPerHour // só estético (aprox 10:06 dentro do bloco)
|
|
|
|
const eventStyle = computed(() => {
|
|
// converte startTime para offset
|
|
const [hh, mm] = form.startTime.split(':').map(Number)
|
|
const startFloat = hh + (mm / 60)
|
|
|
|
const top = (startFloat - timelineStart) * pxPerHour
|
|
const height = (form.durationMin / 60) * pxPerHour
|
|
|
|
return {
|
|
top: `${Math.max(6, top)}px`,
|
|
height: `${Math.max(38, height)}px`
|
|
}
|
|
})
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Slot selection helpers
|
|
// ---------------------------------------------------------------------
|
|
function isSelectedSlot(t) {
|
|
return form.startTime === t
|
|
}
|
|
function selectSlot(t) {
|
|
form.startTime = t
|
|
}
|
|
function slotSeverity(t) {
|
|
if (isSelectedSlot(t)) return 'primary'
|
|
if (blockedSlots.has(t)) return 'danger'
|
|
return 'secondary'
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Actions (plugue no seu router/toast depois)
|
|
// ---------------------------------------------------------------------
|
|
function onClose() {
|
|
// ex: router.back()
|
|
console.log('close')
|
|
}
|
|
function onSave() {
|
|
// ex: salvar no Supabase
|
|
console.log('save', JSON.parse(JSON.stringify(form)))
|
|
}
|
|
function onAddPatient() {
|
|
console.log('add patient')
|
|
}
|
|
function onManageRecurring() {
|
|
console.log('manage recurring')
|
|
}
|
|
|
|
// ---------------------------------------------------------------------
|
|
// Utils
|
|
// ---------------------------------------------------------------------
|
|
function pad2(n) {
|
|
return String(n).padStart(2, '0')
|
|
}
|
|
|
|
function addMinutesToTime(hhmm, minutesToAdd) {
|
|
const [h, m] = hhmm.split(':').map(Number)
|
|
const total = (h * 60 + m + minutesToAdd) % (24 * 60)
|
|
const nh = Math.floor(total / 60)
|
|
const nm = total % 60
|
|
return `${pad2(nh)}:${pad2(nm)}`
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.activity-shell{
|
|
display:grid;
|
|
grid-template-columns: 1.08fr .92fr;
|
|
gap: 1rem;
|
|
}
|
|
@media (max-width: 1024px){
|
|
.activity-shell{ grid-template-columns: 1fr; }
|
|
}
|
|
|
|
.fake-tabs{
|
|
border-bottom: 1px solid var(--surface-border);
|
|
display:flex;
|
|
gap:.5rem;
|
|
}
|
|
.fake-tab{
|
|
padding:.65rem 1rem;
|
|
border: 1px solid var(--surface-border);
|
|
border-bottom: none;
|
|
border-top-left-radius: .8rem;
|
|
border-top-right-radius: .8rem;
|
|
background: color-mix(in srgb, var(--surface-card), transparent 18%);
|
|
color: var(--text-color-secondary);
|
|
font-weight: 600;
|
|
}
|
|
.fake-tab.is-active{
|
|
background: var(--surface-card);
|
|
color: var(--text-color);
|
|
box-shadow: 0 10px 30px rgba(0,0,0,.04);
|
|
}
|
|
|
|
.field{ margin-top: 1rem; }
|
|
.field-label{
|
|
font-weight: 700;
|
|
color: var(--text-color);
|
|
}
|
|
.field-label .muted{
|
|
font-weight: 600;
|
|
color: var(--text-color-secondary);
|
|
margin-left: .35rem;
|
|
}
|
|
.hint{
|
|
display:block;
|
|
margin-top:.35rem;
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.activity-types{
|
|
margin-top: .6rem;
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:.55rem;
|
|
}
|
|
.activity-type{
|
|
display:flex;
|
|
align-items:center;
|
|
gap:.7rem;
|
|
padding:.65rem .75rem;
|
|
border-radius: 1rem;
|
|
border: 1px solid var(--surface-border);
|
|
background: color-mix(in srgb, var(--surface-card), transparent 22%);
|
|
cursor:pointer;
|
|
}
|
|
.activity-type.is-selected{
|
|
border-color: color-mix(in srgb, var(--primary-color), var(--surface-border) 55%);
|
|
background: color-mix(in srgb, var(--primary-color), transparent 92%);
|
|
}
|
|
.activity-type__label{
|
|
display:flex;
|
|
align-items:center;
|
|
gap:.55rem;
|
|
cursor:pointer;
|
|
}
|
|
.dot{
|
|
width:10px;height:10px;border-radius:999px;
|
|
box-shadow: 0 0 0 4px color-mix(in srgb, currentColor, transparent 85%);
|
|
}
|
|
.name{ font-weight: 650; color: var(--text-color); }
|
|
|
|
.patient-row{
|
|
display:flex;
|
|
gap:.75rem;
|
|
align-items:flex-start;
|
|
border: 1px solid var(--surface-border);
|
|
border-radius: 1.25rem;
|
|
padding: .85rem;
|
|
background: color-mix(in srgb, var(--surface-card), transparent 18%);
|
|
}
|
|
.patient-avatar{
|
|
width: 40px;
|
|
height: 40px;
|
|
}
|
|
.patient-name{
|
|
font-weight: 800;
|
|
}
|
|
.patient-tag{ height: 22px; }
|
|
.patient-meta{
|
|
margin-top:.25rem;
|
|
display:flex;
|
|
align-items:center;
|
|
gap:.45rem;
|
|
color: var(--text-color-secondary);
|
|
font-size: .9rem;
|
|
}
|
|
.patient-meta .sep{
|
|
opacity:.7;
|
|
margin: 0 .25rem;
|
|
}
|
|
|
|
.slots-block{
|
|
border: 1px solid var(--surface-border);
|
|
border-radius: 1.25rem;
|
|
padding: .85rem;
|
|
background: color-mix(in srgb, var(--surface-card), transparent 18%);
|
|
}
|
|
.slots-row{
|
|
display:flex;
|
|
flex-direction:column;
|
|
gap:.5rem;
|
|
margin-bottom:.85rem;
|
|
}
|
|
.slot-group-title{
|
|
font-weight: 800;
|
|
color: var(--text-color);
|
|
}
|
|
.slot-group-title .muted{
|
|
font-weight: 650;
|
|
color: var(--text-color-secondary);
|
|
margin-left:.35rem;
|
|
}
|
|
.slot-chips{
|
|
display:flex;
|
|
flex-wrap:wrap;
|
|
gap:.5rem;
|
|
}
|
|
.slot-chip{
|
|
border-radius: 999px;
|
|
}
|
|
.legend{
|
|
display:flex;
|
|
flex-wrap:wrap;
|
|
gap:.9rem;
|
|
color: var(--text-color-secondary);
|
|
font-size: .85rem;
|
|
}
|
|
.legend-item{
|
|
display:flex;
|
|
align-items:center;
|
|
gap:.4rem;
|
|
}
|
|
.legend-dot{
|
|
width:10px;height:10px;border-radius:999px;
|
|
border: 1px solid var(--surface-border);
|
|
}
|
|
.legend-dot.is-blocked{ background: color-mix(in srgb, var(--red-500), transparent 10%); }
|
|
.legend-dot.is-free{ background: color-mix(in srgb, var(--surface-500), transparent 55%); }
|
|
.legend-dot.is-selected{ background: color-mix(in srgb, var(--primary-color), transparent 10%); }
|
|
|
|
.preview-top{
|
|
display:flex;
|
|
justify-content:space-between;
|
|
align-items:center;
|
|
gap:1rem;
|
|
}
|
|
.preview-date{
|
|
font-weight: 900;
|
|
font-size: 1.05rem;
|
|
color: var(--text-color);
|
|
}
|
|
.preview-badge{
|
|
font-weight: 900;
|
|
font-size: .8rem;
|
|
letter-spacing:.06em;
|
|
color: color-mix(in srgb, var(--red-500), var(--text-color) 15%);
|
|
}
|
|
|
|
.timeline{
|
|
display:grid;
|
|
grid-template-columns: 68px 1fr;
|
|
gap:.75rem;
|
|
align-items:start;
|
|
}
|
|
.timeline-left .hour{
|
|
height: calc(84px);
|
|
position:relative;
|
|
padding-right:.4rem;
|
|
}
|
|
.hour-label{
|
|
font-weight: 900;
|
|
color: var(--text-color);
|
|
font-size: .9rem;
|
|
}
|
|
.half-label{
|
|
position:absolute;
|
|
top: 41px;
|
|
font-size: .8rem;
|
|
color: var(--text-color-secondary);
|
|
}
|
|
|
|
.timeline-grid{
|
|
position:relative;
|
|
border-radius: 1.25rem;
|
|
border: 1px solid var(--surface-border);
|
|
overflow:hidden;
|
|
background: color-mix(in srgb, var(--surface-card), transparent 12%);
|
|
}
|
|
.grid-hour{
|
|
height: 84px;
|
|
position:relative;
|
|
}
|
|
.grid-line{
|
|
position:absolute;
|
|
left:0; right:0; top:0;
|
|
height:1px;
|
|
background: color-mix(in srgb, var(--surface-border), transparent 5%);
|
|
}
|
|
.grid-half{
|
|
position:absolute;
|
|
left:0; right:0; top:42px;
|
|
height:1px;
|
|
background: color-mix(in srgb, var(--surface-border), transparent 35%);
|
|
}
|
|
|
|
.event-chip{
|
|
position:absolute;
|
|
left: 14px;
|
|
right: 18%;
|
|
border-radius: 12px;
|
|
padding: .7rem .85rem;
|
|
background: color-mix(in srgb, var(--primary-color), transparent 10%);
|
|
color: var(--primary-color-text);
|
|
box-shadow: 0 18px 40px rgba(0,0,0,.10);
|
|
}
|
|
.event-title{
|
|
font-weight: 900;
|
|
}
|
|
.event-sub{
|
|
opacity: .92;
|
|
font-weight: 650;
|
|
font-size: .9rem;
|
|
margin-top:.15rem;
|
|
}
|
|
|
|
.now-marker{
|
|
position:absolute;
|
|
left: 0;
|
|
right: 0;
|
|
pointer-events:none;
|
|
}
|
|
.now-pill{
|
|
position:absolute;
|
|
left: -56px;
|
|
top: -10px;
|
|
background: #111;
|
|
color:#fff;
|
|
font-weight: 900;
|
|
font-size: .75rem;
|
|
padding:.2rem .4rem;
|
|
border-radius: .45rem;
|
|
}
|
|
.now-line{
|
|
position:absolute;
|
|
left:0; right:0;
|
|
top:0;
|
|
height:2px;
|
|
background: color-mix(in srgb, var(--surface-900), transparent 15%);
|
|
opacity:.45;
|
|
}
|
|
|
|
.conflict{
|
|
display:flex;
|
|
align-items:flex-start;
|
|
gap:.6rem;
|
|
padding:.75rem .9rem;
|
|
border-radius: 1rem;
|
|
border: 1px solid color-mix(in srgb, var(--orange-500), var(--surface-border) 45%);
|
|
background: color-mix(in srgb, var(--orange-100), transparent 30%);
|
|
color: color-mix(in srgb, var(--orange-900), var(--text-color) 20%);
|
|
}
|
|
.conflict i{ margin-top:.1rem; }
|
|
.conflict-text{ line-height:1.2rem; }
|
|
|
|
.section-title{
|
|
font-weight: 900;
|
|
color: var(--text-color);
|
|
}
|
|
|
|
.repeat-options{
|
|
display:flex;
|
|
flex-wrap:wrap;
|
|
gap:.6rem;
|
|
}
|
|
.repeat-pill{
|
|
display:flex;
|
|
align-items:center;
|
|
gap:.55rem;
|
|
padding:.55rem .75rem;
|
|
border-radius: 999px;
|
|
border: 1px solid var(--surface-border);
|
|
background: color-mix(in srgb, var(--surface-card), transparent 18%);
|
|
cursor:pointer;
|
|
}
|
|
.repeat-pill.is-on{
|
|
border-color: color-mix(in srgb, var(--primary-color), var(--surface-border) 55%);
|
|
background: color-mix(in srgb, var(--primary-color), transparent 92%);
|
|
}
|
|
|
|
.attention{
|
|
border-radius: 1.25rem;
|
|
border: 1px solid color-mix(in srgb, var(--red-500), var(--surface-border) 45%);
|
|
background: color-mix(in srgb, var(--red-50), transparent 18%);
|
|
padding: 1rem 1rem .9rem;
|
|
}
|
|
.attention-title{
|
|
font-weight: 950;
|
|
color: color-mix(in srgb, var(--red-700), var(--text-color) 15%);
|
|
margin-bottom:.45rem;
|
|
}
|
|
.attention-list{
|
|
margin: 0;
|
|
padding-left: 1.1rem;
|
|
color: color-mix(in srgb, var(--red-900), var(--text-color) 25%);
|
|
}
|
|
.attention-list li{ margin:.35rem 0; }
|
|
.linklike{
|
|
cursor:pointer;
|
|
text-decoration: underline;
|
|
font-weight: 800;
|
|
}
|
|
|
|
.footer-actions{
|
|
display:flex;
|
|
justify-content:flex-end;
|
|
gap:.75rem;
|
|
flex-wrap:wrap;
|
|
}
|
|
</style> |