panels-origin/web-panel/components/DeskStatusMessages.vue
2026-08-21 16:55:01 -05:00

320 lines
7.4 KiB
Vue

<template>
<div
v-if="messages.length"
ref="rootEl"
class="desk-status-messages"
:class="{ 'is-open': open }"
>
<div class="desk-status-messages-panel" role="dialog" :aria-hidden="!open">
<div class="desk-status-messages-inner">
<div class="desk-status-messages-head">
<strong>Avisos de {{ screenLabel }}</strong>
<span class="muted">{{ messages.length }}</span>
</div>
<ul class="desk-status-messages-list">
<li v-for="item in messages" :key="item.key">
<button type="button" class="desk-status-message" :class="`is-${item.severity}`" @click="activate(item)">
<i :class="iconFor(item.severity)" />
<span>
<strong>{{ item.title }}</strong>
<small v-if="item.detail">{{ item.detail }}</small>
</span>
</button>
</li>
</ul>
</div>
</div>
<button
type="button"
class="statusbar-item desk-status-messages-trigger"
:aria-expanded="open"
:title="summaryTitle"
@click="toggle"
>
<i :class="summaryIcon" />
<span>{{ summaryLabel }}</span>
<i class="pi pi-angle-up desk-status-messages-caret" />
</button>
</div>
</template>
<script setup lang="ts">
import type { DeskStatusMessage, PadronImssFilter } from "~/composables/useDesktop";
const { statusMessages, tabLabel, padronImssFilter, treeKey } = useDesktop();
const rootEl = ref<HTMLElement | null>(null);
const open = ref(false);
const pinned = ref(false);
let autoTimer: ReturnType<typeof setTimeout> | null = null;
const messages = computed(() => statusMessages.value);
const screenLabel = computed(() => tabLabel.value || "esta pantalla");
const severityRank: Record<DeskStatusMessage["severity"], number> = {
danger: 3,
warn: 2,
info: 1,
};
const topSeverity = computed(() => {
let best: DeskStatusMessage["severity"] = "info";
for (const item of messages.value) {
if (severityRank[item.severity] > severityRank[best]) best = item.severity;
}
return best;
});
const summaryIcon = computed(() => iconFor(topSeverity.value));
const summaryLabel = computed(() => {
if (messages.value.length === 1) return messages.value[0].title;
return `${messages.value.length} avisos`;
});
const summaryTitle = computed(() => messages.value.map((item) => item.title).join(" · "));
const signature = computed(() =>
messages.value.map((item) => `${item.key}:${item.title}:${item.severity}`).join("|"),
);
function iconFor(severity: DeskStatusMessage["severity"]) {
if (severity === "warn") return "pi pi-exclamation-triangle";
if (severity === "info") return "pi pi-info-circle";
return "pi pi-exclamation-circle";
}
function clearAuto() {
if (autoTimer) {
clearTimeout(autoTimer);
autoTimer = null;
}
}
function collapse() {
open.value = false;
pinned.value = false;
clearAuto();
}
function expandTemporary() {
pinned.value = false;
open.value = true;
clearAuto();
autoTimer = setTimeout(() => {
if (!pinned.value) open.value = false;
autoTimer = null;
}, 4500);
}
function toggle() {
if (open.value && pinned.value) {
collapse();
return;
}
if (open.value && !pinned.value) {
pinned.value = true;
clearAuto();
return;
}
open.value = true;
pinned.value = true;
clearAuto();
}
async function activate(item: DeskStatusMessage) {
if (item.treeKey) {
treeKey.value = item.treeKey;
} else if (item.filter && item.route === "/padron") {
treeKey.value = "all";
}
if (item.filter) {
padronImssFilter.value = item.filter as PadronImssFilter;
}
if (item.route && useRoute().path !== item.route) {
await navigateTo(item.route);
}
collapse();
}
function onDocPointer(event: PointerEvent) {
if (!open.value || !rootEl.value) return;
if (rootEl.value.contains(event.target as Node)) return;
collapse();
}
watch(signature, (value, previous) => {
if (!value) {
collapse();
return;
}
if (value !== previous) expandTemporary();
}, { immediate: true });
onMounted(() => {
document.addEventListener("pointerdown", onDocPointer);
});
onBeforeUnmount(() => {
document.removeEventListener("pointerdown", onDocPointer);
clearAuto();
});
</script>
<style scoped>
.desk-status-messages {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-end;
overflow: visible;
}
.desk-status-messages-trigger {
border: 0;
background: transparent;
cursor: pointer;
font: inherit;
max-width: min(360px, 42vw);
padding-inline: 10px;
}
.desk-status-messages-trigger:hover {
color: #344156;
}
.desk-status-messages-caret {
font-size: 11px !important;
color: #7a8798 !important;
transition: transform .35s ease;
}
.desk-status-messages.is-open .desk-status-messages-caret {
transform: rotate(180deg);
}
.desk-status-messages-panel {
position: absolute;
right: 0;
bottom: calc(100% + 8px);
width: min(360px, 72vw);
display: grid;
grid-template-rows: 0fr;
opacity: 0;
transform: translateY(12px);
pointer-events: none;
transition:
grid-template-rows .45s cubic-bezier(.22, 1, .36, 1),
opacity .28s ease,
transform .45s cubic-bezier(.22, 1, .36, 1);
z-index: 40;
}
.desk-status-messages.is-open .desk-status-messages-panel {
grid-template-rows: 1fr;
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.desk-status-messages-inner {
min-height: 0;
overflow: hidden;
border: 1px solid #e1e6ed;
border-radius: 12px;
background: #fff;
box-shadow: 0 12px 28px rgba(23, 32, 51, .14);
}
.desk-status-messages-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px 8px;
border-bottom: 1px solid #edf0f4;
color: #344156;
font-size: 12px;
}
.desk-status-messages-list {
list-style: none;
margin: 0;
padding: 6px;
max-height: 240px;
overflow: auto;
}
.desk-status-message {
width: 100%;
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px;
border: 0;
border-radius: 8px;
background: transparent;
text-align: left;
cursor: pointer;
color: #405069;
}
.desk-status-message:hover {
background: #f5f7fa;
}
.desk-status-message > i {
margin-top: 2px;
font-size: 14px;
}
.desk-status-message span {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
.desk-status-message strong {
font-size: 12px;
font-weight: 600;
color: #2d3a4d;
}
.desk-status-message small {
font-size: 11px;
line-height: 1.35;
color: #6b7785;
}
.desk-status-message.is-danger > i { color: #c2414b; }
.desk-status-message.is-warn > i { color: #b7791f; }
.desk-status-message.is-info > i { color: #2f6fe4; }
.muted {
color: #6b7785;
font-size: 11px;
}
:global(html.dark) .desk-status-messages-trigger:hover {
color: #e2e8f0;
}
:global(html.dark) .desk-status-messages-caret {
color: #94a3b8 !important;
}
:global(html.dark) .desk-status-messages-inner {
border-color: #243044;
background: #111b2e;
box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
}
:global(html.dark) .desk-status-messages-head {
border-bottom-color: #243044;
color: #e2e8f0;
}
:global(html.dark) .desk-status-message {
color: #cbd5e1;
}
:global(html.dark) .desk-status-message:hover {
background: #1a2744;
}
:global(html.dark) .desk-status-message strong {
color: #f1f5f9;
}
:global(html.dark) .desk-status-message small,
:global(html.dark) .muted {
color: #94a3b8;
}
:global(html.dark) .desk-status-message.is-info > i {
color: #60a5fa;
}
</style>