|
@@ -570,6 +570,11 @@
|
|
|
.chat-status { padding: 14px 18px; font-size: 12.5px; color: var(--text-muted); }
|
|
.chat-status { padding: 14px 18px; font-size: 12.5px; color: var(--text-muted); }
|
|
|
.chat-error-txt { color: #c04a2a; }
|
|
.chat-error-txt { color: #c04a2a; }
|
|
|
.chat-reply { padding: 10px 18px 14px; }
|
|
.chat-reply { padding: 10px 18px 14px; }
|
|
|
|
|
+ .chat-template-select {
|
|
|
|
|
+ width: 100%; margin-bottom: 7px; padding: 5px 8px; border-radius: 5px;
|
|
|
|
|
+ border: 1px solid var(--border); background: var(--bg); color: var(--text);
|
|
|
|
|
+ font-size: 12px; font-family: var(--sans); cursor: pointer;
|
|
|
|
|
+ }
|
|
|
.chat-reply-input {
|
|
.chat-reply-input {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
font-family: var(--sans);
|
|
font-family: var(--sans);
|
|
@@ -1293,6 +1298,14 @@
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="chat-reply">
|
|
<div class="chat-reply">
|
|
|
|
|
+ <select
|
|
|
|
|
+ v-if="Object.keys(chatTemplates).length"
|
|
|
|
|
+ class="chat-template-select"
|
|
|
|
|
+ @change="e => { applyTemplate(e.target.value); e.target.value = ''; }"
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="">Insert template…</option>
|
|
|
|
|
+ <option v-for="(_, name) in chatTemplates" :key="name" :value="name">{{ name.replace(/_/g, ' ') }}</option>
|
|
|
|
|
+ </select>
|
|
|
<textarea
|
|
<textarea
|
|
|
v-model="chatReply"
|
|
v-model="chatReply"
|
|
|
class="chat-reply-input"
|
|
class="chat-reply-input"
|
|
@@ -1413,13 +1426,15 @@ createApp({
|
|
|
const healthStatus = ref('idle');
|
|
const healthStatus = ref('idle');
|
|
|
|
|
|
|
|
// ── Chat ───────────────────────────────────────────────
|
|
// ── Chat ───────────────────────────────────────────────
|
|
|
- const chatThread = ref(null);
|
|
|
|
|
- const chatLoading = ref(false);
|
|
|
|
|
- const chatError = ref(null);
|
|
|
|
|
- const chatReply = ref('');
|
|
|
|
|
- const chatSending = ref(false);
|
|
|
|
|
- const chatSendError = ref(null);
|
|
|
|
|
- const chatSendReady = ref(true);
|
|
|
|
|
|
|
+ const chatThread = ref(null);
|
|
|
|
|
+ const chatLoading = ref(false);
|
|
|
|
|
+ const chatError = ref(null);
|
|
|
|
|
+ const chatReply = ref('');
|
|
|
|
|
+ const chatSending = ref(false);
|
|
|
|
|
+ const chatSendError = ref(null);
|
|
|
|
|
+ const chatSendReady = ref(true);
|
|
|
|
|
+ const chatTemplates = ref({});
|
|
|
|
|
+ const chatTemplatesDtFmt = ref('%-d %B %Y');
|
|
|
const syncEnabled = ref(false);
|
|
const syncEnabled = ref(false);
|
|
|
const bulkEnabled = ref(false);
|
|
const bulkEnabled = ref(false);
|
|
|
const bulkDays = ref(new Set());
|
|
const bulkDays = ref(new Set());
|
|
@@ -1533,6 +1548,43 @@ createApp({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ function _strftime(fmt, date) {
|
|
|
|
|
+ const pad = n => String(n).padStart(2, '0');
|
|
|
|
|
+ const MONTHS_LONG = ['January','February','March','April','May','June','July','August','September','October','November','December'];
|
|
|
|
|
+ const MONTHS_SHORT = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
|
|
|
|
|
+ return fmt
|
|
|
|
|
+ .replace(/%-d/g, String(date.getDate()))
|
|
|
|
|
+ .replace(/%d/g, pad(date.getDate()))
|
|
|
|
|
+ .replace(/%-m/g, String(date.getMonth() + 1))
|
|
|
|
|
+ .replace(/%m/g, pad(date.getMonth() + 1))
|
|
|
|
|
+ .replace(/%B/g, MONTHS_LONG[date.getMonth()])
|
|
|
|
|
+ .replace(/%b/g, MONTHS_SHORT[date.getMonth()])
|
|
|
|
|
+ .replace(/%Y/g, String(date.getFullYear()))
|
|
|
|
|
+ .replace(/%y/g, String(date.getFullYear()).slice(-2));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ function applyTemplate(name) {
|
|
|
|
|
+ const raw = chatTemplates.value[name];
|
|
|
|
|
+ if (!raw) return;
|
|
|
|
|
+ const sel = selection.value;
|
|
|
|
|
+ const dtFmt = chatTemplatesDtFmt.value;
|
|
|
|
|
+ const checkin = sel?.checkin_date ? parseLocal(sel.checkin_date) : null;
|
|
|
|
|
+ const checkout = sel?.checkout_date ? parseLocal(sel.checkout_date) : null;
|
|
|
|
|
+ const fmtDate_ = (d, fmt) => d ? _strftime(fmt || dtFmt, d) : '';
|
|
|
|
|
+ const resId = [serviceName(sel?.service), sel?.reservation_id].filter(Boolean).join(' ');
|
|
|
|
|
+ chatReply.value = raw.replace(/\{(\w+)(?::([^}]*))?\}/g, (_, key, fmt) => {
|
|
|
|
|
+ if (key === 'guest_name') return sel?.guest_name ?? '';
|
|
|
|
|
+ if (key === 'first_name') return (sel?.guest_name ?? '').split(' ')[0] ?? '';
|
|
|
|
|
+ if (key === 'wifi_key') return sel?.wifi_key ?? '';
|
|
|
|
|
+ if (key === 'wifi_ssid') return sel?.wifi_ssid ?? '';
|
|
|
|
|
+ if (key === 'door_code') return sel?.door_code ?? '';
|
|
|
|
|
+ if (key === 'checkin') return fmtDate_(checkin, fmt);
|
|
|
|
|
+ if (key === 'checkout') return fmtDate_(checkout, fmt);
|
|
|
|
|
+ if (key === 'reservation_id') return resId;
|
|
|
|
|
+ return '';
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
function fmtChatTime(ts) {
|
|
function fmtChatTime(ts) {
|
|
|
if (!ts) return '';
|
|
if (!ts) return '';
|
|
|
const n = Number(ts);
|
|
const n = Number(ts);
|
|
@@ -1938,6 +1990,7 @@ createApp({
|
|
|
nights: span.nights,
|
|
nights: span.nights,
|
|
|
chat_id: span.chat_id,
|
|
chat_id: span.chat_id,
|
|
|
wifi_key: span.wifi_key,
|
|
wifi_key: span.wifi_key,
|
|
|
|
|
+ wifi_ssid: span.wifi_ssid,
|
|
|
door_code: span.door_code,
|
|
door_code: span.door_code,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -1966,6 +2019,7 @@ createApp({
|
|
|
nights,
|
|
nights,
|
|
|
chat_id: res.chat_id ?? null,
|
|
chat_id: res.chat_id ?? null,
|
|
|
wifi_key: res.wifi_key ?? null,
|
|
wifi_key: res.wifi_key ?? null,
|
|
|
|
|
+ wifi_ssid: res.wifi_ssid ?? null,
|
|
|
door_code: res.door_code ?? null,
|
|
door_code: res.door_code ?? null,
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
@@ -2439,6 +2493,11 @@ createApp({
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
apiFetch(`${API_BASE}/services`).then(r => r?.json()).then(v => { if (v) services.value = v; }).catch(() => {});
|
|
apiFetch(`${API_BASE}/services`).then(r => r?.json()).then(v => { if (v) services.value = v; }).catch(() => {});
|
|
|
|
|
+ apiFetch(`${API_BASE}/chat/templates`).then(r => r?.json()).then(v => {
|
|
|
|
|
+ if (!v) return;
|
|
|
|
|
+ if (v.templates) chatTemplates.value = v.templates;
|
|
|
|
|
+ if (v.default_datetime_format) chatTemplatesDtFmt.value = v.default_datetime_format;
|
|
|
|
|
+ }).catch(() => {});
|
|
|
fetchMonth();
|
|
fetchMonth();
|
|
|
});
|
|
});
|
|
|
|
|
|
|
@@ -2453,8 +2512,8 @@ createApp({
|
|
|
bulkStatusSummary, bulkActiveDiscountType, bulkToggleClosed,
|
|
bulkStatusSummary, bulkActiveDiscountType, bulkToggleClosed,
|
|
|
CAT, DAYS,
|
|
CAT, DAYS,
|
|
|
chatThread, chatLoading, chatError, chatReply, chatSending, chatSendError, chatSendReady,
|
|
chatThread, chatLoading, chatError, chatReply, chatSending, chatSendError, chatSendReady,
|
|
|
- selectionSupportsChat, visibleChatMessages, chatSenderLabel, chatMessagesEl,
|
|
|
|
|
- loadChat, sendChatMessage, fmtChatTime,
|
|
|
|
|
|
|
+ chatTemplates, selectionSupportsChat, visibleChatMessages, chatSenderLabel, chatMessagesEl,
|
|
|
|
|
+ loadChat, sendChatMessage, fmtChatTime, applyTemplate,
|
|
|
runHealthCheck, toggleSync, toggleBulk, fetchDiff, fixDay, fixResChanges, fixBusy, fixDayBusy, fixMonthBusy, FIX,
|
|
runHealthCheck, toggleSync, toggleBulk, fetchDiff, fixDay, fixResChanges, fixBusy, fixDayBusy, fixMonthBusy, FIX,
|
|
|
monthCats, fixMonth, fixMonthFeeds, fixMonthScripts, reloadCaches,
|
|
monthCats, fixMonth, fixMonthFeeds, fixMonthScripts, reloadCaches,
|
|
|
toggleClosed, commitPrice, switchDiscountType, commitDiscount,
|
|
toggleClosed, commitPrice, switchDiscountType, commitDiscount,
|