|
@@ -619,6 +619,12 @@
|
|
|
}
|
|
}
|
|
|
.chat-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
.chat-send-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
.chat-send-btn:not(:disabled):hover { opacity: 0.82; }
|
|
.chat-send-btn:not(:disabled):hover { opacity: 0.82; }
|
|
|
|
|
+ .chat-reload-btn {
|
|
|
|
|
+ background: none; border: none; cursor: pointer; font-size: 14px;
|
|
|
|
|
+ color: #6b7280; padding: 0 0 0 6px; line-height: 1; vertical-align: middle;
|
|
|
|
|
+ }
|
|
|
|
|
+ .chat-reload-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
|
|
|
+ .chat-reload-btn:not(:disabled):hover { color: #111; }
|
|
|
|
|
|
|
|
/* ── Res changes card (no-selection state) ───────────────── */
|
|
/* ── Res changes card (no-selection state) ───────────────── */
|
|
|
.res-changes-card {
|
|
.res-changes-card {
|
|
@@ -1324,7 +1330,10 @@
|
|
|
<div v-if="selectionSupportsChat && selection.chat_id" class="panel-card">
|
|
<div v-if="selectionSupportsChat && selection.chat_id" class="panel-card">
|
|
|
<div class="panel-head">
|
|
<div class="panel-head">
|
|
|
<div class="panel-title">Messages</div>
|
|
<div class="panel-title">Messages</div>
|
|
|
- <div class="panel-subtitle" v-if="chatThread">{{ visibleChatMessages.length }} message{{ visibleChatMessages.length !== 1 ? 's' : '' }}</div>
|
|
|
|
|
|
|
+ <div class="panel-subtitle" v-if="chatThread">
|
|
|
|
|
+ {{ visibleChatMessages.length }} message{{ visibleChatMessages.length !== 1 ? 's' : '' }}
|
|
|
|
|
+ <button class="chat-reload-btn" @click="loadChat" :disabled="chatLoading" title="Refresh messages">↺</button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="chatLoading" class="chat-status">Loading…</div>
|
|
<div v-if="chatLoading" class="chat-status">Loading…</div>
|
|
|
<div v-else-if="chatError" class="chat-status chat-error-txt">{{ chatError }}</div>
|
|
<div v-else-if="chatError" class="chat-status chat-error-txt">{{ chatError }}</div>
|
|
@@ -1349,15 +1358,15 @@
|
|
|
<textarea
|
|
<textarea
|
|
|
v-model="chatReply"
|
|
v-model="chatReply"
|
|
|
class="chat-reply-input"
|
|
class="chat-reply-input"
|
|
|
- :placeholder="chatSendReady ? 'Type a reply…' : 'Sending not available for this service.'"
|
|
|
|
|
- :disabled="!chatSendReady"
|
|
|
|
|
|
|
+ placeholder="Type a reply…"
|
|
|
|
|
+ :disabled="chatSending"
|
|
|
rows="6"
|
|
rows="6"
|
|
|
></textarea>
|
|
></textarea>
|
|
|
<div class="chat-reply-footer">
|
|
<div class="chat-reply-footer">
|
|
|
<span v-if="chatSendError" class="chat-send-err">{{ chatSendError }}</span>
|
|
<span v-if="chatSendError" class="chat-send-err">{{ chatSendError }}</span>
|
|
|
<button
|
|
<button
|
|
|
class="chat-send-btn"
|
|
class="chat-send-btn"
|
|
|
- :disabled="!chatSendReady || !chatReply.trim() || chatSending"
|
|
|
|
|
|
|
+ :disabled="!chatReply.trim() || chatSending"
|
|
|
@click="sendChatMessage"
|
|
@click="sendChatMessage"
|
|
|
>{{ chatSending ? 'Sending…' : 'Send' }}</button>
|
|
>{{ chatSending ? 'Sending…' : 'Send' }}</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -1478,7 +1487,6 @@ createApp({
|
|
|
const chatReply = ref('');
|
|
const chatReply = ref('');
|
|
|
const chatSending = ref(false);
|
|
const chatSending = ref(false);
|
|
|
const chatSendError = ref(null);
|
|
const chatSendError = ref(null);
|
|
|
- const chatSendReady = ref(true);
|
|
|
|
|
const chatTemplates = ref({});
|
|
const chatTemplates = ref({});
|
|
|
const chatTemplatesDtFmt = ref('%a, %b %-d @ %-I %p');
|
|
const chatTemplatesDtFmt = ref('%a, %b %-d @ %-I %p');
|
|
|
const syncEnabled = ref(false);
|
|
const syncEnabled = ref(false);
|
|
@@ -1494,10 +1502,8 @@ createApp({
|
|
|
const c = healthChecks.value;
|
|
const c = healthChecks.value;
|
|
|
if (!c) return [];
|
|
if (!c) return [];
|
|
|
const out = [];
|
|
const out = [];
|
|
|
- if (c.caldav_connection && !c.caldav_connection.ok)
|
|
|
|
|
- out.push(`CalDAV: ${c.caldav_connection.reason}`);
|
|
|
|
|
- if (c.calendar && !c.calendar.ok)
|
|
|
|
|
- out.push(`Calendar: ${c.calendar.reason}`);
|
|
|
|
|
|
|
+ if (c.store_connected && !c.store_connected.ok)
|
|
|
|
|
+ out.push(`Store: ${c.store_connected.reason}`);
|
|
|
for (const [key, v] of Object.entries(c.credentials || {}))
|
|
for (const [key, v] of Object.entries(c.credentials || {}))
|
|
|
if (!v.ok) out.push(`${serviceName(key)}: ${v.reason}`);
|
|
if (!v.ok) out.push(`${serviceName(key)}: ${v.reason}`);
|
|
|
return out;
|
|
return out;
|
|
@@ -1538,23 +1544,11 @@ createApp({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async function loadChatSendReady(serviceKey) {
|
|
|
|
|
- try {
|
|
|
|
|
- const resp = await apiFetch(`${API_BASE}/chat/${serviceKey}/ready`);
|
|
|
|
|
- if (!resp) return;
|
|
|
|
|
- const data = await resp.json();
|
|
|
|
|
- chatSendReady.value = data.ready ?? true;
|
|
|
|
|
- } catch (e) {
|
|
|
|
|
- chatSendReady.value = false;
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
async function loadChat() {
|
|
async function loadChat() {
|
|
|
const sel = selection.value;
|
|
const sel = selection.value;
|
|
|
if (!sel?.chat_id || !sel?.service) return;
|
|
if (!sel?.chat_id || !sel?.service) return;
|
|
|
chatLoading.value = true;
|
|
chatLoading.value = true;
|
|
|
chatError.value = null;
|
|
chatError.value = null;
|
|
|
- loadChatSendReady(sel.service);
|
|
|
|
|
try {
|
|
try {
|
|
|
const resp = await apiFetch(`${API_BASE}/chat/${sel.service}/${sel.chat_id}`);
|
|
const resp = await apiFetch(`${API_BASE}/chat/${sel.service}/${sel.chat_id}`);
|
|
|
if (!resp) { chatLoading.value = false; return; }
|
|
if (!resp) { chatLoading.value = false; return; }
|
|
@@ -1574,6 +1568,7 @@ createApp({
|
|
|
if (!body || chatSending.value) return;
|
|
if (!body || chatSending.value) return;
|
|
|
const sel = selection.value;
|
|
const sel = selection.value;
|
|
|
if (!sel?.chat_id || !sel?.service) return;
|
|
if (!sel?.chat_id || !sel?.service) return;
|
|
|
|
|
+ const sendChatId = sel.chat_id;
|
|
|
chatSending.value = true;
|
|
chatSending.value = true;
|
|
|
chatSendError.value = null;
|
|
chatSendError.value = null;
|
|
|
try {
|
|
try {
|
|
@@ -1582,15 +1577,19 @@ createApp({
|
|
|
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body }) }
|
|
{ method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ body }) }
|
|
|
);
|
|
);
|
|
|
if (!resp) { chatSending.value = false; return; }
|
|
if (!resp) { chatSending.value = false; return; }
|
|
|
- if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
|
|
|
|
|
|
+ if (!resp.ok) {
|
|
|
|
|
+ const detail = await resp.json().catch(() => ({}));
|
|
|
|
|
+ throw new Error(detail?.detail || `HTTP ${resp.status}`);
|
|
|
|
|
+ }
|
|
|
chatReply.value = '';
|
|
chatReply.value = '';
|
|
|
- await loadChat();
|
|
|
|
|
|
|
+ await new Promise(r => setTimeout(r, 3000));
|
|
|
|
|
+ if (selection.value?.chat_id === sendChatId) await loadChat();
|
|
|
} catch (e) {
|
|
} catch (e) {
|
|
|
- chatSendError.value = 'Failed to send.';
|
|
|
|
|
- setTimeout(() => { chatSendError.value = null; }, 4000);
|
|
|
|
|
|
|
+ chatSendError.value = e.message || 'Failed to send.';
|
|
|
|
|
+ setTimeout(() => { chatSendError.value = null; }, 6000);
|
|
|
console.error('Chat send error:', e);
|
|
console.error('Chat send error:', e);
|
|
|
} finally {
|
|
} finally {
|
|
|
- chatSending.value = false;
|
|
|
|
|
|
|
+ if (selection.value?.chat_id === sendChatId) chatSending.value = false;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -1696,7 +1695,6 @@ createApp({
|
|
|
if (sel?.type === 'res' && sel.chat_id && selectionSupportsChat.value) {
|
|
if (sel?.type === 'res' && sel.chat_id && selectionSupportsChat.value) {
|
|
|
chatThread.value = null;
|
|
chatThread.value = null;
|
|
|
chatError.value = null;
|
|
chatError.value = null;
|
|
|
- chatSendReady.value = true;
|
|
|
|
|
loadChat();
|
|
loadChat();
|
|
|
}
|
|
}
|
|
|
});
|
|
});
|
|
@@ -2078,7 +2076,6 @@ createApp({
|
|
|
chatThread.value = null;
|
|
chatThread.value = null;
|
|
|
chatError.value = null;
|
|
chatError.value = null;
|
|
|
chatReply.value = '';
|
|
chatReply.value = '';
|
|
|
- chatSendReady.value = true;
|
|
|
|
|
selection.value = {
|
|
selection.value = {
|
|
|
type: 'res',
|
|
type: 'res',
|
|
|
key: span.resKey,
|
|
key: span.resKey,
|
|
@@ -2617,7 +2614,7 @@ createApp({
|
|
|
bulkEnabled, bulkDays, bulkSortedKeys, bulkPriceSummary, bulkDiscountSummary,
|
|
bulkEnabled, bulkDays, bulkSortedKeys, bulkPriceSummary, bulkDiscountSummary,
|
|
|
bulkStatusSummary, bulkActiveDiscountType, bulkToggleClosed,
|
|
bulkStatusSummary, bulkActiveDiscountType, bulkToggleClosed,
|
|
|
CAT, DAYS,
|
|
CAT, DAYS,
|
|
|
- chatThread, chatLoading, chatError, chatReply, chatSending, chatSendError, chatSendReady,
|
|
|
|
|
|
|
+ chatThread, chatLoading, chatError, chatReply, chatSending, chatSendError,
|
|
|
chatTemplates, selectionSupportsChat, visibleChatMessages, chatSenderLabel, chatMessagesEl,
|
|
chatTemplates, selectionSupportsChat, visibleChatMessages, chatSenderLabel, chatMessagesEl,
|
|
|
loadChat, sendChatMessage, fmtChatTime, applyTemplate,
|
|
loadChat, sendChatMessage, fmtChatTime, applyTemplate,
|
|
|
runHealthCheck, toggleSync, toggleBulk, fetchDiff, fixDay, fixResChanges, fixBusy, fixDayBusy, fixMonthBusy, FIX,
|
|
runHealthCheck, toggleSync, toggleBulk, fetchDiff, fixDay, fixResChanges, fixBusy, fixDayBusy, fixMonthBusy, FIX,
|