Преглед изворни кода

Add send_ready() to ChatClient; disable chat send UI when not ready

Booking.com returns false until 2FA is implemented; Airbnb returns true.
New GET /chat/{service_key}/ready endpoint exposes readiness to the frontend.
ZiB пре 4 месеци
родитељ
комит
5bf623d190

+ 6 - 0
room_cal/api/_routes/chat.py

@@ -45,6 +45,12 @@ class _SendBody(BaseModel):
   body: str
 
 
+@router.get('/chat/{service_key}/ready')
+async def chat_send_ready(service_key: str):
+  client = _get_chat_client(service_key)
+  return {'ready': client.send_ready()}
+
+
 @router.get('/chat/{service_key}/{chat_id}')
 async def get_thread(service_key: str, chat_id: str):
   client = _get_chat_client(service_key)

+ 3 - 0
room_cal/service_api/_airbnb.py

@@ -374,6 +374,9 @@ class _AirbnbClient:
   def _global_thread_id(self, thread_id: str) -> str:
     return base64.b64encode(f'MessageThread:{thread_id}'.encode()).decode()
 
+  def send_ready(self) -> bool:
+    return True
+
   async def get_thread(self, thread_id: str) -> AirbnbThread:
     """Fetch a conversation thread and its messages by numeric thread ID."""
     if is_stale('airbnb'):

+ 3 - 0
room_cal/service_api/_booking.py

@@ -553,6 +553,9 @@ class _BookingClient:
       for rid, r_data in reservations.items()
     ]
 
+  def send_ready(self) -> bool:
+    return False
+
   async def get_thread(self, reservation_id: str) -> BookingThread:
     """Fetch a conversation thread and its messages by reservation ID."""
     if is_stale('booking'):

+ 1 - 0
room_cal/service_api/_protocols.py

@@ -16,3 +16,4 @@ class ChatClient(ServiceClient, Protocol):
   # chat_id maps to each platform's native thread/conversation identifier
   async def get_thread(self, chat_id: str) -> Any: ...
   async def send_message(self, chat_id: str, body: str) -> None: ...
+  def send_ready(self) -> bool: ...

+ 30 - 14
room_cal/vue/calendar.html

@@ -1296,14 +1296,15 @@
               <textarea
                 v-model="chatReply"
                 class="chat-reply-input"
-                placeholder="Type a reply…"
+                :placeholder="chatSendReady ? 'Type a reply…' : 'Sending not available for this service.'"
+                :disabled="!chatSendReady"
                 rows="6"
               ></textarea>
               <div class="chat-reply-footer">
                 <span v-if="chatSendError" class="chat-send-err">{{ chatSendError }}</span>
                 <button
                   class="chat-send-btn"
-                  :disabled="!chatReply.trim() || chatSending"
+                  :disabled="!chatSendReady || !chatReply.trim() || chatSending"
                   @click="sendChatMessage"
                 >{{ chatSending ? 'Sending…' : 'Send' }}</button>
               </div>
@@ -1412,12 +1413,13 @@ createApp({
     const healthStatus  = ref('idle');
 
     // ── 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 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 syncEnabled   = ref(false);
     const bulkEnabled   = ref(false);
     const bulkDays      = ref(new Set());
@@ -1475,11 +1477,23 @@ 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() {
       const sel = selection.value;
       if (!sel?.chat_id || !sel?.service) return;
       chatLoading.value = true;
       chatError.value = null;
+      loadChatSendReady(sel.service);
       try {
         const resp = await apiFetch(`${API_BASE}/chat/${sel.service}/${sel.chat_id}`);
         if (!resp) { chatLoading.value = false; return; }
@@ -1564,8 +1578,9 @@ createApp({
 
     watch(selection, (sel) => {
       if (sel?.type === 'res' && sel.chat_id && selectionSupportsChat.value) {
-        chatThread.value = null;
-        chatError.value  = null;
+        chatThread.value    = null;
+        chatError.value     = null;
+        chatSendReady.value = true;
         loadChat();
       }
     });
@@ -1908,9 +1923,10 @@ createApp({
     }
 
     function selectReservation(span) {
-      chatThread.value = null;
-      chatError.value  = null;
-      chatReply.value  = '';
+      chatThread.value    = null;
+      chatError.value     = null;
+      chatReply.value     = '';
+      chatSendReady.value = true;
       selection.value = {
         type:           'res',
         key:            span.resKey,
@@ -2436,7 +2452,7 @@ createApp({
       bulkEnabled, bulkDays, bulkSortedKeys, bulkPriceSummary, bulkDiscountSummary,
       bulkStatusSummary, bulkActiveDiscountType, bulkToggleClosed,
       CAT, DAYS,
-      chatThread, chatLoading, chatError, chatReply, chatSending, chatSendError,
+      chatThread, chatLoading, chatError, chatReply, chatSending, chatSendError, chatSendReady,
       selectionSupportsChat, visibleChatMessages, chatSenderLabel, chatMessagesEl,
       loadChat, sendChatMessage, fmtChatTime,
       runHealthCheck, toggleSync, toggleBulk, fetchDiff, fixDay, fixResChanges, fixBusy, fixDayBusy, fixMonthBusy, FIX,