Ajuste em Massa - Paciente, Terapeuta, Clinica e Admin - Inicio agenda
This commit is contained in:
246
src/components/agenda/PausasChipsEditor.vue
Normal file
246
src/components/agenda/PausasChipsEditor.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<!-- src/components/agenda/PausasChipsEditor.vue -->
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
|
||||
import Button from 'primevue/button'
|
||||
import Dialog from 'primevue/dialog'
|
||||
import FloatLabel from 'primevue/floatlabel'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Tag from 'primevue/tag'
|
||||
import Divider from 'primevue/divider'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
|
||||
const toast = useToast()
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: Array, default: () => [] } // [{id,label,inicio,fim}]
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
function isValidHHMM(v) {
|
||||
return /^\d{2}:\d{2}$/.test(String(v || '').trim())
|
||||
}
|
||||
function hhmmToMin(hhmm) {
|
||||
const [h, m] = String(hhmm).split(':').map(Number)
|
||||
return h * 60 + m
|
||||
}
|
||||
function minToHHMM(min) {
|
||||
const h = Math.floor(min / 60) % 24
|
||||
const m = min % 60
|
||||
return String(h).padStart(2, '0') + ':' + String(m).padStart(2, '0')
|
||||
}
|
||||
function newId() {
|
||||
return Math.random().toString(16).slice(2) + Date.now().toString(16)
|
||||
}
|
||||
|
||||
const internal = ref([])
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(v) => {
|
||||
internal.value = (Array.isArray(v) ? v : []).map(p => ({
|
||||
id: p.id || newId(),
|
||||
label: String(p.label || 'Pausa'),
|
||||
inicio: String(p.inicio || '').slice(0, 5),
|
||||
fim: String(p.fim || '').slice(0, 5)
|
||||
}))
|
||||
},
|
||||
{ immediate: true, deep: true }
|
||||
)
|
||||
|
||||
function pushUpdate(next) {
|
||||
// ordena por início
|
||||
const sorted = [...next].sort((a, b) => hhmmToMin(a.inicio) - hhmmToMin(b.inicio))
|
||||
internal.value = sorted
|
||||
emit('update:modelValue', sorted)
|
||||
}
|
||||
|
||||
// união de intervalos existentes
|
||||
function normalizeIntervals(list) {
|
||||
const intervals = (list || [])
|
||||
.map(p => ({ s: hhmmToMin(p.inicio), e: hhmmToMin(p.fim) }))
|
||||
.sort((a, b) => a.s - b.s)
|
||||
|
||||
const merged = []
|
||||
for (const it of intervals) {
|
||||
if (!merged.length) merged.push({ ...it })
|
||||
else {
|
||||
const last = merged[merged.length - 1]
|
||||
if (it.s <= last.e) last.e = Math.max(last.e, it.e)
|
||||
else merged.push({ ...it })
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
|
||||
// retorna “sobras” de [s,e] depois de remover intervalos ocupados
|
||||
function subtractIntervals(s, e, occupiedMerged) {
|
||||
let segments = [{ s, e }]
|
||||
for (const occ of occupiedMerged) {
|
||||
const next = []
|
||||
for (const seg of segments) {
|
||||
// sem interseção
|
||||
if (seg.e <= occ.s || seg.s >= occ.e) {
|
||||
next.push(seg)
|
||||
continue
|
||||
}
|
||||
// corta esquerda
|
||||
if (seg.s < occ.s) next.push({ s: seg.s, e: Math.min(occ.s, seg.e) })
|
||||
// corta direita
|
||||
if (seg.e > occ.e) next.push({ s: Math.max(occ.e, seg.s), e: seg.e })
|
||||
}
|
||||
segments = next
|
||||
if (!segments.length) break
|
||||
}
|
||||
// remove segmentos muito pequenos (0)
|
||||
return segments.filter(x => x.e > x.s)
|
||||
}
|
||||
|
||||
function addPauseSmart({ label, inicio, fim }) {
|
||||
if (!isValidHHMM(inicio) || !isValidHHMM(fim)) {
|
||||
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'Horários devem estar em HH:MM.', life: 2200 })
|
||||
return
|
||||
}
|
||||
if (fim <= inicio) {
|
||||
toast.add({ severity: 'warn', summary: 'Atenção', detail: 'O fim deve ser maior que o início.', life: 2200 })
|
||||
return
|
||||
}
|
||||
|
||||
const s = hhmmToMin(inicio)
|
||||
const e = hhmmToMin(fim)
|
||||
|
||||
const occupied = normalizeIntervals(internal.value)
|
||||
const segments = subtractIntervals(s, e, occupied)
|
||||
|
||||
if (!segments.length) {
|
||||
toast.add({ severity: 'info', summary: 'Nada a adicionar', detail: 'Esse período já está coberto por outra pausa.', life: 2400 })
|
||||
return
|
||||
}
|
||||
|
||||
const toAdd = segments.map(seg => ({
|
||||
id: newId(),
|
||||
label: label || 'Pausa',
|
||||
inicio: minToHHMM(seg.s),
|
||||
fim: minToHHMM(seg.e)
|
||||
}))
|
||||
|
||||
// se houve “recorte”, avisa
|
||||
if (segments.length !== 1 || (segments[0].s !== s || segments[0].e !== e)) {
|
||||
toast.add({
|
||||
severity: 'info',
|
||||
summary: 'Ajuste automático',
|
||||
detail: 'Havia conflito com outra pausa; adicionei apenas o trecho que não sobrepõe.',
|
||||
life: 3200
|
||||
})
|
||||
}
|
||||
|
||||
pushUpdate([...internal.value, ...toAdd])
|
||||
}
|
||||
|
||||
function removePause(id) {
|
||||
pushUpdate(internal.value.filter(p => p.id !== id))
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
// UI: presets + custom dialog
|
||||
// ======================================================
|
||||
const presets = [
|
||||
{ label: 'Almoço', inicio: '12:00', fim: '13:00' },
|
||||
{ label: 'Almoço', inicio: '13:00', fim: '14:00' },
|
||||
{ label: 'Janta', inicio: '18:00', fim: '19:00' }
|
||||
]
|
||||
|
||||
const dlg = ref(false)
|
||||
const form = ref({ label: 'Pausa', inicio: '12:00', fim: '13:00' })
|
||||
|
||||
function openCustom() {
|
||||
form.value = { label: 'Pausa', inicio: '12:00', fim: '13:00' }
|
||||
dlg.value = true
|
||||
}
|
||||
function saveCustom() {
|
||||
addPauseSmart(form.value)
|
||||
dlg.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<!-- actions -->
|
||||
<div class="flex flex-wrap gap-2 items-center">
|
||||
<Button
|
||||
v-for="(p, idx) in presets"
|
||||
:key="'pre_'+idx"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
outlined
|
||||
icon="pi pi-plus"
|
||||
:label="`${p.label} (${p.inicio}–${p.fim})`"
|
||||
@click="addPauseSmart(p)"
|
||||
/>
|
||||
<Button size="small" icon="pi pi-sliders-h" label="Customizar" @click="openCustom" />
|
||||
</div>
|
||||
|
||||
<Divider class="my-2" />
|
||||
|
||||
<!-- chips/badges -->
|
||||
<div v-if="!internal.length" class="text-600 text-sm">
|
||||
Nenhuma pausa adicionada.
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="p in internal"
|
||||
:key="p.id"
|
||||
class="inline-flex items-center gap-2 rounded-full border border-[var(--surface-border)] bg-[var(--surface-card)] px-3 py-1"
|
||||
>
|
||||
<Tag :value="p.label" severity="secondary" />
|
||||
<span class="text-600 text-sm">{{ p.inicio }}–{{ p.fim }}</span>
|
||||
<Button icon="pi pi-times" text rounded severity="danger" @click="removePause(p.id)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- custom dialog -->
|
||||
<Dialog v-model:visible="dlg" modal header="Adicionar pausa" :style="{ width: '520px' }">
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-12">
|
||||
<FloatLabel variant="on">
|
||||
<InputText v-model="form.label" class="w-full" inputId="plabel" placeholder="Ex.: Almoço" />
|
||||
<label for="plabel">Nome</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<FloatLabel variant="on">
|
||||
<InputText v-model="form.inicio" class="w-full" inputId="pinicio" placeholder="12:00" />
|
||||
<label for="pinicio">Início (HH:MM)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 md:col-span-6">
|
||||
<FloatLabel variant="on">
|
||||
<InputText v-model="form.fim" class="w-full" inputId="pfim" placeholder="13:00" />
|
||||
<label for="pfim">Fim (HH:MM)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<div v-if="isValidHHMM(form.inicio) && isValidHHMM(form.fim) && form.fim <= form.inicio" class="col-span-12 text-sm text-red-500">
|
||||
O fim precisa ser maior que o início.
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 text-600 text-xs">
|
||||
Se houver conflito com outra pausa, o sistema adiciona automaticamente apenas o trecho que não sobrepõe.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancelar" icon="pi pi-times" severity="secondary" outlined @click="dlg = false" />
|
||||
<Button
|
||||
label="Adicionar"
|
||||
icon="pi pi-check"
|
||||
:disabled="!isValidHHMM(form.inicio) || !isValidHHMM(form.fim) || form.fim <= form.inicio"
|
||||
@click="saveCustom"
|
||||
/>
|
||||
</template>
|
||||
</Dialog>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user