From 989dd70e1340d697364a55c7ffa7581b8aebd67a Mon Sep 17 00:00:00 2001 From: yihong1120 Date: Tue, 24 Dec 2024 19:16:26 +0800 Subject: [PATCH] Fix form editing errors --- .../frontend/dist/assets/config-Bg4sgpja.css | 1 - .../frontend/dist/assets/config-BheEa4pg.js | 1 + .../frontend/dist/assets/config-BrNUDwsF.css | 1 + .../frontend/dist/assets/config-Dm3IWCdK.js | 73 ----- .../streaming_web/frontend/dist/config.html | 171 ++++++++++- .../streaming_web/frontend/public/config.html | 82 ++++- .../frontend/public/css/config.css | 20 +- .../frontend/public/js/config.js | 290 ++++++++++++------ 8 files changed, 457 insertions(+), 182 deletions(-) delete mode 100644 examples/streaming_web/frontend/dist/assets/config-Bg4sgpja.css create mode 100644 examples/streaming_web/frontend/dist/assets/config-BheEa4pg.js create mode 100644 examples/streaming_web/frontend/dist/assets/config-BrNUDwsF.css delete mode 100644 examples/streaming_web/frontend/dist/assets/config-Dm3IWCdK.js diff --git a/examples/streaming_web/frontend/dist/assets/config-Bg4sgpja.css b/examples/streaming_web/frontend/dist/assets/config-Bg4sgpja.css deleted file mode 100644 index 96a9c6c..0000000 --- a/examples/streaming_web/frontend/dist/assets/config-Bg4sgpja.css +++ /dev/null @@ -1 +0,0 @@ -body{font-family:Roboto,sans-serif;background-color:#f2f2f2;margin:0;padding:20px}.container{max-width:800px;margin:0 auto;background-color:#fff;padding:30px;border-radius:8px;box-shadow:0 0 10px #0000001a}h1{text-align:center;color:#333}button{padding:10px 20px;margin-top:10px;margin-right:10px;font-size:16px;cursor:pointer;background-color:#007bff;color:#fff;border:none;border-radius:5px}button:hover{background-color:#0056b3}.buttons,#form-controls{text-align:center}.config-item{background-color:#fafafa;border:1px solid #ddd;border-radius:8px;padding:20px;margin-bottom:20px}.config-item label{display:block;margin-bottom:15px;font-weight:700}.config-item input[type=text],.config-item input[type=date],.config-item select{width:100%;padding:8px;margin-top:5px;font-size:16px;border:1px solid #ccc;border-radius:4px}.config-item input[type=checkbox]{margin-right:10px}field set{border:1px solid #ccc;border-radius:8px;padding:15px;margin-top:20px}legend{font-weight:700;color:#007bff;padding:0 10px}.config-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.config-header .site-stream{display:flex;align-items:center;gap:10px}.config-header .delete-config-btn{background-color:transparent;color:#ff5c5c;border:none;font-size:24px;cursor:pointer}.config-header .delete-config-btn:hover{color:#e60000}.notification-item{position:relative;border:1px solid #ccc;padding:15px;margin-bottom:15px;border-radius:8px;background-color:#f9f9f9}.notification-item .delete-notification{position:absolute;top:0;right:0;transform:translate(50%,-50%);background:#ff5c5c;border:none;font-size:16px;cursor:pointer;color:#fff;width:24px;height:24px;border-radius:50%;padding:0;line-height:1;box-sizing:border-box;display:flex;align-items:center;justify-content:center}.notification-item .delete-notification:hover{background:#e60000}.notification-item .delete-notification i{color:#fff}.notification-content label{display:block;margin-bottom:10px;font-weight:400}.notification-content input,.notification-content select{width:100%;padding:8px;margin-top:5px;font-size:16px;border:1px solid #ccc;border-radius:4px}.error-message{color:red;font-size:14px;margin-bottom:5px}@media (max-width: 600px){.config-header{flex-direction:column;align-items:flex-start}.config-header .site-stream{flex-direction:column;gap:5px}button{width:100%;margin:10px 0}.buttons,#form-controls{text-align:center}}.error{border:1px solid red!important}button,.delete-notification{transition:background-color .3s,color .3s}input,select{transition:border-color .3s}input:focus,select:focus{border-color:#007bff}.hidden{display:none} diff --git a/examples/streaming_web/frontend/dist/assets/config-BheEa4pg.js b/examples/streaming_web/frontend/dist/assets/config-BheEa4pg.js new file mode 100644 index 0000000..269b3c2 --- /dev/null +++ b/examples/streaming_web/frontend/dist/assets/config-BheEa4pg.js @@ -0,0 +1 @@ +import"./modulepreload-polyfill-B5Qt9EMX.js";const v=document.getElementById("config-container"),D=document.getElementById("edit-btn"),N=document.getElementById("add-config-btn"),M=document.getElementById("save-btn"),j=document.getElementById("cancel-btn"),W=document.getElementById("form-controls");let m=[],k=!1;function y(){return new Date().toISOString().split("T")[0]}async function B(){try{const i=await fetch("/api/config");if(!i.ok)throw new Error("Failed to fetch configuration.");m=(await i.json()).config.map(o=>({...o,notifications:Object.entries(o.notifications).map(([u,t])=>({token:u,language:t}))})),h()}catch(i){console.error(i)}}function $(i){return i.replace(/_/g," ").replace(/\b\w/g,l=>l.toUpperCase())}function g(){const i=v.children;m=Array.from(i).map((l,o)=>{const u=l.querySelectorAll("input, select"),t={notifications:[],detection_items:{}};u.forEach(n=>{const{name:r,type:_}=n;if(r==="line_token"||r==="language"){const c=n.getAttribute("data-notif-index");t.notifications[c]||(t.notifications[c]={token:"",language:"en"}),r==="line_token"?t.notifications[c].token=n.value.trim():t.notifications[c].language=n.value}else r==="expire_date"?t.expire_date=n.value.trim():r==="detect_with_server"?t.detect_with_server=n.checked:r==="store_in_redis"?t.store_in_redis=n.checked:r.startsWith("detect_")?t.detection_items[r]=n.checked:r==="work_start_hour"?t.work_start_hour=parseInt(n.value,10):r==="work_end_hour"?t.work_end_hour=parseInt(n.value,10):r&&(t[r]=n.value.trim())});const e=l.querySelector("input[name='no_expire_date']");return e&&(e.checked?t.expire_date="No Expire Date":(!t.expire_date||t.expire_date==="No Expire Date")&&(t.expire_date=y())),t.notifications=t.notifications.filter(n=>n.token),t.store_in_redis=!!t.store_in_redis,t})}function h(){v.innerHTML="";const i=document.getElementById("config-item-template"),l=document.getElementById("notification-item-template");m.forEach((o,u)=>{const t=i.content.cloneNode(!0),e=t.querySelector(".config-item"),n=e.querySelector("input[name='site']"),r=e.querySelector("input[name='stream_name']"),_=e.querySelector("input[name='video_url']"),c=e.querySelector("select[name='model_key']"),s=e.querySelector("input[name='expire_date']"),p=e.querySelector("input[type='text'][value='No Expire Date']"),A=e.querySelector("input[name='detect_with_server']"),T=e.querySelector("input[name='store_in_redis']"),H=e.querySelectorAll("input[type='checkbox'][name^='detect_']:not([name='detect_with_server']):not([name='store_in_redis'])"),O=e.querySelector("select[name='work_start_hour']"),F=e.querySelector("select[name='work_end_hour']"),S=e.querySelector(".add-notification"),w=e.querySelector(".delete-config-btn"),q=e.querySelector(".expire-date-container");n.value=o.site||"",r.value=o.stream_name||"",_.value=o.video_url||"",c.value=o.model_key||"yolo11n",O.value=o.work_start_hour!==void 0?o.work_start_hour:7,F.value=o.work_end_hour!==void 0?o.work_end_hour:18,o.expire_date==="No Expire Date"?(s.value="",s.style.display="none",p.style.display=""):(s.value=o.expire_date||y(),s.style.display="",p.style.display="none"),A.checked=!!o.detect_with_server,T.checked=!!o.store_in_redis,H.forEach(a=>{const d=a.name;a.checked=!!o.detection_items[d];const f=a.parentElement;f.lastChild.textContent=$(d)});const C=e.querySelector(".notifications-container");if(C.innerHTML="",o.notifications.forEach((a,d)=>{const f=l.content.cloneNode(!0),E=f.querySelector(".notification-item"),b=E.querySelector("input[name='line_token']"),I=E.querySelector("select[name='language']"),L=E.querySelector(".delete-notification");b.value=a.token,I.value=a.language,b.setAttribute("data-notif-index",d),I.setAttribute("data-notif-index",d),k?L.style.display="block":L.style.display="none",C.appendChild(f)}),k){const a=document.createElement("input");a.type="checkbox",a.name="no_expire_date",a.checked=o.expire_date==="No Expire Date",a.id=`no-expire-date-${u}`;const d=document.createElement("label");d.htmlFor=`no-expire-date-${u}`,d.appendChild(a),d.appendChild(document.createTextNode(" No Expire Date")),q.appendChild(document.createElement("br")),q.appendChild(d),a.checked?(s.style.display="none",p.style.display=""):(s.style.display="",p.style.display="none"),a.addEventListener("change",()=>{a.checked?(s.value="",s.style.display="none",p.style.display=""):(s.style.display="",p.style.display="none",(!o.expire_date||o.expire_date==="No Expire Date")&&(s.value=y(),o.expire_date=y()))})}else o.expire_date==="No Expire Date"?(s.style.display="none",p.style.display=""):(s.style.display="",p.style.display="none");k?(w.style.display="block",S.style.display="inline-block"):(w.style.display="none",S.style.display="none"),w.addEventListener("click",()=>{g(),m.splice(u,1),h()}),C.addEventListener("click",a=>{if(a.target.closest(".delete-notification")){const d=a.target.closest(".notification-item"),f=parseInt(d.querySelector("input[name='line_token']").getAttribute("data-notif-index"));g(),m[u].notifications.splice(f,1),h()}}),S.addEventListener("click",()=>{g(),m[u].notifications.push({token:"",language:"en"}),h()}),e.querySelectorAll("input, select").forEach(a=>{k?a.removeAttribute("disabled"):a.setAttribute("disabled","true")}),v.appendChild(t)})}function x(i){k=i,h(),D.classList.toggle("hidden",i),N.classList.toggle("hidden",!i),W.classList.toggle("hidden",!i),i||B()}async function U(){document.querySelectorAll(".error-message").forEach(l=>l.remove()),document.querySelectorAll(".error").forEach(l=>l.classList.remove("error"));let i=!0;try{g();const l=m.map((e,n)=>{const r=v.children[n];if(["site","stream_name","video_url"].forEach(_=>{if(!e[_]){i=!1;const c=r.querySelector(`input[name='${_}']`);if(c&&(c.classList.add("error"),!c.previousElementSibling||!c.previousElementSibling.classList.contains("error-message"))){const s=document.createElement("div");s.className="error-message",s.textContent="This field is required.",c.parentNode.insertBefore(s,c)}}}),e.work_start_hour>=e.work_end_hour){i=!1;const _=r.querySelector("select[name='work_start_hour']"),c=r.querySelector("select[name='work_end_hour']");if(_.classList.add("error"),c.classList.add("error"),!c.previousElementSibling||!c.previousElementSibling.classList.contains("error-message")){const s=document.createElement("div");s.className="error-message",s.textContent="Work Start Hour cannot be greater than or equal to Work End Hour.",c.parentNode.insertBefore(s,c)}}return e.expire_date!=="No Expire Date"&&!e.expire_date&&(e.expire_date=y()),e});if(!i)return;const u=l.map(e=>{const n={};return e.notifications.forEach(r=>{n[r.token]=r.language}),{...e,notifications:n,no_expire_date:void 0}}).map(e=>{const n={...e};return Object.keys(n).forEach(r=>{n[r]===void 0&&delete n[r]}),n});if(!(await fetch("/api/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:u})})).ok)throw new Error("Failed to save configuration.");x(!1)}catch(l){console.error(l)}}D.addEventListener("click",()=>x(!0));j.addEventListener("click",()=>x(!1));M.addEventListener("click",U);N.addEventListener("click",()=>{g();const i=y();m.push({site:"",stream_name:"",video_url:"",model_key:"yolo11n",expire_date:i,detect_with_server:!1,store_in_redis:!1,work_start_hour:7,work_end_hour:18,notifications:[],detection_items:{detect_no_safety_vest_or_helmet:!1,detect_near_machinery_or_vehicle:!1,detect_in_restricted_area:!1}}),h(),x(!0)});document.addEventListener("DOMContentLoaded",B); diff --git a/examples/streaming_web/frontend/dist/assets/config-BrNUDwsF.css b/examples/streaming_web/frontend/dist/assets/config-BrNUDwsF.css new file mode 100644 index 0000000..cd15af9 --- /dev/null +++ b/examples/streaming_web/frontend/dist/assets/config-BrNUDwsF.css @@ -0,0 +1 @@ +body{font-family:Roboto,sans-serif;background-color:#f2f2f2;margin:0;padding:20px}.container{max-width:800px;margin:0 auto;background-color:#fff;padding:30px;border-radius:8px;box-shadow:0 0 10px #0000001a}h1{text-align:center;color:#333}button{padding:10px 20px;margin-top:10px;margin-right:10px;font-size:16px;cursor:pointer;background-color:#007bff;color:#fff;border:none;border-radius:5px}button:hover{background-color:#0056b3}.buttons,#form-controls{text-align:center}.config-item{background-color:#fafafa;border:1px solid #ddd;border-radius:8px;padding:20px;margin-bottom:20px}.config-item label{display:block;margin-bottom:15px;font-weight:700}.config-item input[type=text],.config-item input[type=date],.config-item select{width:100%;padding:8px;margin-top:5px;font-size:16px;border:1px solid #ccc;border-radius:4px}.config-item input[type=checkbox]{margin-right:10px}fieldset{border:1px solid #ccc;border-radius:8px;padding:15px;margin-top:20px}legend{font-weight:700;color:#007bff;padding:0 10px}.config-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.config-header .site-stream{display:flex;align-items:center;gap:10px}.config-header .delete-config-btn{background-color:transparent;color:#ff5c5c;border:none;font-size:24px;cursor:pointer}.config-header .delete-config-btn:hover{color:#e60000}.work-hours{display:flex;flex-direction:row;gap:20px;margin-bottom:15px}.work-hours-label{font-weight:700;display:flex;flex-direction:column}.notification-item{position:relative;border:1px solid #ccc;padding:15px;margin-bottom:15px;border-radius:8px;background-color:#f9f9f9}.notification-item .delete-notification{position:absolute;top:0;right:0;transform:translate(50%,-50%);background:#ff5c5c;border:none;font-size:16px;cursor:pointer;color:#fff;width:24px;height:24px;border-radius:50%;padding:0;line-height:1;box-sizing:border-box;display:flex;align-items:center;justify-content:center}.notification-item .delete-notification:hover{background:#e60000}.notification-item .delete-notification i{color:#fff}.notification-content label{display:block;margin-bottom:10px;font-weight:400}.notification-content input,.notification-content select{width:100%;padding:8px;margin-top:5px;font-size:16px;border:1px solid #ccc;border-radius:4px}.error-message{color:red;font-size:14px;margin-bottom:5px}@media (max-width: 600px){.config-header{flex-direction:column;align-items:flex-start}.config-header .site-stream{flex-direction:column;gap:5px}button{width:100%;margin:10px 0}.buttons,#form-controls{text-align:center}.work-hours{flex-direction:column}}.error{border:1px solid red!important}button,.delete-notification{transition:background-color .3s,color .3s}input,select{transition:border-color .3s}input:focus,select:focus{border-color:#007bff}.hidden{display:none} diff --git a/examples/streaming_web/frontend/dist/assets/config-Dm3IWCdK.js b/examples/streaming_web/frontend/dist/assets/config-Dm3IWCdK.js deleted file mode 100644 index d8bfd57..0000000 --- a/examples/streaming_web/frontend/dist/assets/config-Dm3IWCdK.js +++ /dev/null @@ -1,73 +0,0 @@ -import"./modulepreload-polyfill-B5Qt9EMX.js";const v=document.getElementById("config-container"),x=document.getElementById("edit-btn"),b=document.getElementById("add-config-btn"),$=document.getElementById("save-btn"),k=document.getElementById("cancel-btn"),S=document.getElementById("form-controls");let f=[],o=!1;async function h(){try{const e=await fetch("/api/config");if(!e.ok)throw new Error("Failed to fetch configuration.");f=(await e.json()).config.map(a=>({...a,notifications:Object.entries(a.notifications).map(([d,t])=>({token:d,language:t}))})),u()}catch(e){console.error(e)}}function D(e){return e.replace(/_/g," ").replace(/\b\w/g,n=>n.toUpperCase())}function y(){const e=v.children;f=Array.from(e).map((n,a)=>{const d=n.querySelectorAll("input, select"),t={notifications:[],detection_items:{}};return d.forEach(i=>{if(i.name==="line_token"||i.name==="language"){const s=i.getAttribute("data-notif-index");t.notifications[s]||(t.notifications[s]={token:"",language:"en"}),i.name==="line_token"?t.notifications[s].token=i.value.trim():i.name==="language"&&(t.notifications[s].language=i.value)}else i.name==="no_expire_date"?(t.no_expire_date=i.checked,i.checked&&(t.expire_date="No Expire Date")):i.name==="expire_date"?(!t.expire_date&&i.type==="date"&&(t.expire_date=i.value||new Date().toISOString().split("T")[0]),t.previous_expire_date=i.value):i.name.startsWith("detect_")?i.name==="detect_with_server"?t.detect_with_server=i.checked:t.detection_items[i.name]=i.checked:i.name&&(t[i.name]=i.value.trim())}),t})}function u(){v.innerHTML="",f.forEach((e,n)=>{const a=document.createElement("div");a.className="config-item";let d;e.expire_date==="No Expire Date"?d="":e.expire_date?d=e.expire_date:(d=new Date().toISOString().split("T")[0],e.expire_date=d),a.innerHTML=` -
-
- - - - -
- -
- - - - -
- Detection Items - ${Object.entries(e.detection_items).map(([l,m])=>` - - `).join("")} -
-
- Notifications -
- ${e.notifications.map((l,m)=>` -
- ${o?'':""} -
- - -
-
- `).join("")} -
- ${o?'':""} -
- `,a.querySelector(".delete-config-btn").addEventListener("click",()=>{y(),f.splice(n,1),u()}),a.querySelector(".notifications-container").addEventListener("click",l=>{if(l.target.closest(".delete-notification")){const m=n,_=l.target.closest(".notification-item"),E=parseInt(_.getAttribute("data-notif-index"));y(),f[m].notifications.splice(E,1),u()}});const s=a.querySelector(".add-notification");s==null||s.addEventListener("click",()=>{y(),f[n].notifications.push({token:"",language:"en"}),u()});const r=a.querySelector("input[name='expire_date']"),c=a.querySelector("input[name='no_expire_date']"),p=a.querySelector("input[type='text'][value='No Expire Date']");c&&(c.checked?(r.style.display="none",p.style.display=""):(r.style.display="",p.style.display="none"),c.addEventListener("change",()=>{c.checked?(e.previous_expire_date=r.value,r.style.display="none",p.style.display=""):(r.style.display="",p.style.display="none",r.value=e.previous_expire_date||new Date().toISOString().split("T")[0])})),v.appendChild(a)})}function g(e){o=e,u(),x.classList.toggle("hidden",e),b.classList.toggle("hidden",!e),S.classList.toggle("hidden",!e),e||h()}async function C(){document.querySelectorAll(".error-message").forEach(n=>n.remove()),document.querySelectorAll(".error").forEach(n=>n.classList.remove("error"));let e=!0;try{y();const n=f.map((t,i)=>{const s=v.children[i];return["site","stream_name","video_url"].forEach(r=>{if(!t[r]){e=!1;const c=s.querySelector(`input[name='${r}']`);if(c.classList.add("error"),!c.previousElementSibling||!c.previousElementSibling.classList.contains("error-message")){const p=document.createElement("div");p.className="error-message",p.textContent="This field is required.",c.parentNode.insertBefore(p,c)}}}),t.notifications=t.notifications.filter(r=>r.token),!t.no_expire_date&&!t.expire_date&&(t.expire_date=new Date().toISOString().split("T")[0]),delete t.previous_expire_date,t});if(!e)return;const a=n.map(t=>{const i={};return t.notifications.forEach(s=>{i[s.token]=s.language}),{...t,notifications:i}});if(!(await fetch("/api/config",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({config:a})})).ok)throw new Error("Failed to save configuration.");g(!1)}catch(n){console.error(n)}}x.addEventListener("click",()=>g(!0));k.addEventListener("click",()=>g(!1));$.addEventListener("click",C);b.addEventListener("click",()=>{y();const e=new Date().toISOString().split("T")[0];f.push({video_url:"",site:"",stream_name:"",model_key:"yolo11n",notifications:[],expire_date:e,no_expire_date:!1,detect_with_server:!1,detection_items:{detect_no_safety_vest_or_helmet:!1,detect_near_machinery_or_vehicle:!1,detect_in_restricted_area:!1}}),u(),g(!0)});document.addEventListener("DOMContentLoaded",h); diff --git a/examples/streaming_web/frontend/dist/config.html b/examples/streaming_web/frontend/dist/config.html index d3fdff5..7f7f5ef 100644 --- a/examples/streaming_web/frontend/dist/config.html +++ b/examples/streaming_web/frontend/dist/config.html @@ -15,9 +15,9 @@ - + - +
@@ -51,5 +51,172 @@

Edit Configuration

+ + + + + + diff --git a/examples/streaming_web/frontend/public/config.html b/examples/streaming_web/frontend/public/config.html index 7380d4d..ee4abe4 100644 --- a/examples/streaming_web/frontend/public/config.html +++ b/examples/streaming_web/frontend/public/config.html @@ -70,9 +70,12 @@

Edit Configuration

+ + + + + + + + +
+ + + +
+
Detection Items
+
Notifications -
-
+
diff --git a/examples/streaming_web/frontend/public/css/config.css b/examples/streaming_web/frontend/public/css/config.css index 940ab73..c8bdda7 100644 --- a/examples/streaming_web/frontend/public/css/config.css +++ b/examples/streaming_web/frontend/public/css/config.css @@ -109,6 +109,20 @@ legend { color: #e60000; } +/* Work hours block */ +.work-hours { + display: flex; + flex-direction: row; + gap: 20px; + margin-bottom: 15px; +} + +.work-hours-label { + font-weight: bold; + display: flex; + flex-direction: column; +} + /* Notification styles */ .notification-item { position: relative; @@ -194,6 +208,10 @@ legend { .buttons, #form-controls { text-align: center; } + + .work-hours { + flex-direction: column; + } } /* Error state styles */ @@ -201,7 +219,7 @@ legend { border: 1px solid red !important; } -/* Filter effect styles */ +/* Transition effect styles */ button, .delete-notification { transition: background-color 0.3s, color 0.3s; } diff --git a/examples/streaming_web/frontend/public/js/config.js b/examples/streaming_web/frontend/public/js/config.js index 81549b8..864e0dd 100644 --- a/examples/streaming_web/frontend/public/js/config.js +++ b/examples/streaming_web/frontend/public/js/config.js @@ -8,11 +8,17 @@ const formControls = document.getElementById("form-controls"); let configData = []; let isEditing = false; +// Fetch today's date in ISO format (YYYY-MM-DD) +function getTodayDate() { + return new Date().toISOString().split("T")[0]; +} + async function fetchConfig() { try { const response = await fetch("/api/config"); if (!response.ok) throw new Error("Failed to fetch configuration."); const data = await response.json(); + // Transform notifications object to array configData = data.config.map(config => ({ ...config, @@ -34,43 +40,73 @@ function updateConfigDataFromForm() { configData = Array.from(configItems).map((container, index) => { const inputs = container.querySelectorAll("input, select"); const config = { notifications: [], detection_items: {} }; + inputs.forEach((input) => { - if (input.name === "line_token" || input.name === "language") { - // Process notification items + const { name, type } = input; + + // Process Notifications + if (name === "line_token" || name === "language") { const notifIndex = input.getAttribute("data-notif-index"); if (!config.notifications[notifIndex]) { config.notifications[notifIndex] = { token: "", language: "en" }; } - if (input.name === "line_token") { + if (name === "line_token") { config.notifications[notifIndex].token = input.value.trim(); - } else if (input.name === "language") { - config.notifications[notifIndex].language = input.value; - } - } else if (input.name === "no_expire_date") { - // Process no_expire_date - config.no_expire_date = input.checked; - if (input.checked) { - config.expire_date = "No Expire Date"; - } - } else if (input.name === "expire_date") { - // Process expire_date - if (!config.expire_date && input.type === "date") { - config.expire_date = input.value || new Date().toISOString().split('T')[0]; - } - // Save the previous expire_date value - config.previous_expire_date = input.value; - } else if (input.name.startsWith("detect_")) { - // Process detection items and detect_with_server - if (input.name === "detect_with_server") { - config.detect_with_server = input.checked; } else { - config.detection_items[input.name] = input.checked; + config.notifications[notifIndex].language = input.value; } - } else if (input.name) { - // Process other input fields - config[input.name] = input.value.trim(); + } + + // Process Expiry Date + else if (name === "expire_date") { + config.expire_date = input.value.trim(); + } + + // Process Checkboxes + else if (name === "detect_with_server") { + config.detect_with_server = input.checked; + } + else if (name === "store_in_redis") { + config.store_in_redis = input.checked; + } + else if (name.startsWith("detect_")) { + config.detection_items[name] = input.checked; + } + + // Process Work Hours + else if (name === "work_start_hour") { + config.work_start_hour = parseInt(input.value, 10); + } + else if (name === "work_end_hour") { + config.work_end_hour = parseInt(input.value, 10); + } + + // Process other fields + else if (name) { + // site, stream_name, video_url, model_key, etc. + config[name] = input.value.trim(); } }); + + // Process "No Expire Date" + const noExpireDateCheckbox = container.querySelector("input[name='no_expire_date']"); + if (noExpireDateCheckbox) { + if (noExpireDateCheckbox.checked) { + config.expire_date = "No Expire Date"; + } else { + // If the expire_date is empty, set it to today + if (!config.expire_date || config.expire_date === "No Expire Date") { + config.expire_date = getTodayDate(); + } + } + } + + // Remove empty notifications + config.notifications = config.notifications.filter(notif => notif.token); + + // Ensure store_in_redis is a boolean + config.store_in_redis = !!config.store_in_redis; + return config; }); } @@ -85,6 +121,7 @@ function renderConfigForm() { const container = configItemTemplate.content.cloneNode(true); const item = container.querySelector(".config-item"); + // Fetch form elements const siteInput = item.querySelector("input[name='site']"); const streamNameInput = item.querySelector("input[name='stream_name']"); const videoUrlInput = item.querySelector("input[name='video_url']"); @@ -92,45 +129,49 @@ function renderConfigForm() { const expireDateInput = item.querySelector("input[name='expire_date']"); const noExpireDateText = item.querySelector("input[type='text'][value='No Expire Date']"); const detectWithServerCheckbox = item.querySelector("input[name='detect_with_server']"); - const detectionItems = item.querySelectorAll("input[type='checkbox'][name^='detect_']:not([name='detect_with_server'])"); + const storeInRedisCheckbox = item.querySelector("input[name='store_in_redis']"); + const detectionItems = item.querySelectorAll("input[type='checkbox'][name^='detect_']:not([name='detect_with_server']):not([name='store_in_redis'])"); + const workStartHourSelect = item.querySelector("select[name='work_start_hour']"); + const workEndHourSelect = item.querySelector("select[name='work_end_hour']"); const addNotificationBtn = item.querySelector(".add-notification"); const deleteConfigBtn = item.querySelector(".delete-config-btn"); const expireDateContainer = item.querySelector(".expire-date-container"); - // Set values + // Default values siteInput.value = config.site || ''; streamNameInput.value = config.stream_name || ''; videoUrlInput.value = config.video_url || ''; - if (modelKeySelect) { - modelKeySelect.value = config.model_key || 'yolo11n'; - } + modelKeySelect.value = config.model_key || 'yolo11n'; + + // Set Work Hours + workStartHourSelect.value = config.work_start_hour !== undefined ? config.work_start_hour : 7; + workEndHourSelect.value = config.work_end_hour !== undefined ? config.work_end_hour : 18; - // Process expire_date - let expireDateValue; + // Set Expire Date if (config.expire_date === "No Expire Date") { - expireDateValue = ""; - } else if (config.expire_date) { - expireDateValue = config.expire_date; + expireDateInput.value = ""; + expireDateInput.style.display = "none"; + noExpireDateText.style.display = ""; } else { - expireDateValue = new Date().toISOString().split('T')[0]; - config.expire_date = expireDateValue; + expireDateInput.value = config.expire_date || getTodayDate(); + expireDateInput.style.display = ""; + noExpireDateText.style.display = "none"; } - expireDateInput.value = expireDateValue; - - // Detect with server + // Set Detect with Server & Store in Redis detectWithServerCheckbox.checked = !!config.detect_with_server; + storeInRedisCheckbox.checked = !!config.store_in_redis; - // Detection Items + // Set Detection Items detectionItems.forEach((checkbox) => { const key = checkbox.name; checkbox.checked = !!config.detection_items[key]; - // Update label text in case keys differ (optional) + // Update the label text, replacing underscores with spaces and capitalizing the first letter of each word const label = checkbox.parentElement; label.lastChild.textContent = formatDetectionItemName(key); }); - // Notifications + // Set Notifications const notificationsContainer = item.querySelector(".notifications-container"); notificationsContainer.innerHTML = ""; config.notifications.forEach((notification, notifIndex) => { @@ -145,7 +186,7 @@ function renderConfigForm() { lineTokenInput.setAttribute("data-notif-index", notifIndex); languageSelect.setAttribute("data-notif-index", notifIndex); - // Show/Hide delete notification button based on edit mode + // According to the edit mode, show/hide delete button if (isEditing) { deleteNotifBtn.style.display = "block"; } else { @@ -155,19 +196,23 @@ function renderConfigForm() { notificationsContainer.appendChild(notifEl); }); - // If in editing mode, add a no_expire_date checkbox + // If the expire_date is empty, set it to today if (isEditing) { const noExpireDateCheckbox = document.createElement("input"); noExpireDateCheckbox.type = "checkbox"; noExpireDateCheckbox.name = "no_expire_date"; noExpireDateCheckbox.checked = config.expire_date === "No Expire Date"; - expireDateContainer.appendChild(document.createElement("br")); + noExpireDateCheckbox.id = `no-expire-date-${index}`; + const noExpireDateLabel = document.createElement("label"); + noExpireDateLabel.htmlFor = `no-expire-date-${index}`; noExpireDateLabel.appendChild(noExpireDateCheckbox); noExpireDateLabel.appendChild(document.createTextNode(" No Expire Date")); + + expireDateContainer.appendChild(document.createElement("br")); expireDateContainer.appendChild(noExpireDateLabel); - // Initialise the display of the expire date input and no expire date text + // Initial show/hide "No Expire Date" text if (noExpireDateCheckbox.checked) { expireDateInput.style.display = "none"; noExpireDateText.style.display = ""; @@ -176,20 +221,24 @@ function renderConfigForm() { noExpireDateText.style.display = "none"; } + // Monitor "No Expire Date" checkbox changes noExpireDateCheckbox.addEventListener("change", () => { if (noExpireDateCheckbox.checked) { - config.previous_expire_date = expireDateInput.value; + expireDateInput.value = ""; expireDateInput.style.display = "none"; noExpireDateText.style.display = ""; } else { expireDateInput.style.display = ""; noExpireDateText.style.display = "none"; - // Restore the previous expire date value - expireDateInput.value = config.previous_expire_date || new Date().toISOString().split('T')[0]; + // If the expire_date is empty, set it to today + if (!config.expire_date || config.expire_date === "No Expire Date") { + expireDateInput.value = getTodayDate(); + config.expire_date = getTodayDate(); + } } }); } else { - // Not in editing mode + // Show/hide "No Expire Date" text if (config.expire_date === "No Expire Date") { expireDateInput.style.display = "none"; noExpireDateText.style.display = ""; @@ -199,7 +248,7 @@ function renderConfigForm() { } } - // Show/Hide delete config button and add notification button based on edit mode + // According to the edit mode, show/hide buttons if (isEditing) { deleteConfigBtn.style.display = "block"; addNotificationBtn.style.display = "inline-block"; @@ -208,18 +257,20 @@ function renderConfigForm() { addNotificationBtn.style.display = "none"; } - // Event: delete config + // Event: Delete Config deleteConfigBtn.addEventListener("click", () => { updateConfigDataFromForm(); configData.splice(index, 1); renderConfigForm(); }); - // Event: delete notification + // Event: Delete Notification notificationsContainer.addEventListener("click", (event) => { if (event.target.closest(".delete-notification")) { const notificationItem = event.target.closest(".notification-item"); - const notifIndex = parseInt(notificationItem.querySelector("input[name='line_token']").getAttribute("data-notif-index")); + const notifIndex = parseInt( + notificationItem.querySelector("input[name='line_token']").getAttribute("data-notif-index") + ); updateConfigDataFromForm(); const updatedConfig = configData[index]; updatedConfig.notifications.splice(notifIndex, 1); @@ -227,20 +278,20 @@ function renderConfigForm() { } }); - // Event: add notification + // Event: Add Notification addNotificationBtn.addEventListener("click", () => { updateConfigDataFromForm(); configData[index].notifications.push({ token: "", language: "en" }); renderConfigForm(); }); - // 根據isEditing狀態設定所有input,select的disabled狀態 - const fields = item.querySelectorAll('input, select'); + // According to the edit mode, enable/disable form fields + const fields = item.querySelectorAll("input, select"); fields.forEach(f => { if (!isEditing) { - f.setAttribute('disabled', 'true'); + f.setAttribute("disabled", "true"); } else { - f.removeAttribute('disabled'); + f.removeAttribute("disabled"); } }); @@ -251,83 +302,115 @@ function renderConfigForm() { function toggleEditMode(enable) { isEditing = enable; - // Re-render the form + // Reset form data if exiting edit mode renderConfigForm(); - // Toggle the visibility of the buttons and form controls + // Show/hide buttons and form controls editBtn.classList.toggle("hidden", enable); addConfigBtn.classList.toggle("hidden", !enable); formControls.classList.toggle("hidden", !enable); if (!enable) { - // If exiting edit mode, fetch the config again + // If exiting edit mode, fetch the latest config fetchConfig(); } } async function saveConfig() { - // 清除之前的错误消息 + // Erase previous error messages document.querySelectorAll(".error-message").forEach(el => el.remove()); document.querySelectorAll(".error").forEach(el => el.classList.remove("error")); let isValid = true; try { - updateConfigDataFromForm(); // Update configData + updateConfigDataFromForm(); // Update configData with the latest form values const updatedConfig = configData.map((config, index) => { const container = configContainer.children[index]; - // Validate required fields + // Validate required fields: site, stream_name, video_url ["site", "stream_name", "video_url"].forEach(field => { if (!config[field]) { isValid = false; const input = container.querySelector(`input[name='${field}']`); - input.classList.add("error"); - - // Add an error message if it doesn't exist - if (!input.previousElementSibling || !input.previousElementSibling.classList.contains("error-message")) { - const errorMessage = document.createElement("div"); - errorMessage.className = "error-message"; - errorMessage.textContent = "This field is required."; - input.parentNode.insertBefore(errorMessage, input); + if (input) { + input.classList.add("error"); + if ( + !input.previousElementSibling || + !input.previousElementSibling.classList.contains("error-message") + ) { + const errorMessage = document.createElement("div"); + errorMessage.className = "error-message"; + errorMessage.textContent = "This field is required."; + input.parentNode.insertBefore(errorMessage, input); + } } } }); - // Filter out notifications with empty token - config.notifications = config.notifications.filter(notif => notif.token); - - // Ensure that the expire_date is set if no_expire_date is not checked - if (!config.no_expire_date && !config.expire_date) { - config.expire_date = new Date().toISOString().split('T')[0]; + // Validate work_start_hour < work_end_hour + if (config.work_start_hour >= config.work_end_hour) { + isValid = false; + const workStartHourSelect = container.querySelector(`select[name='work_start_hour']`); + const workEndHourSelect = container.querySelector(`select[name='work_end_hour']`); + workStartHourSelect.classList.add("error"); + workEndHourSelect.classList.add("error"); + + if ( + !workEndHourSelect.previousElementSibling || + !workEndHourSelect.previousElementSibling.classList.contains("error-message") + ) { + const errorMessage = document.createElement("div"); + errorMessage.className = "error-message"; + errorMessage.textContent = + "Work Start Hour cannot be greater than or equal to Work End Hour."; + workEndHourSelect.parentNode.insertBefore(errorMessage, workEndHourSelect); + } } - // Remove the previous_expire_date field - delete config.previous_expire_date; + // Validate Expiry Date + if (config.expire_date !== "No Expire Date" && !config.expire_date) { + config.expire_date = getTodayDate(); + } return config; }); if (!isValid) { - // Not to save the configuration if it's invalid - return; + return; // No need to proceed if there are validation errors } - // Transform notifications array to object + // Set notifications as an object const processedConfig = updatedConfig.map(config => { const notificationsObj = {}; config.notifications.forEach(notif => { notificationsObj[notif.token] = notif.language; }); - return { ...config, notifications: notificationsObj }; + return { + ...config, + notifications: notificationsObj, + // Remove the "No Expire Date" checkbox value + no_expire_date: undefined + }; }); - // Send the updated configuration to the server + // Remove undefined fields + const finalConfig = processedConfig.map(config => { + const cleanedConfig = { ...config }; + Object.keys(cleanedConfig).forEach(key => { + if (cleanedConfig[key] === undefined) { + delete cleanedConfig[key]; + } + }); + return cleanedConfig; + }); + + // Conduct the API request to save the configuration const response = await fetch("/api/config", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ config: processedConfig }), + body: JSON.stringify({ config: finalConfig }) }); if (!response.ok) throw new Error("Failed to save configuration."); @@ -338,27 +421,29 @@ async function saveConfig() { } } -// 按鈕事件處理 +// Process Events editBtn.addEventListener("click", () => toggleEditMode(true)); cancelBtn.addEventListener("click", () => toggleEditMode(false)); saveBtn.addEventListener("click", saveConfig); addConfigBtn.addEventListener("click", () => { updateConfigDataFromForm(); - // Add a new config item with default values - const today = new Date().toISOString().split('T')[0]; // Format: YYYY-MM-DD + // Add a new empty config item + const today = getTodayDate(); configData.push({ - video_url: "", site: "", stream_name: "", - model_key: "yolo11n", // Default model key to yolo11n - notifications: [], // Empty notifications - expire_date: today, - no_expire_date: false, - detect_with_server: false, // Default detect_with_server to false + video_url: "", + model_key: "yolo11n", + expire_date: today, // Default to today + detect_with_server: false, + store_in_redis: false, + work_start_hour: 7, + work_end_hour: 18, + notifications: [], detection_items: { - "detect_no_safety_vest_or_helmet": false, - "detect_near_machinery_or_vehicle": false, - "detect_in_restricted_area": false + detect_no_safety_vest_or_helmet: false, + detect_near_machinery_or_vehicle: false, + detect_in_restricted_area: false } }); @@ -366,4 +451,5 @@ addConfigBtn.addEventListener("click", () => { toggleEditMode(true); }); +// Automatically fetch the configuration when the page loads document.addEventListener("DOMContentLoaded", fetchConfig);