feat: Add all module frontend views
Views added: - ShiftsView (Schichtplanung) - PatrolsView (Wächterkontrolle) - IncidentsView (Vorfallberichte) - VehiclesView (Fahrzeuge) - DocumentsView (Dokumente) - CustomersView (Kunden/CRM) - BillingView (Abrechnung) - ObjectsView (enhanced with contacts, instructions) Updated: - Router with all new routes - Sidebar with complete navigation
This commit is contained in:
File diff suppressed because one or more lines are too long
1
dist/assets/AppLayout-AMYs4jcG.js
vendored
Normal file
1
dist/assets/AppLayout-AMYs4jcG.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/AppLayout-B1xnAEtL.js
vendored
1
dist/assets/AppLayout-B1xnAEtL.js
vendored
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{d as D,u as S,q as M,m as h,c as l,a as o,t as u,F,x as A,A as C,r as v,k as m,o as i,n as L}from"./index-OvQoqblD.js";const O={class:"space-y-6"},V={class:"card"},E={class:"flex items-center justify-between mb-6"},I={class:"text-lg font-semibold"},W={key:0,class:"text-center py-8 text-gray-500"},Y={key:1,class:"grid grid-cols-7 gap-2"},B={class:"text-xs text-gray-500 mb-1"},N=["onClick"],j=D({__name:"AvailabilityView",setup(T){const f=S(),a=v(new Date),y=v([]),g=v(!0),p=m(()=>{const n=a.value.getFullYear(),e=a.value.getMonth(),t=new Date(n,e+1,0).getDate();return Array.from({length:t},(s,r)=>{const d=new Date(n,e,r+1);return{date:d.toISOString().split("T")[0],dayOfWeek:d.toLocaleDateString("de-DE",{weekday:"short"}),day:r+1,isWeekend:d.getDay()===0||d.getDay()===6}})}),_=m(()=>a.value.toLocaleDateString("de-DE",{month:"long",year:"numeric"}));M(c);async function c(){g.value=!0;const n=a.value.getFullYear(),e=a.value.getMonth(),t=new Date(n,e,1).toISOString().split("T")[0],s=new Date(n,e+1,0).toISOString().split("T")[0];try{const r=await h.get(`/availability?from=${t}&to=${s}`);y.value=r.data.availability}catch(r){console.error(r)}finally{g.value=!1}}function b(n){return y.value.find(e=>{var t;return e.date===n&&e.user_id===((t=f.user)==null?void 0:t.id)})}async function k(n){const e=b(n),t=!(e!=null&&e.available);try{await h.post("/availability",{date:n,available:t}),await c()}catch(s){alert(s instanceof Error?s.message:"Fehler")}}function x(){a.value=new Date(a.value.getFullYear(),a.value.getMonth()-1),c()}function w(){a.value=new Date(a.value.getFullYear(),a.value.getMonth()+1),c()}return(n,e)=>(i(),l("div",O,[e[1]||(e[1]=o("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"📅 Verfügbarkeit",-1)),o("div",V,[o("div",E,[o("button",{class:"btn btn-secondary",onClick:x},"←"),o("h2",I,u(_.value),1),o("button",{class:"btn btn-secondary",onClick:w},"→")]),g.value?(i(),l("div",W,"Lädt...")):(i(),l("div",Y,[(i(!0),l(F,null,A(p.value,t=>{var s;return i(),l("div",{key:t.date,class:"text-center"},[o("div",B,u(t.dayOfWeek),1),o("button",{class:L(["w-10 h-10 rounded-lg font-medium transition-colors",t.isWeekend?"bg-gray-100 dark:bg-gray-700":"",(s=b(t.date))!=null&&s.available?"bg-green-500 text-white hover:bg-green-600":"bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500"]),onClick:r=>k(t.date)},u(t.day),11,N)])}),128))])),e[0]||(e[0]=C('<div class="mt-6 flex items-center gap-4 text-sm"><div class="flex items-center gap-2"><span class="w-4 h-4 bg-green-500 rounded"></span><span>Verfügbar</span></div><div class="flex items-center gap-2"><span class="w-4 h-4 bg-gray-200 dark:bg-gray-600 rounded"></span><span>Nicht gemeldet</span></div></div>',1))])]))}});export{j as default};
|
import{d as D,u as S,q as M,m as h,c as l,a as o,t as u,F,x as A,A as C,r as v,k as m,o as i,n as L}from"./index-CWxNv9Fc.js";const O={class:"space-y-6"},V={class:"card"},E={class:"flex items-center justify-between mb-6"},I={class:"text-lg font-semibold"},W={key:0,class:"text-center py-8 text-gray-500"},Y={key:1,class:"grid grid-cols-7 gap-2"},B={class:"text-xs text-gray-500 mb-1"},N=["onClick"],j=D({__name:"AvailabilityView",setup(T){const f=S(),a=v(new Date),y=v([]),g=v(!0),p=m(()=>{const n=a.value.getFullYear(),e=a.value.getMonth(),t=new Date(n,e+1,0).getDate();return Array.from({length:t},(s,r)=>{const d=new Date(n,e,r+1);return{date:d.toISOString().split("T")[0],dayOfWeek:d.toLocaleDateString("de-DE",{weekday:"short"}),day:r+1,isWeekend:d.getDay()===0||d.getDay()===6}})}),_=m(()=>a.value.toLocaleDateString("de-DE",{month:"long",year:"numeric"}));M(c);async function c(){g.value=!0;const n=a.value.getFullYear(),e=a.value.getMonth(),t=new Date(n,e,1).toISOString().split("T")[0],s=new Date(n,e+1,0).toISOString().split("T")[0];try{const r=await h.get(`/availability?from=${t}&to=${s}`);y.value=r.data.availability}catch(r){console.error(r)}finally{g.value=!1}}function b(n){return y.value.find(e=>{var t;return e.date===n&&e.user_id===((t=f.user)==null?void 0:t.id)})}async function k(n){const e=b(n),t=!(e!=null&&e.available);try{await h.post("/availability",{date:n,available:t}),await c()}catch(s){alert(s instanceof Error?s.message:"Fehler")}}function x(){a.value=new Date(a.value.getFullYear(),a.value.getMonth()-1),c()}function w(){a.value=new Date(a.value.getFullYear(),a.value.getMonth()+1),c()}return(n,e)=>(i(),l("div",O,[e[1]||(e[1]=o("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"📅 Verfügbarkeit",-1)),o("div",V,[o("div",E,[o("button",{class:"btn btn-secondary",onClick:x},"←"),o("h2",I,u(_.value),1),o("button",{class:"btn btn-secondary",onClick:w},"→")]),g.value?(i(),l("div",W,"Lädt...")):(i(),l("div",Y,[(i(!0),l(F,null,A(p.value,t=>{var s;return i(),l("div",{key:t.date,class:"text-center"},[o("div",B,u(t.dayOfWeek),1),o("button",{class:L(["w-10 h-10 rounded-lg font-medium transition-colors",t.isWeekend?"bg-gray-100 dark:bg-gray-700":"",(s=b(t.date))!=null&&s.available?"bg-green-500 text-white hover:bg-green-600":"bg-gray-200 dark:bg-gray-600 hover:bg-gray-300 dark:hover:bg-gray-500"]),onClick:r=>k(t.date)},u(t.day),11,N)])}),128))])),e[0]||(e[0]=C('<div class="mt-6 flex items-center gap-4 text-sm"><div class="flex items-center gap-2"><span class="w-4 h-4 bg-green-500 rounded"></span><span>Verfügbar</span></div><div class="flex items-center gap-2"><span class="w-4 h-4 bg-gray-200 dark:bg-gray-600 rounded"></span><span>Nicht gemeldet</span></div></div>',1))])]))}});export{j as default};
|
||||||
1
dist/assets/BillingView-TAMo2net.js
vendored
Normal file
1
dist/assets/BillingView-TAMo2net.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
dist/assets/CustomersView-B4K9IT7m.js
vendored
Normal file
1
dist/assets/CustomersView-B4K9IT7m.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/DocumentsView-cD_00o40.js
vendored
Normal file
1
dist/assets/DocumentsView-cD_00o40.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1,4 +1,4 @@
|
|||||||
import{d as K,c as a,a as n,e as w,b as C,v as F,F as A,x,t as r,r as f,k as m,o as s}from"./index-OvQoqblD.js";const G={class:"space-y-6"},E={class:"flex items-center justify-between"},V={class:"flex items-center gap-3"},W={class:"relative"},N={key:0,class:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"},$=["onClick"],Z={class:"text-3xl mb-3"},I={class:"text-lg font-semibold mb-1 text-gray-900 dark:text-white"},L={class:"text-sm text-gray-500 dark:text-gray-400"},T={class:"text-xs text-gray-400 mt-2"},H={key:1,class:"space-y-4"},O={class:"card bg-gray-50 dark:bg-gray-800"},R={class:"flex items-center gap-3"},q={class:"text-3xl"},j={class:"text-xl font-semibold text-gray-900 dark:text-white"},U={class:"text-sm text-gray-500 dark:text-gray-400"},J={class:"grid gap-3"},Q=["onClick"],X={class:"text-2xl"},Y={class:"font-medium text-gray-900 dark:text-white"},ee={key:2,class:"card"},ne={class:"flex items-center gap-3 mb-6 pb-4 border-b dark:border-gray-700"},te={class:"text-3xl"},ie={class:"text-xl font-semibold text-gray-900 dark:text-white"},re=["innerHTML"],le=K({__name:"HelpView",setup(ae){const l=f(""),u=f(null),o=f(null),g=[{id:"getting-started",name:"Erste Schritte",icon:"🚀",description:"Grundlagen und Einrichtung",articles:[{id:"overview",title:"Übersicht",icon:"📋",content:`
|
import{d as K,c as a,a as n,e as w,b as C,v as F,F as A,x,t as r,r as f,k as m,o as s}from"./index-CWxNv9Fc.js";const G={class:"space-y-6"},E={class:"flex items-center justify-between"},V={class:"flex items-center gap-3"},W={class:"relative"},N={key:0,class:"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"},$=["onClick"],Z={class:"text-3xl mb-3"},I={class:"text-lg font-semibold mb-1 text-gray-900 dark:text-white"},L={class:"text-sm text-gray-500 dark:text-gray-400"},T={class:"text-xs text-gray-400 mt-2"},H={key:1,class:"space-y-4"},O={class:"card bg-gray-50 dark:bg-gray-800"},R={class:"flex items-center gap-3"},q={class:"text-3xl"},j={class:"text-xl font-semibold text-gray-900 dark:text-white"},U={class:"text-sm text-gray-500 dark:text-gray-400"},J={class:"grid gap-3"},Q=["onClick"],X={class:"text-2xl"},Y={class:"font-medium text-gray-900 dark:text-white"},ee={key:2,class:"card"},ne={class:"flex items-center gap-3 mb-6 pb-4 border-b dark:border-gray-700"},te={class:"text-3xl"},ie={class:"text-xl font-semibold text-gray-900 dark:text-white"},re=["innerHTML"],le=K({__name:"HelpView",setup(ae){const l=f(""),u=f(null),o=f(null),g=[{id:"getting-started",name:"Erste Schritte",icon:"🚀",description:"Grundlagen und Einrichtung",articles:[{id:"overview",title:"Übersicht",icon:"📋",content:`
|
||||||
## Was ist SeCu?
|
## Was ist SeCu?
|
||||||
|
|
||||||
SeCu ist eine modulare Mitarbeiterverwaltung speziell für Sicherheitsunternehmen. Die Software hilft Ihnen bei:
|
SeCu ist eine modulare Mitarbeiterverwaltung speziell für Sicherheitsunternehmen. Die Software hilft Ihnen bei:
|
||||||
1
dist/assets/IncidentsView-DPo8woI1.js
vendored
Normal file
1
dist/assets/IncidentsView-DPo8woI1.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{d as V,q as j,m as x,c as n,a as e,F as f,x as _,b as u,v as p,s as B,e as v,r,o,t as l,n as w}from"./index-CWxNv9Fc.js";const C={class:"p-6"},D={class:"flex justify-between items-center mb-6"},M={key:0,class:"text-center py-12"},S={key:1,class:"bg-gray-50 rounded-lg p-12 text-center text-gray-500"},G={key:2,class:"space-y-4"},I={class:"flex justify-between items-start"},K={class:"flex items-start space-x-3"},U={class:"text-2xl"},z={class:"font-semibold"},F={class:"text-sm text-gray-500"},L={class:"text-sm text-gray-400 mt-1"},O={class:"flex flex-col items-end space-y-1"},A={key:0,class:"mt-3 text-sm text-gray-600"},E={class:"mt-3 flex items-center text-xs text-gray-500"},N={key:0,class:"ml-4"},R={key:3,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50"},T={class:"bg-white rounded-lg shadow-xl w-full max-w-lg p-6"},$={class:"space-y-4"},q=["value"],P={class:"flex justify-end space-x-2 mt-6"},J=V({__name:"IncidentsView",setup(W){const c=r(!0),m=r([]),g=r([]),d=r(!1),i=r({title:"",description:"",category_id:"",severity:2,object_id:""});j(async()=>{await y()});async function y(){c.value=!0;try{const[a,t]=await Promise.all([x.get("/incidents"),x.get("/incidents/categories")]);m.value=a.data.incidents||[],g.value=t.data.categories||[]}catch(a){console.error(a)}c.value=!1}async function k(){try{await x.post("/incidents",i.value),d.value=!1,i.value={title:"",description:"",category_id:"",severity:2,object_id:""},await y()}catch(a){alert("Fehler: "+a.message)}}function h(a){return a>=4?"bg-red-100 text-red-700":a>=3?"bg-orange-100 text-orange-700":"bg-yellow-100 text-yellow-700"}function b(a){return{open:{text:"Offen",class:"bg-red-100 text-red-700"},in_progress:{text:"In Bearbeitung",class:"bg-yellow-100 text-yellow-700"},resolved:{text:"Gelöst",class:"bg-green-100 text-green-700"},closed:{text:"Geschlossen",class:"bg-gray-100 text-gray-700"}}[a]||{text:a,class:"bg-gray-100"}}return(a,t)=>(o(),n("div",C,[e("div",D,[t[6]||(t[6]=e("div",null,[e("h1",{class:"text-2xl font-bold"},"🚨 Vorfallberichte"),e("p",{class:"text-gray-500"},"Incidents dokumentieren und verfolgen")],-1)),e("button",{onClick:t[0]||(t[0]=s=>d.value=!0),class:"btn btn-primary"},"+ Vorfall melden")]),c.value?(o(),n("div",M,"Laden...")):m.value.length===0?(o(),n("div",S,[...t[7]||(t[7]=[e("p",{class:"text-4xl mb-4"},"✅",-1),e("p",null,"Keine Vorfälle gemeldet",-1)])])):(o(),n("div",G,[(o(!0),n(f,null,_(m.value,s=>(o(),n("div",{key:s.id,class:"bg-white rounded-lg shadow p-4"},[e("div",I,[e("div",K,[e("span",U,l(s.category_icon||"📋"),1),e("div",null,[e("h3",z,l(s.title),1),e("p",F,l(s.category_name)+" · "+l(s.object_name||"Ohne Objekt"),1),e("p",L,l(new Date(s.occurred_at).toLocaleString("de-DE")),1)])]),e("div",O,[e("span",{class:w(["px-2 py-1 text-xs rounded",b(s.status).class])},l(b(s.status).text),3),e("span",{class:w(["px-2 py-1 text-xs rounded",h(s.severity)])}," Stufe "+l(s.severity),3)])]),s.description?(o(),n("p",A,l(s.description),1)):v("",!0),e("div",E,[e("span",null,"Gemeldet von "+l(s.reporter_first)+" "+l(s.reporter_last),1),s.attachment_count?(o(),n("span",N,"📎 "+l(s.attachment_count)+" Anhänge",1)):v("",!0)])]))),128))])),d.value?(o(),n("div",R,[e("div",T,[t[14]||(t[14]=e("h2",{class:"text-xl font-bold mb-4"},"Vorfall melden",-1)),e("div",$,[e("div",null,[t[8]||(t[8]=e("label",{class:"block text-sm font-medium mb-1"},"Titel *",-1)),u(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>i.value.title=s),class:"input",placeholder:"Kurze Beschreibung"},null,512),[[p,i.value.title]])]),e("div",null,[t[10]||(t[10]=e("label",{class:"block text-sm font-medium mb-1"},"Kategorie",-1)),u(e("select",{"onUpdate:modelValue":t[2]||(t[2]=s=>i.value.category_id=s),class:"input"},[t[9]||(t[9]=e("option",{value:""},"-- Wählen --",-1)),(o(!0),n(f,null,_(g.value,s=>(o(),n("option",{key:s.id,value:s.id},l(s.icon)+" "+l(s.name),9,q))),128))],512),[[B,i.value.category_id]])]),e("div",null,[t[11]||(t[11]=e("label",{class:"block text-sm font-medium mb-1"},"Schweregrad (1-5)",-1)),u(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>i.value.severity=s),type:"range",min:"1",max:"5",class:"w-full"},null,512),[[p,i.value.severity,void 0,{number:!0}]]),t[12]||(t[12]=e("div",{class:"flex justify-between text-xs text-gray-500"},[e("span",null,"Gering"),e("span",null,"Kritisch")],-1))]),e("div",null,[t[13]||(t[13]=e("label",{class:"block text-sm font-medium mb-1"},"Beschreibung",-1)),u(e("textarea",{"onUpdate:modelValue":t[4]||(t[4]=s=>i.value.description=s),rows:"3",class:"input",placeholder:"Details..."},null,512),[[p,i.value.description]])])]),e("div",P,[e("button",{onClick:t[5]||(t[5]=s=>d.value=!1),class:"btn"},"Abbrechen"),e("button",{onClick:k,class:"btn btn-primary"},"Melden")])])])):v("",!0)]))}});export{J as default};
|
||||||
@@ -1 +1 @@
|
|||||||
import{d as h,u as q,r as a,c,a as e,t as g,w as B,b as r,v as n,F as N,e as k,f as R,g as U,o as f}from"./index-OvQoqblD.js";const A={class:"min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8"},M={class:"max-w-md w-full space-y-8"},C={class:"card"},E={class:"text-xl font-semibold text-gray-900 dark:text-white mb-6"},D={class:"grid grid-cols-2 gap-4"},F=["placeholder"],O={key:1,class:"p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm"},T=["disabled"],j={class:"mt-4 text-center"},L=h({__name:"LoginView",setup(z){const w=R(),V=U(),m=q(),o=a(""),u=a(""),i=a(m.orgSlug||""),p=a(""),v=a(!1),s=a(!1),x=a(""),y=a(""),b=a("");async function S(){p.value="",v.value=!0;try{s.value?(await m.register({email:o.value,password:u.value,first_name:x.value,last_name:y.value,phone:b.value,org_slug:i.value}),await m.login(o.value,u.value,i.value)):await m.login(o.value,u.value,i.value);const d=V.query.redirect||"/";w.push(d)}catch(d){p.value=d instanceof Error?d.message:"Anmeldung fehlgeschlagen"}finally{v.value=!1}}return(d,t)=>(f(),c("div",A,[e("div",M,[t[13]||(t[13]=e("div",{class:"text-center"},[e("h1",{class:"text-4xl font-bold text-primary-600"},"🔐 SeCu"),e("p",{class:"mt-2 text-gray-600 dark:text-gray-400"},"Mitarbeiterverwaltung")],-1)),e("div",C,[e("h2",E,g(s.value?"Registrieren":"Anmelden"),1),e("form",{onSubmit:B(S,["prevent"]),class:"space-y-4"},[e("div",null,[t[7]||(t[7]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Organisation ",-1)),r(e("input",{"onUpdate:modelValue":t[0]||(t[0]=l=>i.value=l),type:"text",required:"",class:"input",placeholder:"z.B. demo"},null,512),[[n,i.value]])]),e("div",null,[t[8]||(t[8]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," E-Mail ",-1)),r(e("input",{"onUpdate:modelValue":t[1]||(t[1]=l=>o.value=l),type:"email",required:"",class:"input",placeholder:"name@firma.de"},null,512),[[n,o.value]])]),s.value?(f(),c(N,{key:0},[e("div",D,[e("div",null,[t[9]||(t[9]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Vorname ",-1)),r(e("input",{"onUpdate:modelValue":t[2]||(t[2]=l=>x.value=l),type:"text",required:"",class:"input"},null,512),[[n,x.value]])]),e("div",null,[t[10]||(t[10]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Nachname ",-1)),r(e("input",{"onUpdate:modelValue":t[3]||(t[3]=l=>y.value=l),type:"text",required:"",class:"input"},null,512),[[n,y.value]])])]),e("div",null,[t[11]||(t[11]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Telefon ",-1)),r(e("input",{"onUpdate:modelValue":t[4]||(t[4]=l=>b.value=l),type:"tel",class:"input",placeholder:"Optional"},null,512),[[n,b.value]])])],64)):k("",!0),e("div",null,[t[12]||(t[12]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Passwort ",-1)),r(e("input",{"onUpdate:modelValue":t[5]||(t[5]=l=>u.value=l),type:"password",required:"",class:"input",placeholder:s.value?"Mindestens 8 Zeichen":""},null,8,F),[[n,u.value]])]),p.value?(f(),c("div",O,g(p.value),1)):k("",!0),e("button",{type:"submit",disabled:v.value,class:"btn btn-primary w-full"},g(v.value?"Bitte warten...":s.value?"Registrieren":"Anmelden"),9,T)],32),e("div",j,[e("button",{type:"button",class:"text-sm text-primary-600 hover:text-primary-700",onClick:t[6]||(t[6]=l=>s.value=!s.value)},g(s.value?"Bereits registriert? Anmelden":"Noch kein Konto? Registrieren"),1)])])])]))}});export{L as default};
|
import{d as h,u as q,r as a,c,a as e,t as g,w as B,b as r,v as n,F as N,e as k,f as R,g as U,o as f}from"./index-CWxNv9Fc.js";const A={class:"min-h-screen flex items-center justify-center bg-gray-50 dark:bg-gray-900 py-12 px-4 sm:px-6 lg:px-8"},M={class:"max-w-md w-full space-y-8"},C={class:"card"},E={class:"text-xl font-semibold text-gray-900 dark:text-white mb-6"},D={class:"grid grid-cols-2 gap-4"},F=["placeholder"],O={key:1,class:"p-3 bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 rounded-lg text-sm"},T=["disabled"],j={class:"mt-4 text-center"},L=h({__name:"LoginView",setup(z){const w=R(),V=U(),m=q(),o=a(""),u=a(""),i=a(m.orgSlug||""),p=a(""),v=a(!1),s=a(!1),x=a(""),y=a(""),b=a("");async function S(){p.value="",v.value=!0;try{s.value?(await m.register({email:o.value,password:u.value,first_name:x.value,last_name:y.value,phone:b.value,org_slug:i.value}),await m.login(o.value,u.value,i.value)):await m.login(o.value,u.value,i.value);const d=V.query.redirect||"/";w.push(d)}catch(d){p.value=d instanceof Error?d.message:"Anmeldung fehlgeschlagen"}finally{v.value=!1}}return(d,t)=>(f(),c("div",A,[e("div",M,[t[13]||(t[13]=e("div",{class:"text-center"},[e("h1",{class:"text-4xl font-bold text-primary-600"},"🔐 SeCu"),e("p",{class:"mt-2 text-gray-600 dark:text-gray-400"},"Mitarbeiterverwaltung")],-1)),e("div",C,[e("h2",E,g(s.value?"Registrieren":"Anmelden"),1),e("form",{onSubmit:B(S,["prevent"]),class:"space-y-4"},[e("div",null,[t[7]||(t[7]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Organisation ",-1)),r(e("input",{"onUpdate:modelValue":t[0]||(t[0]=l=>i.value=l),type:"text",required:"",class:"input",placeholder:"z.B. demo"},null,512),[[n,i.value]])]),e("div",null,[t[8]||(t[8]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," E-Mail ",-1)),r(e("input",{"onUpdate:modelValue":t[1]||(t[1]=l=>o.value=l),type:"email",required:"",class:"input",placeholder:"name@firma.de"},null,512),[[n,o.value]])]),s.value?(f(),c(N,{key:0},[e("div",D,[e("div",null,[t[9]||(t[9]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Vorname ",-1)),r(e("input",{"onUpdate:modelValue":t[2]||(t[2]=l=>x.value=l),type:"text",required:"",class:"input"},null,512),[[n,x.value]])]),e("div",null,[t[10]||(t[10]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Nachname ",-1)),r(e("input",{"onUpdate:modelValue":t[3]||(t[3]=l=>y.value=l),type:"text",required:"",class:"input"},null,512),[[n,y.value]])])]),e("div",null,[t[11]||(t[11]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Telefon ",-1)),r(e("input",{"onUpdate:modelValue":t[4]||(t[4]=l=>b.value=l),type:"tel",class:"input",placeholder:"Optional"},null,512),[[n,b.value]])])],64)):k("",!0),e("div",null,[t[12]||(t[12]=e("label",{class:"block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"}," Passwort ",-1)),r(e("input",{"onUpdate:modelValue":t[5]||(t[5]=l=>u.value=l),type:"password",required:"",class:"input",placeholder:s.value?"Mindestens 8 Zeichen":""},null,8,F),[[n,u.value]])]),p.value?(f(),c("div",O,g(p.value),1)):k("",!0),e("button",{type:"submit",disabled:v.value,class:"btn btn-primary w-full"},g(v.value?"Bitte warten...":s.value?"Registrieren":"Anmelden"),9,T)],32),e("div",j,[e("button",{type:"button",class:"text-sm text-primary-600 hover:text-primary-700",onClick:t[6]||(t[6]=l=>s.value=!s.value)},g(s.value?"Bereits registriert? Anmelden":"Noch kein Konto? Registrieren"),1)])])])]))}});export{L as default};
|
||||||
@@ -1 +1 @@
|
|||||||
import{d as k,q as h,m as d,c as l,a as t,t as r,e as u,F as w,x as M,r as c,o as n,n as f}from"./index-OvQoqblD.js";const S={class:"space-y-6"},C={key:0,class:"card bg-gradient-to-r from-primary-500 to-primary-700 text-white"},B={class:"grid grid-cols-2 md:grid-cols-4 gap-4"},V={class:"text-2xl font-bold"},z={class:"text-2xl font-bold"},A={class:"text-2xl font-bold"},F={class:"text-2xl font-bold"},D={class:"card"},E={key:0,class:"text-center py-8 text-gray-500"},L={key:1,class:"space-y-4"},N={class:"flex-1"},$={class:"flex items-center gap-2"},j={class:"font-medium text-gray-900 dark:text-white"},q={key:0,class:"badge badge-primary"},H={key:0,class:"text-sm text-gray-500 mt-1"},P=["disabled","onClick"],K=k({__name:"ModulesView",setup(G){const g=c([]),i=c(!0),o=c(null);h(async()=>{await Promise.all([b(),v()])});async function b(){i.value=!0;try{const s=await d.get("/modules/org");g.value=s.data.modules}catch(s){console.error(s)}finally{i.value=!1}}async function v(){try{const s=await d.get("/modules/developer/status");o.value=s.data}catch{console.log("Dev status not available")}}async function _(s){if(!s.is_core)try{await d.post(`/modules/${s.id}/toggle`,{enabled:!s.enabled}),s.enabled=!s.enabled}catch(e){alert(e instanceof Error?e.message:"Fehler")}}return(s,e)=>{var m,p,y,x;return n(),l("div",S,[e[6]||(e[6]=t("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"⚙️ Module",-1)),o.value?(n(),l("div",C,[e[4]||(e[4]=t("h2",{class:"text-lg font-semibold mb-4"},"System Status",-1)),t("div",B,[t("div",null,[e[0]||(e[0]=t("p",{class:"text-primary-100 text-sm"},"Benutzer",-1)),t("p",V,r(((m=o.value.stats)==null?void 0:m.user_count)||0),1)]),t("div",null,[e[1]||(e[1]=t("p",{class:"text-primary-100 text-sm"},"Aufträge",-1)),t("p",z,r(((p=o.value.stats)==null?void 0:p.order_count)||0),1)]),t("div",null,[e[2]||(e[2]=t("p",{class:"text-primary-100 text-sm"},"Stundenzettel",-1)),t("p",A,r(((y=o.value.stats)==null?void 0:y.timesheet_count)||0),1)]),t("div",null,[e[3]||(e[3]=t("p",{class:"text-primary-100 text-sm"},"Aktive Module",-1)),t("p",F,r(((x=o.value.stats)==null?void 0:x.enabled_modules)||0),1)])])])):u("",!0),t("div",D,[e[5]||(e[5]=t("h2",{class:"text-lg font-semibold mb-4"},"Verfügbare Module",-1)),i.value?(n(),l("div",E,"Lädt...")):(n(),l("div",L,[(n(!0),l(w,null,M(g.value,a=>(n(),l("div",{key:a.id,class:"flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"},[t("div",N,[t("div",$,[t("h3",j,r(a.display_name),1),a.is_core?(n(),l("span",q,"Core")):u("",!0)]),a.description?(n(),l("p",H,r(a.description),1)):u("",!0)]),t("button",{disabled:a.is_core,class:f(["relative inline-flex h-6 w-11 items-center rounded-full transition-colors",a.enabled?"bg-primary-600":"bg-gray-200 dark:bg-gray-600",a.is_core?"opacity-50 cursor-not-allowed":"cursor-pointer"]),onClick:I=>_(a)},[t("span",{class:f(["inline-block h-4 w-4 transform rounded-full bg-white transition-transform",a.enabled?"translate-x-6":"translate-x-1"])},null,2)],10,P)]))),128))]))]),e[7]||(e[7]=t("div",{class:"card"},[t("h2",{class:"text-lg font-semibold mb-2"},"Hinweis"),t("p",{class:"text-gray-500 text-sm"}," Core-Module (Basis-System, Auftragsverwaltung) können nicht deaktiviert werden. Änderungen an Modulen werden sofort wirksam. ")],-1))])}}});export{K as default};
|
import{d as k,q as h,m as d,c as l,a as t,t as r,e as u,F as w,x as M,r as c,o as n,n as f}from"./index-CWxNv9Fc.js";const S={class:"space-y-6"},C={key:0,class:"card bg-gradient-to-r from-primary-500 to-primary-700 text-white"},B={class:"grid grid-cols-2 md:grid-cols-4 gap-4"},V={class:"text-2xl font-bold"},z={class:"text-2xl font-bold"},A={class:"text-2xl font-bold"},F={class:"text-2xl font-bold"},D={class:"card"},E={key:0,class:"text-center py-8 text-gray-500"},L={key:1,class:"space-y-4"},N={class:"flex-1"},$={class:"flex items-center gap-2"},j={class:"font-medium text-gray-900 dark:text-white"},q={key:0,class:"badge badge-primary"},H={key:0,class:"text-sm text-gray-500 mt-1"},P=["disabled","onClick"],K=k({__name:"ModulesView",setup(G){const g=c([]),i=c(!0),o=c(null);h(async()=>{await Promise.all([b(),v()])});async function b(){i.value=!0;try{const s=await d.get("/modules/org");g.value=s.data.modules}catch(s){console.error(s)}finally{i.value=!1}}async function v(){try{const s=await d.get("/modules/developer/status");o.value=s.data}catch{console.log("Dev status not available")}}async function _(s){if(!s.is_core)try{await d.post(`/modules/${s.id}/toggle`,{enabled:!s.enabled}),s.enabled=!s.enabled}catch(e){alert(e instanceof Error?e.message:"Fehler")}}return(s,e)=>{var m,p,y,x;return n(),l("div",S,[e[6]||(e[6]=t("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"⚙️ Module",-1)),o.value?(n(),l("div",C,[e[4]||(e[4]=t("h2",{class:"text-lg font-semibold mb-4"},"System Status",-1)),t("div",B,[t("div",null,[e[0]||(e[0]=t("p",{class:"text-primary-100 text-sm"},"Benutzer",-1)),t("p",V,r(((m=o.value.stats)==null?void 0:m.user_count)||0),1)]),t("div",null,[e[1]||(e[1]=t("p",{class:"text-primary-100 text-sm"},"Aufträge",-1)),t("p",z,r(((p=o.value.stats)==null?void 0:p.order_count)||0),1)]),t("div",null,[e[2]||(e[2]=t("p",{class:"text-primary-100 text-sm"},"Stundenzettel",-1)),t("p",A,r(((y=o.value.stats)==null?void 0:y.timesheet_count)||0),1)]),t("div",null,[e[3]||(e[3]=t("p",{class:"text-primary-100 text-sm"},"Aktive Module",-1)),t("p",F,r(((x=o.value.stats)==null?void 0:x.enabled_modules)||0),1)])])])):u("",!0),t("div",D,[e[5]||(e[5]=t("h2",{class:"text-lg font-semibold mb-4"},"Verfügbare Module",-1)),i.value?(n(),l("div",E,"Lädt...")):(n(),l("div",L,[(n(!0),l(w,null,M(g.value,a=>(n(),l("div",{key:a.id,class:"flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"},[t("div",N,[t("div",$,[t("h3",j,r(a.display_name),1),a.is_core?(n(),l("span",q,"Core")):u("",!0)]),a.description?(n(),l("p",H,r(a.description),1)):u("",!0)]),t("button",{disabled:a.is_core,class:f(["relative inline-flex h-6 w-11 items-center rounded-full transition-colors",a.enabled?"bg-primary-600":"bg-gray-200 dark:bg-gray-600",a.is_core?"opacity-50 cursor-not-allowed":"cursor-pointer"]),onClick:I=>_(a)},[t("span",{class:f(["inline-block h-4 w-4 transform rounded-full bg-white transition-transform",a.enabled?"translate-x-6":"translate-x-1"])},null,2)],10,P)]))),128))]))]),e[7]||(e[7]=t("div",{class:"card"},[t("h2",{class:"text-lg font-semibold mb-2"},"Hinweis"),t("p",{class:"text-gray-500 text-sm"}," Core-Module (Basis-System, Auftragsverwaltung) können nicht deaktiviert werden. Änderungen an Modulen werden sofort wirksam. ")],-1))])}}});export{K as default};
|
||||||
1
dist/assets/ObjectsView-BhTJxexl.js
vendored
1
dist/assets/ObjectsView-BhTJxexl.js
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/ObjectsView-CzlgFF9j.js
vendored
Normal file
1
dist/assets/ObjectsView-CzlgFF9j.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
import{d as A,u as $,q as B,m as p,c as r,i as S,j as V,a as e,t as o,e as l,n as _,y as k,F as h,x as E,g as M,f as N,r as y,l as D,o as i,p as F}from"./index-OvQoqblD.js";const L={key:0,class:"text-center py-12 text-gray-500"},j={key:1,class:"space-y-6"},z={class:"card"},O={class:"flex items-start justify-between"},q={class:"flex items-center gap-3"},I={class:"text-gray-500"},R={class:"text-2xl font-bold text-gray-900 dark:text-white"},Z={key:0,class:"mt-2 text-gray-600 dark:text-gray-400"},H={class:"grid grid-cols-2 md:grid-cols-4 gap-4 mt-6"},K={key:0},T={class:"font-medium"},G={key:1},J={class:"font-medium"},P={key:2},Q={class:"font-medium"},U={class:"font-medium"},W={key:0,class:"mt-6 pt-6 border-t border-gray-200 dark:border-gray-700"},X={class:"flex flex-wrap gap-2"},Y={class:"card"},tt={key:0,class:"text-center py-4 text-gray-500"},et={key:1,class:"space-y-3"},st={class:"font-medium text-gray-900 dark:text-white"},at={class:"text-sm text-gray-500"},nt={class:"flex items-center gap-2"},rt={key:0,class:"card"},ot={class:"text-gray-600 dark:text-gray-400 whitespace-pre-wrap"},dt=A({__name:"OrderDetailView",setup(it){const c=M(),w=N(),m=$(),s=y(null),d=y([]),v=y(!0);B(async()=>{try{const n=await p.get(`/orders/${c.params.id}`);s.value=n.data.order,d.value=n.data.assignments}catch(n){console.error(n),w.push("/orders")}finally{v.value=!1}});async function u(n){try{await p.put(`/orders/${c.params.id}`,{status:n}),s.value.status=n}catch(t){alert(t instanceof Error?t.message:"Fehler")}}async function x(n){try{await p.put(`/orders/${c.params.id}/assignment`,{status:n?"confirmed":"declined"});const t=d.value.find(g=>{var a;return g.user_id===((a=m.user)==null?void 0:a.id)});t&&(t.status=n?"confirmed":"declined")}catch(t){alert(t instanceof Error?t.message:"Fehler")}}function b(n){return{draft:"Entwurf",published:"Veröffentlicht",in_progress:"In Bearbeitung",completed:"Abgeschlossen",cancelled:"Abgesagt",pending:"Ausstehend",confirmed:"Bestätigt",declined:"Abgelehnt"}[n]||n}return(n,t)=>{const g=D("router-link");return v.value?(i(),r("div",L,"Lädt...")):s.value?(i(),r("div",j,[S(g,{to:"/orders",class:"text-primary-600 hover:text-primary-700 text-sm"},{default:V(()=>[...t[6]||(t[6]=[F(" ← Zurück zu Aufträge ",-1)])]),_:1}),e("div",z,[e("div",O,[e("div",null,[e("div",q,[e("span",I,"#"+o(s.value.number),1),e("h1",R,o(s.value.title),1)]),s.value.description?(i(),r("p",Z,o(s.value.description),1)):l("",!0)]),e("span",{class:_(["badge",s.value.status==="completed"?"badge-success":"badge-primary"])},o(b(s.value.status)),3)]),e("div",H,[s.value.location?(i(),r("div",K,[t[7]||(t[7]=e("p",{class:"text-sm text-gray-500"},"Ort",-1)),e("p",T,"📍 "+o(s.value.location),1)])):l("",!0),s.value.start_time?(i(),r("div",G,[t[8]||(t[8]=e("p",{class:"text-sm text-gray-500"},"Start",-1)),e("p",J,o(new Date(s.value.start_time).toLocaleString("de-DE")),1)])):l("",!0),s.value.client_name?(i(),r("div",P,[t[9]||(t[9]=e("p",{class:"text-sm text-gray-500"},"Kunde",-1)),e("p",Q,o(s.value.client_name),1)])):l("",!0),e("div",null,[t[10]||(t[10]=e("p",{class:"text-sm text-gray-500"},"Benötigte MA",-1)),e("p",U,o(d.value.length)+"/"+o(s.value.required_staff),1)])]),k(m).canManageOrders?(i(),r("div",W,[t[11]||(t[11]=e("p",{class:"text-sm text-gray-500 mb-2"},"Status ändern:",-1)),e("div",X,[e("button",{class:"btn btn-secondary text-sm",onClick:t[0]||(t[0]=a=>u("draft"))},"Entwurf"),e("button",{class:"btn btn-primary text-sm",onClick:t[1]||(t[1]=a=>u("published"))},"Veröffentlichen"),e("button",{class:"btn btn-warning text-sm",onClick:t[2]||(t[2]=a=>u("in_progress"))},"In Bearbeitung"),e("button",{class:"btn btn-success text-sm",onClick:t[3]||(t[3]=a=>u("completed"))},"Abschließen")])])):l("",!0)]),e("div",Y,[t[12]||(t[12]=e("h2",{class:"text-lg font-semibold text-gray-900 dark:text-white mb-4"}," 👥 Zugewiesene Mitarbeiter ",-1)),d.value.length===0?(i(),r("div",tt," Noch keine Mitarbeiter zugewiesen ")):(i(),r("div",et,[(i(!0),r(h,null,E(d.value,a=>{var f;return i(),r("div",{key:a.id,class:"flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg"},[e("div",null,[e("p",st,o(a.user_name),1),e("p",at,o(a.user_phone),1)]),e("div",nt,[e("span",{class:_(["badge",a.status==="confirmed"?"badge-success":a.status==="declined"?"badge-danger":"badge-warning"])},o(b(a.status)),3),a.user_id===((f=k(m).user)==null?void 0:f.id)&&a.status==="pending"?(i(),r(h,{key:0},[e("button",{class:"btn btn-success text-sm",onClick:t[4]||(t[4]=C=>x(!0))},"✓"),e("button",{class:"btn btn-danger text-sm",onClick:t[5]||(t[5]=C=>x(!1))},"✗")],64)):l("",!0)])])}),128))]))]),s.value.special_instructions?(i(),r("div",rt,[t[13]||(t[13]=e("h2",{class:"text-lg font-semibold text-gray-900 dark:text-white mb-2"}," 📝 Besondere Hinweise ",-1)),e("p",ot,o(s.value.special_instructions),1)])):l("",!0)])):l("",!0)}}});export{dt as default};
|
import{d as A,u as $,q as B,m as p,c as r,i as S,j as V,a as e,t as o,e as l,n as _,y as k,F as h,x as E,g as M,f as N,r as y,l as D,o as i,p as F}from"./index-CWxNv9Fc.js";const L={key:0,class:"text-center py-12 text-gray-500"},j={key:1,class:"space-y-6"},z={class:"card"},O={class:"flex items-start justify-between"},q={class:"flex items-center gap-3"},I={class:"text-gray-500"},R={class:"text-2xl font-bold text-gray-900 dark:text-white"},Z={key:0,class:"mt-2 text-gray-600 dark:text-gray-400"},H={class:"grid grid-cols-2 md:grid-cols-4 gap-4 mt-6"},K={key:0},T={class:"font-medium"},G={key:1},J={class:"font-medium"},P={key:2},Q={class:"font-medium"},U={class:"font-medium"},W={key:0,class:"mt-6 pt-6 border-t border-gray-200 dark:border-gray-700"},X={class:"flex flex-wrap gap-2"},Y={class:"card"},tt={key:0,class:"text-center py-4 text-gray-500"},et={key:1,class:"space-y-3"},st={class:"font-medium text-gray-900 dark:text-white"},at={class:"text-sm text-gray-500"},nt={class:"flex items-center gap-2"},rt={key:0,class:"card"},ot={class:"text-gray-600 dark:text-gray-400 whitespace-pre-wrap"},dt=A({__name:"OrderDetailView",setup(it){const c=M(),w=N(),m=$(),s=y(null),d=y([]),v=y(!0);B(async()=>{try{const n=await p.get(`/orders/${c.params.id}`);s.value=n.data.order,d.value=n.data.assignments}catch(n){console.error(n),w.push("/orders")}finally{v.value=!1}});async function u(n){try{await p.put(`/orders/${c.params.id}`,{status:n}),s.value.status=n}catch(t){alert(t instanceof Error?t.message:"Fehler")}}async function x(n){try{await p.put(`/orders/${c.params.id}/assignment`,{status:n?"confirmed":"declined"});const t=d.value.find(g=>{var a;return g.user_id===((a=m.user)==null?void 0:a.id)});t&&(t.status=n?"confirmed":"declined")}catch(t){alert(t instanceof Error?t.message:"Fehler")}}function b(n){return{draft:"Entwurf",published:"Veröffentlicht",in_progress:"In Bearbeitung",completed:"Abgeschlossen",cancelled:"Abgesagt",pending:"Ausstehend",confirmed:"Bestätigt",declined:"Abgelehnt"}[n]||n}return(n,t)=>{const g=D("router-link");return v.value?(i(),r("div",L,"Lädt...")):s.value?(i(),r("div",j,[S(g,{to:"/orders",class:"text-primary-600 hover:text-primary-700 text-sm"},{default:V(()=>[...t[6]||(t[6]=[F(" ← Zurück zu Aufträge ",-1)])]),_:1}),e("div",z,[e("div",O,[e("div",null,[e("div",q,[e("span",I,"#"+o(s.value.number),1),e("h1",R,o(s.value.title),1)]),s.value.description?(i(),r("p",Z,o(s.value.description),1)):l("",!0)]),e("span",{class:_(["badge",s.value.status==="completed"?"badge-success":"badge-primary"])},o(b(s.value.status)),3)]),e("div",H,[s.value.location?(i(),r("div",K,[t[7]||(t[7]=e("p",{class:"text-sm text-gray-500"},"Ort",-1)),e("p",T,"📍 "+o(s.value.location),1)])):l("",!0),s.value.start_time?(i(),r("div",G,[t[8]||(t[8]=e("p",{class:"text-sm text-gray-500"},"Start",-1)),e("p",J,o(new Date(s.value.start_time).toLocaleString("de-DE")),1)])):l("",!0),s.value.client_name?(i(),r("div",P,[t[9]||(t[9]=e("p",{class:"text-sm text-gray-500"},"Kunde",-1)),e("p",Q,o(s.value.client_name),1)])):l("",!0),e("div",null,[t[10]||(t[10]=e("p",{class:"text-sm text-gray-500"},"Benötigte MA",-1)),e("p",U,o(d.value.length)+"/"+o(s.value.required_staff),1)])]),k(m).canManageOrders?(i(),r("div",W,[t[11]||(t[11]=e("p",{class:"text-sm text-gray-500 mb-2"},"Status ändern:",-1)),e("div",X,[e("button",{class:"btn btn-secondary text-sm",onClick:t[0]||(t[0]=a=>u("draft"))},"Entwurf"),e("button",{class:"btn btn-primary text-sm",onClick:t[1]||(t[1]=a=>u("published"))},"Veröffentlichen"),e("button",{class:"btn btn-warning text-sm",onClick:t[2]||(t[2]=a=>u("in_progress"))},"In Bearbeitung"),e("button",{class:"btn btn-success text-sm",onClick:t[3]||(t[3]=a=>u("completed"))},"Abschließen")])])):l("",!0)]),e("div",Y,[t[12]||(t[12]=e("h2",{class:"text-lg font-semibold text-gray-900 dark:text-white mb-4"}," 👥 Zugewiesene Mitarbeiter ",-1)),d.value.length===0?(i(),r("div",tt," Noch keine Mitarbeiter zugewiesen ")):(i(),r("div",et,[(i(!0),r(h,null,E(d.value,a=>{var f;return i(),r("div",{key:a.id,class:"flex items-center justify-between p-3 bg-gray-50 dark:bg-gray-700/50 rounded-lg"},[e("div",null,[e("p",st,o(a.user_name),1),e("p",at,o(a.user_phone),1)]),e("div",nt,[e("span",{class:_(["badge",a.status==="confirmed"?"badge-success":a.status==="declined"?"badge-danger":"badge-warning"])},o(b(a.status)),3),a.user_id===((f=k(m).user)==null?void 0:f.id)&&a.status==="pending"?(i(),r(h,{key:0},[e("button",{class:"btn btn-success text-sm",onClick:t[4]||(t[4]=C=>x(!0))},"✓"),e("button",{class:"btn btn-danger text-sm",onClick:t[5]||(t[5]=C=>x(!1))},"✗")],64)):l("",!0)])])}),128))]))]),s.value.special_instructions?(i(),r("div",rt,[t[13]||(t[13]=e("h2",{class:"text-lg font-semibold text-gray-900 dark:text-white mb-2"}," 📝 Besondere Hinweise ",-1)),e("p",ot,o(s.value.special_instructions),1)])):l("",!0)])):l("",!0)}}});export{dt as default};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/PatrolsView-CE7IkVbv.js
vendored
Normal file
1
dist/assets/PatrolsView-CE7IkVbv.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{d as h,q as k,m,c as s,a as t,n as u,t as a,F as g,x as _,e as v,r as d,o as n}from"./index-CWxNv9Fc.js";const f={class:"p-6"},w={class:"flex space-x-1 border-b mb-6"},C={key:0,class:"text-center py-12"},R={key:1},j={key:0,class:"bg-gray-50 rounded-lg p-12 text-center text-gray-500"},M={key:1,class:"grid md:grid-cols-2 lg:grid-cols-3 gap-4"},D={class:"flex justify-between items-start"},B={class:"font-semibold"},E={class:"text-sm text-gray-500"},K={class:"text-xs text-gray-400 mt-1"},L={class:"text-2xl"},T={class:"mt-3 p-2 bg-gray-100 rounded text-xs font-mono break-all"},V={key:2},$={key:0,class:"bg-gray-50 rounded-lg p-12 text-center text-gray-500"},A={key:1,class:"space-y-4"},F={class:"flex justify-between items-start"},N={class:"font-semibold"},P={class:"text-sm text-gray-500"},S={class:"mt-3 flex gap-4 text-sm text-gray-600"},q={key:0},z={key:1},I={key:3},O={key:0,class:"bg-gray-50 rounded-lg p-12 text-center text-gray-500"},Q={key:1,class:"bg-white rounded-lg shadow overflow-hidden"},W={class:"min-w-full divide-y divide-gray-200"},Z={class:"divide-y divide-gray-200"},G={class:"px-4 py-3 text-sm"},H={class:"px-4 py-3 text-sm"},J={class:"px-4 py-3 text-sm"},U={class:"px-4 py-3 text-sm text-gray-500"},tt=h({__name:"PatrolsView",setup(X){const p=d(!0),r=d([]),c=d([]),x=d([]),l=d("checkpoints");k(async()=>{await y()});async function y(){p.value=!0;try{const[i,o,e]=await Promise.all([m.get("/patrols/checkpoints"),m.get("/patrols/routes"),m.get("/patrols/logs")]);r.value=i.data.checkpoints||[],c.value=o.data.routes||[],x.value=e.data.logs||[]}catch(i){console.error(i)}p.value=!1}function b(i){return new Date(i).toLocaleTimeString("de-DE",{hour:"2-digit",minute:"2-digit"})}return(i,o)=>(n(),s("div",f,[o[7]||(o[7]=t("div",{class:"flex justify-between items-center mb-6"},[t("div",null,[t("h1",{class:"text-2xl font-bold"},"📍 Wächterkontrolle"),t("p",{class:"text-gray-500"},"Checkpoints, Routen & Rundgänge")])],-1)),t("div",w,[t("button",{onClick:o[0]||(o[0]=e=>l.value="checkpoints"),class:u(["px-4 py-2 font-medium border-b-2 -mb-px",l.value==="checkpoints"?"border-blue-600 text-blue-600":"border-transparent"])}," Checkpoints ("+a(r.value.length)+") ",3),t("button",{onClick:o[1]||(o[1]=e=>l.value="routes"),class:u(["px-4 py-2 font-medium border-b-2 -mb-px",l.value==="routes"?"border-blue-600 text-blue-600":"border-transparent"])}," Routen ("+a(c.value.length)+") ",3),t("button",{onClick:o[2]||(o[2]=e=>l.value="logs"),class:u(["px-4 py-2 font-medium border-b-2 -mb-px",l.value==="logs"?"border-blue-600 text-blue-600":"border-transparent"])}," Rundgänge ",2)]),p.value?(n(),s("div",C,"Laden...")):l.value==="checkpoints"?(n(),s("div",R,[r.value.length===0?(n(),s("div",j,[...o[3]||(o[3]=[t("p",{class:"text-4xl mb-4"},"📍",-1),t("p",null,"Keine Checkpoints vorhanden",-1),t("p",{class:"text-sm mt-2"},"Erstelle Checkpoints mit QR-Codes für Rundgänge",-1)])])):(n(),s("div",M,[(n(!0),s(g,null,_(r.value,e=>(n(),s("div",{key:e.id,class:"bg-white rounded-lg shadow p-4"},[t("div",D,[t("div",null,[t("h3",B,a(e.name),1),t("p",E,a(e.object_name),1),t("p",K,a(e.location_description),1)]),t("span",L,a(e.checkpoint_type==="nfc"?"📶":"📱"),1)]),t("div",T,a(e.code),1)]))),128))]))])):l.value==="routes"?(n(),s("div",V,[c.value.length===0?(n(),s("div",$,[...o[4]||(o[4]=[t("p",{class:"text-4xl mb-4"},"🗺️",-1),t("p",null,"Keine Routen definiert",-1)])])):(n(),s("div",A,[(n(!0),s(g,null,_(c.value,e=>(n(),s("div",{key:e.id,class:"bg-white rounded-lg shadow p-4"},[t("div",F,[t("div",null,[t("h3",N,a(e.name),1),t("p",P,a(e.object_name),1)]),t("span",{class:u(["px-2 py-1 text-xs rounded",e.is_active?"bg-green-100 text-green-700":"bg-gray-100"])},a(e.is_active?"Aktiv":"Inaktiv"),3)]),t("div",S,[t("span",null,"📍 "+a(e.checkpoint_count||0)+" Checkpoints",1),e.time_limit_minutes?(n(),s("span",q,"⏱️ Max "+a(e.time_limit_minutes)+" Min",1)):v("",!0),e.interval_minutes?(n(),s("span",z,"🔄 Alle "+a(e.interval_minutes)+" Min",1)):v("",!0)])]))),128))]))])):l.value==="logs"?(n(),s("div",I,[x.value.length===0?(n(),s("div",O,[...o[5]||(o[5]=[t("p",{class:"text-4xl mb-4"},"📋",-1),t("p",null,"Keine Rundgänge heute",-1)])])):(n(),s("div",Q,[t("table",W,[o[6]||(o[6]=t("thead",{class:"bg-gray-50"},[t("tr",null,[t("th",{class:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase"},"Zeit"),t("th",{class:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase"},"Mitarbeiter"),t("th",{class:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase"},"Checkpoint"),t("th",{class:"px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase"},"Objekt")])],-1)),t("tbody",Z,[(n(!0),s(g,null,_(x.value,e=>(n(),s("tr",{key:e.id,class:"hover:bg-gray-50"},[t("td",G,a(b(e.scanned_at)),1),t("td",H,a(e.first_name)+" "+a(e.last_name),1),t("td",J,a(e.checkpoint_name),1),t("td",U,a(e.object_name),1)]))),128))])])]))])):v("",!0)]))}});export{tt as default};
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
dist/assets/ShiftsView-gFZrEy1d.js
vendored
Normal file
1
dist/assets/ShiftsView-gFZrEy1d.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{d as E,q as $,m as x,c as i,a as t,F as h,x as k,e as S,t as r,b as c,v,r as d,k as N,o,C}from"./index-CWxNv9Fc.js";const U={class:"p-6"},j={class:"flex justify-between items-center mb-6"},L={class:"bg-white rounded-lg shadow p-4 mb-6"},M={class:"flex flex-wrap gap-2"},z={key:0,class:"text-gray-500"},A={class:"bg-white rounded-lg shadow overflow-hidden"},T={class:"flex items-center justify-between p-4 border-b"},W={class:"font-semibold"},P={class:"grid grid-cols-7 divide-x"},R={class:"text-xs font-semibold text-gray-500 mb-2"},q={key:0,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50"},I={class:"bg-white rounded-lg shadow-xl w-full max-w-md p-6"},K={class:"space-y-4"},O={class:"grid grid-cols-2 gap-4"},G={class:"grid grid-cols-2 gap-4"},H={class:"flex justify-end space-x-2 mt-6"},X=E({__name:"ShiftsView",setup(J){const D=d(!0),p=d([]),w=d([]),f=d(!1),u=d(new Date),l=d({name:"",start_time:"06:00",end_time:"14:00",break_minutes:30,color:"#3B82F6",is_night_shift:!1});$(async()=>{await b()});async function b(){D.value=!0;try{const[n,e]=await Promise.all([x.get("/shifts/definitions"),x.get(`/shifts/assignments?start=${y(g())}&end=${y(_())}`)]);p.value=n.data.shifts||[],w.value=e.data.assignments||[]}catch(n){console.error(n)}D.value=!1}function g(){const n=new Date(u.value);return n.setDate(n.getDate()-n.getDay()+1),n}function _(){const n=new Date(g());return n.setDate(n.getDate()+6),n}function y(n){return n.toISOString().split("T")[0]}const V=N(()=>{const n=[],e=g();for(let s=0;s<7;s++){const a=new Date(e);a.setDate(a.getDate()+s),n.push({date:y(a),label:a.toLocaleDateString("de-DE",{weekday:"short",day:"2-digit"})})}return n});async function F(){try{await x.post("/shifts/definitions",l.value),f.value=!1,l.value={name:"",start_time:"06:00",end_time:"14:00",break_minutes:30,color:"#3B82F6",is_night_shift:!1},await b()}catch(n){alert("Fehler: "+n.message)}}function B(n){return w.value.filter(e=>e.date===n)}return(n,e)=>(o(),i("div",U,[t("div",j,[e[9]||(e[9]=t("div",null,[t("h1",{class:"text-2xl font-bold"},"📅 Schichtplanung"),t("p",{class:"text-gray-500"},"Dienstpläne verwalten")],-1)),t("button",{onClick:e[0]||(e[0]=s=>f.value=!0),class:"btn btn-primary"},"+ Schicht definieren")]),t("div",L,[e[10]||(e[10]=t("h2",{class:"font-semibold mb-3"},"Schicht-Typen",-1)),t("div",M,[(o(!0),i(h,null,k(p.value,s=>{var a,m;return o(),i("div",{key:s.id,style:C({backgroundColor:s.color+"20",borderColor:s.color}),class:"px-3 py-1 rounded-full border text-sm"},r(s.name)+" ("+r((a=s.start_time)==null?void 0:a.slice(0,5))+" - "+r((m=s.end_time)==null?void 0:m.slice(0,5))+") ",5)}),128)),p.value.length===0?(o(),i("div",z,"Keine Schichten definiert")):S("",!0)])]),t("div",A,[t("div",T,[t("button",{onClick:e[1]||(e[1]=s=>{u.value.setDate(u.value.getDate()-7),b()}),class:"text-gray-600 hover:text-gray-900"},"← Vorherige"),t("span",W,r(g().toLocaleDateString("de-DE"))+" - "+r(_().toLocaleDateString("de-DE")),1),t("button",{onClick:e[2]||(e[2]=s=>{u.value.setDate(u.value.getDate()+7),b()}),class:"text-gray-600 hover:text-gray-900"},"Nächste →")]),t("div",P,[(o(!0),i(h,null,k(V.value,s=>(o(),i("div",{key:s.date,class:"min-h-32 p-2"},[t("div",R,r(s.label),1),(o(!0),i(h,null,k(B(s.date),a=>{var m;return o(),i("div",{key:a.id,style:C({backgroundColor:a.color+"40"}),class:"text-xs p-1 rounded mb-1"},r(a.first_name)+" "+r((m=a.last_name)==null?void 0:m.charAt(0))+". ",5)}),128))]))),128))])]),f.value?(o(),i("div",q,[t("div",I,[e[16]||(e[16]=t("h2",{class:"text-xl font-bold mb-4"},"Neue Schicht definieren",-1)),t("div",K,[t("div",null,[e[11]||(e[11]=t("label",{class:"block text-sm font-medium mb-1"},"Name",-1)),c(t("input",{"onUpdate:modelValue":e[3]||(e[3]=s=>l.value.name=s),class:"input",placeholder:"z.B. Frühschicht"},null,512),[[v,l.value.name]])]),t("div",O,[t("div",null,[e[12]||(e[12]=t("label",{class:"block text-sm font-medium mb-1"},"Beginn",-1)),c(t("input",{"onUpdate:modelValue":e[4]||(e[4]=s=>l.value.start_time=s),type:"time",class:"input"},null,512),[[v,l.value.start_time]])]),t("div",null,[e[13]||(e[13]=t("label",{class:"block text-sm font-medium mb-1"},"Ende",-1)),c(t("input",{"onUpdate:modelValue":e[5]||(e[5]=s=>l.value.end_time=s),type:"time",class:"input"},null,512),[[v,l.value.end_time]])])]),t("div",G,[t("div",null,[e[14]||(e[14]=t("label",{class:"block text-sm font-medium mb-1"},"Pause (Min.)",-1)),c(t("input",{"onUpdate:modelValue":e[6]||(e[6]=s=>l.value.break_minutes=s),type:"number",class:"input"},null,512),[[v,l.value.break_minutes,void 0,{number:!0}]])]),t("div",null,[e[15]||(e[15]=t("label",{class:"block text-sm font-medium mb-1"},"Farbe",-1)),c(t("input",{"onUpdate:modelValue":e[7]||(e[7]=s=>l.value.color=s),type:"color",class:"w-full h-10 rounded"},null,512),[[v,l.value.color]])])])]),t("div",H,[t("button",{onClick:e[8]||(e[8]=s=>f.value=!1),class:"btn"},"Abbrechen"),t("button",{onClick:F,class:"btn btn-primary"},"Erstellen")])])])):S("",!0)]))}});export{X as default};
|
||||||
@@ -1 +1 @@
|
|||||||
import{d as A,u as M,q as T,m,c as a,a as s,F as y,x as w,w as D,b as p,v as _,s as E,e as i,r as d,o as r,t as l,p as U,y as h,n as z}from"./index-OvQoqblD.js";const N={class:"space-y-6"},$={class:"flex items-center justify-between"},B={class:"card"},F={key:0,class:"text-center py-8 text-gray-500"},L={key:1,class:"text-center py-8 text-gray-500"},q={key:2,class:"space-y-3"},K={class:"font-medium"},G={class:"text-sm text-gray-500"},O={key:0},P={key:0,class:"text-sm text-gray-500"},H={key:1,class:"text-sm text-gray-500"},I={class:"flex items-center gap-2"},J=["onClick"],Q=["onClick"],R={key:0,class:"fixed inset-0 z-50 flex items-center justify-center bg-black/50"},W={class:"card w-full max-w-md m-4"},X={class:"grid grid-cols-2 gap-4"},Y=["value"],Z={class:"flex justify-end gap-3 pt-4"},se=A({__name:"TimesheetsView",setup(ee){const f=M(),v=d([]),b=d(!0),u=d(!1),o=d({work_date:"",start_time:"",end_time:"",order_id:""}),x=d([]);T(async()=>{await Promise.all([g(),S()])});async function g(){b.value=!0;try{const n=await m.get("/timesheets");v.value=n.data.timesheets}catch(n){console.error(n)}finally{b.value=!1}}async function S(){try{const n=await m.get("/orders");x.value=n.data.orders}catch(n){console.error(n)}}async function j(){try{await m.post("/timesheets",o.value),u.value=!1,o.value={work_date:"",start_time:"",end_time:"",order_id:""},await g()}catch(n){alert(n instanceof Error?n.message:"Fehler")}}async function k(n,e){const t=e==="rejected"?prompt("Ablehnungsgrund:"):null;if(!(e==="rejected"&&!t))try{await m.post(`/timesheets/${n}/review`,{status:e,rejection_reason:t}),await g()}catch(c){alert(c instanceof Error?c.message:"Fehler")}}function C(n){return{pending:"badge-warning",approved:"badge-success",rejected:"badge-danger"}[n]||""}function V(n){return{pending:"Ausstehend",approved:"Genehmigt",rejected:"Abgelehnt"}[n]||n}return(n,e)=>(r(),a("div",N,[s("div",$,[e[6]||(e[6]=s("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"⏱️ Stundenzettel",-1)),s("button",{class:"btn btn-primary",onClick:e[0]||(e[0]=t=>u.value=!0)},"+ Neu")]),s("div",B,[b.value?(r(),a("div",F,"Lädt...")):v.value.length===0?(r(),a("div",L,"Keine Stundenzettel")):(r(),a("div",q,[(r(!0),a(y,null,w(v.value,t=>(r(),a("div",{key:t.id,class:"flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"},[s("div",null,[s("p",K,l(new Date(t.work_date).toLocaleDateString("de-DE")),1),s("p",G,[U(l(t.start_time)+" - "+l(t.end_time)+" ",1),t.hours_worked?(r(),a("span",O,"("+l(t.hours_worked)+"h)",1)):i("",!0)]),t.order_title?(r(),a("p",P,"📋 "+l(t.order_title),1)):i("",!0),h(f).canManageUsers?(r(),a("p",H,"👤 "+l(t.user_name),1)):i("",!0)]),s("div",I,[s("span",{class:z(["badge",C(t.status)])},l(V(t.status)),3),h(f).canManageUsers&&t.status==="pending"?(r(),a(y,{key:0},[s("button",{class:"btn btn-success text-sm",onClick:c=>k(t.id,"approved")},"✓",8,J),s("button",{class:"btn btn-danger text-sm",onClick:c=>k(t.id,"rejected")},"✗",8,Q)],64)):i("",!0)])]))),128))]))]),u.value?(r(),a("div",R,[s("div",W,[e[13]||(e[13]=s("h2",{class:"text-xl font-semibold mb-6"},"Neuer Stundenzettel",-1)),s("form",{onSubmit:D(j,["prevent"]),class:"space-y-4"},[s("div",null,[e[7]||(e[7]=s("label",{class:"block text-sm font-medium mb-1"},"Datum *",-1)),p(s("input",{"onUpdate:modelValue":e[1]||(e[1]=t=>o.value.work_date=t),type:"date",required:"",class:"input"},null,512),[[_,o.value.work_date]])]),s("div",X,[s("div",null,[e[8]||(e[8]=s("label",{class:"block text-sm font-medium mb-1"},"Start",-1)),p(s("input",{"onUpdate:modelValue":e[2]||(e[2]=t=>o.value.start_time=t),type:"time",class:"input"},null,512),[[_,o.value.start_time]])]),s("div",null,[e[9]||(e[9]=s("label",{class:"block text-sm font-medium mb-1"},"Ende",-1)),p(s("input",{"onUpdate:modelValue":e[3]||(e[3]=t=>o.value.end_time=t),type:"time",class:"input"},null,512),[[_,o.value.end_time]])])]),s("div",null,[e[11]||(e[11]=s("label",{class:"block text-sm font-medium mb-1"},"Auftrag",-1)),p(s("select",{"onUpdate:modelValue":e[4]||(e[4]=t=>o.value.order_id=t),class:"input"},[e[10]||(e[10]=s("option",{value:""},"-- Kein Auftrag --",-1)),(r(!0),a(y,null,w(x.value,t=>(r(),a("option",{key:t.id,value:t.id},"#"+l(t.number)+" - "+l(t.title),9,Y))),128))],512),[[E,o.value.order_id]])]),s("div",Z,[s("button",{type:"button",class:"btn btn-secondary",onClick:e[5]||(e[5]=t=>u.value=!1)},"Abbrechen"),e[12]||(e[12]=s("button",{type:"submit",class:"btn btn-primary"},"Einreichen",-1))])],32)])])):i("",!0)]))}});export{se as default};
|
import{d as A,u as M,q as T,m,c as a,a as s,F as y,x as w,w as D,b as p,v as _,s as E,e as i,r as d,o as r,t as l,p as U,y as h,n as z}from"./index-CWxNv9Fc.js";const N={class:"space-y-6"},$={class:"flex items-center justify-between"},B={class:"card"},F={key:0,class:"text-center py-8 text-gray-500"},L={key:1,class:"text-center py-8 text-gray-500"},q={key:2,class:"space-y-3"},K={class:"font-medium"},G={class:"text-sm text-gray-500"},O={key:0},P={key:0,class:"text-sm text-gray-500"},H={key:1,class:"text-sm text-gray-500"},I={class:"flex items-center gap-2"},J=["onClick"],Q=["onClick"],R={key:0,class:"fixed inset-0 z-50 flex items-center justify-center bg-black/50"},W={class:"card w-full max-w-md m-4"},X={class:"grid grid-cols-2 gap-4"},Y=["value"],Z={class:"flex justify-end gap-3 pt-4"},se=A({__name:"TimesheetsView",setup(ee){const f=M(),v=d([]),b=d(!0),u=d(!1),o=d({work_date:"",start_time:"",end_time:"",order_id:""}),x=d([]);T(async()=>{await Promise.all([g(),S()])});async function g(){b.value=!0;try{const n=await m.get("/timesheets");v.value=n.data.timesheets}catch(n){console.error(n)}finally{b.value=!1}}async function S(){try{const n=await m.get("/orders");x.value=n.data.orders}catch(n){console.error(n)}}async function j(){try{await m.post("/timesheets",o.value),u.value=!1,o.value={work_date:"",start_time:"",end_time:"",order_id:""},await g()}catch(n){alert(n instanceof Error?n.message:"Fehler")}}async function k(n,e){const t=e==="rejected"?prompt("Ablehnungsgrund:"):null;if(!(e==="rejected"&&!t))try{await m.post(`/timesheets/${n}/review`,{status:e,rejection_reason:t}),await g()}catch(c){alert(c instanceof Error?c.message:"Fehler")}}function C(n){return{pending:"badge-warning",approved:"badge-success",rejected:"badge-danger"}[n]||""}function V(n){return{pending:"Ausstehend",approved:"Genehmigt",rejected:"Abgelehnt"}[n]||n}return(n,e)=>(r(),a("div",N,[s("div",$,[e[6]||(e[6]=s("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"⏱️ Stundenzettel",-1)),s("button",{class:"btn btn-primary",onClick:e[0]||(e[0]=t=>u.value=!0)},"+ Neu")]),s("div",B,[b.value?(r(),a("div",F,"Lädt...")):v.value.length===0?(r(),a("div",L,"Keine Stundenzettel")):(r(),a("div",q,[(r(!0),a(y,null,w(v.value,t=>(r(),a("div",{key:t.id,class:"flex items-center justify-between p-4 bg-gray-50 dark:bg-gray-700/50 rounded-lg"},[s("div",null,[s("p",K,l(new Date(t.work_date).toLocaleDateString("de-DE")),1),s("p",G,[U(l(t.start_time)+" - "+l(t.end_time)+" ",1),t.hours_worked?(r(),a("span",O,"("+l(t.hours_worked)+"h)",1)):i("",!0)]),t.order_title?(r(),a("p",P,"📋 "+l(t.order_title),1)):i("",!0),h(f).canManageUsers?(r(),a("p",H,"👤 "+l(t.user_name),1)):i("",!0)]),s("div",I,[s("span",{class:z(["badge",C(t.status)])},l(V(t.status)),3),h(f).canManageUsers&&t.status==="pending"?(r(),a(y,{key:0},[s("button",{class:"btn btn-success text-sm",onClick:c=>k(t.id,"approved")},"✓",8,J),s("button",{class:"btn btn-danger text-sm",onClick:c=>k(t.id,"rejected")},"✗",8,Q)],64)):i("",!0)])]))),128))]))]),u.value?(r(),a("div",R,[s("div",W,[e[13]||(e[13]=s("h2",{class:"text-xl font-semibold mb-6"},"Neuer Stundenzettel",-1)),s("form",{onSubmit:D(j,["prevent"]),class:"space-y-4"},[s("div",null,[e[7]||(e[7]=s("label",{class:"block text-sm font-medium mb-1"},"Datum *",-1)),p(s("input",{"onUpdate:modelValue":e[1]||(e[1]=t=>o.value.work_date=t),type:"date",required:"",class:"input"},null,512),[[_,o.value.work_date]])]),s("div",X,[s("div",null,[e[8]||(e[8]=s("label",{class:"block text-sm font-medium mb-1"},"Start",-1)),p(s("input",{"onUpdate:modelValue":e[2]||(e[2]=t=>o.value.start_time=t),type:"time",class:"input"},null,512),[[_,o.value.start_time]])]),s("div",null,[e[9]||(e[9]=s("label",{class:"block text-sm font-medium mb-1"},"Ende",-1)),p(s("input",{"onUpdate:modelValue":e[3]||(e[3]=t=>o.value.end_time=t),type:"time",class:"input"},null,512),[[_,o.value.end_time]])])]),s("div",null,[e[11]||(e[11]=s("label",{class:"block text-sm font-medium mb-1"},"Auftrag",-1)),p(s("select",{"onUpdate:modelValue":e[4]||(e[4]=t=>o.value.order_id=t),class:"input"},[e[10]||(e[10]=s("option",{value:""},"-- Kein Auftrag --",-1)),(r(!0),a(y,null,w(x.value,t=>(r(),a("option",{key:t.id,value:t.id},"#"+l(t.number)+" - "+l(t.title),9,Y))),128))],512),[[E,o.value.order_id]])]),s("div",Z,[s("button",{type:"button",class:"btn btn-secondary",onClick:e[5]||(e[5]=t=>u.value=!1)},"Abbrechen"),e[12]||(e[12]=s("button",{type:"submit",class:"btn btn-primary"},"Einreichen",-1))])],32)])])):i("",!0)]))}});export{se as default};
|
||||||
@@ -1 +1 @@
|
|||||||
import{d as C,u as U,q as V,m,c as n,a as e,F as E,x as N,w as S,b as o,v as d,y as x,s as q,e as y,r as b,o as i,t as r,n as k}from"./index-OvQoqblD.js";const A={class:"space-y-6"},D={class:"flex items-center justify-between"},$={class:"card"},B={key:0,class:"text-center py-8 text-gray-500"},F={key:1,class:"text-center py-8 text-gray-500"},R={key:2,class:"w-full"},j={class:"py-3 font-medium"},L={class:"py-3 text-gray-500"},z={class:"py-3"},T={class:"py-3"},I={class:"py-3 text-right"},K=["onClick"],P={key:0,class:"fixed inset-0 z-50 flex items-center justify-center bg-black/50"},G={class:"card w-full max-w-md m-4"},H={class:"grid grid-cols-2 gap-4"},J={key:0},O={class:"flex justify-end gap-3 pt-4"},Y=C({__name:"UsersView",setup(Q){const f=U(),p=b([]),c=b(!0),u=b(!1),l=b({email:"",password:"",first_name:"",last_name:"",phone:"",role:"mitarbeiter"});V(async()=>{await v()});async function v(){c.value=!0;try{const a=await m.get("/users");p.value=a.data.users}catch(a){console.error(a)}finally{c.value=!1}}async function w(){try{await m.post("/users",l.value),u.value=!1,l.value={email:"",password:"",first_name:"",last_name:"",phone:"",role:"mitarbeiter"},await v()}catch(a){alert(a instanceof Error?a.message:"Fehler beim Erstellen")}}async function _(a){try{a.active?await m.delete(`/users/${a.id}`):await m.put(`/users/${a.id}`,{active:!0}),await v()}catch(t){alert(t instanceof Error?t.message:"Fehler")}}function h(a){return{chef:"badge-danger",disponent:"badge-primary",mitarbeiter:"badge-success"}[a]||"badge-secondary"}function M(a){return{chef:"Chef",disponent:"Disponent",mitarbeiter:"Mitarbeiter"}[a]||a}return(a,t)=>(i(),n("div",A,[e("div",D,[t[8]||(t[8]=e("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"👥 Mitarbeiter",-1)),e("button",{class:"btn btn-primary",onClick:t[0]||(t[0]=s=>u.value=!0)},"+ Neu")]),e("div",$,[c.value?(i(),n("div",B,"Lädt...")):p.value.length===0?(i(),n("div",F,"Keine Mitarbeiter")):(i(),n("table",R,[t[9]||(t[9]=e("thead",null,[e("tr",{class:"text-left text-sm text-gray-500 border-b border-gray-200 dark:border-gray-700"},[e("th",{class:"pb-3"},"Name"),e("th",{class:"pb-3"},"E-Mail"),e("th",{class:"pb-3"},"Rolle"),e("th",{class:"pb-3"},"Status"),e("th",{class:"pb-3"})])],-1)),e("tbody",null,[(i(!0),n(E,null,N(p.value,s=>{var g;return i(),n("tr",{key:s.id,class:"border-b border-gray-100 dark:border-gray-800"},[e("td",j,r(s.first_name)+" "+r(s.last_name),1),e("td",L,r(s.email),1),e("td",z,[e("span",{class:k(["badge",h(s.role)])},r(M(s.role)),3)]),e("td",T,[e("span",{class:k(s.active?"text-green-600":"text-red-600")},r(s.active?"Aktiv":"Inaktiv"),3)]),e("td",I,[s.id!==((g=x(f).user)==null?void 0:g.id)&&s.role!=="chef"?(i(),n("button",{key:0,class:"text-sm text-gray-500 hover:text-red-600",onClick:W=>_(s)},r(s.active?"Deaktivieren":"Aktivieren"),9,K)):y("",!0)])])}),128))])]))]),u.value?(i(),n("div",P,[e("div",G,[t[18]||(t[18]=e("h2",{class:"text-xl font-semibold mb-6"},"Neuer Mitarbeiter",-1)),e("form",{onSubmit:S(w,["prevent"]),class:"space-y-4"},[e("div",H,[e("div",null,[t[10]||(t[10]=e("label",{class:"block text-sm font-medium mb-1"},"Vorname *",-1)),o(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>l.value.first_name=s),type:"text",required:"",class:"input"},null,512),[[d,l.value.first_name]])]),e("div",null,[t[11]||(t[11]=e("label",{class:"block text-sm font-medium mb-1"},"Nachname *",-1)),o(e("input",{"onUpdate:modelValue":t[2]||(t[2]=s=>l.value.last_name=s),type:"text",required:"",class:"input"},null,512),[[d,l.value.last_name]])])]),e("div",null,[t[12]||(t[12]=e("label",{class:"block text-sm font-medium mb-1"},"E-Mail *",-1)),o(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>l.value.email=s),type:"email",required:"",class:"input"},null,512),[[d,l.value.email]])]),e("div",null,[t[13]||(t[13]=e("label",{class:"block text-sm font-medium mb-1"},"Passwort *",-1)),o(e("input",{"onUpdate:modelValue":t[4]||(t[4]=s=>l.value.password=s),type:"password",required:"",class:"input"},null,512),[[d,l.value.password]])]),e("div",null,[t[14]||(t[14]=e("label",{class:"block text-sm font-medium mb-1"},"Telefon",-1)),o(e("input",{"onUpdate:modelValue":t[5]||(t[5]=s=>l.value.phone=s),type:"tel",class:"input"},null,512),[[d,l.value.phone]])]),x(f).isChef?(i(),n("div",J,[t[16]||(t[16]=e("label",{class:"block text-sm font-medium mb-1"},"Rolle",-1)),o(e("select",{"onUpdate:modelValue":t[6]||(t[6]=s=>l.value.role=s),class:"input"},[...t[15]||(t[15]=[e("option",{value:"mitarbeiter"},"Mitarbeiter",-1),e("option",{value:"disponent"},"Disponent",-1)])],512),[[q,l.value.role]])])):y("",!0),e("div",O,[e("button",{type:"button",class:"btn btn-secondary",onClick:t[7]||(t[7]=s=>u.value=!1)},"Abbrechen"),t[17]||(t[17]=e("button",{type:"submit",class:"btn btn-primary"},"Erstellen",-1))])],32)])])):y("",!0)]))}});export{Y as default};
|
import{d as C,u as U,q as V,m,c as n,a as e,F as E,x as N,w as S,b as o,v as d,y as x,s as q,e as y,r as b,o as i,t as r,n as k}from"./index-CWxNv9Fc.js";const A={class:"space-y-6"},D={class:"flex items-center justify-between"},$={class:"card"},B={key:0,class:"text-center py-8 text-gray-500"},F={key:1,class:"text-center py-8 text-gray-500"},R={key:2,class:"w-full"},j={class:"py-3 font-medium"},L={class:"py-3 text-gray-500"},z={class:"py-3"},T={class:"py-3"},I={class:"py-3 text-right"},K=["onClick"],P={key:0,class:"fixed inset-0 z-50 flex items-center justify-center bg-black/50"},G={class:"card w-full max-w-md m-4"},H={class:"grid grid-cols-2 gap-4"},J={key:0},O={class:"flex justify-end gap-3 pt-4"},Y=C({__name:"UsersView",setup(Q){const f=U(),p=b([]),c=b(!0),u=b(!1),l=b({email:"",password:"",first_name:"",last_name:"",phone:"",role:"mitarbeiter"});V(async()=>{await v()});async function v(){c.value=!0;try{const a=await m.get("/users");p.value=a.data.users}catch(a){console.error(a)}finally{c.value=!1}}async function w(){try{await m.post("/users",l.value),u.value=!1,l.value={email:"",password:"",first_name:"",last_name:"",phone:"",role:"mitarbeiter"},await v()}catch(a){alert(a instanceof Error?a.message:"Fehler beim Erstellen")}}async function _(a){try{a.active?await m.delete(`/users/${a.id}`):await m.put(`/users/${a.id}`,{active:!0}),await v()}catch(t){alert(t instanceof Error?t.message:"Fehler")}}function h(a){return{chef:"badge-danger",disponent:"badge-primary",mitarbeiter:"badge-success"}[a]||"badge-secondary"}function M(a){return{chef:"Chef",disponent:"Disponent",mitarbeiter:"Mitarbeiter"}[a]||a}return(a,t)=>(i(),n("div",A,[e("div",D,[t[8]||(t[8]=e("h1",{class:"text-2xl font-bold text-gray-900 dark:text-white"},"👥 Mitarbeiter",-1)),e("button",{class:"btn btn-primary",onClick:t[0]||(t[0]=s=>u.value=!0)},"+ Neu")]),e("div",$,[c.value?(i(),n("div",B,"Lädt...")):p.value.length===0?(i(),n("div",F,"Keine Mitarbeiter")):(i(),n("table",R,[t[9]||(t[9]=e("thead",null,[e("tr",{class:"text-left text-sm text-gray-500 border-b border-gray-200 dark:border-gray-700"},[e("th",{class:"pb-3"},"Name"),e("th",{class:"pb-3"},"E-Mail"),e("th",{class:"pb-3"},"Rolle"),e("th",{class:"pb-3"},"Status"),e("th",{class:"pb-3"})])],-1)),e("tbody",null,[(i(!0),n(E,null,N(p.value,s=>{var g;return i(),n("tr",{key:s.id,class:"border-b border-gray-100 dark:border-gray-800"},[e("td",j,r(s.first_name)+" "+r(s.last_name),1),e("td",L,r(s.email),1),e("td",z,[e("span",{class:k(["badge",h(s.role)])},r(M(s.role)),3)]),e("td",T,[e("span",{class:k(s.active?"text-green-600":"text-red-600")},r(s.active?"Aktiv":"Inaktiv"),3)]),e("td",I,[s.id!==((g=x(f).user)==null?void 0:g.id)&&s.role!=="chef"?(i(),n("button",{key:0,class:"text-sm text-gray-500 hover:text-red-600",onClick:W=>_(s)},r(s.active?"Deaktivieren":"Aktivieren"),9,K)):y("",!0)])])}),128))])]))]),u.value?(i(),n("div",P,[e("div",G,[t[18]||(t[18]=e("h2",{class:"text-xl font-semibold mb-6"},"Neuer Mitarbeiter",-1)),e("form",{onSubmit:S(w,["prevent"]),class:"space-y-4"},[e("div",H,[e("div",null,[t[10]||(t[10]=e("label",{class:"block text-sm font-medium mb-1"},"Vorname *",-1)),o(e("input",{"onUpdate:modelValue":t[1]||(t[1]=s=>l.value.first_name=s),type:"text",required:"",class:"input"},null,512),[[d,l.value.first_name]])]),e("div",null,[t[11]||(t[11]=e("label",{class:"block text-sm font-medium mb-1"},"Nachname *",-1)),o(e("input",{"onUpdate:modelValue":t[2]||(t[2]=s=>l.value.last_name=s),type:"text",required:"",class:"input"},null,512),[[d,l.value.last_name]])])]),e("div",null,[t[12]||(t[12]=e("label",{class:"block text-sm font-medium mb-1"},"E-Mail *",-1)),o(e("input",{"onUpdate:modelValue":t[3]||(t[3]=s=>l.value.email=s),type:"email",required:"",class:"input"},null,512),[[d,l.value.email]])]),e("div",null,[t[13]||(t[13]=e("label",{class:"block text-sm font-medium mb-1"},"Passwort *",-1)),o(e("input",{"onUpdate:modelValue":t[4]||(t[4]=s=>l.value.password=s),type:"password",required:"",class:"input"},null,512),[[d,l.value.password]])]),e("div",null,[t[14]||(t[14]=e("label",{class:"block text-sm font-medium mb-1"},"Telefon",-1)),o(e("input",{"onUpdate:modelValue":t[5]||(t[5]=s=>l.value.phone=s),type:"tel",class:"input"},null,512),[[d,l.value.phone]])]),x(f).isChef?(i(),n("div",J,[t[16]||(t[16]=e("label",{class:"block text-sm font-medium mb-1"},"Rolle",-1)),o(e("select",{"onUpdate:modelValue":t[6]||(t[6]=s=>l.value.role=s),class:"input"},[...t[15]||(t[15]=[e("option",{value:"mitarbeiter"},"Mitarbeiter",-1),e("option",{value:"disponent"},"Disponent",-1)])],512),[[q,l.value.role]])])):y("",!0),e("div",O,[e("button",{type:"button",class:"btn btn-secondary",onClick:t[7]||(t[7]=s=>u.value=!1)},"Abbrechen"),t[17]||(t[17]=e("button",{type:"submit",class:"btn btn-primary"},"Erstellen",-1))])],32)])])):y("",!0)]))}});export{Y as default};
|
||||||
1
dist/assets/VehiclesView-C2kXdRXZ.js
vendored
Normal file
1
dist/assets/VehiclesView-C2kXdRXZ.js
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
import{d as _,q as h,m as y,c as n,a as e,F as V,x as z,b as r,v as u,s as B,e as g,r as p,o,t as i,n as f,p as k}from"./index-CWxNv9Fc.js";const D={class:"p-6"},F={class:"flex justify-between items-center mb-6"},M={key:0,class:"text-center py-12"},j={key:1,class:"bg-gray-50 rounded-lg p-12 text-center text-gray-500"},C={key:2,class:"grid md:grid-cols-2 lg:grid-cols-3 gap-4"},S={class:"flex justify-between items-start"},E={class:"font-semibold text-lg"},K={class:"text-gray-600"},U={key:0,class:"text-sm text-gray-400"},L={class:"mt-4 grid grid-cols-2 gap-2 text-sm"},N={key:3,class:"fixed inset-0 bg-black/50 flex items-center justify-center z-50"},T={class:"bg-white rounded-lg shadow-xl w-full max-w-md p-6"},A={class:"space-y-4"},W={class:"grid grid-cols-2 gap-4"},$={class:"grid grid-cols-2 gap-4"},q={class:"flex justify-end space-x-2 mt-6"},P=_({__name:"VehiclesView",setup(H){const c=p(!0),m=p([]),d=p(!1),s=p({license_plate:"",brand:"",model:"",year:2024,color:"",fuel_type:"diesel"});h(async()=>{await v()});async function v(){c.value=!0;try{const a=await y.get("/vehicles");m.value=a.data.vehicles||[]}catch(a){console.error(a)}c.value=!1}async function w(){try{await y.post("/vehicles",s.value),d.value=!1,s.value={license_plate:"",brand:"",model:"",year:2024,color:"",fuel_type:"diesel"},await v()}catch(a){alert("Fehler: "+a.message)}}function b(a){return{available:{text:"Verfügbar",class:"bg-green-100 text-green-700",icon:"✅"},in_use:{text:"Im Einsatz",class:"bg-blue-100 text-blue-700",icon:"🚗"},maintenance:{text:"Wartung",class:"bg-yellow-100 text-yellow-700",icon:"🔧"},retired:{text:"Stillgelegt",class:"bg-gray-100 text-gray-700",icon:"⛔"}}[a]||{text:a,class:"bg-gray-100",icon:""}}return(a,t)=>(o(),n("div",D,[e("div",F,[t[7]||(t[7]=e("div",null,[e("h1",{class:"text-2xl font-bold"},"🚗 Fahrzeuge"),e("p",{class:"text-gray-500"},"Fuhrpark verwalten")],-1)),e("button",{onClick:t[0]||(t[0]=l=>d.value=!0),class:"btn btn-primary"},"+ Fahrzeug")]),c.value?(o(),n("div",M,"Laden...")):m.value.length===0?(o(),n("div",j,[...t[8]||(t[8]=[e("p",{class:"text-4xl mb-4"},"🚗",-1),e("p",null,"Keine Fahrzeuge vorhanden",-1)])])):(o(),n("div",C,[(o(!0),n(V,null,z(m.value,l=>{var x;return o(),n("div",{key:l.id,class:"bg-white rounded-lg shadow p-4"},[e("div",S,[e("div",null,[e("h3",E,i(l.license_plate),1),e("p",K,i(l.brand)+" "+i(l.model),1),l.year?(o(),n("p",U,"Baujahr "+i(l.year),1)):g("",!0)]),e("span",{class:f(["px-2 py-1 text-xs rounded",b(l.status).class])},i(b(l.status).icon)+" "+i(b(l.status).text),3)]),e("div",L,[e("div",null,[t[9]||(t[9]=e("span",{class:"text-gray-500"},"KM-Stand:",-1)),k(" "+i(((x=l.current_mileage)==null?void 0:x.toLocaleString())||"-"),1)]),e("div",null,[t[10]||(t[10]=e("span",{class:"text-gray-500"},"Kraftstoff:",-1)),k(" "+i(l.fuel_type),1)]),l.tuev_expires?(o(),n("div",{key:0,class:f(["col-span-2",new Date(l.tuev_expires)<new Date?"text-red-600":""])}," TÜV: "+i(new Date(l.tuev_expires).toLocaleDateString("de-DE")),3)):g("",!0)])])}),128))])),d.value?(o(),n("div",N,[e("div",T,[t[17]||(t[17]=e("h2",{class:"text-xl font-bold mb-4"},"Neues Fahrzeug",-1)),e("div",A,[e("div",null,[t[11]||(t[11]=e("label",{class:"block text-sm font-medium mb-1"},"Kennzeichen *",-1)),r(e("input",{"onUpdate:modelValue":t[1]||(t[1]=l=>s.value.license_plate=l),class:"input",placeholder:"B-AB 1234"},null,512),[[u,s.value.license_plate]])]),e("div",W,[e("div",null,[t[12]||(t[12]=e("label",{class:"block text-sm font-medium mb-1"},"Marke",-1)),r(e("input",{"onUpdate:modelValue":t[2]||(t[2]=l=>s.value.brand=l),class:"input",placeholder:"VW"},null,512),[[u,s.value.brand]])]),e("div",null,[t[13]||(t[13]=e("label",{class:"block text-sm font-medium mb-1"},"Modell",-1)),r(e("input",{"onUpdate:modelValue":t[3]||(t[3]=l=>s.value.model=l),class:"input",placeholder:"Passat"},null,512),[[u,s.value.model]])])]),e("div",$,[e("div",null,[t[14]||(t[14]=e("label",{class:"block text-sm font-medium mb-1"},"Baujahr",-1)),r(e("input",{"onUpdate:modelValue":t[4]||(t[4]=l=>s.value.year=l),type:"number",class:"input"},null,512),[[u,s.value.year,void 0,{number:!0}]])]),e("div",null,[t[16]||(t[16]=e("label",{class:"block text-sm font-medium mb-1"},"Kraftstoff",-1)),r(e("select",{"onUpdate:modelValue":t[5]||(t[5]=l=>s.value.fuel_type=l),class:"input"},[...t[15]||(t[15]=[e("option",{value:"diesel"},"Diesel",-1),e("option",{value:"petrol"},"Benzin",-1),e("option",{value:"electric"},"Elektro",-1),e("option",{value:"hybrid"},"Hybrid",-1)])],512),[[B,s.value.fuel_type]])])])]),e("div",q,[e("button",{onClick:t[6]||(t[6]=l=>d.value=!1),class:"btn"},"Abbrechen"),e("button",{onClick:w,class:"btn btn-primary"},"Erstellen")])])])):g("",!0)]))}});export{P as default};
|
||||||
1
dist/assets/index-38_8_Zmu.css
vendored
1
dist/assets/index-38_8_Zmu.css
vendored
File diff suppressed because one or more lines are too long
1
dist/assets/index-BtrpDjiv.css
vendored
Normal file
1
dist/assets/index-BtrpDjiv.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
dist/index.html
vendored
4
dist/index.html
vendored
@@ -18,8 +18,8 @@
|
|||||||
|
|
||||||
<!-- Android -->
|
<!-- Android -->
|
||||||
<meta name="mobile-web-app-capable" content="yes" />
|
<meta name="mobile-web-app-capable" content="yes" />
|
||||||
<script type="module" crossorigin src="/app/assets/index-OvQoqblD.js"></script>
|
<script type="module" crossorigin src="/app/assets/index-CWxNv9Fc.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/app/assets/index-38_8_Zmu.css">
|
<link rel="stylesheet" crossorigin href="/app/assets/index-BtrpDjiv.css">
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-gray-50 dark:bg-gray-900">
|
<body class="bg-gray-50 dark:bg-gray-900">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
|
|||||||
@@ -21,18 +21,34 @@ const navigation = computed(() => {
|
|||||||
]
|
]
|
||||||
|
|
||||||
if (authStore.canManageUsers) {
|
if (authStore.canManageUsers) {
|
||||||
items.push({ name: 'Mitarbeiter', href: '/users', icon: '👥' })
|
items.push(
|
||||||
|
{ name: 'Mitarbeiter', href: '/users', icon: '👥' },
|
||||||
|
{ name: 'Schichtplanung', href: '/shifts', icon: '📅' }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
{ name: 'Verfügbarkeit', href: '/availability', icon: '📅' },
|
{ name: 'Verfügbarkeit', href: '/availability', icon: '🗓️' },
|
||||||
{ name: 'Stundenzettel', href: '/timesheets', icon: '⏱️' },
|
{ name: 'Stundenzettel', href: '/timesheets', icon: '⏱️' },
|
||||||
{ name: 'Qualifikationen', href: '/qualifications', icon: '🎓' },
|
{ name: 'Qualifikationen', href: '/qualifications', icon: '🎓' },
|
||||||
{ name: 'Objekte', href: '/objects', icon: '🏢' },
|
{ name: 'Objekte', href: '/objects', icon: '🏢' },
|
||||||
|
{ name: 'Rundgänge', href: '/patrols', icon: '📍' },
|
||||||
|
{ name: 'Vorfälle', href: '/incidents', icon: '🚨' },
|
||||||
|
{ name: 'Dokumente', href: '/documents', icon: '📁' },
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if (authStore.canManageUsers) {
|
||||||
|
items.push(
|
||||||
|
{ name: 'Fahrzeuge', href: '/vehicles', icon: '🚗' },
|
||||||
|
{ name: 'Kunden', href: '/customers', icon: '🤝' }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if (authStore.isChef) {
|
if (authStore.isChef) {
|
||||||
items.push({ name: 'Module', href: '/modules', icon: '⚙️' })
|
items.push(
|
||||||
|
{ name: 'Abrechnung', href: '/billing', icon: '💰' },
|
||||||
|
{ name: 'Module', href: '/modules', icon: '⚙️' }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
items.push(
|
items.push(
|
||||||
|
|||||||
@@ -88,6 +88,45 @@ const router = createRouter({
|
|||||||
path: 'objects',
|
path: 'objects',
|
||||||
name: 'objects',
|
name: 'objects',
|
||||||
component: () => import('@/views/ObjectsView.vue')
|
component: () => import('@/views/ObjectsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'shifts',
|
||||||
|
name: 'shifts',
|
||||||
|
component: () => import('@/views/ShiftsView.vue'),
|
||||||
|
meta: { roles: ['chef', 'disponent'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'patrols',
|
||||||
|
name: 'patrols',
|
||||||
|
component: () => import('@/views/PatrolsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'incidents',
|
||||||
|
name: 'incidents',
|
||||||
|
component: () => import('@/views/IncidentsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'vehicles',
|
||||||
|
name: 'vehicles',
|
||||||
|
component: () => import('@/views/VehiclesView.vue'),
|
||||||
|
meta: { roles: ['chef', 'disponent'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'documents',
|
||||||
|
name: 'documents',
|
||||||
|
component: () => import('@/views/DocumentsView.vue')
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'customers',
|
||||||
|
name: 'customers',
|
||||||
|
component: () => import('@/views/CustomersView.vue'),
|
||||||
|
meta: { roles: ['chef', 'disponent'] }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'billing',
|
||||||
|
name: 'billing',
|
||||||
|
component: () => import('@/views/BillingView.vue'),
|
||||||
|
meta: { roles: ['chef'] }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
165
src/views/BillingView.vue
Normal file
165
src/views/BillingView.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const invoices = ref<any[]>([])
|
||||||
|
const rates = ref<any[]>([])
|
||||||
|
const stats = ref<any>({})
|
||||||
|
const activeTab = ref<'invoices' | 'rates'>('invoices')
|
||||||
|
|
||||||
|
onMounted(async () => { await loadData() })
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [invRes, ratesRes, statsRes] = await Promise.all([
|
||||||
|
api.get<any>('/billing/invoices'),
|
||||||
|
api.get<any>('/billing/rates'),
|
||||||
|
api.get<any>('/billing/stats')
|
||||||
|
])
|
||||||
|
invoices.value = invRes.data.invoices || []
|
||||||
|
rates.value = ratesRes.data.rates || []
|
||||||
|
stats.value = statsRes.data.stats || {}
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendInvoice(id: string) {
|
||||||
|
try {
|
||||||
|
await api.put(`/billing/invoices/${id}/send`, {})
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function markPaid(id: string) {
|
||||||
|
try {
|
||||||
|
await api.put(`/billing/invoices/${id}/pay`, { payment_method: 'Überweisung' })
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusBadge(s: string) {
|
||||||
|
const map: Record<string, any> = {
|
||||||
|
draft: { text: 'Entwurf', class: 'bg-gray-100 text-gray-700' },
|
||||||
|
sent: { text: 'Gesendet', class: 'bg-blue-100 text-blue-700' },
|
||||||
|
paid: { text: 'Bezahlt', class: 'bg-green-100 text-green-700' },
|
||||||
|
overdue: { text: 'Überfällig', class: 'bg-red-100 text-red-700' },
|
||||||
|
cancelled: { text: 'Storniert', class: 'bg-gray-100 text-gray-700' }
|
||||||
|
}
|
||||||
|
return map[s] || { text: s, class: 'bg-gray-100' }
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCurrency(n: number): string {
|
||||||
|
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(n || 0)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">💰 Abrechnung</h1>
|
||||||
|
<p class="text-gray-500">Rechnungen & Sätze</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Stats Cards -->
|
||||||
|
<div class="grid grid-cols-4 gap-4 mb-6">
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<p class="text-sm text-gray-500">Offene Rechnungen</p>
|
||||||
|
<p class="text-2xl font-bold text-blue-600">{{ stats.open_invoices || 0 }}</p>
|
||||||
|
<p class="text-sm text-gray-500">{{ formatCurrency(stats.open_amount) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<p class="text-sm text-gray-500">Überfällig</p>
|
||||||
|
<p class="text-2xl font-bold text-red-600">{{ stats.overdue_invoices || 0 }}</p>
|
||||||
|
<p class="text-sm text-gray-500">{{ formatCurrency(stats.overdue_amount) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<p class="text-sm text-gray-500">Bezahlt (30 Tage)</p>
|
||||||
|
<p class="text-2xl font-bold text-green-600">{{ formatCurrency(stats.paid_last_30_days) }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<p class="text-sm text-gray-500">Stundensätze</p>
|
||||||
|
<p class="text-2xl font-bold">{{ rates.length }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex space-x-1 border-b mb-6">
|
||||||
|
<button @click="activeTab = 'invoices'"
|
||||||
|
:class="['px-4 py-2 font-medium border-b-2 -mb-px', activeTab === 'invoices' ? 'border-blue-600 text-blue-600' : 'border-transparent']">
|
||||||
|
Rechnungen
|
||||||
|
</button>
|
||||||
|
<button @click="activeTab = 'rates'"
|
||||||
|
:class="['px-4 py-2 font-medium border-b-2 -mb-px', activeTab === 'rates' ? 'border-blue-600 text-blue-600' : 'border-transparent']">
|
||||||
|
Stundensätze
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="text-center py-12">Laden...</div>
|
||||||
|
|
||||||
|
<!-- Invoices -->
|
||||||
|
<div v-else-if="activeTab === 'invoices'">
|
||||||
|
<div v-if="invoices.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">📄</p>
|
||||||
|
<p>Keine Rechnungen vorhanden</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nr.</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Kunde</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Datum</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Betrag</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
|
||||||
|
<th class="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase">Aktionen</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
<tr v-for="inv in invoices" :key="inv.id" class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 text-sm font-medium">{{ inv.invoice_number }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm">{{ inv.customer_name }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm">{{ new Date(inv.invoice_date).toLocaleDateString('de-DE') }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-right font-semibold">{{ formatCurrency(inv.total) }}</td>
|
||||||
|
<td class="px-4 py-3">
|
||||||
|
<span :class="['px-2 py-1 text-xs rounded', statusBadge(inv.status).class]">
|
||||||
|
{{ statusBadge(inv.status).text }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td class="px-4 py-3 text-right text-sm space-x-2">
|
||||||
|
<button v-if="inv.status === 'draft'" @click="sendInvoice(inv.id)" class="text-blue-600 hover:underline">Senden</button>
|
||||||
|
<button v-if="inv.status === 'sent' || inv.status === 'overdue'" @click="markPaid(inv.id)" class="text-green-600 hover:underline">Bezahlt</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Rates -->
|
||||||
|
<div v-else-if="activeTab === 'rates'">
|
||||||
|
<div v-if="rates.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">💵</p>
|
||||||
|
<p>Keine Stundensätze definiert</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div v-for="r in rates" :key="r.id" class="bg-white rounded-lg shadow p-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">{{ r.name }}</h3>
|
||||||
|
<p class="text-sm text-gray-500">{{ r.customer_name || 'Allgemein' }}</p>
|
||||||
|
</div>
|
||||||
|
<span v-if="r.is_default" class="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded">Standard</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-2xl font-bold mt-2">{{ formatCurrency(r.amount) }}</p>
|
||||||
|
<p class="text-sm text-gray-500">pro {{ r.rate_type === 'hourly' ? 'Stunde' : r.rate_type === 'daily' ? 'Tag' : 'Monat' }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
177
src/views/CustomersView.vue
Normal file
177
src/views/CustomersView.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const customers = ref<any[]>([])
|
||||||
|
const showModal = ref(false)
|
||||||
|
const selectedCustomer = ref<any>(null)
|
||||||
|
const form = ref({ company_name: '', contact_person: '', email: '', phone: '', address: '', city: '', postal_code: '' })
|
||||||
|
|
||||||
|
onMounted(async () => { await loadData() })
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.get<any>('/customers')
|
||||||
|
customers.value = res.data.customers || []
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCustomer(id: string) {
|
||||||
|
try {
|
||||||
|
const res = await api.get<any>(`/customers/${id}`)
|
||||||
|
selectedCustomer.value = res.data
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createCustomer() {
|
||||||
|
try {
|
||||||
|
await api.post('/customers', form.value)
|
||||||
|
showModal.value = false
|
||||||
|
form.value = { company_name: '', contact_person: '', email: '', phone: '', address: '', city: '', postal_code: '' }
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusBadge(s: string) {
|
||||||
|
const map: Record<string, any> = {
|
||||||
|
active: { text: 'Aktiv', class: 'bg-green-100 text-green-700' },
|
||||||
|
inactive: { text: 'Inaktiv', class: 'bg-gray-100 text-gray-700' },
|
||||||
|
prospect: { text: 'Interessent', class: 'bg-blue-100 text-blue-700' }
|
||||||
|
}
|
||||||
|
return map[s] || { text: s, class: 'bg-gray-100' }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">👥 Kunden / CRM</h1>
|
||||||
|
<p class="text-gray-500">Kundenverwaltung & Verträge</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showModal = true" class="btn btn-primary">+ Kunde</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex gap-6">
|
||||||
|
<!-- Customer List -->
|
||||||
|
<div class="w-1/2">
|
||||||
|
<div v-if="loading" class="text-center py-12">Laden...</div>
|
||||||
|
|
||||||
|
<div v-else-if="customers.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">👥</p>
|
||||||
|
<p>Keine Kunden vorhanden</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-2">
|
||||||
|
<div v-for="c in customers" :key="c.id"
|
||||||
|
@click="loadCustomer(c.id)"
|
||||||
|
:class="['bg-white rounded-lg shadow p-4 cursor-pointer hover:shadow-md transition-shadow',
|
||||||
|
selectedCustomer?.id === c.id ? 'ring-2 ring-blue-500' : '']">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">{{ c.company_name }}</h3>
|
||||||
|
<p class="text-sm text-gray-500">{{ c.contact_person }}</p>
|
||||||
|
</div>
|
||||||
|
<span :class="['px-2 py-1 text-xs rounded', statusBadge(c.status).class]">
|
||||||
|
{{ statusBadge(c.status).text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 flex gap-4 text-xs text-gray-500">
|
||||||
|
<span v-if="c.active_contracts">📄 {{ c.active_contracts }} Verträge</span>
|
||||||
|
<span v-if="c.object_count">🏢 {{ c.object_count }} Objekte</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Customer Detail -->
|
||||||
|
<div class="w-1/2">
|
||||||
|
<div v-if="!selectedCustomer" class="bg-gray-50 rounded-lg p-12 text-center text-gray-400">
|
||||||
|
<p>Wähle einen Kunden aus</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="bg-white rounded-lg shadow p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-4">{{ selectedCustomer.company_name }}</h2>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">Kontakt</h3>
|
||||||
|
<p>{{ selectedCustomer.contact_person }}</p>
|
||||||
|
<p class="text-sm">📧 {{ selectedCustomer.email || '-' }}</p>
|
||||||
|
<p class="text-sm">📞 {{ selectedCustomer.phone || '-' }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">Adresse</h3>
|
||||||
|
<p>{{ selectedCustomer.address }}</p>
|
||||||
|
<p>{{ selectedCustomer.postal_code }} {{ selectedCustomer.city }}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedCustomer.contracts?.length">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">Verträge ({{ selectedCustomer.contracts.length }})</h3>
|
||||||
|
<div v-for="con in selectedCustomer.contracts" :key="con.id" class="text-sm p-2 bg-gray-50 rounded mb-1">
|
||||||
|
<span class="font-medium">{{ con.title || con.contract_number }}</span>
|
||||||
|
<span v-if="con.monthly_value" class="ml-2 text-green-600">{{ con.monthly_value }}€/Monat</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="selectedCustomer.objects?.length">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">Objekte ({{ selectedCustomer.objects.length }})</h3>
|
||||||
|
<div v-for="obj in selectedCustomer.objects" :key="obj.id" class="text-sm">
|
||||||
|
🏢 {{ obj.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Neuer Kunde</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Firmenname *</label>
|
||||||
|
<input v-model="form.company_name" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Ansprechpartner</label>
|
||||||
|
<input v-model="form.contact_person" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">E-Mail</label>
|
||||||
|
<input v-model="form.email" type="email" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Telefon</label>
|
||||||
|
<input v-model="form.phone" class="input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Adresse</label>
|
||||||
|
<input v-model="form.address" class="input" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">PLZ</label>
|
||||||
|
<input v-model="form.postal_code" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Stadt</label>
|
||||||
|
<input v-model="form.city" class="input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2 mt-6">
|
||||||
|
<button @click="showModal = false" class="btn">Abbrechen</button>
|
||||||
|
<button @click="createCustomer" class="btn btn-primary">Erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
137
src/views/DocumentsView.vue
Normal file
137
src/views/DocumentsView.vue
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const documents = ref<any[]>([])
|
||||||
|
const categories = ref<any[]>([])
|
||||||
|
const pendingDocs = ref<any[]>([])
|
||||||
|
const showModal = ref(false)
|
||||||
|
const form = ref({ title: '', description: '', category_id: '', file_url: '', is_mandatory: false })
|
||||||
|
|
||||||
|
onMounted(async () => { await loadData() })
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [docRes, catRes, pendRes] = await Promise.all([
|
||||||
|
api.get<any>('/documents'),
|
||||||
|
api.get<any>('/documents/categories'),
|
||||||
|
api.get<any>('/documents/pending/list')
|
||||||
|
])
|
||||||
|
documents.value = docRes.data.documents || []
|
||||||
|
categories.value = catRes.data.categories || []
|
||||||
|
pendingDocs.value = pendRes.data.documents || []
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function acknowledge(docId: string) {
|
||||||
|
try {
|
||||||
|
await api.post(`/documents/${docId}/acknowledge`, {})
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createDocument() {
|
||||||
|
try {
|
||||||
|
await api.post('/documents', form.value)
|
||||||
|
showModal.value = false
|
||||||
|
form.value = { title: '', description: '', category_id: '', file_url: '', is_mandatory: false }
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">📁 Dokumente</h1>
|
||||||
|
<p class="text-gray-500">Unterlagen & Bestätigungen</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showModal = true" class="btn btn-primary">+ Dokument</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Pending Documents Alert -->
|
||||||
|
<div v-if="pendingDocs.length > 0" class="bg-yellow-50 border-l-4 border-yellow-400 p-4 mb-6">
|
||||||
|
<div class="flex">
|
||||||
|
<span class="text-yellow-600 text-xl mr-3">⚠️</span>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-yellow-800">{{ pendingDocs.length }} Dokument(e) zu bestätigen</h3>
|
||||||
|
<div class="mt-2 space-y-2">
|
||||||
|
<div v-for="doc in pendingDocs" :key="doc.id" class="flex items-center justify-between bg-white p-2 rounded">
|
||||||
|
<span>{{ doc.category_icon }} {{ doc.title }}</span>
|
||||||
|
<button @click="acknowledge(doc.id)" class="text-sm text-blue-600 hover:underline">Bestätigen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="text-center py-12">Laden...</div>
|
||||||
|
|
||||||
|
<div v-else-if="documents.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">📄</p>
|
||||||
|
<p>Keine Dokumente vorhanden</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div v-for="doc in documents" :key="doc.id" class="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<span class="text-2xl">{{ doc.category_icon || '📄' }}</span>
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<h3 class="font-semibold truncate">{{ doc.title }}</h3>
|
||||||
|
<p class="text-sm text-gray-500">{{ doc.category_name }}</p>
|
||||||
|
<p v-if="doc.description" class="text-sm text-gray-400 mt-1 line-clamp-2">{{ doc.description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex items-center justify-between text-xs">
|
||||||
|
<span v-if="doc.is_mandatory" class="px-2 py-1 bg-red-100 text-red-700 rounded">Pflicht</span>
|
||||||
|
<span v-if="doc.acknowledged" class="text-green-600">✓ Bestätigt</span>
|
||||||
|
<span v-else-if="doc.is_mandatory" class="text-orange-600">Ausstehend</span>
|
||||||
|
</div>
|
||||||
|
<a v-if="doc.file_url" :href="doc.file_url" target="_blank"
|
||||||
|
class="mt-3 block text-center text-sm text-blue-600 hover:underline">
|
||||||
|
📥 Herunterladen
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Neues Dokument</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Titel *</label>
|
||||||
|
<input v-model="form.title" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Kategorie</label>
|
||||||
|
<select v-model="form.category_id" class="input">
|
||||||
|
<option value="">-- Wählen --</option>
|
||||||
|
<option v-for="c in categories" :key="c.id" :value="c.id">{{ c.icon }} {{ c.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Datei-URL</label>
|
||||||
|
<input v-model="form.file_url" class="input" placeholder="https://..." />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Beschreibung</label>
|
||||||
|
<textarea v-model="form.description" rows="2" class="input"></textarea>
|
||||||
|
</div>
|
||||||
|
<label class="flex items-center">
|
||||||
|
<input v-model="form.is_mandatory" type="checkbox" class="mr-2" />
|
||||||
|
<span class="text-sm">Pflichtdokument (Bestätigung erforderlich)</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2 mt-6">
|
||||||
|
<button @click="showModal = false" class="btn">Abbrechen</button>
|
||||||
|
<button @click="createDocument" class="btn btn-primary">Erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
134
src/views/IncidentsView.vue
Normal file
134
src/views/IncidentsView.vue
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const incidents = ref<any[]>([])
|
||||||
|
const categories = ref<any[]>([])
|
||||||
|
const showModal = ref(false)
|
||||||
|
const form = ref({ title: '', description: '', category_id: '', severity: 2, object_id: '' })
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [incRes, catRes] = await Promise.all([
|
||||||
|
api.get<any>('/incidents'),
|
||||||
|
api.get<any>('/incidents/categories')
|
||||||
|
])
|
||||||
|
incidents.value = incRes.data.incidents || []
|
||||||
|
categories.value = catRes.data.categories || []
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createIncident() {
|
||||||
|
try {
|
||||||
|
await api.post('/incidents', form.value)
|
||||||
|
showModal.value = false
|
||||||
|
form.value = { title: '', description: '', category_id: '', severity: 2, object_id: '' }
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function severityColor(s: number): string {
|
||||||
|
if (s >= 4) return 'bg-red-100 text-red-700'
|
||||||
|
if (s >= 3) return 'bg-orange-100 text-orange-700'
|
||||||
|
return 'bg-yellow-100 text-yellow-700'
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusBadge(s: string): { text: string, class: string } {
|
||||||
|
const map: Record<string, any> = {
|
||||||
|
open: { text: 'Offen', class: 'bg-red-100 text-red-700' },
|
||||||
|
in_progress: { text: 'In Bearbeitung', class: 'bg-yellow-100 text-yellow-700' },
|
||||||
|
resolved: { text: 'Gelöst', class: 'bg-green-100 text-green-700' },
|
||||||
|
closed: { text: 'Geschlossen', class: 'bg-gray-100 text-gray-700' }
|
||||||
|
}
|
||||||
|
return map[s] || { text: s, class: 'bg-gray-100' }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">🚨 Vorfallberichte</h1>
|
||||||
|
<p class="text-gray-500">Incidents dokumentieren und verfolgen</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showModal = true" class="btn btn-primary">+ Vorfall melden</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="text-center py-12">Laden...</div>
|
||||||
|
|
||||||
|
<div v-else-if="incidents.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">✅</p>
|
||||||
|
<p>Keine Vorfälle gemeldet</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="space-y-4">
|
||||||
|
<div v-for="inc in incidents" :key="inc.id" class="bg-white rounded-lg shadow p-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div class="flex items-start space-x-3">
|
||||||
|
<span class="text-2xl">{{ inc.category_icon || '📋' }}</span>
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">{{ inc.title }}</h3>
|
||||||
|
<p class="text-sm text-gray-500">{{ inc.category_name }} · {{ inc.object_name || 'Ohne Objekt' }}</p>
|
||||||
|
<p class="text-sm text-gray-400 mt-1">{{ new Date(inc.occurred_at).toLocaleString('de-DE') }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end space-y-1">
|
||||||
|
<span :class="['px-2 py-1 text-xs rounded', statusBadge(inc.status).class]">
|
||||||
|
{{ statusBadge(inc.status).text }}
|
||||||
|
</span>
|
||||||
|
<span :class="['px-2 py-1 text-xs rounded', severityColor(inc.severity)]">
|
||||||
|
Stufe {{ inc.severity }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-if="inc.description" class="mt-3 text-sm text-gray-600">{{ inc.description }}</p>
|
||||||
|
<div class="mt-3 flex items-center text-xs text-gray-500">
|
||||||
|
<span>Gemeldet von {{ inc.reporter_first }} {{ inc.reporter_last }}</span>
|
||||||
|
<span v-if="inc.attachment_count" class="ml-4">📎 {{ inc.attachment_count }} Anhänge</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-lg p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Vorfall melden</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Titel *</label>
|
||||||
|
<input v-model="form.title" class="input" placeholder="Kurze Beschreibung" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Kategorie</label>
|
||||||
|
<select v-model="form.category_id" class="input">
|
||||||
|
<option value="">-- Wählen --</option>
|
||||||
|
<option v-for="c in categories" :key="c.id" :value="c.id">{{ c.icon }} {{ c.name }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Schweregrad (1-5)</label>
|
||||||
|
<input v-model.number="form.severity" type="range" min="1" max="5" class="w-full" />
|
||||||
|
<div class="flex justify-between text-xs text-gray-500">
|
||||||
|
<span>Gering</span><span>Kritisch</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Beschreibung</label>
|
||||||
|
<textarea v-model="form.description" rows="3" class="input" placeholder="Details..."></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2 mt-6">
|
||||||
|
<button @click="showModal = false" class="btn">Abbrechen</button>
|
||||||
|
<button @click="createIncident" class="btn btn-primary">Melden</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -1,718 +1,189 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { api } from '@/api'
|
import { api } from '@/api'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
|
||||||
|
|
||||||
interface ObjectType {
|
|
||||||
key: string
|
|
||||||
name: string
|
|
||||||
icon: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SecObject {
|
|
||||||
id: number
|
|
||||||
name: string
|
|
||||||
short_name?: string
|
|
||||||
object_number?: string
|
|
||||||
object_type: string
|
|
||||||
street?: string
|
|
||||||
house_number?: string
|
|
||||||
postal_code?: string
|
|
||||||
city?: string
|
|
||||||
phone?: string
|
|
||||||
email?: string
|
|
||||||
description?: string
|
|
||||||
customer_name?: string
|
|
||||||
status: string
|
|
||||||
contact_count: number
|
|
||||||
checkpoint_count: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const loading = ref(true)
|
const loading = ref(true)
|
||||||
const objects = ref<SecObject[]>([])
|
const objects = ref<any[]>([])
|
||||||
const objectTypes = ref<ObjectType[]>([])
|
|
||||||
const selectedObject = ref<any>(null)
|
const selectedObject = ref<any>(null)
|
||||||
|
|
||||||
// Filters
|
|
||||||
const searchQuery = ref('')
|
|
||||||
const filterType = ref('')
|
|
||||||
const filterStatus = ref('active')
|
|
||||||
|
|
||||||
// Modal
|
|
||||||
const showModal = ref(false)
|
const showModal = ref(false)
|
||||||
const showDetailModal = ref(false)
|
const form = ref({ name: '', address: '', city: '', postal_code: '', customer_id: '' })
|
||||||
const editingObject = ref<any>(null)
|
|
||||||
const formData = ref({
|
|
||||||
name: '',
|
|
||||||
short_name: '',
|
|
||||||
object_number: '',
|
|
||||||
object_type: 'other',
|
|
||||||
street: '',
|
|
||||||
house_number: '',
|
|
||||||
postal_code: '',
|
|
||||||
city: '',
|
|
||||||
phone: '',
|
|
||||||
email: '',
|
|
||||||
description: '',
|
|
||||||
access_info: '',
|
|
||||||
parking_info: '',
|
|
||||||
customer_name: '',
|
|
||||||
size_sqm: null as number | null,
|
|
||||||
floors: null as number | null
|
|
||||||
})
|
|
||||||
const saving = ref(false)
|
|
||||||
|
|
||||||
// Contact form
|
onMounted(async () => { await loadData() })
|
||||||
const showContactModal = ref(false)
|
|
||||||
const contactForm = ref({
|
|
||||||
name: '',
|
|
||||||
role: '',
|
|
||||||
company: '',
|
|
||||||
phone: '',
|
|
||||||
mobile: '',
|
|
||||||
email: '',
|
|
||||||
availability: '',
|
|
||||||
is_primary: false,
|
|
||||||
is_emergency: false,
|
|
||||||
notes: ''
|
|
||||||
})
|
|
||||||
|
|
||||||
// Instruction form
|
async function loadData() {
|
||||||
const showInstructionModal = ref(false)
|
loading.value = true
|
||||||
const instructionForm = ref({
|
try {
|
||||||
title: '',
|
const res = await api.get<any>('/objects')
|
||||||
category: 'general',
|
objects.value = res.data.objects || []
|
||||||
content: '',
|
} catch (e) { console.error(e) }
|
||||||
is_critical: false
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
loadObjects(),
|
|
||||||
loadObjectTypes()
|
|
||||||
])
|
|
||||||
loading.value = false
|
loading.value = false
|
||||||
})
|
}
|
||||||
|
|
||||||
async function loadObjects() {
|
async function loadObject(id: string) {
|
||||||
try {
|
try {
|
||||||
const params = new URLSearchParams()
|
const res = await api.get<any>(`/objects/${id}`)
|
||||||
if (filterStatus.value) params.append('status', filterStatus.value)
|
selectedObject.value = res.data
|
||||||
if (filterType.value) params.append('type', filterType.value)
|
} catch (e) { console.error(e) }
|
||||||
if (searchQuery.value) params.append('search', searchQuery.value)
|
|
||||||
|
|
||||||
objects.value = await api.get(`/objects?${params}`)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load objects:', e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadObjectTypes() {
|
async function createObject() {
|
||||||
try {
|
try {
|
||||||
objectTypes.value = await api.get('/objects/types')
|
await api.post('/objects', form.value)
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load types:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadObjectDetail(id: number) {
|
|
||||||
try {
|
|
||||||
selectedObject.value = await api.get(`/objects/${id}`)
|
|
||||||
showDetailModal.value = true
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to load object:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredObjects = computed(() => {
|
|
||||||
let result = [...objects.value]
|
|
||||||
|
|
||||||
if (searchQuery.value) {
|
|
||||||
const query = searchQuery.value.toLowerCase()
|
|
||||||
result = result.filter(o =>
|
|
||||||
o.name.toLowerCase().includes(query) ||
|
|
||||||
o.city?.toLowerCase().includes(query) ||
|
|
||||||
o.object_number?.toLowerCase().includes(query) ||
|
|
||||||
o.customer_name?.toLowerCase().includes(query)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
})
|
|
||||||
|
|
||||||
function openAddModal() {
|
|
||||||
editingObject.value = null
|
|
||||||
formData.value = {
|
|
||||||
name: '',
|
|
||||||
short_name: '',
|
|
||||||
object_number: '',
|
|
||||||
object_type: 'other',
|
|
||||||
street: '',
|
|
||||||
house_number: '',
|
|
||||||
postal_code: '',
|
|
||||||
city: '',
|
|
||||||
phone: '',
|
|
||||||
email: '',
|
|
||||||
description: '',
|
|
||||||
access_info: '',
|
|
||||||
parking_info: '',
|
|
||||||
customer_name: '',
|
|
||||||
size_sqm: null,
|
|
||||||
floors: null
|
|
||||||
}
|
|
||||||
showModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
function openEditModal(obj: SecObject) {
|
|
||||||
editingObject.value = obj
|
|
||||||
formData.value = { ...obj } as any
|
|
||||||
showModal.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveObject() {
|
|
||||||
if (!formData.value.name) return
|
|
||||||
|
|
||||||
saving.value = true
|
|
||||||
try {
|
|
||||||
if (editingObject.value) {
|
|
||||||
await api.put(`/objects/${editingObject.value.id}`, formData.value)
|
|
||||||
} else {
|
|
||||||
await api.post('/objects', formData.value)
|
|
||||||
}
|
|
||||||
showModal.value = false
|
showModal.value = false
|
||||||
await loadObjects()
|
form.value = { name: '', address: '', city: '', postal_code: '', customer_id: '' }
|
||||||
} catch (e) {
|
await loadData()
|
||||||
console.error('Failed to save:', e)
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
||||||
} finally {
|
}
|
||||||
saving.value = false
|
|
||||||
|
function statusBadge(s: string) {
|
||||||
|
const map: Record<string, any> = {
|
||||||
|
active: { text: 'Aktiv', class: 'bg-green-100 text-green-700', icon: '🟢' },
|
||||||
|
inactive: { text: 'Inaktiv', class: 'bg-gray-100 text-gray-700', icon: '⚪' },
|
||||||
|
pending: { text: 'Ausstehend', class: 'bg-yellow-100 text-yellow-700', icon: '🟡' }
|
||||||
}
|
}
|
||||||
}
|
return map[s] || { text: s, class: 'bg-gray-100', icon: '' }
|
||||||
|
|
||||||
async function deleteObject(id: number) {
|
|
||||||
if (!confirm('Objekt wirklich archivieren?')) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.delete(`/objects/${id}`)
|
|
||||||
await loadObjects()
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to delete:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addContact() {
|
|
||||||
if (!contactForm.value.name || !selectedObject.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.post(`/objects/${selectedObject.value.id}/contacts`, contactForm.value)
|
|
||||||
await loadObjectDetail(selectedObject.value.id)
|
|
||||||
showContactModal.value = false
|
|
||||||
contactForm.value = { name: '', role: '', company: '', phone: '', mobile: '', email: '', availability: '', is_primary: false, is_emergency: false, notes: '' }
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to add contact:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteContact(contactId: number) {
|
|
||||||
if (!confirm('Kontakt löschen?')) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.delete(`/objects/${selectedObject.value.id}/contacts/${contactId}`)
|
|
||||||
await loadObjectDetail(selectedObject.value.id)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to delete:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addInstruction() {
|
|
||||||
if (!instructionForm.value.title || !instructionForm.value.content || !selectedObject.value) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.post(`/objects/${selectedObject.value.id}/instructions`, instructionForm.value)
|
|
||||||
await loadObjectDetail(selectedObject.value.id)
|
|
||||||
showInstructionModal.value = false
|
|
||||||
instructionForm.value = { title: '', category: 'general', content: '', is_critical: false }
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to add instruction:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteInstruction(instructionId: number) {
|
|
||||||
if (!confirm('Dienstanweisung löschen?')) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
await api.delete(`/objects/${selectedObject.value.id}/instructions/${instructionId}`)
|
|
||||||
await loadObjectDetail(selectedObject.value.id)
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to delete:', e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypeIcon(type: string): string {
|
|
||||||
const t = objectTypes.value.find(t => t.key === type)
|
|
||||||
return t?.icon || '📍'
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTypeName(type: string): string {
|
|
||||||
const t = objectTypes.value.find(t => t.key === type)
|
|
||||||
return t?.name || type
|
|
||||||
}
|
|
||||||
|
|
||||||
function getInstructionCategoryName(cat: string): string {
|
|
||||||
const cats: Record<string, string> = {
|
|
||||||
general: 'Allgemein',
|
|
||||||
patrol: 'Rundgang',
|
|
||||||
emergency: 'Notfall',
|
|
||||||
access: 'Zugang',
|
|
||||||
reporting: 'Meldewesen'
|
|
||||||
}
|
|
||||||
return cats[cat] || cat
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="space-y-6">
|
<div class="p-6">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex justify-between items-center mb-6">
|
||||||
<h1 class="text-2xl font-bold text-gray-900 dark:text-white">🏢 Objekte</h1>
|
<div>
|
||||||
<button
|
<h1 class="text-2xl font-bold">🏢 Objektverwaltung</h1>
|
||||||
v-if="authStore.canManageUsers"
|
<p class="text-gray-500">Standorte, Kontakte & Anweisungen</p>
|
||||||
@click="openAddModal"
|
|
||||||
class="btn btn-primary"
|
|
||||||
>
|
|
||||||
➕ Neues Objekt
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Filters -->
|
|
||||||
<div class="card">
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">🔍 Suche</label>
|
|
||||||
<input
|
|
||||||
v-model="searchQuery"
|
|
||||||
type="text"
|
|
||||||
class="input"
|
|
||||||
placeholder="Name, Stadt, Kunde..."
|
|
||||||
@input="loadObjects"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">🏷️ Typ</label>
|
|
||||||
<select v-model="filterType" class="input" @change="loadObjects">
|
|
||||||
<option value="">Alle Typen</option>
|
|
||||||
<option v-for="type in objectTypes" :key="type.key" :value="type.key">
|
|
||||||
{{ type.icon }} {{ type.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">📊 Status</label>
|
|
||||||
<select v-model="filterStatus" class="input" @change="loadObjects">
|
|
||||||
<option value="active">Aktiv</option>
|
|
||||||
<option value="inactive">Inaktiv</option>
|
|
||||||
<option value="archived">Archiviert</option>
|
|
||||||
<option value="all">Alle</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-end">
|
|
||||||
<p class="text-sm text-gray-500">
|
|
||||||
{{ filteredObjects.length }} Objekte
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<button @click="showModal = true" class="btn btn-primary">+ Objekt</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Loading -->
|
<div class="flex gap-6">
|
||||||
<div v-if="loading" class="text-center py-12">
|
<!-- Object List -->
|
||||||
<div class="animate-spin text-4xl">⏳</div>
|
<div class="w-1/2">
|
||||||
</div>
|
<div v-if="loading" class="text-center py-12">Laden...</div>
|
||||||
|
|
||||||
|
<div v-else-if="objects.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">🏢</p>
|
||||||
|
<p>Keine Objekte vorhanden</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Objects Grid -->
|
<div v-else class="space-y-2">
|
||||||
<div v-else-if="filteredObjects.length === 0" class="card text-center py-12 text-gray-500">
|
<div v-for="obj in objects" :key="obj.id"
|
||||||
Keine Objekte gefunden
|
@click="loadObject(obj.id)"
|
||||||
</div>
|
:class="['bg-white rounded-lg shadow p-4 cursor-pointer hover:shadow-md transition-shadow',
|
||||||
|
selectedObject?.id === obj.id ? 'ring-2 ring-blue-500' : '']">
|
||||||
<div v-else class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
<div class="flex justify-between items-start">
|
||||||
<div
|
<div>
|
||||||
v-for="obj in filteredObjects"
|
<h3 class="font-semibold">{{ obj.name }}</h3>
|
||||||
:key="obj.id"
|
<p class="text-sm text-gray-500">{{ obj.address }}</p>
|
||||||
class="card hover:shadow-lg transition-all cursor-pointer"
|
<p class="text-sm text-gray-400">{{ obj.postal_code }} {{ obj.city }}</p>
|
||||||
@click="loadObjectDetail(obj.id)"
|
</div>
|
||||||
>
|
<span :class="['px-2 py-1 text-xs rounded', statusBadge(obj.status).class]">
|
||||||
<div class="flex items-start gap-4">
|
{{ statusBadge(obj.status).icon }} {{ statusBadge(obj.status).text }}
|
||||||
<div class="text-4xl">{{ getTypeIcon(obj.object_type) }}</div>
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<h3 class="font-semibold text-gray-900 dark:text-white truncate">
|
|
||||||
{{ obj.name }}
|
|
||||||
</h3>
|
|
||||||
<p v-if="obj.city" class="text-sm text-gray-500">
|
|
||||||
📍 {{ obj.postal_code }} {{ obj.city }}
|
|
||||||
</p>
|
|
||||||
<p v-if="obj.customer_name" class="text-sm text-gray-500">
|
|
||||||
👤 {{ obj.customer_name }}
|
|
||||||
</p>
|
|
||||||
<div class="flex gap-2 mt-2">
|
|
||||||
<span class="text-xs bg-gray-100 dark:bg-gray-700 px-2 py-1 rounded">
|
|
||||||
{{ getTypeName(obj.object_type) }}
|
|
||||||
</span>
|
|
||||||
<span v-if="obj.contact_count" class="text-xs bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 px-2 py-1 rounded">
|
|
||||||
👥 {{ obj.contact_count }}
|
|
||||||
</span>
|
|
||||||
<span v-if="obj.checkpoint_count" class="text-xs bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-2 py-1 rounded">
|
|
||||||
📍 {{ obj.checkpoint_count }} CP
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div v-if="obj.customer_name" class="mt-2 text-xs text-gray-500">
|
||||||
|
👤 {{ obj.customer_name }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Object Detail -->
|
||||||
|
<div class="w-1/2">
|
||||||
|
<div v-if="!selectedObject" class="bg-gray-50 rounded-lg p-12 text-center text-gray-400">
|
||||||
|
<p>Wähle ein Objekt aus</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Quick Actions -->
|
<div v-else class="bg-white rounded-lg shadow p-6">
|
||||||
<div v-if="authStore.canManageUsers" class="flex gap-2 mt-4 pt-3 border-t dark:border-gray-700" @click.stop>
|
<h2 class="text-xl font-bold mb-1">{{ selectedObject.name }}</h2>
|
||||||
<button @click="openEditModal(obj)" class="text-sm text-blue-600 hover:underline">
|
<p class="text-gray-500 mb-4">{{ selectedObject.address }}, {{ selectedObject.postal_code }} {{ selectedObject.city }}</p>
|
||||||
✏️ Bearbeiten
|
|
||||||
</button>
|
<div class="space-y-6">
|
||||||
<button @click="deleteObject(obj.id)" class="text-sm text-red-600 hover:underline">
|
<!-- Contacts -->
|
||||||
🗑️ Archivieren
|
<div>
|
||||||
</button>
|
<h3 class="text-sm font-semibold text-gray-500 mb-2 flex items-center">
|
||||||
</div>
|
📞 Ansprechpartner
|
||||||
</div>
|
<span class="ml-2 text-xs bg-gray-100 px-2 py-0.5 rounded">{{ selectedObject.contacts?.length || 0 }}</span>
|
||||||
</div>
|
</h3>
|
||||||
|
<div v-if="selectedObject.contacts?.length" class="space-y-2">
|
||||||
<!-- Detail Modal -->
|
<div v-for="c in selectedObject.contacts" :key="c.id" class="p-2 bg-gray-50 rounded text-sm">
|
||||||
<div v-if="showDetailModal && selectedObject" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
<div class="font-medium">{{ c.name }} <span class="text-gray-400 font-normal">{{ c.role }}</span></div>
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-4xl mx-4 max-h-[90vh] overflow-y-auto">
|
<div class="text-gray-600">📞 {{ c.phone }} · 📧 {{ c.email }}</div>
|
||||||
<div class="p-6">
|
|
||||||
<!-- Header -->
|
|
||||||
<div class="flex items-start justify-between mb-6">
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<span class="text-4xl">{{ getTypeIcon(selectedObject.object_type) }}</span>
|
|
||||||
<div>
|
|
||||||
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">{{ selectedObject.name }}</h2>
|
|
||||||
<p v-if="selectedObject.city" class="text-gray-500">
|
|
||||||
{{ selectedObject.street }} {{ selectedObject.house_number }},
|
|
||||||
{{ selectedObject.postal_code }} {{ selectedObject.city }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button @click="showDetailModal = false" class="text-2xl hover:bg-gray-100 dark:hover:bg-gray-700 p-2 rounded">
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Info Grid -->
|
|
||||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
|
|
||||||
<div v-if="selectedObject.phone" class="bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
|
||||||
<p class="text-xs text-gray-500">📞 Telefon</p>
|
|
||||||
<p class="font-medium">{{ selectedObject.phone }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedObject.email" class="bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
|
||||||
<p class="text-xs text-gray-500">📧 E-Mail</p>
|
|
||||||
<p class="font-medium truncate">{{ selectedObject.email }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedObject.customer_name" class="bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
|
||||||
<p class="text-xs text-gray-500">👤 Kunde</p>
|
|
||||||
<p class="font-medium">{{ selectedObject.customer_name }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedObject.object_number" class="bg-gray-50 dark:bg-gray-700 p-3 rounded">
|
|
||||||
<p class="text-xs text-gray-500"># Objektnummer</p>
|
|
||||||
<p class="font-medium">{{ selectedObject.object_number }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Description -->
|
|
||||||
<div v-if="selectedObject.description" class="mb-6">
|
|
||||||
<h3 class="font-semibold mb-2">📝 Beschreibung</h3>
|
|
||||||
<p class="text-gray-600 dark:text-gray-300 whitespace-pre-wrap">{{ selectedObject.description }}</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Access Info -->
|
|
||||||
<div v-if="selectedObject.access_info || selectedObject.parking_info" class="grid grid-cols-2 gap-4 mb-6">
|
|
||||||
<div v-if="selectedObject.access_info" class="bg-yellow-50 dark:bg-yellow-900/20 p-4 rounded">
|
|
||||||
<h3 class="font-semibold mb-2">🔑 Zugang</h3>
|
|
||||||
<p class="text-sm whitespace-pre-wrap">{{ selectedObject.access_info }}</p>
|
|
||||||
</div>
|
|
||||||
<div v-if="selectedObject.parking_info" class="bg-blue-50 dark:bg-blue-900/20 p-4 rounded">
|
|
||||||
<h3 class="font-semibold mb-2">🅿️ Parken</h3>
|
|
||||||
<p class="text-sm whitespace-pre-wrap">{{ selectedObject.parking_info }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Contacts -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<h3 class="font-semibold">👥 Ansprechpartner</h3>
|
|
||||||
<button v-if="authStore.canManageUsers" @click="showContactModal = true" class="text-sm text-blue-600 hover:underline">
|
|
||||||
➕ Hinzufügen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!selectedObject.contacts?.length" class="text-gray-500 text-sm">
|
|
||||||
Keine Kontakte hinterlegt
|
|
||||||
</div>
|
|
||||||
<div v-else class="grid gap-2">
|
|
||||||
<div
|
|
||||||
v-for="contact in selectedObject.contacts"
|
|
||||||
:key="contact.id"
|
|
||||||
class="flex items-center justify-between bg-gray-50 dark:bg-gray-700 p-3 rounded"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<p class="font-medium">
|
|
||||||
{{ contact.name }}
|
|
||||||
<span v-if="contact.is_primary" class="text-xs bg-blue-500 text-white px-1 rounded ml-1">Haupt</span>
|
|
||||||
<span v-if="contact.is_emergency" class="text-xs bg-red-500 text-white px-1 rounded ml-1">Notfall</span>
|
|
||||||
</p>
|
|
||||||
<p v-if="contact.role" class="text-sm text-gray-500">{{ contact.role }}</p>
|
|
||||||
<p v-if="contact.phone || contact.mobile" class="text-sm">
|
|
||||||
📞 {{ contact.phone || contact.mobile }}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<button v-if="authStore.canManageUsers" @click="deleteContact(contact.id)" class="text-red-500 hover:text-red-700">
|
|
||||||
🗑️
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Instructions -->
|
|
||||||
<div class="mb-6">
|
|
||||||
<div class="flex items-center justify-between mb-3">
|
|
||||||
<h3 class="font-semibold">📋 Dienstanweisungen</h3>
|
|
||||||
<button v-if="authStore.canManageUsers" @click="showInstructionModal = true" class="text-sm text-blue-600 hover:underline">
|
|
||||||
➕ Hinzufügen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div v-if="!selectedObject.instructions?.length" class="text-gray-500 text-sm">
|
|
||||||
Keine Dienstanweisungen hinterlegt
|
|
||||||
</div>
|
|
||||||
<div v-else class="space-y-3">
|
|
||||||
<div
|
|
||||||
v-for="instr in selectedObject.instructions"
|
|
||||||
:key="instr.id"
|
|
||||||
:class="['p-4 rounded border-l-4', instr.is_critical ? 'border-red-500 bg-red-50 dark:bg-red-900/20' : 'border-gray-300 bg-gray-50 dark:bg-gray-700']"
|
|
||||||
>
|
|
||||||
<div class="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<p class="font-medium">
|
|
||||||
{{ instr.title }}
|
|
||||||
<span class="text-xs text-gray-500 ml-2">{{ getInstructionCategoryName(instr.category) }}</span>
|
|
||||||
</p>
|
|
||||||
<p class="text-sm mt-1 whitespace-pre-wrap text-gray-600 dark:text-gray-300">{{ instr.content }}</p>
|
|
||||||
</div>
|
|
||||||
<button v-if="authStore.canManageUsers" @click="deleteInstruction(instr.id)" class="text-red-500 hover:text-red-700 ml-2">
|
|
||||||
🗑️
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<p v-else class="text-sm text-gray-400">Keine Kontakte hinterlegt</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Checkpoints -->
|
<!-- Instructions -->
|
||||||
<div v-if="selectedObject.checkpoints?.length" class="mb-6">
|
<div>
|
||||||
<h3 class="font-semibold mb-3">📍 Kontrollpunkte</h3>
|
<h3 class="text-sm font-semibold text-gray-500 mb-2 flex items-center">
|
||||||
<div class="flex flex-wrap gap-2">
|
📋 Anweisungen
|
||||||
<span
|
<span class="ml-2 text-xs bg-gray-100 px-2 py-0.5 rounded">{{ selectedObject.instructions?.length || 0 }}</span>
|
||||||
v-for="cp in selectedObject.checkpoints"
|
</h3>
|
||||||
:key="cp.id"
|
<div v-if="selectedObject.instructions?.length" class="space-y-2">
|
||||||
class="bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 px-3 py-1 rounded text-sm"
|
<div v-for="ins in selectedObject.instructions" :key="ins.id"
|
||||||
>
|
:class="['p-2 rounded text-sm', ins.priority === 'critical' ? 'bg-red-50 border-l-4 border-red-500' :
|
||||||
{{ cp.name }}
|
ins.priority === 'high' ? 'bg-orange-50 border-l-4 border-orange-500' : 'bg-gray-50']">
|
||||||
</span>
|
<div class="font-medium">{{ ins.title }}</div>
|
||||||
|
<p class="text-gray-600">{{ ins.content }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p v-else class="text-sm text-gray-400">Keine Anweisungen hinterlegt</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Close Button -->
|
<!-- Documents -->
|
||||||
<div class="flex justify-end pt-4 border-t dark:border-gray-700">
|
<div v-if="selectedObject.documents?.length">
|
||||||
<button @click="showDetailModal = false" class="btn">
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">📁 Dokumente</h3>
|
||||||
Schließen
|
<div class="flex flex-wrap gap-2">
|
||||||
</button>
|
<a v-for="d in selectedObject.documents" :key="d.id"
|
||||||
|
:href="d.file_url" target="_blank"
|
||||||
|
class="px-2 py-1 bg-blue-50 text-blue-700 rounded text-sm hover:bg-blue-100">
|
||||||
|
{{ d.file_name || 'Dokument' }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Checkpoints -->
|
||||||
|
<div v-if="selectedObject.checkpoints?.length">
|
||||||
|
<h3 class="text-sm font-semibold text-gray-500 mb-2">📍 Checkpoints</h3>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<span v-for="cp in selectedObject.checkpoints" :key="cp.id"
|
||||||
|
class="px-2 py-1 bg-gray-100 rounded text-sm">
|
||||||
|
{{ cp.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Add/Edit Object Modal -->
|
<!-- Modal -->
|
||||||
<div v-if="showModal" class="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto">
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
||||||
<div class="p-6">
|
<h2 class="text-xl font-bold mb-4">Neues Objekt</h2>
|
||||||
<h2 class="text-xl font-semibold mb-4">
|
<div class="space-y-4">
|
||||||
{{ editingObject ? '✏️ Objekt bearbeiten' : '➕ Neues Objekt' }}
|
<div>
|
||||||
</h2>
|
<label class="block text-sm font-medium mb-1">Name *</label>
|
||||||
|
<input v-model="form.name" class="input" placeholder="z.B. Hauptgebäude Musterstraße" />
|
||||||
<form @submit.prevent="saveObject" class="space-y-4">
|
</div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<div>
|
||||||
<div class="col-span-2">
|
<label class="block text-sm font-medium mb-1">Adresse</label>
|
||||||
<label class="block text-sm font-medium mb-1">Name *</label>
|
<input v-model="form.address" class="input" />
|
||||||
<input v-model="formData.name" type="text" class="input" required />
|
</div>
|
||||||
</div>
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Kurzname</label>
|
|
||||||
<input v-model="formData.short_name" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Objektnummer</label>
|
|
||||||
<input v-model="formData.object_number" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Typ</label>
|
|
||||||
<select v-model="formData.object_type" class="input">
|
|
||||||
<option v-for="type in objectTypes" :key="type.key" :value="type.key">
|
|
||||||
{{ type.icon }} {{ type.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Kunde</label>
|
|
||||||
<input v-model="formData.customer_name" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="dark:border-gray-700">
|
|
||||||
|
|
||||||
<div class="grid grid-cols-4 gap-4">
|
|
||||||
<div class="col-span-2">
|
|
||||||
<label class="block text-sm font-medium mb-1">Straße</label>
|
|
||||||
<input v-model="formData.street" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Nr.</label>
|
|
||||||
<input v-model="formData.house_number" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">PLZ</label>
|
|
||||||
<input v-model="formData.postal_code" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
<label class="block text-sm font-medium mb-1">Stadt</label>
|
|
||||||
<input v-model="formData.city" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Telefon</label>
|
|
||||||
<input v-model="formData.phone" type="tel" class="input" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">E-Mail</label>
|
|
||||||
<input v-model="formData.email" type="email" class="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-sm font-medium mb-1">Beschreibung</label>
|
<label class="block text-sm font-medium mb-1">PLZ</label>
|
||||||
<textarea v-model="formData.description" class="input" rows="2"></textarea>
|
<input v-model="form.postal_code" class="input" />
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
<div class="grid grid-cols-2 gap-4">
|
<label class="block text-sm font-medium mb-1">Stadt</label>
|
||||||
<div>
|
<input v-model="form.city" class="input" />
|
||||||
<label class="block text-sm font-medium mb-1">🔑 Zugangsinfos</label>
|
|
||||||
<textarea v-model="formData.access_info" class="input" rows="2" placeholder="Schlüssel, Codes..."></textarea>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">🅿️ Parkhinweise</label>
|
|
||||||
<textarea v-model="formData.parking_info" class="input" rows="2"></textarea>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div class="flex gap-3 pt-4">
|
|
||||||
<button type="button" @click="showModal = false" class="btn flex-1">
|
|
||||||
Abbrechen
|
|
||||||
</button>
|
|
||||||
<button type="submit" :disabled="saving" class="btn btn-primary flex-1">
|
|
||||||
{{ saving ? 'Speichern...' : 'Speichern' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="flex justify-end space-x-2 mt-6">
|
||||||
</div>
|
<button @click="showModal = false" class="btn">Abbrechen</button>
|
||||||
|
<button @click="createObject" class="btn btn-primary">Erstellen</button>
|
||||||
<!-- Add Contact Modal -->
|
|
||||||
<div v-if="showContactModal" class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50">
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
|
||||||
<div class="p-6">
|
|
||||||
<h2 class="text-xl font-semibold mb-4">👤 Kontakt hinzufügen</h2>
|
|
||||||
<form @submit.prevent="addContact" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Name *</label>
|
|
||||||
<input v-model="contactForm.name" type="text" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Rolle</label>
|
|
||||||
<input v-model="contactForm.role" type="text" class="input" placeholder="z.B. Hausmeister" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Firma</label>
|
|
||||||
<input v-model="contactForm.company" type="text" class="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Telefon</label>
|
|
||||||
<input v-model="contactForm.phone" type="tel" class="input" />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Mobil</label>
|
|
||||||
<input v-model="contactForm.mobile" type="tel" class="input" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">E-Mail</label>
|
|
||||||
<input v-model="contactForm.email" type="email" class="input" />
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-4">
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input v-model="contactForm.is_primary" type="checkbox" class="rounded" />
|
|
||||||
<span class="text-sm">Hauptkontakt</span>
|
|
||||||
</label>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input v-model="contactForm.is_emergency" type="checkbox" class="rounded" />
|
|
||||||
<span class="text-sm">Notfallkontakt</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button type="button" @click="showContactModal = false" class="btn flex-1">Abbrechen</button>
|
|
||||||
<button type="submit" class="btn btn-primary flex-1">Hinzufügen</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Add Instruction Modal -->
|
|
||||||
<div v-if="showInstructionModal" class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50">
|
|
||||||
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-xl w-full max-w-md mx-4">
|
|
||||||
<div class="p-6">
|
|
||||||
<h2 class="text-xl font-semibold mb-4">📋 Dienstanweisung hinzufügen</h2>
|
|
||||||
<form @submit.prevent="addInstruction" class="space-y-4">
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Titel *</label>
|
|
||||||
<input v-model="instructionForm.title" type="text" class="input" required />
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Kategorie</label>
|
|
||||||
<select v-model="instructionForm.category" class="input">
|
|
||||||
<option value="general">Allgemein</option>
|
|
||||||
<option value="patrol">Rundgang</option>
|
|
||||||
<option value="emergency">Notfall</option>
|
|
||||||
<option value="access">Zugang</option>
|
|
||||||
<option value="reporting">Meldewesen</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-sm font-medium mb-1">Inhalt *</label>
|
|
||||||
<textarea v-model="instructionForm.content" class="input" rows="4" required></textarea>
|
|
||||||
</div>
|
|
||||||
<label class="flex items-center gap-2">
|
|
||||||
<input v-model="instructionForm.is_critical" type="checkbox" class="rounded" />
|
|
||||||
<span class="text-sm text-red-600">⚠️ Als kritisch markieren</span>
|
|
||||||
</label>
|
|
||||||
<div class="flex gap-3">
|
|
||||||
<button type="button" @click="showInstructionModal = false" class="btn flex-1">Abbrechen</button>
|
|
||||||
<button type="submit" class="btn btn-primary flex-1">Hinzufügen</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
142
src/views/PatrolsView.vue
Normal file
142
src/views/PatrolsView.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const checkpoints = ref<any[]>([])
|
||||||
|
const routes = ref<any[]>([])
|
||||||
|
const logs = ref<any[]>([])
|
||||||
|
const activeTab = ref<'checkpoints' | 'routes' | 'logs'>('checkpoints')
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [cpRes, routesRes, logsRes] = await Promise.all([
|
||||||
|
api.get<any>('/patrols/checkpoints'),
|
||||||
|
api.get<any>('/patrols/routes'),
|
||||||
|
api.get<any>('/patrols/logs')
|
||||||
|
])
|
||||||
|
checkpoints.value = cpRes.data.checkpoints || []
|
||||||
|
routes.value = routesRes.data.routes || []
|
||||||
|
logs.value = logsRes.data.logs || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatTime(ts: string): string {
|
||||||
|
return new Date(ts).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">📍 Wächterkontrolle</h1>
|
||||||
|
<p class="text-gray-500">Checkpoints, Routen & Rundgänge</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<div class="flex space-x-1 border-b mb-6">
|
||||||
|
<button @click="activeTab = 'checkpoints'"
|
||||||
|
:class="['px-4 py-2 font-medium border-b-2 -mb-px', activeTab === 'checkpoints' ? 'border-blue-600 text-blue-600' : 'border-transparent']">
|
||||||
|
Checkpoints ({{ checkpoints.length }})
|
||||||
|
</button>
|
||||||
|
<button @click="activeTab = 'routes'"
|
||||||
|
:class="['px-4 py-2 font-medium border-b-2 -mb-px', activeTab === 'routes' ? 'border-blue-600 text-blue-600' : 'border-transparent']">
|
||||||
|
Routen ({{ routes.length }})
|
||||||
|
</button>
|
||||||
|
<button @click="activeTab = 'logs'"
|
||||||
|
:class="['px-4 py-2 font-medium border-b-2 -mb-px', activeTab === 'logs' ? 'border-blue-600 text-blue-600' : 'border-transparent']">
|
||||||
|
Rundgänge
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="text-center py-12">Laden...</div>
|
||||||
|
|
||||||
|
<!-- Checkpoints -->
|
||||||
|
<div v-else-if="activeTab === 'checkpoints'">
|
||||||
|
<div v-if="checkpoints.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">📍</p>
|
||||||
|
<p>Keine Checkpoints vorhanden</p>
|
||||||
|
<p class="text-sm mt-2">Erstelle Checkpoints mit QR-Codes für Rundgänge</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div v-for="cp in checkpoints" :key="cp.id" class="bg-white rounded-lg shadow p-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">{{ cp.name }}</h3>
|
||||||
|
<p class="text-sm text-gray-500">{{ cp.object_name }}</p>
|
||||||
|
<p class="text-xs text-gray-400 mt-1">{{ cp.location_description }}</p>
|
||||||
|
</div>
|
||||||
|
<span class="text-2xl">{{ cp.checkpoint_type === 'nfc' ? '📶' : '📱' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 p-2 bg-gray-100 rounded text-xs font-mono break-all">
|
||||||
|
{{ cp.code }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Routes -->
|
||||||
|
<div v-else-if="activeTab === 'routes'">
|
||||||
|
<div v-if="routes.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">🗺️</p>
|
||||||
|
<p>Keine Routen definiert</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="space-y-4">
|
||||||
|
<div v-for="route in routes" :key="route.id" class="bg-white rounded-lg shadow p-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold">{{ route.name }}</h3>
|
||||||
|
<p class="text-sm text-gray-500">{{ route.object_name }}</p>
|
||||||
|
</div>
|
||||||
|
<span :class="['px-2 py-1 text-xs rounded', route.is_active ? 'bg-green-100 text-green-700' : 'bg-gray-100']">
|
||||||
|
{{ route.is_active ? 'Aktiv' : 'Inaktiv' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 flex gap-4 text-sm text-gray-600">
|
||||||
|
<span>📍 {{ route.checkpoint_count || 0 }} Checkpoints</span>
|
||||||
|
<span v-if="route.time_limit_minutes">⏱️ Max {{ route.time_limit_minutes }} Min</span>
|
||||||
|
<span v-if="route.interval_minutes">🔄 Alle {{ route.interval_minutes }} Min</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Logs -->
|
||||||
|
<div v-else-if="activeTab === 'logs'">
|
||||||
|
<div v-if="logs.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">📋</p>
|
||||||
|
<p>Keine Rundgänge heute</p>
|
||||||
|
</div>
|
||||||
|
<div v-else class="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<table class="min-w-full divide-y divide-gray-200">
|
||||||
|
<thead class="bg-gray-50">
|
||||||
|
<tr>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Zeit</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Mitarbeiter</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Checkpoint</th>
|
||||||
|
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Objekt</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="divide-y divide-gray-200">
|
||||||
|
<tr v-for="log in logs" :key="log.id" class="hover:bg-gray-50">
|
||||||
|
<td class="px-4 py-3 text-sm">{{ formatTime(log.scanned_at) }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm">{{ log.first_name }} {{ log.last_name }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm">{{ log.checkpoint_name }}</td>
|
||||||
|
<td class="px-4 py-3 text-sm text-gray-500">{{ log.object_name }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
162
src/views/ShiftsView.vue
Normal file
162
src/views/ShiftsView.vue
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted, computed } from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const shifts = ref<any[]>([])
|
||||||
|
const assignments = ref<any[]>([])
|
||||||
|
const showModal = ref(false)
|
||||||
|
const currentWeek = ref(new Date())
|
||||||
|
|
||||||
|
const form = ref({
|
||||||
|
name: '',
|
||||||
|
start_time: '06:00',
|
||||||
|
end_time: '14:00',
|
||||||
|
break_minutes: 30,
|
||||||
|
color: '#3B82F6',
|
||||||
|
is_night_shift: false
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await loadData()
|
||||||
|
})
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const [shiftsRes, assignRes] = await Promise.all([
|
||||||
|
api.get<any>('/shifts/definitions'),
|
||||||
|
api.get<any>(`/shifts/assignments?start=${formatDate(getWeekStart())}&end=${formatDate(getWeekEnd())}`)
|
||||||
|
])
|
||||||
|
shifts.value = shiftsRes.data.shifts || []
|
||||||
|
assignments.value = assignRes.data.assignments || []
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e)
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWeekStart(): Date {
|
||||||
|
const d = new Date(currentWeek.value)
|
||||||
|
d.setDate(d.getDate() - d.getDay() + 1)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
function getWeekEnd(): Date {
|
||||||
|
const d = new Date(getWeekStart())
|
||||||
|
d.setDate(d.getDate() + 6)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(d: Date): string {
|
||||||
|
return d.toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
const weekDays = computed(() => {
|
||||||
|
const days = []
|
||||||
|
const start = getWeekStart()
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
|
const d = new Date(start)
|
||||||
|
d.setDate(d.getDate() + i)
|
||||||
|
days.push({ date: formatDate(d), label: d.toLocaleDateString('de-DE', { weekday: 'short', day: '2-digit' }) })
|
||||||
|
}
|
||||||
|
return days
|
||||||
|
})
|
||||||
|
|
||||||
|
async function createShift() {
|
||||||
|
try {
|
||||||
|
await api.post('/shifts/definitions', form.value)
|
||||||
|
showModal.value = false
|
||||||
|
form.value = { name: '', start_time: '06:00', end_time: '14:00', break_minutes: 30, color: '#3B82F6', is_night_shift: false }
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) {
|
||||||
|
alert('Fehler: ' + e.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getAssignmentsForDay(date: string) {
|
||||||
|
return assignments.value.filter(a => a.date === date)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">📅 Schichtplanung</h1>
|
||||||
|
<p class="text-gray-500">Dienstpläne verwalten</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showModal = true" class="btn btn-primary">+ Schicht definieren</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shift Definitions -->
|
||||||
|
<div class="bg-white rounded-lg shadow p-4 mb-6">
|
||||||
|
<h2 class="font-semibold mb-3">Schicht-Typen</h2>
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
<div v-for="s in shifts" :key="s.id"
|
||||||
|
:style="{ backgroundColor: s.color + '20', borderColor: s.color }"
|
||||||
|
class="px-3 py-1 rounded-full border text-sm">
|
||||||
|
{{ s.name }} ({{ s.start_time?.slice(0,5) }} - {{ s.end_time?.slice(0,5) }})
|
||||||
|
</div>
|
||||||
|
<div v-if="shifts.length === 0" class="text-gray-500">Keine Schichten definiert</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Week View -->
|
||||||
|
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||||||
|
<div class="flex items-center justify-between p-4 border-b">
|
||||||
|
<button @click="currentWeek.setDate(currentWeek.getDate() - 7); loadData()" class="text-gray-600 hover:text-gray-900">← Vorherige</button>
|
||||||
|
<span class="font-semibold">{{ getWeekStart().toLocaleDateString('de-DE') }} - {{ getWeekEnd().toLocaleDateString('de-DE') }}</span>
|
||||||
|
<button @click="currentWeek.setDate(currentWeek.getDate() + 7); loadData()" class="text-gray-600 hover:text-gray-900">Nächste →</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-7 divide-x">
|
||||||
|
<div v-for="day in weekDays" :key="day.date" class="min-h-32 p-2">
|
||||||
|
<div class="text-xs font-semibold text-gray-500 mb-2">{{ day.label }}</div>
|
||||||
|
<div v-for="a in getAssignmentsForDay(day.date)" :key="a.id"
|
||||||
|
:style="{ backgroundColor: a.color + '40' }"
|
||||||
|
class="text-xs p-1 rounded mb-1">
|
||||||
|
{{ a.first_name }} {{ a.last_name?.charAt(0) }}.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Neue Schicht definieren</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Name</label>
|
||||||
|
<input v-model="form.name" class="input" placeholder="z.B. Frühschicht" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Beginn</label>
|
||||||
|
<input v-model="form.start_time" type="time" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Ende</label>
|
||||||
|
<input v-model="form.end_time" type="time" class="input" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Pause (Min.)</label>
|
||||||
|
<input v-model.number="form.break_minutes" type="number" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Farbe</label>
|
||||||
|
<input v-model="form.color" type="color" class="w-full h-10 rounded" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2 mt-6">
|
||||||
|
<button @click="showModal = false" class="btn">Abbrechen</button>
|
||||||
|
<button @click="createShift" class="btn btn-primary">Erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
122
src/views/VehiclesView.vue
Normal file
122
src/views/VehiclesView.vue
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, onMounted } from 'vue'
|
||||||
|
import { api } from '@/api'
|
||||||
|
|
||||||
|
const loading = ref(true)
|
||||||
|
const vehicles = ref<any[]>([])
|
||||||
|
const showModal = ref(false)
|
||||||
|
const form = ref({ license_plate: '', brand: '', model: '', year: 2024, color: '', fuel_type: 'diesel' })
|
||||||
|
|
||||||
|
onMounted(async () => { await loadData() })
|
||||||
|
|
||||||
|
async function loadData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const res = await api.get<any>('/vehicles')
|
||||||
|
vehicles.value = res.data.vehicles || []
|
||||||
|
} catch (e) { console.error(e) }
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createVehicle() {
|
||||||
|
try {
|
||||||
|
await api.post('/vehicles', form.value)
|
||||||
|
showModal.value = false
|
||||||
|
form.value = { license_plate: '', brand: '', model: '', year: 2024, color: '', fuel_type: 'diesel' }
|
||||||
|
await loadData()
|
||||||
|
} catch (e: any) { alert('Fehler: ' + e.message) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusBadge(s: string): { text: string, class: string, icon: string } {
|
||||||
|
const map: Record<string, any> = {
|
||||||
|
available: { text: 'Verfügbar', class: 'bg-green-100 text-green-700', icon: '✅' },
|
||||||
|
in_use: { text: 'Im Einsatz', class: 'bg-blue-100 text-blue-700', icon: '🚗' },
|
||||||
|
maintenance: { text: 'Wartung', class: 'bg-yellow-100 text-yellow-700', icon: '🔧' },
|
||||||
|
retired: { text: 'Stillgelegt', class: 'bg-gray-100 text-gray-700', icon: '⛔' }
|
||||||
|
}
|
||||||
|
return map[s] || { text: s, class: 'bg-gray-100', icon: '' }
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="p-6">
|
||||||
|
<div class="flex justify-between items-center mb-6">
|
||||||
|
<div>
|
||||||
|
<h1 class="text-2xl font-bold">🚗 Fahrzeuge</h1>
|
||||||
|
<p class="text-gray-500">Fuhrpark verwalten</p>
|
||||||
|
</div>
|
||||||
|
<button @click="showModal = true" class="btn btn-primary">+ Fahrzeug</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="loading" class="text-center py-12">Laden...</div>
|
||||||
|
|
||||||
|
<div v-else-if="vehicles.length === 0" class="bg-gray-50 rounded-lg p-12 text-center text-gray-500">
|
||||||
|
<p class="text-4xl mb-4">🚗</p>
|
||||||
|
<p>Keine Fahrzeuge vorhanden</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
<div v-for="v in vehicles" :key="v.id" class="bg-white rounded-lg shadow p-4">
|
||||||
|
<div class="flex justify-between items-start">
|
||||||
|
<div>
|
||||||
|
<h3 class="font-semibold text-lg">{{ v.license_plate }}</h3>
|
||||||
|
<p class="text-gray-600">{{ v.brand }} {{ v.model }}</p>
|
||||||
|
<p v-if="v.year" class="text-sm text-gray-400">Baujahr {{ v.year }}</p>
|
||||||
|
</div>
|
||||||
|
<span :class="['px-2 py-1 text-xs rounded', statusBadge(v.status).class]">
|
||||||
|
{{ statusBadge(v.status).icon }} {{ statusBadge(v.status).text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-4 grid grid-cols-2 gap-2 text-sm">
|
||||||
|
<div><span class="text-gray-500">KM-Stand:</span> {{ v.current_mileage?.toLocaleString() || '-' }}</div>
|
||||||
|
<div><span class="text-gray-500">Kraftstoff:</span> {{ v.fuel_type }}</div>
|
||||||
|
<div v-if="v.tuev_expires" :class="['col-span-2', new Date(v.tuev_expires) < new Date() ? 'text-red-600' : '']">
|
||||||
|
TÜV: {{ new Date(v.tuev_expires).toLocaleDateString('de-DE') }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<div v-if="showModal" class="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||||
|
<div class="bg-white rounded-lg shadow-xl w-full max-w-md p-6">
|
||||||
|
<h2 class="text-xl font-bold mb-4">Neues Fahrzeug</h2>
|
||||||
|
<div class="space-y-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Kennzeichen *</label>
|
||||||
|
<input v-model="form.license_plate" class="input" placeholder="B-AB 1234" />
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Marke</label>
|
||||||
|
<input v-model="form.brand" class="input" placeholder="VW" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Modell</label>
|
||||||
|
<input v-model="form.model" class="input" placeholder="Passat" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Baujahr</label>
|
||||||
|
<input v-model.number="form.year" type="number" class="input" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-sm font-medium mb-1">Kraftstoff</label>
|
||||||
|
<select v-model="form.fuel_type" class="input">
|
||||||
|
<option value="diesel">Diesel</option>
|
||||||
|
<option value="petrol">Benzin</option>
|
||||||
|
<option value="electric">Elektro</option>
|
||||||
|
<option value="hybrid">Hybrid</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end space-x-2 mt-6">
|
||||||
|
<button @click="showModal = false" class="btn">Abbrechen</button>
|
||||||
|
<button @click="createVehicle" class="btn btn-primary">Erstellen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
Reference in New Issue
Block a user