dev.html 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>room_cal dev</title>
  5. <style>
  6. body { font-family: monospace; font-size: 13px; max-width: 860px; margin: 20px auto; padding: 0 16px; }
  7. h1 { font-size: 1.1em; margin-bottom: 16px; }
  8. h2 { font-size: 1em; border-bottom: 1px solid #bbb; padding-bottom: 2px; margin: 20px 0 8px; }
  9. .row { margin: 4px 0; display: flex; align-items: center; gap: 6px; }
  10. label.lbl { width: 110px; flex-shrink: 0; color: #555; }
  11. input[type=text], input[type=number], input[type=date], select {
  12. font-family: monospace; font-size: 12px; padding: 2px 4px; border: 1px solid #bbb;
  13. }
  14. input[type=date] { width: 130px; }
  15. input[type=number] { width: 90px; }
  16. input[type=text] { width: 160px; }
  17. select { padding: 2px; }
  18. button {
  19. font-family: monospace; font-size: 12px; cursor: pointer;
  20. padding: 2px 8px; border: 1px solid #888; background: #f0f0f0;
  21. }
  22. button:hover { background: #e0e0e0; }
  23. button.del { border-color: #c00; color: #c00; }
  24. button.del:hover { background: #fee; }
  25. .result {
  26. background: #f7f7f7; border: 1px solid #ddd; padding: 6px 8px;
  27. margin-top: 6px; white-space: pre-wrap; font-size: 12px;
  28. max-height: 280px; overflow: auto; display: none;
  29. }
  30. .result.show { display: block; }
  31. .result.err { border-color: #c00; background: #fff0f0; color: #900; }
  32. .cb-group { display: flex; gap: 12px; flex-wrap: wrap; }
  33. .cb-group label { width: auto; display: flex; align-items: center; gap: 3px; cursor: pointer; }
  34. .dash { color: #888; }
  35. #base-url { width: 240px; }
  36. .section-note { color: #888; font-size: 11px; margin-left: 4px; }
  37. </style>
  38. </head>
  39. <body>
  40. <h1>room_cal API dev</h1>
  41. <div class="row">
  42. <label class="lbl">base URL</label>
  43. <input id="base-url" type="text">
  44. </div>
  45. <!-- ── SYSTEM ───────────────────────────────────── -->
  46. <h2>System</h2>
  47. <div class="row">
  48. <button onclick="doReload()">POST /reload</button>
  49. <span class="section-note">flush in-memory CalDAV event caches</span>
  50. </div>
  51. <div class="result" id="res-reload"></div>
  52. <!-- ── HEALTH ───────────────────────────────────── -->
  53. <h2>Health</h2>
  54. <div class="row">
  55. <button onclick="doHealth()">GET /health</button>
  56. <span class="section-note">CalDAV connection (Docker probe, unauthenticated)</span>
  57. </div>
  58. <div class="result" id="res-health"></div>
  59. <div class="row" style="margin-top:4px">
  60. <button onclick="doStatus()">GET /status</button>
  61. <span class="section-note">full status including credential probes (authenticated)</span>
  62. </div>
  63. <div class="result" id="res-status"></div>
  64. <!-- ── SERVICES ───────────────────────────────────── -->
  65. <h2>Services</h2>
  66. <div class="row">
  67. <button onclick="doServices()">GET /services</button>
  68. </div>
  69. <div class="result" id="res-services"></div>
  70. <!-- ── CALC ──────────────────────────────────────── -->
  71. <h2>Calc</h2>
  72. <div class="row">
  73. <label class="lbl">transform</label>
  74. <select id="calc-transform">
  75. <option>net_to_list</option><option>list_to_net</option>
  76. <option>list_to_retail</option><option>retail_to_list</option>
  77. <option>net_to_retail</option><option>retail_to_net</option>
  78. </select>
  79. </div>
  80. <div class="row">
  81. <label class="lbl">service</label>
  82. <select id="calc-service"></select>
  83. </div>
  84. <div class="row">
  85. <label class="lbl">amount</label>
  86. <input id="calc-amount" type="number" value="100">
  87. <button onclick="doCalc()">GET</button>
  88. </div>
  89. <div class="result" id="res-calc"></div>
  90. <!-- ── PRICES ─────────────────────────────────────── -->
  91. <h2>Prices</h2>
  92. <div class="row">
  93. <label class="lbl">date</label>
  94. <input id="price-d1" type="date">
  95. <span class="dash">–</span>
  96. <input id="price-d2" type="date" title="end date for range (leave blank for single day)">
  97. <button onclick="doGetPrice()">GET</button>
  98. <button onclick="doPostPrice()">POST</button>
  99. <span class="section-note">POST uses start date only</span>
  100. </div>
  101. <div class="row">
  102. <label class="lbl">net value</label>
  103. <input id="price-value" type="number" step="0.01" placeholder="e.g. 120">
  104. </div>
  105. <div class="result" id="res-prices"></div>
  106. <!-- ── RESERVATIONS ───────────────────────────────── -->
  107. <h2>Reservations</h2>
  108. <div class="row">
  109. <label class="lbl">date</label>
  110. <input id="rsv-d1" type="date">
  111. <span class="dash">–</span>
  112. <input id="rsv-d2" type="date" title="end date for range">
  113. <button onclick="doGetReservation()">GET</button>
  114. <span class="section-note">GET uses date range</span>
  115. </div>
  116. <div class="row">
  117. <label class="lbl">uid</label>
  118. <input id="rsv-uid" type="text" placeholder="reservation uid">
  119. </div>
  120. <div class="row">
  121. <label class="lbl">checkin_date</label>
  122. <input id="rsv-checkin" type="date" title="required for PATCH">
  123. </div>
  124. <div class="row">
  125. <label class="lbl">door_code</label>
  126. <input id="rsv-door-code" type="text" placeholder="4-digit code">
  127. </div>
  128. <div class="row">
  129. <label class="lbl">wifi_key</label>
  130. <input id="rsv-wifi-key" type="text" placeholder="wifi password">
  131. </div>
  132. <div class="row">
  133. <label class="lbl">wifi_ssid</label>
  134. <input id="rsv-wifi-ssid" type="text" placeholder="network name">
  135. </div>
  136. <div class="row">
  137. <button onclick="doPatchReservation()">PATCH /reservation/:uid</button>
  138. <span class="section-note">only non-empty fields are sent</span>
  139. </div>
  140. <div class="result" id="res-reservations"></div>
  141. <!-- ── CLOSED ─────────────────────────────────────── -->
  142. <h2>Closed</h2>
  143. <div class="row">
  144. <label class="lbl">date</label>
  145. <input id="cls-d1" type="date">
  146. <span class="dash">–</span>
  147. <input id="cls-d2" type="date" title="end date for range">
  148. <button onclick="doGetClosed()">GET</button>
  149. <button onclick="doPostClosed()">POST</button>
  150. <button class="del" onclick="doDeleteClosed()">DELETE</button>
  151. <span class="section-note">POST/DELETE use start date only</span>
  152. </div>
  153. <div class="row">
  154. <label class="lbl">services</label>
  155. <div class="cb-group" id="closed-checks"></div>
  156. </div>
  157. <div class="result" id="res-closed"></div>
  158. <!-- ── DISCOUNTS ──────────────────────────────────── -->
  159. <h2>Discounts</h2>
  160. <div class="row">
  161. <label class="lbl">date</label>
  162. <input id="disc-d1" type="date">
  163. <span class="dash">–</span>
  164. <input id="disc-d2" type="date" title="end date for range">
  165. <button onclick="doGetDiscount()">GET</button>
  166. <button onclick="doPostDiscount()">POST</button>
  167. <button class="del" onclick="doDeleteDiscount()">DELETE</button>
  168. <span class="section-note">POST/DELETE use start date only</span>
  169. </div>
  170. <div class="row">
  171. <label class="lbl">type</label>
  172. <select id="disc-type"><option value="percent">percent</option><option value="flat">flat</option></select>
  173. <label class="lbl" style="width:auto;margin-left:8px">value</label>
  174. <input id="disc-value" type="number" step="0.01" placeholder="e.g. 10">
  175. </div>
  176. <div class="result" id="res-discounts"></div>
  177. <!-- ── WORKFLOWS ───────────────────────────────────── -->
  178. <h2>Workflows <span class="section-note">(awaits local write, backgrounds push)</span></h2>
  179. <div class="row">
  180. <label class="lbl">date</label>
  181. <input id="wf-d1" type="date">
  182. <span class="dash">–</span>
  183. <input id="wf-d2" type="date" title="end date for range workflows">
  184. </div>
  185. <div class="row">
  186. <label class="lbl">price</label>
  187. <input id="wf-price-value" type="number" step="0.01" placeholder="net value">
  188. <button onclick="doWfSetPrice()">POST /price</button>
  189. </div>
  190. <div class="row">
  191. <label class="lbl">discount</label>
  192. <select id="wf-disc-type"><option value="percent">percent</option><option value="flat">flat</option></select>
  193. <input id="wf-disc-value" type="number" step="0.01" placeholder="value" style="margin-left:4px">
  194. <button onclick="doWfSetDiscount()">POST /discount</button>
  195. <button class="del" onclick="doWfDeleteDiscount()">DELETE /discount</button>
  196. </div>
  197. <div class="row">
  198. <label class="lbl">closed</label>
  199. <div class="cb-group" id="wf-closed-checks"></div>
  200. <button onclick="doWfSetClosed()">POST /closed</button>
  201. <button class="del" onclick="doWfDeleteClosed()">DELETE /closed</button>
  202. <button onclick="doWfSetClosedRange()">POST /closed range</button>
  203. <span class="section-note">range uses both dates</span>
  204. </div>
  205. <div class="row">
  206. <label class="lbl">sync range</label>
  207. <button onclick="doWfReservations()">POST /reservations</button>
  208. <button onclick="doWfIcalFeeds()">POST /ical-feeds</button>
  209. <button onclick="doWfUserCal()">POST /user-cal</button>
  210. <span class="section-note">uses date range above</span>
  211. </div>
  212. <div class="result" id="res-workflows"></div>
  213. <!-- ── ACTIONS ────────────────────────────────────── -->
  214. <h2>Actions</h2>
  215. <div class="row">
  216. <label class="lbl">action</label>
  217. <select id="action-name" onchange="renderActionKwargs()">
  218. <option>fetch-reservations</option><option>fetch-closed</option>
  219. <option>fetch-user-cal</option><option>push-closed</option>
  220. <option>fetch-feed</option><option>fetch-prices</option>
  221. <option>push-prices</option><option>fill-prices</option>
  222. <option>apply-discounts</option><option>apply-rules</option>
  223. <option>local-refresh</option><option>local-sync</option>
  224. <option>push-busy</option><option>diff</option>
  225. </select>
  226. </div>
  227. <div class="row">
  228. <label class="lbl">start date</label>
  229. <input id="action-d1" type="date">
  230. <span class="dash">–</span>
  231. <input id="action-d2" type="date" title="end date (optional)">
  232. </div>
  233. <div id="action-kwargs"></div>
  234. <div class="row">
  235. <button onclick="doAction()">POST</button>
  236. </div>
  237. <div class="result" id="res-actions"></div>
  238. <!-- ── CHAT ──────────────────────────────────────── -->
  239. <h2>Chat <span class="section-note">(service-specific messaging)</span></h2>
  240. <div class="row">
  241. <button onclick="doChatTemplates()">GET /chat/templates</button>
  242. </div>
  243. <div class="result" id="res-chat-templates"></div>
  244. <div class="row" style="margin-top:8px">
  245. <label class="lbl">service</label>
  246. <select id="chat-service"></select>
  247. <button onclick="doChatReady()" style="margin-left:6px">GET ready</button>
  248. </div>
  249. <div class="result" id="res-chat-ready"></div>
  250. <div class="row" style="margin-top:8px">
  251. <label class="lbl">chat_id</label>
  252. <input id="chat-id" type="text" placeholder="thread / reservation id">
  253. <button onclick="doChatGet()">GET thread</button>
  254. </div>
  255. <div class="row">
  256. <label class="lbl">message</label>
  257. <input id="chat-body" type="text" placeholder="message text" style="width:260px">
  258. <button onclick="doChatPost()">POST send</button>
  259. </div>
  260. <div class="result" id="res-chat"></div>
  261. <!-- ── SMS ──────────────────────────────────────── -->
  262. <h2>SMS Challenge</h2>
  263. <div class="row">
  264. <label class="lbl">code</label>
  265. <input id="sms-code" type="text" placeholder="e.g. 123456" style="width:120px">
  266. <button onclick="doSmsPost()">POST /sms/code</button>
  267. <span class="section-note">deliver a code to whoever is polling sms_code.txt</span>
  268. </div>
  269. <div class="result" id="res-sms"></div>
  270. <!-- ── CREDENTIALS ────────────────────────────────── -->
  271. <h2>Credentials</h2>
  272. <div class="row">
  273. <button onclick="doCredProbeAll()">GET /credentials</button>
  274. <span class="section-note">live session probe for all services</span>
  275. </div>
  276. <div class="result" id="res-cred-probe-all"></div>
  277. <div class="row" style="margin-top:8px">
  278. <label class="lbl">service</label>
  279. <select id="cred-service"></select>
  280. <button onclick="doCredProbe()">GET</button>
  281. <span class="section-note">probe single service</span>
  282. </div>
  283. <div class="result" id="res-cred-probe"></div>
  284. <div class="row" style="margin-top:8px">
  285. <button onclick="doCredRefreshAll()">POST /credentials</button>
  286. <span class="section-note">trigger automatic refresh for all services (may take minutes)</span>
  287. </div>
  288. <div class="row" style="margin-top:4px">
  289. <label class="lbl">service</label>
  290. <select id="cred-refresh-service"></select>
  291. <button onclick="doCredRefresh()">POST refresh</button>
  292. <span class="section-note">trigger refresh for one service</span>
  293. </div>
  294. <div class="result" id="res-cred-refresh"></div>
  295. <div id="cred-fields" style="margin-top:8px">
  296. <div class="row cred-row">
  297. <label class="lbl" style="width:60px">key</label>
  298. <input class="cred-key" type="text" placeholder="e.g. token">
  299. <label class="lbl" style="width:40px">val</label>
  300. <input class="cred-val" type="text" placeholder="value" style="width:220px">
  301. <button onclick="addCredRow(this)">+</button>
  302. </div>
  303. </div>
  304. <div class="row" style="margin-top:6px">
  305. <button onclick="doCredPut()">PUT</button>
  306. </div>
  307. <div class="result" id="res-credentials"></div>
  308. <script>
  309. const API_BASE = '';
  310. document.getElementById('base-url').value = API_BASE;
  311. const ACTION_KWARGS = {
  312. 'fetch-feed': [['feed_name','text','feed name'],['title','text','event title (optional)']],
  313. 'fetch-prices': [['service','text','service key (optional)']],
  314. 'fill-prices': [['weekday','number','weekday net price'],['weekend','number','weekend net price']],
  315. 'apply-rules': [['pipeline_name','text','pipeline name']],
  316. };
  317. let servicesList = [];
  318. function base() {
  319. return document.getElementById('base-url').value.replace(/\/+$/, '');
  320. }
  321. async function api(method, path, body) {
  322. const opts = { method, headers: {} };
  323. if (body !== undefined) {
  324. opts.headers['Content-Type'] = 'application/json';
  325. opts.body = JSON.stringify(body);
  326. }
  327. try {
  328. const r = await fetch(base() + path, opts);
  329. if (r.status === 204) return { ok: true, status: 204, data: '204 No Content' };
  330. const text = await r.text();
  331. let data;
  332. try { data = JSON.parse(text); } catch { data = text; }
  333. return { ok: r.ok, status: r.status, data };
  334. } catch (e) {
  335. return { ok: false, status: 0, data: String(e) };
  336. }
  337. }
  338. function pending(id) {
  339. const el = document.getElementById(id);
  340. el.className = 'result show';
  341. el.textContent = '⋯';
  342. }
  343. function show(id, result) {
  344. const el = document.getElementById(id);
  345. el.className = 'result show' + (result.ok ? '' : ' err');
  346. const d = result.data;
  347. const body = typeof d === 'string' ? d : JSON.stringify(d, null, 2);
  348. el.textContent = result.ok ? body : `HTTP ${result.status}: ${body}`;
  349. }
  350. function dateArg(d1id, d2id) {
  351. const s = document.getElementById(d1id).value;
  352. const e = document.getElementById(d2id).value;
  353. if (!s) return null;
  354. return (e && e !== s) ? `${s}/${e}` : s;
  355. }
  356. // ── reload ──
  357. async function doReload() {
  358. pending('res-reload');
  359. show('res-reload', await api('POST', '/reload'));
  360. }
  361. // ── health ──
  362. async function doHealth() {
  363. pending('res-health');
  364. show('res-health', await api('GET', '/health'));
  365. }
  366. async function doStatus() {
  367. pending('res-status');
  368. show('res-status', await api('GET', '/status'));
  369. }
  370. // ── services ──
  371. async function doServices() {
  372. pending('res-services');
  373. const r = await api('GET', '/services');
  374. show('res-services', r);
  375. if (r.ok && Array.isArray(r.data)) {
  376. servicesList = r.data;
  377. populateDropdowns();
  378. renderClosedChecks();
  379. }
  380. }
  381. function populateDropdowns() {
  382. ['calc-service', 'cred-service', 'cred-refresh-service', 'chat-service'].forEach(id => {
  383. const el = document.getElementById(id);
  384. const prev = el.value;
  385. el.innerHTML = servicesList.map(s =>
  386. `<option value="${s.key}">${s.key} — ${s.name}</option>`
  387. ).join('');
  388. if (prev) el.value = prev;
  389. });
  390. }
  391. function renderClosedChecks(checked = []) {
  392. ['closed-checks', 'wf-closed-checks'].forEach(groupId => {
  393. const el = document.getElementById(groupId);
  394. if (!el) return;
  395. el.innerHTML = servicesList.map(s =>
  396. `<label><input type="checkbox" value="${s.key}" id="${groupId}-${s.key}"${checked.includes(s.key) ? ' checked' : ''}> ${s.name}</label>`
  397. ).join('');
  398. });
  399. }
  400. function getCheckedServices() {
  401. return Array.from(document.querySelectorAll('#closed-checks input:checked')).map(c => c.value);
  402. }
  403. function getWfCheckedServices() {
  404. return Array.from(document.querySelectorAll('#wf-closed-checks input:checked')).map(c => c.value);
  405. }
  406. // ── calc ──
  407. async function doCalc() {
  408. const t = document.getElementById('calc-transform').value;
  409. const s = document.getElementById('calc-service').value;
  410. const a = document.getElementById('calc-amount').value;
  411. if (!s || !a) return;
  412. pending('res-calc');
  413. show('res-calc', await api('GET', `/calc/${t}?service=${encodeURIComponent(s)}&amount=${a}`));
  414. }
  415. // ── prices ──
  416. async function doGetPrice() {
  417. const d1 = document.getElementById('price-d1').value;
  418. const d2 = document.getElementById('price-d2').value || d1;
  419. if (!d1) return;
  420. pending('res-prices');
  421. const r = await api('GET', `/price/${d1}/${d2}`);
  422. show('res-prices', r);
  423. if (r.ok && r.data && r.data[d1] && r.data[d1].net !== undefined) {
  424. document.getElementById('price-value').value = r.data[d1].net;
  425. }
  426. }
  427. async function doPostPrice() {
  428. const d = document.getElementById('price-d1').value;
  429. if (!d) return;
  430. const v = document.getElementById('price-value').value;
  431. if (!v) return;
  432. pending('res-prices');
  433. show('res-prices', await api('POST', `/price/${d}`, { value: parseFloat(v) }));
  434. }
  435. // ── reservations ──
  436. async function doGetReservation() {
  437. const range = dateArg('rsv-d1', 'rsv-d2');
  438. if (!range) return;
  439. pending('res-reservations');
  440. show('res-reservations', await api('GET', `/reservation/${range}`));
  441. }
  442. async function doPatchReservation() {
  443. const uid = document.getElementById('rsv-uid').value.trim();
  444. if (!uid) return;
  445. const checkin_date = document.getElementById('rsv-checkin').value;
  446. if (!checkin_date) return;
  447. const body = { checkin_date };
  448. const door_code = document.getElementById('rsv-door-code').value.trim();
  449. const wifi_key = document.getElementById('rsv-wifi-key').value.trim();
  450. const wifi_ssid = document.getElementById('rsv-wifi-ssid').value.trim();
  451. if (door_code) body.door_code = door_code;
  452. if (wifi_key) body.wifi_key = wifi_key;
  453. if (wifi_ssid) body.wifi_ssid = wifi_ssid;
  454. pending('res-reservations');
  455. show('res-reservations', await api('PATCH', `/reservation/${uid}`, body));
  456. }
  457. // ── closed ──
  458. async function doGetClosed() {
  459. const range = dateArg('cls-d1', 'cls-d2');
  460. if (!range) return;
  461. pending('res-closed');
  462. const r = await api('GET', `/closed/${range}`);
  463. show('res-closed', r);
  464. if (r.ok && Array.isArray(r.data)) {
  465. renderClosedChecks(r.data);
  466. }
  467. }
  468. async function doPostClosed() {
  469. const d = document.getElementById('cls-d1').value;
  470. if (!d) return;
  471. pending('res-closed');
  472. show('res-closed', await api('POST', `/closed/${d}`, { services: getCheckedServices() }));
  473. }
  474. async function doDeleteClosed() {
  475. const d = document.getElementById('cls-d1').value;
  476. if (!d) return;
  477. pending('res-closed');
  478. show('res-closed', await api('DELETE', `/closed/${d}`));
  479. }
  480. // ── discounts ──
  481. async function doGetDiscount() {
  482. const range = dateArg('disc-d1', 'disc-d2');
  483. if (!range) return;
  484. pending('res-discounts');
  485. const r = await api('GET', `/discount/${range}`);
  486. show('res-discounts', r);
  487. if (r.ok && r.data && r.data.type) {
  488. document.getElementById('disc-type').value = r.data.type;
  489. document.getElementById('disc-value').value = r.data.value ?? '';
  490. }
  491. }
  492. async function doPostDiscount() {
  493. const d = document.getElementById('disc-d1').value;
  494. if (!d) return;
  495. const type = document.getElementById('disc-type').value;
  496. const value = document.getElementById('disc-value').value;
  497. if (!value) return;
  498. pending('res-discounts');
  499. show('res-discounts', await api('POST', `/discount/${d}`, { type, value: parseFloat(value) }));
  500. }
  501. async function doDeleteDiscount() {
  502. const d = document.getElementById('disc-d1').value;
  503. if (!d) return;
  504. pending('res-discounts');
  505. show('res-discounts', await api('DELETE', `/discount/${d}`));
  506. }
  507. // ── workflows ──
  508. async function doWfSetPrice() {
  509. const d = document.getElementById('wf-d1').value;
  510. if (!d) return;
  511. const v = document.getElementById('wf-price-value').value;
  512. if (!v) return;
  513. pending('res-workflows');
  514. show('res-workflows', await api('POST', `/workflow/price/${d}`, { value: parseFloat(v) }));
  515. }
  516. async function doWfSetDiscount() {
  517. const d = document.getElementById('wf-d1').value;
  518. if (!d) return;
  519. const type = document.getElementById('wf-disc-type').value;
  520. const value = document.getElementById('wf-disc-value').value;
  521. if (!value) return;
  522. pending('res-workflows');
  523. show('res-workflows', await api('POST', `/workflow/discount/${d}`, { type, value: parseFloat(value) }));
  524. }
  525. async function doWfDeleteDiscount() {
  526. const d = document.getElementById('wf-d1').value;
  527. if (!d) return;
  528. pending('res-workflows');
  529. show('res-workflows', await api('DELETE', `/workflow/discount/${d}`));
  530. }
  531. async function doWfSetClosed() {
  532. const d = document.getElementById('wf-d1').value;
  533. if (!d) return;
  534. pending('res-workflows');
  535. show('res-workflows', await api('POST', `/workflow/closed/${d}`, { services: getWfCheckedServices() }));
  536. }
  537. async function doWfDeleteClosed() {
  538. const d = document.getElementById('wf-d1').value;
  539. if (!d) return;
  540. pending('res-workflows');
  541. show('res-workflows', await api('DELETE', `/workflow/closed/${d}`));
  542. }
  543. async function doWfSetClosedRange() {
  544. const r = wfRange();
  545. if (!r) return;
  546. pending('res-workflows');
  547. show('res-workflows', await api('POST', `/workflow/closed/${r.d1}/${r.d2}`, { services: getWfCheckedServices() }));
  548. }
  549. function wfRange() {
  550. const d1 = document.getElementById('wf-d1').value;
  551. const d2 = document.getElementById('wf-d2').value || d1;
  552. return d1 ? { d1, d2 } : null;
  553. }
  554. async function doWfReservations() {
  555. const r = wfRange();
  556. if (!r) return;
  557. pending('res-workflows');
  558. show('res-workflows', await api('POST', `/workflow/reservations/${r.d1}/${r.d2}`));
  559. }
  560. async function doWfIcalFeeds() {
  561. const r = wfRange();
  562. if (!r) return;
  563. pending('res-workflows');
  564. show('res-workflows', await api('POST', `/workflow/ical-feeds/${r.d1}/${r.d2}`));
  565. }
  566. async function doWfUserCal() {
  567. const r = wfRange();
  568. if (!r) return;
  569. pending('res-workflows');
  570. show('res-workflows', await api('POST', `/workflow/user-cal/${r.d1}/${r.d2}`));
  571. }
  572. // ── actions ──
  573. function renderActionKwargs() {
  574. const action = document.getElementById('action-name').value;
  575. const kwargs = ACTION_KWARGS[action] || [];
  576. document.getElementById('action-kwargs').innerHTML = kwargs.map(([key, type, ph]) =>
  577. `<div class="row"><label class="lbl">${key}</label><input id="akw-${key}" type="${type}" placeholder="${ph}" style="width:200px"></div>`
  578. ).join('');
  579. }
  580. async function doAction() {
  581. const action = document.getElementById('action-name').value;
  582. const range = dateArg('action-d1', 'action-d2');
  583. if (!range) return;
  584. const kwargs = {};
  585. (ACTION_KWARGS[action] || []).forEach(([key]) => {
  586. const el = document.getElementById(`akw-${key}`);
  587. if (el && el.value) kwargs[key] = el.value;
  588. });
  589. pending('res-actions');
  590. show('res-actions', await api('POST', `/action/${action}/${range}`, kwargs));
  591. }
  592. // ── chat ──
  593. async function doChatTemplates() {
  594. pending('res-chat-templates');
  595. show('res-chat-templates', await api('GET', '/chat/templates'));
  596. }
  597. async function doChatReady() {
  598. const service = document.getElementById('chat-service').value;
  599. if (!service) return;
  600. pending('res-chat-ready');
  601. show('res-chat-ready', await api('GET', `/chat/${service}/ready`));
  602. }
  603. async function doChatGet() {
  604. const service = document.getElementById('chat-service').value;
  605. const chatId = document.getElementById('chat-id').value.trim();
  606. if (!service || !chatId) return;
  607. pending('res-chat');
  608. show('res-chat', await api('GET', `/chat/${service}/${chatId}`));
  609. }
  610. async function doChatPost() {
  611. const service = document.getElementById('chat-service').value;
  612. const chatId = document.getElementById('chat-id').value.trim();
  613. const body = document.getElementById('chat-body').value.trim();
  614. if (!service || !chatId || !body) return;
  615. pending('res-chat');
  616. show('res-chat', await api('POST', `/chat/${service}/${chatId}`, { body }));
  617. }
  618. // ── sms ──
  619. async function doSmsPost() {
  620. const code = document.getElementById('sms-code').value.trim();
  621. if (!code) return;
  622. pending('res-sms');
  623. show('res-sms', await api('POST', '/sms/code', { code }));
  624. }
  625. // ── credentials ──
  626. async function doCredRefreshAll() {
  627. pending('res-cred-refresh');
  628. show('res-cred-refresh', await api('POST', '/credentials', {}));
  629. }
  630. async function doCredRefresh() {
  631. const service = document.getElementById('cred-refresh-service').value;
  632. if (!service) return;
  633. pending('res-cred-refresh');
  634. show('res-cred-refresh', await api('POST', '/credentials', { service }));
  635. }
  636. async function doCredProbeAll() {
  637. pending('res-cred-probe-all');
  638. show('res-cred-probe-all', await api('GET', '/credentials'));
  639. }
  640. async function doCredProbe() {
  641. const service = document.getElementById('cred-service').value;
  642. if (!service) return;
  643. pending('res-cred-probe');
  644. show('res-cred-probe', await api('GET', `/credentials/${service}`));
  645. }
  646. function addCredRow(btn) {
  647. const row = document.createElement('div');
  648. row.className = 'row cred-row';
  649. row.innerHTML = `<label class="lbl" style="width:60px">key</label><input class="cred-key" type="text" placeholder="key"><label class="lbl" style="width:40px">val</label><input class="cred-val" type="text" placeholder="value" style="width:220px"><button onclick="this.parentElement.remove()">−</button>`;
  650. btn.closest('#cred-fields').appendChild(row);
  651. }
  652. async function doCredPut() {
  653. const service = document.getElementById('cred-service').value;
  654. if (!service) return;
  655. const creds = {};
  656. document.querySelectorAll('.cred-row').forEach(row => {
  657. const k = row.querySelector('.cred-key').value.trim();
  658. const v = row.querySelector('.cred-val').value;
  659. if (k) creds[k] = v;
  660. });
  661. pending('res-credentials');
  662. show('res-credentials', await api('PUT', `/credentials/${service}`, creds));
  663. }
  664. // ── init ──
  665. const today = new Date().toISOString().slice(0, 10);
  666. ['price-d1','rsv-d1','cls-d1','disc-d1','wf-d1','action-d1'].forEach(id => {
  667. document.getElementById(id).value = today;
  668. });
  669. const nextMonth = new Date(); nextMonth.setDate(nextMonth.getDate() + 30);
  670. document.getElementById('wf-d2').value = nextMonth.toISOString().slice(0, 10);
  671. renderActionKwargs();
  672. doServices();
  673. </script>
  674. </body>
  675. </html>