feat: light theme redesign + DataTable with sort/filter/resize + bug fixes
- Sidebar: white background, orange gradient logo, orange active pill - TopBar: glassmorphism (white/80 + backdrop-blur-xl) - AppLayout: warm gradient background mesh - DataTable: new reusable component with column sort, filter, resize - DevopsView: rebuilt with DataTable; connection shows "all assigned work items" - ADO work item URLs: use org-level URL (no project in path) - CalendarBlock: planned blocks show task title instead of project name - Reports export: replaced <a download> with fetch+blob to send JWT auth header - Sidebar Tasks: fixed path /board/ (trailing slash for Apache ProxyPass) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e16e4a16a5
commit
09cfc0a89a
43 changed files with 596 additions and 267 deletions
|
|
@ -85,7 +85,7 @@ async def sync_user_work_items(user: User, db: AsyncSession) -> int:
|
|||
wi.assigned_to_email = assigned_email
|
||||
wi.iteration_path = f.get("System.IterationPath", "")
|
||||
wi.area_path = f.get("System.AreaPath", "")
|
||||
wi.url = f"https://dev.azure.com/{integ.organization}/{integ.project}/_workitems/edit/{ado_id}"
|
||||
wi.url = f"https://dev.azure.com/{integ.organization}/_workitems/edit/{ado_id}"
|
||||
wi.fields_json = f
|
||||
wi.synced_at = datetime.now(timezone.utc)
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
import{d as p,u as y,y as h,c as r,a as t,e as n,k as v,w as d,f as b,s as m,o as s,F as g,r as k,t as a,q as u,h as A}from"./index-BP_aNEdP.js";import{a as w}from"./admin-C27haAMd.js";import{_ as B,a as S}from"./CardContent.vue_vue_type_script_setup_true_lang-B8VjSxLa.js";import{_ as f}from"./Badge.vue_vue_type_script_setup_true_lang-B2H6z2RD.js";import{_ as V}from"./Spinner.vue_vue_type_script_setup_true_lang-Cu49-Cc4.js";import{a as $}from"./utils-7WVCegLb.js";const N={class:"p-6"},C={key:0,class:"flex items-center justify-center h-20"},D={class:"w-full"},E={class:"px-4 py-3"},F={class:"text-sm font-medium text-foreground"},R={class:"px-4 py-3 text-sm text-muted-foreground"},U={class:"px-4 py-3"},j={class:"px-4 py-3"},q={class:"px-4 py-3 text-xs text-muted-foreground"},H=p({__name:"AdminView",setup(I){const x=y(),_=b(),i=m([]),l=m(!1);return h(async()=>{if(!x.isAdmin){_.push("/");return}l.value=!0;try{const c=await w.users();i.value=c.data}finally{l.value=!1}}),(c,o)=>(s(),r("div",N,[o[1]||(o[1]=t("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Admin — Users",-1)),l.value?(s(),r("div",C,[n(V,{class:"text-primary"})])):(s(),v(B,{key:1},{default:d(()=>[n(S,{class:"p-0"},{default:d(()=>[t("table",D,[o[0]||(o[0]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"User"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Email"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Role"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Status"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Joined")])],-1)),t("tbody",null,[(s(!0),r(g,null,k(i.value,e=>(s(),r("tr",{key:e.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",E,[t("p",F,a(e.username),1)]),t("td",R,a(e.email),1),t("td",U,[n(f,{variant:e.role==="admin"?"default":"secondary",class:"text-xs"},{default:d(()=>[u(a(e.role),1)]),_:2},1032,["variant"])]),t("td",j,[n(f,{variant:e.is_active?"success":"outline",class:"text-xs"},{default:d(()=>[u(a(e.is_active?"Active":"Inactive"),1)]),_:2},1032,["variant"])]),t("td",q,a(A($)(e.created_at)),1)]))),128))])])]),_:1})]),_:1}))]))}});export{H as default};
|
||||
import{d as p,u as y,y as h,c as r,a as t,e as n,k as v,w as d,f as b,s as m,o as s,F as g,r as k,t as a,q as u,h as A}from"./index-DVV3ZbZ2.js";import{a as w}from"./admin-xS9EtPqv.js";import{_ as B,a as S}from"./CardContent.vue_vue_type_script_setup_true_lang-DdGXaWEa.js";import{_ as f}from"./Badge.vue_vue_type_script_setup_true_lang-WigJUOyH.js";import{_ as V}from"./Spinner.vue_vue_type_script_setup_true_lang-BkmDerVR.js";import{a as $}from"./utils-7WVCegLb.js";const N={class:"p-6"},C={key:0,class:"flex items-center justify-center h-20"},D={class:"w-full"},E={class:"px-4 py-3"},F={class:"text-sm font-medium text-foreground"},R={class:"px-4 py-3 text-sm text-muted-foreground"},U={class:"px-4 py-3"},j={class:"px-4 py-3"},q={class:"px-4 py-3 text-xs text-muted-foreground"},H=p({__name:"AdminView",setup(I){const x=y(),_=b(),i=m([]),l=m(!1);return h(async()=>{if(!x.isAdmin){_.push("/");return}l.value=!0;try{const c=await w.users();i.value=c.data}finally{l.value=!1}}),(c,o)=>(s(),r("div",N,[o[1]||(o[1]=t("h2",{class:"text-lg font-semibold text-foreground mb-6"},"Admin — Users",-1)),l.value?(s(),r("div",C,[n(V,{class:"text-primary"})])):(s(),v(B,{key:1},{default:d(()=>[n(S,{class:"p-0"},{default:d(()=>[t("table",D,[o[0]||(o[0]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"User"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Email"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Role"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Status"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Joined")])],-1)),t("tbody",null,[(s(!0),r(g,null,k(i.value,e=>(s(),r("tr",{key:e.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",E,[t("p",F,a(e.username),1)]),t("td",R,a(e.email),1),t("td",U,[n(f,{variant:e.role==="admin"?"default":"secondary",class:"text-xs"},{default:d(()=>[u(a(e.role),1)]),_:2},1032,["variant"])]),t("td",j,[n(f,{variant:e.is_active?"success":"outline",class:"text-xs"},{default:d(()=>[u(a(e.is_active?"Active":"Inactive"),1)]),_:2},1032,["variant"])]),t("td",q,a(A($)(e.created_at)),1)]))),128))])])]),_:1})]),_:1}))]))}});export{H as default};
|
||||
File diff suppressed because one or more lines are too long
1
src/static/assets/AppLayout-wYWKlGO6.js
Normal file
1
src/static/assets/AppLayout-wYWKlGO6.js
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{c as a}from"./utils-7WVCegLb.js";import{d as n,o,c as s,n as d,h as i,p as c}from"./index-BP_aNEdP.js";const f=n({__name:"Badge",props:{variant:{default:"default"},class:{}},setup(r){const e=r;return(t,l)=>(o(),s("span",{class:d(i(a)("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors",{"bg-primary text-primary-foreground":e.variant==="default","bg-secondary text-secondary-foreground":e.variant==="secondary","bg-destructive text-destructive-foreground":e.variant==="destructive","border border-border text-foreground":e.variant==="outline","bg-emerald-500/20 text-emerald-400":e.variant==="success","bg-amber-500/20 text-amber-400":e.variant==="warning"},e.class))},[c(t.$slots,"default")],2))}});export{f as _};
|
||||
import{c as a}from"./utils-7WVCegLb.js";import{d as n,o,c as s,n as d,h as i,p as c}from"./index-DVV3ZbZ2.js";const f=n({__name:"Badge",props:{variant:{default:"default"},class:{}},setup(r){const e=r;return(t,l)=>(o(),s("span",{class:d(i(a)("inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors",{"bg-primary text-primary-foreground":e.variant==="default","bg-secondary text-secondary-foreground":e.variant==="secondary","bg-destructive text-destructive-foreground":e.variant==="destructive","border border-border text-foreground":e.variant==="outline","bg-emerald-500/20 text-emerald-400":e.variant==="success","bg-amber-500/20 text-amber-400":e.variant==="warning"},e.class))},[c(t.$slots,"default")],2))}});export{f as _};
|
||||
|
|
@ -1 +1 @@
|
|||
import{_ as c}from"./Spinner.vue_vue_type_script_setup_true_lang-Cu49-Cc4.js";import{c as l}from"./utils-7WVCegLb.js";import{d as u,c as f,n as m,k as b,i as v,p as g,j as p,o as n}from"./index-BP_aNEdP.js";const y=["type","disabled"],z=u({__name:"Button",props:{variant:{default:"default"},size:{default:"md"},loading:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},type:{default:"button"},class:{}},emits:["click"],setup(t,{emit:s}){const e=t,a=s,r=p(()=>l("inline-flex items-center justify-center rounded-md font-medium transition-colors","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:pointer-events-none disabled:opacity-50",{"bg-primary text-primary-foreground hover:bg-primary/90":e.variant==="default","border border-input bg-background hover:bg-accent hover:text-accent-foreground":e.variant==="outline","hover:bg-accent hover:text-accent-foreground":e.variant==="ghost","bg-destructive text-destructive-foreground hover:bg-destructive/90":e.variant==="destructive","bg-secondary text-secondary-foreground hover:bg-secondary/80":e.variant==="secondary","underline-offset-4 hover:underline text-primary":e.variant==="link","h-8 px-3 text-xs":e.size==="sm","h-10 px-4 py-2 text-sm":e.size==="md","h-11 px-8 text-base":e.size==="lg","h-9 w-9 p-0":e.size==="icon"},e.class));return(i,o)=>(n(),f("button",{class:m(r.value),type:t.type,disabled:t.disabled||t.loading,onClick:o[0]||(o[0]=d=>a("click",d))},[t.loading?(n(),b(c,{key:0,size:"sm",class:"mr-2"})):v("",!0),g(i.$slots,"default")],10,y))}});export{z as _};
|
||||
import{_ as c}from"./Spinner.vue_vue_type_script_setup_true_lang-BkmDerVR.js";import{c as l}from"./utils-7WVCegLb.js";import{d as u,c as f,n as m,k as b,i as v,p as g,j as p,o as n}from"./index-DVV3ZbZ2.js";const y=["type","disabled"],z=u({__name:"Button",props:{variant:{default:"default"},size:{default:"md"},loading:{type:Boolean,default:!1},disabled:{type:Boolean,default:!1},type:{default:"button"},class:{}},emits:["click"],setup(t,{emit:s}){const e=t,a=s,r=p(()=>l("inline-flex items-center justify-center rounded-md font-medium transition-colors","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:pointer-events-none disabled:opacity-50",{"bg-primary text-primary-foreground hover:bg-primary/90":e.variant==="default","border border-input bg-background hover:bg-accent hover:text-accent-foreground":e.variant==="outline","hover:bg-accent hover:text-accent-foreground":e.variant==="ghost","bg-destructive text-destructive-foreground hover:bg-destructive/90":e.variant==="destructive","bg-secondary text-secondary-foreground hover:bg-secondary/80":e.variant==="secondary","underline-offset-4 hover:underline text-primary":e.variant==="link","h-8 px-3 text-xs":e.size==="sm","h-10 px-4 py-2 text-sm":e.size==="md","h-11 px-8 text-base":e.size==="lg","h-9 w-9 p-0":e.size==="icon"},e.class));return(i,o)=>(n(),f("button",{class:m(r.value),type:t.type,disabled:t.disabled||t.loading,onClick:o[0]||(o[0]=d=>a("click",d))},[t.loading?(n(),b(c,{key:0,size:"sm",class:"mr-2"})):v("",!0),g(i.$slots,"default")],10,y))}});export{z as _};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
.calendar-block--manual[data-v-978cfc69]{background-image:repeating-linear-gradient(45deg,transparent,transparent 3px,rgba(255,255,255,.1) 3px,rgba(255,255,255,.1) 6px)}
|
||||
.calendar-block--manual[data-v-53efb3d3]{background-image:repeating-linear-gradient(45deg,transparent,transparent 3px,rgba(255,255,255,.1) 3px,rgba(255,255,255,.1) 6px)}
|
||||
|
|
@ -1 +1 @@
|
|||
import{c as e}from"./utils-7WVCegLb.js";import{d as o,c as n,n as t,h as c,p,o as l}from"./index-BP_aNEdP.js";const _=o({__name:"Card",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),n("div",{class:t(c(e)("rounded-lg border bg-card text-card-foreground shadow-sm",a.class))},[p(r.$slots,"default")],2))}}),f=o({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),n("div",{class:t(c(e)("p-6 pt-0",a.class))},[p(r.$slots,"default")],2))}});export{_,f as a};
|
||||
import{c as e}from"./utils-7WVCegLb.js";import{d as o,c as n,n as t,h as c,p,o as l}from"./index-DVV3ZbZ2.js";const _=o({__name:"Card",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),n("div",{class:t(c(e)("rounded-lg border bg-card text-card-foreground shadow-sm",a.class))},[p(r.$slots,"default")],2))}}),f=o({__name:"CardContent",props:{class:{}},setup(s){const a=s;return(r,d)=>(l(),n("div",{class:t(c(e)("p-6 pt-0",a.class))},[p(r.$slots,"default")],2))}});export{_,f as a};
|
||||
|
|
@ -1 +1 @@
|
|||
import{c as t}from"./utils-7WVCegLb.js";import{d as n,o,c as r,n as c,h as l,p}from"./index-BP_aNEdP.js";const f=n({__name:"CardHeader",props:{class:{}},setup(s){const e=s;return(a,i)=>(o(),r("div",{class:c(l(t)("flex flex-col space-y-1.5 p-6",e.class))},[p(a.$slots,"default")],2))}}),_=n({__name:"CardTitle",props:{class:{}},setup(s){const e=s;return(a,i)=>(o(),r("h3",{class:c(l(t)("text-lg font-semibold leading-none tracking-tight",e.class))},[p(a.$slots,"default")],2))}});export{f as _,_ as a};
|
||||
import{c as t}from"./utils-7WVCegLb.js";import{d as n,o,c as r,n as c,h as l,p}from"./index-DVV3ZbZ2.js";const f=n({__name:"CardHeader",props:{class:{}},setup(s){const e=s;return(a,i)=>(o(),r("div",{class:c(l(t)("flex flex-col space-y-1.5 p-6",e.class))},[p(a.$slots,"default")],2))}}),_=n({__name:"CardTitle",props:{class:{}},setup(s){const e=s;return(a,i)=>(o(),r("h3",{class:c(l(t)("text-lg font-semibold leading-none tracking-tight",e.class))},[p(a.$slots,"default")],2))}});export{f as _,_ as a};
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{u as D}from"./devops-DxRDHPW5.js";import{_}from"./Input.vue_vue_type_script_setup_true_lang-SeygKlpx.js";import{_ as V}from"./Button.vue_vue_type_script_setup_true_lang-B_YE_XNW.js";import{d as j,s as c,o as i,c as m,h as a,a as o,q as g,t as d,i as p,e as v,w as k,k as z,K as u}from"./index-BP_aNEdP.js";const B={class:"space-y-4"},I={key:0,class:"text-xs text-muted-foreground space-y-1"},N={class:"text-foreground"},P={class:"text-foreground"},S={key:0},U={key:1,class:"text-red-400"},b={class:"grid grid-cols-2 gap-3"},A={class:"space-y-1.5"},F={class:"space-y-1.5"},O={class:"space-y-1.5"},q={class:"flex items-center gap-2"},G=j({__name:"DevopsConnectForm",setup(E){var y,x;const t=D(),n=c(((y=t.integration)==null?void 0:y.organization)??""),r=c(((x=t.integration)==null?void 0:x.project)??""),s=c(""),f=c(!1);async function w(){if(!n.value||!r.value||!s.value){u.error("All fields are required");return}f.value=!0;try{await t.saveIntegration({organization:n.value,project:r.value,pat:s.value}),s.value="",u.success("Integration saved")}catch{u.error("Failed to save integration")}finally{f.value=!1}}async function C(){if(confirm("Delete ADO integration?"))try{await t.deleteIntegration(),n.value="",r.value="",s.value="",u.success("Integration deleted")}catch{u.error("Failed to delete integration")}}return(K,e)=>(i(),m("div",B,[a(t).integration?(i(),m("div",I,[o("p",null,[e[3]||(e[3]=g(" Connected to ",-1)),o("strong",N,d(a(t).integration.organization),1),e[4]||(e[4]=g(" / ",-1)),o("strong",P,d(a(t).integration.project),1)]),a(t).integration.last_synced_at?(i(),m("p",S," Last synced: "+d(new Date(a(t).integration.last_synced_at).toLocaleString()),1)):p("",!0),a(t).integration.last_sync_error?(i(),m("p",U," Error: "+d(a(t).integration.last_sync_error),1)):p("",!0)])):p("",!0),o("div",b,[o("div",A,[e[5]||(e[5]=o("label",{class:"text-sm font-medium text-foreground"},"Organization",-1)),v(_,{modelValue:n.value,"onUpdate:modelValue":e[0]||(e[0]=l=>n.value=l),placeholder:"myorg"},null,8,["modelValue"])]),o("div",F,[e[6]||(e[6]=o("label",{class:"text-sm font-medium text-foreground"},"Project",-1)),v(_,{modelValue:r.value,"onUpdate:modelValue":e[1]||(e[1]=l=>r.value=l),placeholder:"myproject"},null,8,["modelValue"])])]),o("div",O,[e[7]||(e[7]=o("label",{class:"text-sm font-medium text-foreground"}," Personal Access Token ",-1)),v(_,{modelValue:s.value,"onUpdate:modelValue":e[2]||(e[2]=l=>s.value=l),type:"password",placeholder:"••••••••",autocomplete:"new-password"},null,8,["modelValue"])]),o("div",q,[v(V,{loading:f.value,onClick:w},{default:k(()=>[g(d(a(t).integration?"Update":"Connect"),1)]),_:1},8,["loading"]),a(t).integration?(i(),z(V,{key:0,variant:"destructive",size:"sm",onClick:C},{default:k(()=>[...e[8]||(e[8]=[g(" Disconnect ",-1)])]),_:1})):p("",!0)])]))}});export{G as _};
|
||||
import{u as D}from"./devops-HjUgCfao.js";import{_}from"./Input.vue_vue_type_script_setup_true_lang-BM1xhrlf.js";import{_ as V}from"./Button.vue_vue_type_script_setup_true_lang-ClV4YfXb.js";import{d as j,s as c,o as i,c as m,h as a,a as o,q as g,t as d,i as p,e as v,w as k,k as z,K as u}from"./index-DVV3ZbZ2.js";const B={class:"space-y-4"},I={key:0,class:"text-xs text-muted-foreground space-y-1"},N={class:"text-foreground"},P={class:"text-foreground"},S={key:0},U={key:1,class:"text-red-400"},b={class:"grid grid-cols-2 gap-3"},A={class:"space-y-1.5"},F={class:"space-y-1.5"},O={class:"space-y-1.5"},q={class:"flex items-center gap-2"},G=j({__name:"DevopsConnectForm",setup(E){var y,x;const t=D(),n=c(((y=t.integration)==null?void 0:y.organization)??""),r=c(((x=t.integration)==null?void 0:x.project)??""),s=c(""),f=c(!1);async function w(){if(!n.value||!r.value||!s.value){u.error("All fields are required");return}f.value=!0;try{await t.saveIntegration({organization:n.value,project:r.value,pat:s.value}),s.value="",u.success("Integration saved")}catch{u.error("Failed to save integration")}finally{f.value=!1}}async function C(){if(confirm("Delete ADO integration?"))try{await t.deleteIntegration(),n.value="",r.value="",s.value="",u.success("Integration deleted")}catch{u.error("Failed to delete integration")}}return(K,e)=>(i(),m("div",B,[a(t).integration?(i(),m("div",I,[o("p",null,[e[3]||(e[3]=g(" Connected to ",-1)),o("strong",N,d(a(t).integration.organization),1),e[4]||(e[4]=g(" / ",-1)),o("strong",P,d(a(t).integration.project),1)]),a(t).integration.last_synced_at?(i(),m("p",S," Last synced: "+d(new Date(a(t).integration.last_synced_at).toLocaleString()),1)):p("",!0),a(t).integration.last_sync_error?(i(),m("p",U," Error: "+d(a(t).integration.last_sync_error),1)):p("",!0)])):p("",!0),o("div",b,[o("div",A,[e[5]||(e[5]=o("label",{class:"text-sm font-medium text-foreground"},"Organization",-1)),v(_,{modelValue:n.value,"onUpdate:modelValue":e[0]||(e[0]=l=>n.value=l),placeholder:"myorg"},null,8,["modelValue"])]),o("div",F,[e[6]||(e[6]=o("label",{class:"text-sm font-medium text-foreground"},"Project",-1)),v(_,{modelValue:r.value,"onUpdate:modelValue":e[1]||(e[1]=l=>r.value=l),placeholder:"myproject"},null,8,["modelValue"])])]),o("div",O,[e[7]||(e[7]=o("label",{class:"text-sm font-medium text-foreground"}," Personal Access Token ",-1)),v(_,{modelValue:s.value,"onUpdate:modelValue":e[2]||(e[2]=l=>s.value=l),type:"password",placeholder:"••••••••",autocomplete:"new-password"},null,8,["modelValue"])]),o("div",q,[v(V,{loading:f.value,onClick:w},{default:k(()=>[g(d(a(t).integration?"Update":"Connect"),1)]),_:1},8,["loading"]),a(t).integration?(i(),z(V,{key:0,variant:"destructive",size:"sm",onClick:C},{default:k(()=>[...e[8]||(e[8]=[g(" Disconnect ",-1)])]),_:1})):p("",!0)])]))}});export{G as _};
|
||||
1
src/static/assets/DevopsView-BtWCUdc6.js
Normal file
1
src/static/assets/DevopsView-BtWCUdc6.js
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 +1 @@
|
|||
import{d as y,y as k,G as b,k as h,H as g,e as c,T as x,w as u,o as a,c as n,a as o,p as r,t as m,i,n as w}from"./index-BP_aNEdP.js";import{_ as $}from"./Button.vue_vue_type_script_setup_true_lang-B_YE_XNW.js";const C={key:0,class:"fixed inset-0 z-50 flex items-center justify-center p-4"},B=["aria-label"],j={key:0,class:"flex items-center justify-between p-6 pb-4"},z={class:"text-lg font-semibold text-foreground"},E={key:0,class:"text-sm text-muted-foreground mt-1"},L={class:"px-6 pb-4"},M={key:1,class:"flex justify-end gap-2 px-6 pb-6"},V=y({__name:"Dialog",props:{open:{type:Boolean},title:{},description:{},maxWidth:{default:"max-w-lg"}},emits:["close"],setup(e,{emit:f}){const p=e,l=f;function d(t){t.key==="Escape"&&p.open&&l("close")}return k(()=>document.addEventListener("keydown",d)),b(()=>document.removeEventListener("keydown",d)),(t,s)=>(a(),h(g,{to:"body"},[c(x,{"enter-active-class":"transition-opacity duration-200","enter-from-class":"opacity-0","enter-to-class":"opacity-100","leave-active-class":"transition-opacity duration-200","leave-from-class":"opacity-100","leave-to-class":"opacity-0"},{default:u(()=>[e.open?(a(),n("div",C,[o("div",{class:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:s[0]||(s[0]=v=>l("close"))}),o("div",{class:w(["relative w-full bg-card border border-border rounded-lg shadow-xl z-10",e.maxWidth]),role:"dialog","aria-modal":!0,"aria-label":e.title},[e.title||t.$slots.header?(a(),n("div",j,[o("div",null,[r(t.$slots,"header",{},()=>[o("h2",z,m(e.title),1),e.description?(a(),n("p",E,m(e.description),1)):i("",!0)])]),c($,{variant:"ghost",size:"icon",class:"shrink-0",onClick:s[1]||(s[1]=v=>l("close"))},{default:u(()=>[...s[2]||(s[2]=[o("svg",{class:"h-4 w-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[o("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)])]),_:1})])):i("",!0),o("div",L,[r(t.$slots,"default")]),t.$slots.footer?(a(),n("div",M,[r(t.$slots,"footer")])):i("",!0)],10,B)])):i("",!0)]),_:3})]))}});export{V as _};
|
||||
import{d as y,y as k,G as b,k as h,H as g,e as c,T as x,w as u,o as a,c as n,a as o,p as r,t as m,i,n as w}from"./index-DVV3ZbZ2.js";import{_ as $}from"./Button.vue_vue_type_script_setup_true_lang-ClV4YfXb.js";const C={key:0,class:"fixed inset-0 z-50 flex items-center justify-center p-4"},B=["aria-label"],j={key:0,class:"flex items-center justify-between p-6 pb-4"},z={class:"text-lg font-semibold text-foreground"},E={key:0,class:"text-sm text-muted-foreground mt-1"},L={class:"px-6 pb-4"},M={key:1,class:"flex justify-end gap-2 px-6 pb-6"},V=y({__name:"Dialog",props:{open:{type:Boolean},title:{},description:{},maxWidth:{default:"max-w-lg"}},emits:["close"],setup(e,{emit:f}){const p=e,l=f;function d(t){t.key==="Escape"&&p.open&&l("close")}return k(()=>document.addEventListener("keydown",d)),b(()=>document.removeEventListener("keydown",d)),(t,s)=>(a(),h(g,{to:"body"},[c(x,{"enter-active-class":"transition-opacity duration-200","enter-from-class":"opacity-0","enter-to-class":"opacity-100","leave-active-class":"transition-opacity duration-200","leave-from-class":"opacity-100","leave-to-class":"opacity-0"},{default:u(()=>[e.open?(a(),n("div",C,[o("div",{class:"absolute inset-0 bg-black/60 backdrop-blur-sm",onClick:s[0]||(s[0]=v=>l("close"))}),o("div",{class:w(["relative w-full bg-card border border-border rounded-lg shadow-xl z-10",e.maxWidth]),role:"dialog","aria-modal":!0,"aria-label":e.title},[e.title||t.$slots.header?(a(),n("div",j,[o("div",null,[r(t.$slots,"header",{},()=>[o("h2",z,m(e.title),1),e.description?(a(),n("p",E,m(e.description),1)):i("",!0)])]),c($,{variant:"ghost",size:"icon",class:"shrink-0",onClick:s[1]||(s[1]=v=>l("close"))},{default:u(()=>[...s[2]||(s[2]=[o("svg",{class:"h-4 w-4",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[o("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M6 18L18 6M6 6l12 12"})],-1)])]),_:1})])):i("",!0),o("div",L,[r(t.$slots,"default")]),t.$slots.footer?(a(),n("div",M,[r(t.$slots,"footer")])):i("",!0)],10,B)])):i("",!0)]),_:3})]))}});export{V as _};
|
||||
|
|
@ -1 +1 @@
|
|||
import{c as i}from"./utils-7WVCegLb.js";import{d,o as s,c as u,n as m,h as r}from"./index-BP_aNEdP.js";const c=["id","name","type","value","placeholder","disabled","autocomplete","min","max","step"],g=d({__name:"Input",props:{modelValue:{},type:{},placeholder:{},disabled:{type:Boolean},class:{},id:{},name:{},autocomplete:{},min:{},max:{},step:{}},emits:["update:modelValue","change","blur","focus"],setup(e,{emit:n}){const a=e,o=n;return(f,t)=>(s(),u("input",{id:e.id,name:e.name,type:e.type??"text",value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,autocomplete:e.autocomplete,min:e.min,max:e.max,step:e.step,class:m(r(i)("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm","ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium","placeholder:text-muted-foreground","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:cursor-not-allowed disabled:opacity-50",a.class)),onInput:t[0]||(t[0]=l=>o("update:modelValue",l.target.value)),onChange:t[1]||(t[1]=l=>o("change",l.target.value)),onBlur:t[2]||(t[2]=l=>o("blur",l)),onFocus:t[3]||(t[3]=l=>o("focus",l))},null,42,c))}});export{g as _};
|
||||
import{c as i}from"./utils-7WVCegLb.js";import{d,o as s,c as u,n as m,h as r}from"./index-DVV3ZbZ2.js";const c=["id","name","type","value","placeholder","disabled","autocomplete","min","max","step"],g=d({__name:"Input",props:{modelValue:{},type:{},placeholder:{},disabled:{type:Boolean},class:{},id:{},name:{},autocomplete:{},min:{},max:{},step:{}},emits:["update:modelValue","change","blur","focus"],setup(e,{emit:n}){const a=e,o=n;return(f,t)=>(s(),u("input",{id:e.id,name:e.name,type:e.type??"text",value:e.modelValue,placeholder:e.placeholder,disabled:e.disabled,autocomplete:e.autocomplete,min:e.min,max:e.max,step:e.step,class:m(r(i)("flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm","ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium","placeholder:text-muted-foreground","focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2","disabled:cursor-not-allowed disabled:opacity-50",a.class)),onInput:t[0]||(t[0]=l=>o("update:modelValue",l.target.value)),onChange:t[1]||(t[1]=l=>o("change",l.target.value)),onBlur:t[2]||(t[2]=l=>o("blur",l)),onFocus:t[3]||(t[3]=l=>o("focus",l))},null,42,c))}});export{g as _};
|
||||
|
|
@ -1 +1 @@
|
|||
import{a as b}from"./admin-C27haAMd.js";import{_ as K,a as $}from"./CardContent.vue_vue_type_script_setup_true_lang-B8VjSxLa.js";import{_ as v}from"./Button.vue_vue_type_script_setup_true_lang-B_YE_XNW.js";import{_ as V}from"./Dialog.vue_vue_type_script_setup_true_lang-v8iqnRk8.js";import{_ as N}from"./Input.vue_vue_type_script_setup_true_lang-SeygKlpx.js";import{_ as A}from"./Spinner.vue_vue_type_script_setup_true_lang-Cu49-Cc4.js";import{d as B,y as L,c as l,a as t,e as r,w as n,s as i,o as a,q as p,F as P,r as F,t as u,h as k,k as I,i as j,K as y}from"./index-BP_aNEdP.js";import{a as h}from"./utils-7WVCegLb.js";const D={class:"p-6"},R={class:"flex items-center justify-between mb-6"},z={key:0,class:"flex items-center justify-center h-20"},M={key:1,class:"text-center text-muted-foreground py-8 text-sm"},T={key:2,class:"w-full"},U={class:"px-4 py-3 text-sm text-foreground"},q={class:"px-4 py-3 text-sm font-mono text-muted-foreground"},E={class:"px-4 py-3 text-xs text-muted-foreground"},H={class:"px-4 py-3 text-xs text-muted-foreground"},S={class:"px-4 py-3 text-right"},G={class:"space-y-4"},J={key:0,class:"rounded-md bg-emerald-500/10 border border-emerald-500/30 p-3"},O={class:"text-xs font-mono text-foreground break-all"},Q={key:1,class:"space-y-1.5"},re=B({__name:"KeysView",setup(W){const f=i([]),_=i(!1),c=i(!1),m=i(""),x=i(!1),d=i(null);L(()=>g());async function g(){_.value=!0;try{const o=await b.keys();f.value=o.data}finally{_.value=!1}}async function w(){if(m.value.trim()){x.value=!0;try{const o=await b.createKey({label:m.value});d.value=o.data.key,y.success("API key created"),await g(),m.value=""}catch{y.error("Failed to create key")}finally{x.value=!1}}}async function C(o){if(confirm(`Revoke key "${o.label}"? This cannot be undone.`))try{await b.revokeKey(o.id),y.success("Key revoked"),f.value=f.value.filter(e=>e.id!==o.id)}catch{y.error("Failed to revoke key")}}return(o,e)=>(a(),l("div",D,[t("div",R,[e[5]||(e[5]=t("h2",{class:"text-lg font-semibold text-foreground"},"API Keys",-1)),r(v,{size:"sm",onClick:e[0]||(e[0]=s=>{c.value=!0,d.value=null})},{default:n(()=>[...e[4]||(e[4]=[t("svg",{class:"h-4 w-4 mr-1.5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 4v16m8-8H4"})],-1),p(" New Key ",-1)])]),_:1})]),r(K,null,{default:n(()=>[r($,{class:"p-0"},{default:n(()=>[_.value?(a(),l("div",z,[r(A,{class:"text-primary"})])):f.value.length===0?(a(),l("div",M," No API keys ")):(a(),l("table",T,[e[7]||(e[7]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Label"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Prefix"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Created"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Last Used"),t("th",{class:"px-4 py-3"})])],-1)),t("tbody",null,[(a(!0),l(P,null,F(f.value,s=>(a(),l("tr",{key:s.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",U,u(s.label),1),t("td",q,u(s.prefix)+"...",1),t("td",E,u(k(h)(s.created_at)),1),t("td",H,u(s.last_used?k(h)(s.last_used):"Never"),1),t("td",S,[r(v,{variant:"ghost",size:"sm",class:"text-destructive",onClick:X=>C(s)},{default:n(()=>[...e[6]||(e[6]=[p(" Revoke ",-1)])]),_:1},8,["onClick"])])]))),128))])]))]),_:1})]),_:1}),r(V,{open:c.value,title:"Create API Key",onClose:e[3]||(e[3]=s=>c.value=!1)},{footer:n(()=>[r(v,{variant:"outline",onClick:e[2]||(e[2]=s=>c.value=!1)},{default:n(()=>[p(u(d.value?"Done":"Cancel"),1)]),_:1}),d.value?j("",!0):(a(),I(v,{key:0,loading:x.value,onClick:w},{default:n(()=>[...e[10]||(e[10]=[p(" Create ",-1)])]),_:1},8,["loading"]))]),default:n(()=>[t("div",G,[d.value?(a(),l("div",J,[e[8]||(e[8]=t("p",{class:"text-xs text-emerald-400 font-medium mb-1"},"Key created — save it now!",-1)),t("p",O,u(d.value),1)])):(a(),l("div",Q,[e[9]||(e[9]=t("label",{class:"text-sm font-medium text-foreground"},"Label",-1)),r(N,{modelValue:m.value,"onUpdate:modelValue":e[1]||(e[1]=s=>m.value=s),placeholder:"e.g. claude-collector",disabled:x.value},null,8,["modelValue","disabled"])]))])]),_:1},8,["open"])]))}});export{re as default};
|
||||
import{a as b}from"./admin-xS9EtPqv.js";import{_ as K,a as $}from"./CardContent.vue_vue_type_script_setup_true_lang-DdGXaWEa.js";import{_ as v}from"./Button.vue_vue_type_script_setup_true_lang-ClV4YfXb.js";import{_ as V}from"./Dialog.vue_vue_type_script_setup_true_lang-CSPK-Rcg.js";import{_ as N}from"./Input.vue_vue_type_script_setup_true_lang-BM1xhrlf.js";import{_ as A}from"./Spinner.vue_vue_type_script_setup_true_lang-BkmDerVR.js";import{d as B,y as L,c as l,a as t,e as r,w as n,s as i,o as a,q as p,F as P,r as F,t as u,h as k,k as I,i as j,K as y}from"./index-DVV3ZbZ2.js";import{a as h}from"./utils-7WVCegLb.js";const D={class:"p-6"},R={class:"flex items-center justify-between mb-6"},z={key:0,class:"flex items-center justify-center h-20"},M={key:1,class:"text-center text-muted-foreground py-8 text-sm"},T={key:2,class:"w-full"},U={class:"px-4 py-3 text-sm text-foreground"},q={class:"px-4 py-3 text-sm font-mono text-muted-foreground"},E={class:"px-4 py-3 text-xs text-muted-foreground"},H={class:"px-4 py-3 text-xs text-muted-foreground"},S={class:"px-4 py-3 text-right"},G={class:"space-y-4"},J={key:0,class:"rounded-md bg-emerald-500/10 border border-emerald-500/30 p-3"},O={class:"text-xs font-mono text-foreground break-all"},Q={key:1,class:"space-y-1.5"},re=B({__name:"KeysView",setup(W){const f=i([]),_=i(!1),c=i(!1),m=i(""),x=i(!1),d=i(null);L(()=>g());async function g(){_.value=!0;try{const o=await b.keys();f.value=o.data}finally{_.value=!1}}async function w(){if(m.value.trim()){x.value=!0;try{const o=await b.createKey({label:m.value});d.value=o.data.key,y.success("API key created"),await g(),m.value=""}catch{y.error("Failed to create key")}finally{x.value=!1}}}async function C(o){if(confirm(`Revoke key "${o.label}"? This cannot be undone.`))try{await b.revokeKey(o.id),y.success("Key revoked"),f.value=f.value.filter(e=>e.id!==o.id)}catch{y.error("Failed to revoke key")}}return(o,e)=>(a(),l("div",D,[t("div",R,[e[5]||(e[5]=t("h2",{class:"text-lg font-semibold text-foreground"},"API Keys",-1)),r(v,{size:"sm",onClick:e[0]||(e[0]=s=>{c.value=!0,d.value=null})},{default:n(()=>[...e[4]||(e[4]=[t("svg",{class:"h-4 w-4 mr-1.5",fill:"none",stroke:"currentColor",viewBox:"0 0 24 24"},[t("path",{"stroke-linecap":"round","stroke-linejoin":"round","stroke-width":"2",d:"M12 4v16m8-8H4"})],-1),p(" New Key ",-1)])]),_:1})]),r(K,null,{default:n(()=>[r($,{class:"p-0"},{default:n(()=>[_.value?(a(),l("div",z,[r(A,{class:"text-primary"})])):f.value.length===0?(a(),l("div",M," No API keys ")):(a(),l("table",T,[e[7]||(e[7]=t("thead",null,[t("tr",{class:"border-b border-border"},[t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Label"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Prefix"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Created"),t("th",{class:"text-left text-xs font-medium text-muted-foreground px-4 py-3"},"Last Used"),t("th",{class:"px-4 py-3"})])],-1)),t("tbody",null,[(a(!0),l(P,null,F(f.value,s=>(a(),l("tr",{key:s.id,class:"border-b border-border last:border-0 hover:bg-muted/30"},[t("td",U,u(s.label),1),t("td",q,u(s.prefix)+"...",1),t("td",E,u(k(h)(s.created_at)),1),t("td",H,u(s.last_used?k(h)(s.last_used):"Never"),1),t("td",S,[r(v,{variant:"ghost",size:"sm",class:"text-destructive",onClick:X=>C(s)},{default:n(()=>[...e[6]||(e[6]=[p(" Revoke ",-1)])]),_:1},8,["onClick"])])]))),128))])]))]),_:1})]),_:1}),r(V,{open:c.value,title:"Create API Key",onClose:e[3]||(e[3]=s=>c.value=!1)},{footer:n(()=>[r(v,{variant:"outline",onClick:e[2]||(e[2]=s=>c.value=!1)},{default:n(()=>[p(u(d.value?"Done":"Cancel"),1)]),_:1}),d.value?j("",!0):(a(),I(v,{key:0,loading:x.value,onClick:w},{default:n(()=>[...e[10]||(e[10]=[p(" Create ",-1)])]),_:1},8,["loading"]))]),default:n(()=>[t("div",G,[d.value?(a(),l("div",J,[e[8]||(e[8]=t("p",{class:"text-xs text-emerald-400 font-medium mb-1"},"Key created — save it now!",-1)),t("p",O,u(d.value),1)])):(a(),l("div",Q,[e[9]||(e[9]=t("label",{class:"text-sm font-medium text-foreground"},"Label",-1)),r(N,{modelValue:m.value,"onUpdate:modelValue":e[1]||(e[1]=s=>m.value=s),placeholder:"e.g. claude-collector",disabled:x.value},null,8,["modelValue","disabled"])]))])]),_:1},8,["open"])]))}});export{re as default};
|
||||
|
|
@ -1 +1 @@
|
|||
import{G as T,s as g,d as J,u as O,y as V,c as f,a as o,n as b,h as l,t as v,k as $,w as x,i as k,e as C,o as c,q as w,F as B,r as F,j as z}from"./index-BP_aNEdP.js";import{_ as A,a as D}from"./CardContent.vue_vue_type_script_setup_true_lang-B8VjSxLa.js";import{_ as N}from"./Button.vue_vue_type_script_setup_true_lang-B_YE_XNW.js";import"./utils-7WVCegLb.js";import"./Spinner.vue_vue_type_script_setup_true_lang-Cu49-Cc4.js";function U(E){const e=g([]),i=g(!1),m=g(null);let s=null,r=null,u=!1;function p(){if(!u)try{s=new EventSource(E),s.onopen=()=>{i.value=!0,m.value=null},s.onmessage=n=>{try{const y=JSON.parse(n.data);e.value.push({type:"message",data:y}),e.value.length>200&&e.value.shift()}catch{e.value.push({type:"message",data:n.data})}},s.addEventListener("session_start",n=>{try{e.value.push({type:"session_start",data:JSON.parse(n.data)})}catch{e.value.push({type:"session_start",data:n.data})}e.value.length>200&&e.value.shift()}),s.addEventListener("session_end",n=>{try{e.value.push({type:"session_end",data:JSON.parse(n.data)})}catch{e.value.push({type:"session_end",data:n.data})}e.value.length>200&&e.value.shift()}),s.addEventListener("activity",n=>{try{e.value.push({type:"activity",data:JSON.parse(n.data)})}catch{e.value.push({type:"activity",data:n.data})}e.value.length>200&&e.value.shift()}),s.onerror=()=>{i.value=!1,m.value="Connection lost, reconnecting...",s==null||s.close(),s=null,u||(r=setTimeout(()=>p(),5e3))}}catch{m.value="Failed to connect to event stream",u||(r=setTimeout(()=>p(),5e3))}}function _(){u=!0,r&&clearTimeout(r),s==null||s.close(),s=null,i.value=!1}function h(){e.value=[]}return T(()=>{_()}),{events:e,connected:i,error:m,connect:p,disconnect:_,clearEvents:h}}const I={class:"p-6 h-full flex flex-col"},R={class:"flex items-center gap-3 mb-4"},q={class:"flex items-center gap-2"},G={class:"text-xs text-muted-foreground"},M={key:0,class:"mb-4 text-xs text-amber-400 bg-amber-500/10 border border-amber-500/30 rounded px-3 py-2"},P={key:0,class:"flex items-center justify-center h-full text-sm text-muted-foreground"},W={key:1,class:"overflow-y-auto h-full font-mono text-xs"},H={class:"flex-1 min-w-0"},K={class:"flex items-center gap-2 flex-wrap"},Q={key:0,class:"text-muted-foreground"},X={class:"text-muted-foreground truncate mt-0.5"},ne=J({__name:"LiveView",setup(E){const e=O(),i=e.getToken(),m=`/cc-dashboard/api/events${i?`?token=${encodeURIComponent(i)}`:""}`,{events:s,connected:r,error:u,connect:p,clearEvents:_}=U(m);V(()=>{e.isAuthenticated&&i&&p()});const h=z(()=>[...s.value].reverse().slice(0,100));function n(t){return t==="session_start"?"text-emerald-400":t==="session_end"?"text-amber-400":t==="activity"?"text-blue-400":"text-muted-foreground"}function y(t){return t==="session_start"?"▶":t==="session_end"?"■":t==="activity"?"●":"○"}function j(t){if(typeof t=="string")return t;if(t&&typeof t=="object"){const a=t;return a.message||a.summary||JSON.stringify(t)}return String(t)}function S(t){if(t&&typeof t=="object"){const a=t;return a.display_name||a.project_id||""}return""}return(t,a)=>(c(),f("div",I,[o("div",R,[a[2]||(a[2]=o("h2",{class:"text-lg font-semibold text-foreground flex-1"},"Live Feed",-1)),o("div",q,[o("div",{class:b(["h-2 w-2 rounded-full",l(r)?"bg-emerald-500 animate-pulse":"bg-red-500"])},null,2),o("span",G,v(l(r)?"Connected":"Disconnected"),1)]),l(r)?k("",!0):(c(),$(N,{key:0,variant:"outline",size:"sm",onClick:l(p)},{default:x(()=>[...a[0]||(a[0]=[w(" Reconnect ",-1)])]),_:1},8,["onClick"])),C(N,{variant:"ghost",size:"sm",onClick:l(_)},{default:x(()=>[...a[1]||(a[1]=[w(" Clear ",-1)])]),_:1},8,["onClick"])]),l(u)&&!l(r)?(c(),f("div",M,v(l(u)),1)):k("",!0),C(A,{class:"flex-1 overflow-hidden"},{default:x(()=>[C(D,{class:"p-0 h-full"},{default:x(()=>[h.value.length===0?(c(),f("div",P,[...a[3]||(a[3]=[o("div",{class:"text-center"},[o("div",{class:"text-2xl mb-2"},"📡"),o("p",null,"Waiting for events..."),o("p",{class:"text-xs mt-1"},"Activity will appear here in real-time")],-1)])])):(c(),f("div",W,[(c(!0),f(B,null,F(h.value,(d,L)=>(c(),f("div",{key:L,class:"flex items-start gap-2 px-4 py-1.5 hover:bg-muted/50 border-b border-border/30"},[o("span",{class:b([n(d.type),"shrink-0 mt-0.5"])},v(y(d.type)),3),o("div",H,[o("div",K,[o("span",{class:b([n(d.type),"font-medium"])},v(d.type),3),S(d.data)?(c(),f("span",Q,v(S(d.data)),1)):k("",!0)]),o("p",X,v(j(d.data)),1)])]))),128))]))]),_:1})]),_:1})]))}});export{ne as default};
|
||||
import{G as T,s as g,d as J,u as O,y as V,c as f,a as o,n as b,h as l,t as v,k as $,w as x,i as k,e as C,o as c,q as w,F as B,r as F,j as z}from"./index-DVV3ZbZ2.js";import{_ as A,a as D}from"./CardContent.vue_vue_type_script_setup_true_lang-DdGXaWEa.js";import{_ as N}from"./Button.vue_vue_type_script_setup_true_lang-ClV4YfXb.js";import"./utils-7WVCegLb.js";import"./Spinner.vue_vue_type_script_setup_true_lang-BkmDerVR.js";function U(E){const e=g([]),i=g(!1),m=g(null);let s=null,r=null,u=!1;function p(){if(!u)try{s=new EventSource(E),s.onopen=()=>{i.value=!0,m.value=null},s.onmessage=n=>{try{const y=JSON.parse(n.data);e.value.push({type:"message",data:y}),e.value.length>200&&e.value.shift()}catch{e.value.push({type:"message",data:n.data})}},s.addEventListener("session_start",n=>{try{e.value.push({type:"session_start",data:JSON.parse(n.data)})}catch{e.value.push({type:"session_start",data:n.data})}e.value.length>200&&e.value.shift()}),s.addEventListener("session_end",n=>{try{e.value.push({type:"session_end",data:JSON.parse(n.data)})}catch{e.value.push({type:"session_end",data:n.data})}e.value.length>200&&e.value.shift()}),s.addEventListener("activity",n=>{try{e.value.push({type:"activity",data:JSON.parse(n.data)})}catch{e.value.push({type:"activity",data:n.data})}e.value.length>200&&e.value.shift()}),s.onerror=()=>{i.value=!1,m.value="Connection lost, reconnecting...",s==null||s.close(),s=null,u||(r=setTimeout(()=>p(),5e3))}}catch{m.value="Failed to connect to event stream",u||(r=setTimeout(()=>p(),5e3))}}function _(){u=!0,r&&clearTimeout(r),s==null||s.close(),s=null,i.value=!1}function h(){e.value=[]}return T(()=>{_()}),{events:e,connected:i,error:m,connect:p,disconnect:_,clearEvents:h}}const I={class:"p-6 h-full flex flex-col"},R={class:"flex items-center gap-3 mb-4"},q={class:"flex items-center gap-2"},G={class:"text-xs text-muted-foreground"},M={key:0,class:"mb-4 text-xs text-amber-400 bg-amber-500/10 border border-amber-500/30 rounded px-3 py-2"},P={key:0,class:"flex items-center justify-center h-full text-sm text-muted-foreground"},W={key:1,class:"overflow-y-auto h-full font-mono text-xs"},H={class:"flex-1 min-w-0"},K={class:"flex items-center gap-2 flex-wrap"},Q={key:0,class:"text-muted-foreground"},X={class:"text-muted-foreground truncate mt-0.5"},ne=J({__name:"LiveView",setup(E){const e=O(),i=e.getToken(),m=`/cc-dashboard/api/events${i?`?token=${encodeURIComponent(i)}`:""}`,{events:s,connected:r,error:u,connect:p,clearEvents:_}=U(m);V(()=>{e.isAuthenticated&&i&&p()});const h=z(()=>[...s.value].reverse().slice(0,100));function n(t){return t==="session_start"?"text-emerald-400":t==="session_end"?"text-amber-400":t==="activity"?"text-blue-400":"text-muted-foreground"}function y(t){return t==="session_start"?"▶":t==="session_end"?"■":t==="activity"?"●":"○"}function j(t){if(typeof t=="string")return t;if(t&&typeof t=="object"){const a=t;return a.message||a.summary||JSON.stringify(t)}return String(t)}function S(t){if(t&&typeof t=="object"){const a=t;return a.display_name||a.project_id||""}return""}return(t,a)=>(c(),f("div",I,[o("div",R,[a[2]||(a[2]=o("h2",{class:"text-lg font-semibold text-foreground flex-1"},"Live Feed",-1)),o("div",q,[o("div",{class:b(["h-2 w-2 rounded-full",l(r)?"bg-emerald-500 animate-pulse":"bg-red-500"])},null,2),o("span",G,v(l(r)?"Connected":"Disconnected"),1)]),l(r)?k("",!0):(c(),$(N,{key:0,variant:"outline",size:"sm",onClick:l(p)},{default:x(()=>[...a[0]||(a[0]=[w(" Reconnect ",-1)])]),_:1},8,["onClick"])),C(N,{variant:"ghost",size:"sm",onClick:l(_)},{default:x(()=>[...a[1]||(a[1]=[w(" Clear ",-1)])]),_:1},8,["onClick"])]),l(u)&&!l(r)?(c(),f("div",M,v(l(u)),1)):k("",!0),C(A,{class:"flex-1 overflow-hidden"},{default:x(()=>[C(D,{class:"p-0 h-full"},{default:x(()=>[h.value.length===0?(c(),f("div",P,[...a[3]||(a[3]=[o("div",{class:"text-center"},[o("div",{class:"text-2xl mb-2"},"📡"),o("p",null,"Waiting for events..."),o("p",{class:"text-xs mt-1"},"Activity will appear here in real-time")],-1)])])):(c(),f("div",W,[(c(!0),f(B,null,F(h.value,(d,L)=>(c(),f("div",{key:L,class:"flex items-start gap-2 px-4 py-1.5 hover:bg-muted/50 border-b border-border/30"},[o("span",{class:b([n(d.type),"shrink-0 mt-0.5"])},v(y(d.type)),3),o("div",H,[o("div",K,[o("span",{class:b([n(d.type),"font-medium"])},v(d.type),3),S(d.data)?(c(),f("span",Q,v(S(d.data)),1)):k("",!0)]),o("p",X,v(j(d.data)),1)])]))),128))]))]),_:1})]),_:1})]))}});export{ne as default};
|
||||
|
|
@ -1 +1 @@
|
|||
import{d as h,u as f,c as o,a as t,b as m,e as a,w as d,o as r,f as g,g as p,h as i,t as x,i as w}from"./index-BP_aNEdP.js";import{_ as y,a as b}from"./CardContent.vue_vue_type_script_setup_true_lang-B8VjSxLa.js";import"./utils-7WVCegLb.js";const v={class:"min-h-screen flex items-center justify-center bg-background p-4"},_={class:"w-full max-w-sm"},k={class:"space-y-4"},C={key:0,class:"rounded-md bg-destructive/10 border border-destructive/30 px-3 py-2 text-sm text-destructive"},B=["disabled"],V={key:0},S={key:1},M=h({__name:"LoginView",setup(F){const c=g(),l=p(),s=f();async function u(){try{await s.loginWithMicrosoft();const n=l.query.redirect;c.push(n??"/")}catch{}}return(n,e)=>(r(),o("div",v,[t("div",_,[e[2]||(e[2]=m('<div class="text-center mb-8"><div class="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-primary mb-3"><svg class="h-7 w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg></div><h1 class="text-2xl font-bold text-foreground">CC Dashboard</h1><p class="text-sm text-muted-foreground mt-1">Corporate Planning Hub</p></div>',1)),a(y,null,{default:d(()=>[a(b,{class:"pt-6"},{default:d(()=>[t("div",k,[i(s).error?(r(),o("div",C,x(i(s).error),1)):w("",!0),t("button",{type:"button",disabled:i(s).loading,class:"w-full flex items-center justify-center gap-3 rounded-md border border-border bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",onClick:u},[e[0]||(e[0]=t("svg",{class:"h-5 w-5 shrink-0",viewBox:"0 0 21 21",fill:"none",xmlns:"http://www.w3.org/2000/svg"},[t("rect",{x:"1",y:"1",width:"9",height:"9",fill:"#F25022"}),t("rect",{x:"11",y:"1",width:"9",height:"9",fill:"#7FBA00"}),t("rect",{x:"1",y:"11",width:"9",height:"9",fill:"#00A4EF"}),t("rect",{x:"11",y:"11",width:"9",height:"9",fill:"#FFB900"})],-1)),i(s).loading?(r(),o("span",V,"Signing in…")):(r(),o("span",S,"Sign in with Microsoft"))],8,B),e[1]||(e[1]=t("p",{class:"text-center text-xs text-muted-foreground"}," Use your @oliver.agency account ",-1))])]),_:1})]),_:1})])]))}});export{M as default};
|
||||
import{d as h,u as f,c as o,a as t,b as m,e as a,w as d,o as r,f as g,g as p,h as i,t as x,i as w}from"./index-DVV3ZbZ2.js";import{_ as y,a as b}from"./CardContent.vue_vue_type_script_setup_true_lang-DdGXaWEa.js";import"./utils-7WVCegLb.js";const v={class:"min-h-screen flex items-center justify-center bg-background p-4"},_={class:"w-full max-w-sm"},k={class:"space-y-4"},C={key:0,class:"rounded-md bg-destructive/10 border border-destructive/30 px-3 py-2 text-sm text-destructive"},B=["disabled"],V={key:0},S={key:1},M=h({__name:"LoginView",setup(F){const c=g(),l=p(),s=f();async function u(){try{await s.loginWithMicrosoft();const n=l.query.redirect;c.push(n??"/")}catch{}}return(n,e)=>(r(),o("div",v,[t("div",_,[e[2]||(e[2]=m('<div class="text-center mb-8"><div class="inline-flex h-12 w-12 items-center justify-center rounded-xl bg-primary mb-3"><svg class="h-7 w-7 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path></svg></div><h1 class="text-2xl font-bold text-foreground">CC Dashboard</h1><p class="text-sm text-muted-foreground mt-1">Corporate Planning Hub</p></div>',1)),a(y,null,{default:d(()=>[a(b,{class:"pt-6"},{default:d(()=>[t("div",k,[i(s).error?(r(),o("div",C,x(i(s).error),1)):w("",!0),t("button",{type:"button",disabled:i(s).loading,class:"w-full flex items-center justify-center gap-3 rounded-md border border-border bg-white px-4 py-2.5 text-sm font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed transition-colors",onClick:u},[e[0]||(e[0]=t("svg",{class:"h-5 w-5 shrink-0",viewBox:"0 0 21 21",fill:"none",xmlns:"http://www.w3.org/2000/svg"},[t("rect",{x:"1",y:"1",width:"9",height:"9",fill:"#F25022"}),t("rect",{x:"11",y:"1",width:"9",height:"9",fill:"#7FBA00"}),t("rect",{x:"1",y:"11",width:"9",height:"9",fill:"#00A4EF"}),t("rect",{x:"11",y:"11",width:"9",height:"9",fill:"#FFB900"})],-1)),i(s).loading?(r(),o("span",V,"Signing in…")):(r(),o("span",S,"Sign in with Microsoft"))],8,B),e[1]||(e[1]=t("p",{class:"text-center text-xs text-muted-foreground"}," Use your @oliver.agency account ",-1))])]),_:1})]),_:1})])]))}});export{M as default};
|
||||
|
|
@ -1 +1 @@
|
|||
import{c as r}from"./utils-7WVCegLb.js";import{d as s,o as n,c as t,n as l,h as c,a as d,B as u}from"./index-BP_aNEdP.js";const h=s({__name:"Progress",props:{value:{},max:{default:100},class:{},color:{default:"default"}},setup(a){const e=a,o=()=>Math.min(100,Math.max(0,e.value/e.max*100));return(i,m)=>(n(),t("div",{class:l(c(r)("relative h-2 w-full overflow-hidden rounded-full bg-secondary",e.class))},[d("div",{class:l(["h-full rounded-full transition-all duration-300",{"bg-primary":a.color==="default","bg-emerald-500":a.color==="success","bg-amber-500":a.color==="warning","bg-red-500":a.color==="danger"}]),style:u({width:`${o()}%`})},null,6)],2))}});export{h as _};
|
||||
import{c as r}from"./utils-7WVCegLb.js";import{d as s,o as n,c as t,n as l,h as c,a as d,B as u}from"./index-DVV3ZbZ2.js";const h=s({__name:"Progress",props:{value:{},max:{default:100},class:{},color:{default:"default"}},setup(a){const e=a,o=()=>Math.min(100,Math.max(0,e.value/e.max*100));return(i,m)=>(n(),t("div",{class:l(c(r)("relative h-2 w-full overflow-hidden rounded-full bg-secondary",e.class))},[d("div",{class:l(["h-full rounded-full transition-all duration-300",{"bg-primary":a.color==="default","bg-emerald-500":a.color==="success","bg-amber-500":a.color==="warning","bg-red-500":a.color==="danger"}]),style:u({width:`${o()}%`})},null,6)],2))}});export{h as _};
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/static/assets/ReportsView-CgmFyw5q.css
Normal file
1
src/static/assets/ReportsView-CgmFyw5q.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
[data-v-32c92954] .prose{color:hsl(var(--foreground))}[data-v-32c92954] .prose h1{color:hsl(var(--foreground));font-weight:600;margin-top:1rem;margin-bottom:.5rem}[data-v-32c92954] .prose p{margin-bottom:.75rem;color:hsl(var(--muted-foreground))}[data-v-32c92954] .prose ul{margin-left:1.25rem;color:hsl(var(--muted-foreground))}[data-v-32c92954] .prose li{margin-bottom:.25rem}[data-v-32c92954] .prose code{background:hsl(var(--muted));padding:.125rem .25rem;border-radius:.25rem;font-size:.85em;word-break:break-word;overflow-wrap:anywhere}[data-v-32c92954] .prose pre{white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;max-width:100%;overflow-x:auto}[data-v-32c92954] .prose pre code{word-break:break-word;overflow-wrap:anywhere}
|
||||
|
|
@ -1 +0,0 @@
|
|||
[data-v-5c75fc6a] .prose{color:hsl(var(--foreground))}[data-v-5c75fc6a] .prose h1{color:hsl(var(--foreground));font-weight:600;margin-top:1rem;margin-bottom:.5rem}[data-v-5c75fc6a] .prose p{margin-bottom:.75rem;color:hsl(var(--muted-foreground))}[data-v-5c75fc6a] .prose ul{margin-left:1.25rem;color:hsl(var(--muted-foreground))}[data-v-5c75fc6a] .prose li{margin-bottom:.25rem}[data-v-5c75fc6a] .prose code{background:hsl(var(--muted));padding:.125rem .25rem;border-radius:.25rem;font-size:.85em;word-break:break-word;overflow-wrap:anywhere}[data-v-5c75fc6a] .prose pre{white-space:pre-wrap;word-break:break-word;overflow-wrap:anywhere;max-width:100%;overflow-x:auto}[data-v-5c75fc6a] .prose pre code{word-break:break-word;overflow-wrap:anywhere}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -1 +1 @@
|
|||
import{d as N,u as E,y as P,c as U,a,e as t,w as s,s as f,o as k,q as u,h as c,k as z,i as B,E as I,K as x}from"./index-BP_aNEdP.js";import{u as F}from"./devops-DxRDHPW5.js";import{_ as w,a as V}from"./CardContent.vue_vue_type_script_setup_true_lang-B8VjSxLa.js";import{_ as $,a as S}from"./CardTitle.vue_vue_type_script_setup_true_lang-KH4UhlTi.js";import{_ as y}from"./Input.vue_vue_type_script_setup_true_lang-SeygKlpx.js";import{_}from"./Button.vue_vue_type_script_setup_true_lang-B_YE_XNW.js";import{_ as O}from"./DevopsConnectForm.vue_vue_type_script_setup_true_lang-DQc_dk7n.js";import{i as C}from"./utils-7WVCegLb.js";import"./Spinner.vue_vue_type_script_setup_true_lang-Cu49-Cc4.js";function T(i,l){const n=`/cc-dashboard/api/export/timesheet.csv?from=${i}&to=${l}`,o=document.createElement("a");o.href=n,o.download=`timesheet-${i}-${l}.csv`,o.click()}function A(i,l){const n=`/cc-dashboard/api/export/timesheet.ics?from=${i}&to=${l}`,o=document.createElement("a");o.href=n,o.download=`timesheet-${i}-${l}.ics`,o.click()}const H={class:"p-6 space-y-6 max-w-2xl"},K={class:"space-y-1.5"},M={class:"space-y-1.5"},j={class:"flex items-center justify-between"},q={class:"flex items-center gap-3 flex-wrap"},h={class:"space-y-1.5"},G={class:"space-y-1.5"},J={class:"flex items-center gap-2"},se=N({__name:"SettingsView",setup(i){const l=E(),n=F(),o=f(""),p=f(0),g=f(!1),d=f(""),m=f("");P(()=>{l.user&&(o.value=l.user.username,p.value=l.user.daily_overhead_hours??0),n.fetchIntegration();const v=new Date;m.value=C(v);const e=new Date(v);e.setDate(v.getDate()-30),d.value=C(e)});async function D(){g.value=!0;try{await I.patch("/api/auth/me",{username:o.value,daily_overhead_hours:p.value}),await l.fetchMe(),x.success("Profile saved")}catch{x.error("Failed to save profile")}finally{g.value=!1}}async function b(){try{await n.sync(),x.success("Sync complete")}catch{x.error(n.error??"Sync failed")}}return(v,e)=>(k(),U("div",H,[e[18]||(e[18]=a("h2",{class:"text-lg font-semibold text-foreground"},"Settings",-1)),t(w,null,{default:s(()=>[t($,null,{default:s(()=>[t(S,{class:"text-sm"},{default:s(()=>[...e[6]||(e[6]=[u("Profile",-1)])]),_:1})]),_:1}),t(V,{class:"space-y-4"},{default:s(()=>[a("div",K,[e[7]||(e[7]=a("label",{class:"text-sm font-medium text-foreground"},"Username",-1)),t(y,{modelValue:o.value,"onUpdate:modelValue":e[0]||(e[0]=r=>o.value=r),placeholder:"username"},null,8,["modelValue"])]),a("div",M,[e[8]||(e[8]=a("label",{class:"text-sm font-medium text-foreground"},"Daily Overhead Hours",-1)),t(y,{modelValue:p.value,"onUpdate:modelValue":e[1]||(e[1]=r=>p.value=r),type:"number",min:"0",max:"8",step:"0.25",class:"w-32"},null,8,["modelValue"]),e[9]||(e[9]=a("p",{class:"text-xs text-muted-foreground"}," Hours per day to add for overhead / meetings ",-1))]),t(_,{loading:g.value,onClick:D},{default:s(()=>[...e[10]||(e[10]=[u("Save Profile",-1)])]),_:1},8,["loading"])]),_:1})]),_:1}),t(w,null,{default:s(()=>[t($,null,{default:s(()=>[a("div",j,[t(S,{class:"text-sm"},{default:s(()=>[...e[11]||(e[11]=[u("Azure DevOps Integration",-1)])]),_:1}),c(n).integration?(k(),z(_,{key:0,variant:"outline",size:"sm",loading:c(n).syncing,onClick:b},{default:s(()=>[...e[12]||(e[12]=[u(" Sync Now ",-1)])]),_:1},8,["loading"])):B("",!0)])]),_:1}),t(V,null,{default:s(()=>[t(O)]),_:1})]),_:1}),t(w,null,{default:s(()=>[t($,null,{default:s(()=>[t(S,{class:"text-sm"},{default:s(()=>[...e[13]||(e[13]=[u("Export",-1)])]),_:1})]),_:1}),t(V,{class:"space-y-4"},{default:s(()=>[a("div",q,[a("div",h,[e[14]||(e[14]=a("label",{class:"text-xs text-muted-foreground"},"From",-1)),t(y,{modelValue:d.value,"onUpdate:modelValue":e[2]||(e[2]=r=>d.value=r),type:"date",class:"h-8 text-xs"},null,8,["modelValue"])]),a("div",G,[e[15]||(e[15]=a("label",{class:"text-xs text-muted-foreground"},"To",-1)),t(y,{modelValue:m.value,"onUpdate:modelValue":e[3]||(e[3]=r=>m.value=r),type:"date",class:"h-8 text-xs"},null,8,["modelValue"])])]),a("div",J,[t(_,{variant:"outline",size:"sm",onClick:e[4]||(e[4]=r=>c(T)(d.value,m.value))},{default:s(()=>[...e[16]||(e[16]=[u(" Download CSV ",-1)])]),_:1}),t(_,{variant:"outline",size:"sm",onClick:e[5]||(e[5]=r=>c(A)(d.value,m.value))},{default:s(()=>[...e[17]||(e[17]=[u(" Download ICS ",-1)])]),_:1})])]),_:1})]),_:1})]))}});export{se as default};
|
||||
import{d as N,u as E,y as P,c as U,a,e as t,w as s,s as f,o as k,q as u,h as c,k as z,i as B,E as I,K as x}from"./index-DVV3ZbZ2.js";import{u as F}from"./devops-HjUgCfao.js";import{_ as w,a as V}from"./CardContent.vue_vue_type_script_setup_true_lang-DdGXaWEa.js";import{_ as $,a as S}from"./CardTitle.vue_vue_type_script_setup_true_lang-CT9DRTds.js";import{_ as y}from"./Input.vue_vue_type_script_setup_true_lang-BM1xhrlf.js";import{_}from"./Button.vue_vue_type_script_setup_true_lang-ClV4YfXb.js";import{_ as O}from"./DevopsConnectForm.vue_vue_type_script_setup_true_lang-5nCaOxp1.js";import{i as C}from"./utils-7WVCegLb.js";import"./Spinner.vue_vue_type_script_setup_true_lang-BkmDerVR.js";function T(i,l){const n=`/cc-dashboard/api/export/timesheet.csv?from=${i}&to=${l}`,o=document.createElement("a");o.href=n,o.download=`timesheet-${i}-${l}.csv`,o.click()}function A(i,l){const n=`/cc-dashboard/api/export/timesheet.ics?from=${i}&to=${l}`,o=document.createElement("a");o.href=n,o.download=`timesheet-${i}-${l}.ics`,o.click()}const H={class:"p-6 space-y-6 max-w-2xl"},K={class:"space-y-1.5"},M={class:"space-y-1.5"},j={class:"flex items-center justify-between"},q={class:"flex items-center gap-3 flex-wrap"},h={class:"space-y-1.5"},G={class:"space-y-1.5"},J={class:"flex items-center gap-2"},se=N({__name:"SettingsView",setup(i){const l=E(),n=F(),o=f(""),p=f(0),g=f(!1),d=f(""),m=f("");P(()=>{l.user&&(o.value=l.user.username,p.value=l.user.daily_overhead_hours??0),n.fetchIntegration();const v=new Date;m.value=C(v);const e=new Date(v);e.setDate(v.getDate()-30),d.value=C(e)});async function D(){g.value=!0;try{await I.patch("/api/auth/me",{username:o.value,daily_overhead_hours:p.value}),await l.fetchMe(),x.success("Profile saved")}catch{x.error("Failed to save profile")}finally{g.value=!1}}async function b(){try{await n.sync(),x.success("Sync complete")}catch{x.error(n.error??"Sync failed")}}return(v,e)=>(k(),U("div",H,[e[18]||(e[18]=a("h2",{class:"text-lg font-semibold text-foreground"},"Settings",-1)),t(w,null,{default:s(()=>[t($,null,{default:s(()=>[t(S,{class:"text-sm"},{default:s(()=>[...e[6]||(e[6]=[u("Profile",-1)])]),_:1})]),_:1}),t(V,{class:"space-y-4"},{default:s(()=>[a("div",K,[e[7]||(e[7]=a("label",{class:"text-sm font-medium text-foreground"},"Username",-1)),t(y,{modelValue:o.value,"onUpdate:modelValue":e[0]||(e[0]=r=>o.value=r),placeholder:"username"},null,8,["modelValue"])]),a("div",M,[e[8]||(e[8]=a("label",{class:"text-sm font-medium text-foreground"},"Daily Overhead Hours",-1)),t(y,{modelValue:p.value,"onUpdate:modelValue":e[1]||(e[1]=r=>p.value=r),type:"number",min:"0",max:"8",step:"0.25",class:"w-32"},null,8,["modelValue"]),e[9]||(e[9]=a("p",{class:"text-xs text-muted-foreground"}," Hours per day to add for overhead / meetings ",-1))]),t(_,{loading:g.value,onClick:D},{default:s(()=>[...e[10]||(e[10]=[u("Save Profile",-1)])]),_:1},8,["loading"])]),_:1})]),_:1}),t(w,null,{default:s(()=>[t($,null,{default:s(()=>[a("div",j,[t(S,{class:"text-sm"},{default:s(()=>[...e[11]||(e[11]=[u("Azure DevOps Integration",-1)])]),_:1}),c(n).integration?(k(),z(_,{key:0,variant:"outline",size:"sm",loading:c(n).syncing,onClick:b},{default:s(()=>[...e[12]||(e[12]=[u(" Sync Now ",-1)])]),_:1},8,["loading"])):B("",!0)])]),_:1}),t(V,null,{default:s(()=>[t(O)]),_:1})]),_:1}),t(w,null,{default:s(()=>[t($,null,{default:s(()=>[t(S,{class:"text-sm"},{default:s(()=>[...e[13]||(e[13]=[u("Export",-1)])]),_:1})]),_:1}),t(V,{class:"space-y-4"},{default:s(()=>[a("div",q,[a("div",h,[e[14]||(e[14]=a("label",{class:"text-xs text-muted-foreground"},"From",-1)),t(y,{modelValue:d.value,"onUpdate:modelValue":e[2]||(e[2]=r=>d.value=r),type:"date",class:"h-8 text-xs"},null,8,["modelValue"])]),a("div",G,[e[15]||(e[15]=a("label",{class:"text-xs text-muted-foreground"},"To",-1)),t(y,{modelValue:m.value,"onUpdate:modelValue":e[3]||(e[3]=r=>m.value=r),type:"date",class:"h-8 text-xs"},null,8,["modelValue"])])]),a("div",J,[t(_,{variant:"outline",size:"sm",onClick:e[4]||(e[4]=r=>c(T)(d.value,m.value))},{default:s(()=>[...e[16]||(e[16]=[u(" Download CSV ",-1)])]),_:1}),t(_,{variant:"outline",size:"sm",onClick:e[5]||(e[5]=r=>c(A)(d.value,m.value))},{default:s(()=>[...e[17]||(e[17]=[u(" Download ICS ",-1)])]),_:1})])]),_:1})]),_:1})]))}});export{se as default};
|
||||
|
|
@ -1 +1 @@
|
|||
import{d as l,o as n,c as o,n as t,a as r}from"./index-BP_aNEdP.js";const i=l({__name:"Spinner",props:{size:{},class:{}},setup(s){return(a,e)=>(n(),o("svg",{class:t(["animate-spin text-current",s.size==="sm"?"h-3 w-3":s.size==="lg"?"h-6 w-6":"h-4 w-4",a.$props.class]),xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[...e[0]||(e[0]=[r("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor","stroke-width":"4"},null,-1),r("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"},null,-1)])],2))}});export{i as _};
|
||||
import{d as l,o as n,c as o,n as t,a as r}from"./index-DVV3ZbZ2.js";const i=l({__name:"Spinner",props:{size:{},class:{}},setup(s){return(a,e)=>(n(),o("svg",{class:t(["animate-spin text-current",s.size==="sm"?"h-3 w-3":s.size==="lg"?"h-6 w-6":"h-4 w-4",a.$props.class]),xmlns:"http://www.w3.org/2000/svg",fill:"none",viewBox:"0 0 24 24"},[...e[0]||(e[0]=[r("circle",{class:"opacity-25",cx:"12",cy:"12",r:"10",stroke:"currentColor","stroke-width":"4"},null,-1),r("path",{class:"opacity-75",fill:"currentColor",d:"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"},null,-1)])],2))}});export{i as _};
|
||||
|
|
@ -1 +1 @@
|
|||
import{E as e}from"./index-BP_aNEdP.js";const i={users:()=>e.get("/api/admin/users"),keys:()=>e.get("/api/keys"),createKey:s=>e.post("/api/keys",s),revokeKey:s=>e.delete(`/api/keys/${s}`)};export{i as a};
|
||||
import{E as e}from"./index-DVV3ZbZ2.js";const i={users:()=>e.get("/api/admin/users"),keys:()=>e.get("/api/keys"),createKey:s=>e.post("/api/keys",s),revokeKey:s=>e.delete(`/api/keys/${s}`)};export{i as a};
|
||||
|
|
@ -1 +1 @@
|
|||
import{E as t}from"./index-BP_aNEdP.js";const e={summary:a=>t.get("/api/dashboard/summary",{params:a}),projects:a=>t.get("/api/dashboard/projects",{params:a}),timeline:a=>t.get("/api/dashboard/timeline",{params:a}),monthly:a=>t.get("/api/dashboard/monthly",{params:a}),dow:a=>t.get("/api/dashboard/dow",{params:a}),tools:a=>t.get("/api/dashboard/tools",{params:a}),activity:a=>t.get("/api/dashboard/activity",{params:a}),calendar:a=>t.get("/api/dashboard/calendar",{params:a}),project:(a,o)=>t.get("/api/dashboard/project/"+a,{params:o})};export{e as d};
|
||||
import{E as t}from"./index-DVV3ZbZ2.js";const e={summary:a=>t.get("/api/dashboard/summary",{params:a}),projects:a=>t.get("/api/dashboard/projects",{params:a}),timeline:a=>t.get("/api/dashboard/timeline",{params:a}),monthly:a=>t.get("/api/dashboard/monthly",{params:a}),dow:a=>t.get("/api/dashboard/dow",{params:a}),tools:a=>t.get("/api/dashboard/tools",{params:a}),activity:a=>t.get("/api/dashboard/activity",{params:a}),calendar:a=>t.get("/api/dashboard/calendar",{params:a}),project:(a,o)=>t.get("/api/dashboard/project/"+a,{params:o})};export{e as d};
|
||||
|
|
@ -1 +1 @@
|
|||
import{E as s,C as I,s as o}from"./index-BP_aNEdP.js";const i={getIntegration:()=>s.get("/api/devops/integration"),saveIntegration:e=>s.put("/api/devops/integration",e),deleteIntegration:()=>s.delete("/api/devops/integration"),sync:()=>s.post("/api/devops/sync"),workItems:e=>s.get("/api/devops/work-items",{params:e?{state:e}:void 0})},m=I("devops",()=>{const e=o(null),l=o([]),r=o(!1),n=o(!1),c=o(null);async function u(){n.value=!0;try{const t=await i.getIntegration();e.value=t.data}catch{e.value=null}finally{n.value=!1}}async function d(t){const a=await i.saveIntegration(t);e.value=a.data}async function g(){await i.deleteIntegration(),e.value=null}async function f(){var t,a;r.value=!0,c.value=null;try{await i.sync(),await u()}catch(v){const p=v;throw c.value=((a=(t=p.response)==null?void 0:t.data)==null?void 0:a.detail)??p.message??"Sync failed",v}finally{r.value=!1}}async function y(t){n.value=!0;try{const a=await i.workItems(t);l.value=a.data}catch{l.value=[]}finally{n.value=!1}}return{integration:e,workItems:l,syncing:r,loading:n,error:c,fetchIntegration:u,saveIntegration:d,deleteIntegration:g,sync:f,fetchWorkItems:y}});export{m as u};
|
||||
import{E as s,C as I,s as o}from"./index-DVV3ZbZ2.js";const i={getIntegration:()=>s.get("/api/devops/integration"),saveIntegration:e=>s.put("/api/devops/integration",e),deleteIntegration:()=>s.delete("/api/devops/integration"),sync:()=>s.post("/api/devops/sync"),workItems:e=>s.get("/api/devops/work-items",{params:e?{state:e}:void 0})},m=I("devops",()=>{const e=o(null),l=o([]),r=o(!1),n=o(!1),c=o(null);async function u(){n.value=!0;try{const t=await i.getIntegration();e.value=t.data}catch{e.value=null}finally{n.value=!1}}async function d(t){const a=await i.saveIntegration(t);e.value=a.data}async function g(){await i.deleteIntegration(),e.value=null}async function f(){var t,a;r.value=!0,c.value=null;try{await i.sync(),await u()}catch(v){const p=v;throw c.value=((a=(t=p.response)==null?void 0:t.data)==null?void 0:a.detail)??p.message??"Sync failed",v}finally{r.value=!1}}async function y(t){n.value=!0;try{const a=await i.workItems(t);l.value=a.data}catch{l.value=[]}finally{n.value=!1}}return{integration:e,workItems:l,syncing:r,loading:n,error:c,fetchIntegration:u,saveIntegration:d,deleteIntegration:g,sync:f,fetchWorkItems:y}});export{m as u};
|
||||
File diff suppressed because one or more lines are too long
1
src/static/assets/index-CS_oOq1J.css
Normal file
1
src/static/assets/index-CS_oOq1J.css
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 +1 @@
|
|||
import{E as l,C as w,s as i}from"./index-BP_aNEdP.js";const o={list:a=>l.get("/api/tasks",{params:a}),get:a=>l.get(`/api/tasks/${a}`),create:a=>l.post("/api/tasks",a),update:(a,s)=>l.patch(`/api/tasks/${a}`,s),remove:a=>l.delete(`/api/tasks/${a}`),complete:a=>l.post(`/api/tasks/${a}/complete`),blocks:a=>l.get(`/api/tasks/${a}/blocks`),createBlock:(a,s)=>l.post(`/api/tasks/${a}/blocks`,s),updateBlock:(a,s)=>l.patch(`/api/tasks/blocks/${a}`,s),deleteBlock:a=>l.delete(`/api/tasks/blocks/${a}`)},b=Object.freeze(Object.defineProperty({__proto__:null,tasksApi:o},Symbol.toStringTag,{value:"Module"})),$=w("tasks",()=>{const a=i([]),s=i(!1),n=i(null);async function u(t){s.value=!0,n.value=null;try{const e=await o.list({date:t});a.value=e.data}catch(e){const c=e;n.value=c.message??"Failed to fetch tasks"}finally{s.value=!1}}async function d(t){s.value=!0,n.value=null;try{const e=await o.list(t?{project_id:t}:void 0);a.value=e.data}catch(e){const c=e;n.value=c.message??"Failed to fetch tasks"}finally{s.value=!1}}async function p(t){const e=await o.create(t);return a.value.push(e.data),e.data}async function k(t,e){const c=await o.update(t,e),r=a.value.findIndex(h=>h.id===t);return r!==-1&&(a.value[r]=c.data),c.data}async function f(t){await o.remove(t),a.value=a.value.filter(e=>e.id!==t)}async function v(t){const e=await o.complete(t),c=a.value.findIndex(r=>r.id===t);return c!==-1&&(a.value[c]=e.data),e.data}async function y(t,e){return(await o.createBlock(t,e)).data}async function m(t,e){return(await o.updateBlock(t,e)).data}async function g(t){await o.deleteBlock(t)}return{tasks:a,loading:s,error:n,fetchForDate:u,fetchAll:d,create:p,update:k,remove:f,complete:v,createBlock:y,updateBlock:m,deleteBlock:g}});export{b as t,$ as u};
|
||||
import{E as l,C as w,s as i}from"./index-DVV3ZbZ2.js";const o={list:a=>l.get("/api/tasks",{params:a}),get:a=>l.get(`/api/tasks/${a}`),create:a=>l.post("/api/tasks",a),update:(a,s)=>l.patch(`/api/tasks/${a}`,s),remove:a=>l.delete(`/api/tasks/${a}`),complete:a=>l.post(`/api/tasks/${a}/complete`),blocks:a=>l.get(`/api/tasks/${a}/blocks`),createBlock:(a,s)=>l.post(`/api/tasks/${a}/blocks`,s),updateBlock:(a,s)=>l.patch(`/api/tasks/blocks/${a}`,s),deleteBlock:a=>l.delete(`/api/tasks/blocks/${a}`)},b=Object.freeze(Object.defineProperty({__proto__:null,tasksApi:o},Symbol.toStringTag,{value:"Module"})),$=w("tasks",()=>{const a=i([]),s=i(!1),n=i(null);async function u(t){s.value=!0,n.value=null;try{const e=await o.list({date:t});a.value=e.data}catch(e){const c=e;n.value=c.message??"Failed to fetch tasks"}finally{s.value=!1}}async function d(t){s.value=!0,n.value=null;try{const e=await o.list(t?{project_id:t}:void 0);a.value=e.data}catch(e){const c=e;n.value=c.message??"Failed to fetch tasks"}finally{s.value=!1}}async function p(t){const e=await o.create(t);return a.value.push(e.data),e.data}async function k(t,e){const c=await o.update(t,e),r=a.value.findIndex(h=>h.id===t);return r!==-1&&(a.value[r]=c.data),c.data}async function f(t){await o.remove(t),a.value=a.value.filter(e=>e.id!==t)}async function v(t){const e=await o.complete(t),c=a.value.findIndex(r=>r.id===t);return c!==-1&&(a.value[c]=e.data),e.data}async function y(t,e){return(await o.createBlock(t,e)).data}async function m(t,e){return(await o.updateBlock(t,e)).data}async function g(t){await o.deleteBlock(t)}return{tasks:a,loading:s,error:n,fetchForDate:u,fetchAll:d,create:p,update:k,remove:f,complete:v,createBlock:y,updateBlock:m,deleteBlock:g}});export{b as t,$ as u};
|
||||
|
|
@ -14,8 +14,8 @@
|
|||
else { document.documentElement.classList.remove('dark'); }
|
||||
})();
|
||||
</script>
|
||||
<script type="module" crossorigin src="/cc-dashboard/static/assets/index-BP_aNEdP.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/cc-dashboard/static/assets/index-Bq2H3vqg.css">
|
||||
<script type="module" crossorigin src="/cc-dashboard/static/assets/index-DVV3ZbZ2.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/cc-dashboard/static/assets/index-CS_oOq1J.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ const isShort = computed(() => effectiveHeight.value < 40)
|
|||
>
|
||||
<!-- Content -->
|
||||
<div class="px-1.5 py-1 h-full flex flex-col text-white overflow-hidden">
|
||||
<p class="text-xs font-semibold leading-tight truncate">{{ block.display_name }}</p>
|
||||
<p class="text-xs font-semibold leading-tight truncate">{{ (block.kind === 'planned' && block.title) ? block.title : block.display_name }}</p>
|
||||
<p v-if="!isShort && block.job_number" class="text-xs opacity-75 truncate">
|
||||
{{ block.job_number }}
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ const pageTitle = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="h-screen flex overflow-hidden bg-background">
|
||||
<div class="h-screen flex overflow-hidden" style="background: linear-gradient(135deg, #f0f4fa 0%, #f8f9fd 50%, #fef9f5 100%)">
|
||||
<!-- Mobile backdrop -->
|
||||
<Transition
|
||||
enter-active-class="transition-opacity duration-200"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ interface NavItem {
|
|||
const navItems: NavItem[] = [
|
||||
{ name: 'Dashboard', path: '/', icon: 'grid' },
|
||||
{ name: 'Calendar', path: '/calendar', icon: 'calendar' },
|
||||
{ name: 'Tasks', path: '/board', icon: 'check-square', external: true },
|
||||
{ name: 'Tasks', path: '/board/', icon: 'check-square', external: true },
|
||||
{ name: 'Projects', path: '/projects', icon: 'folder' },
|
||||
{ name: 'Live Feed', path: '/live', icon: 'activity' },
|
||||
{ name: 'Reports', path: '/reports', icon: 'file-text' },
|
||||
|
|
@ -45,79 +45,79 @@ const userInitials = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<aside class="flex flex-col h-full bg-[hsl(222_44%_7%)] border-r border-border">
|
||||
<aside class="flex flex-col h-full bg-white border-r border-slate-200/80">
|
||||
<!-- Logo -->
|
||||
<div class="h-14 flex items-center px-4 border-b border-border shrink-0">
|
||||
<div class="h-16 flex items-center px-5 border-b border-slate-100 shrink-0">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="h-8 w-8 rounded-lg bg-primary/10 ring-1 ring-primary/25 flex items-center justify-center">
|
||||
<svg class="h-4 w-4 text-primary" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
<div class="h-9 w-9 rounded-xl bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center shadow-md shadow-orange-200">
|
||||
<svg class="h-4.5 w-4.5 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5"
|
||||
d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<p class="font-bold text-sm text-foreground leading-none tracking-tight">CC Dashboard</p>
|
||||
<p class="text-[10px] text-white/45 mt-0.5 tracking-wide">Oliver Agency</p>
|
||||
<p class="font-bold text-sm text-slate-800 leading-none tracking-tight">CC Dashboard</p>
|
||||
<p class="text-[10px] text-slate-400 mt-0.5 tracking-wide font-medium">Oliver Agency</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Navigation -->
|
||||
<nav class="flex-1 px-2 py-3 space-y-0.5 overflow-y-auto">
|
||||
<nav class="flex-1 px-3 py-4 space-y-0.5 overflow-y-auto">
|
||||
<component
|
||||
:is="item.external ? 'a' : RouterLink"
|
||||
v-for="item in visibleItems"
|
||||
:key="item.path"
|
||||
v-bind="item.external ? { href: item.path, target: '_blank', rel: 'noopener' } : { to: item.path }"
|
||||
:class="[
|
||||
'relative flex items-center gap-3 px-3 h-10 rounded-lg text-sm font-medium transition-all duration-150 group',
|
||||
'relative flex items-center gap-3 px-3 h-10 rounded-xl text-sm font-medium transition-all duration-200 group',
|
||||
isActive(item.path)
|
||||
? 'bg-primary/10 text-primary'
|
||||
: 'text-white/60 hover:bg-white/5 hover:text-white',
|
||||
? 'bg-orange-50 text-orange-600'
|
||||
: 'text-slate-500 hover:bg-slate-50 hover:text-slate-800',
|
||||
]"
|
||||
@click="emit('close')"
|
||||
>
|
||||
<!-- Active left indicator -->
|
||||
<!-- Active left indicator pill -->
|
||||
<span
|
||||
v-if="isActive(item.path)"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 bg-primary rounded-r-full"
|
||||
class="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-6 bg-orange-500 rounded-r-full"
|
||||
/>
|
||||
|
||||
<!-- Icons -->
|
||||
<svg
|
||||
v-if="item.icon === 'grid'"
|
||||
:class="['h-4 w-4 shrink-0 transition-colors', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']"
|
||||
:class="['h-4 w-4 shrink-0 transition-colors', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'calendar'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'calendar'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'check-square'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'check-square'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'folder'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'folder'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7a2 2 0 012-2h3.586a1 1 0 01.707.293l1.414 1.414A1 1 0 0011.414 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V7z" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'activity'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'activity'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'file-text'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'file-text'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'key'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'key'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'devops'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'devops'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'settings'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'settings'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
<svg v-else-if="item.icon === 'shield'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-primary' : 'text-white/40 group-hover:text-white/70']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-else-if="item.icon === 'shield'" :class="['h-4 w-4 shrink-0', isActive(item.path) ? 'text-orange-500' : 'text-slate-400 group-hover:text-slate-600']" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
|
||||
</svg>
|
||||
|
||||
|
|
@ -126,16 +126,16 @@ const userInitials = computed(() => {
|
|||
</nav>
|
||||
|
||||
<!-- User info at bottom -->
|
||||
<div class="p-3 border-t border-border shrink-0">
|
||||
<div class="flex items-center gap-3 px-2 py-2 rounded-lg">
|
||||
<div class="h-7 w-7 rounded-full bg-primary/15 ring-1 ring-primary/20 flex items-center justify-center text-[10px] font-bold text-primary shrink-0">
|
||||
<div class="p-4 border-t border-slate-100 shrink-0">
|
||||
<div class="flex items-center gap-3 px-2 py-2 rounded-xl bg-slate-50">
|
||||
<div class="h-8 w-8 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-[11px] font-bold text-white shrink-0 shadow-sm shadow-orange-200">
|
||||
{{ userInitials }}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-xs font-medium text-foreground truncate">{{ authStore.user?.username ?? authStore.user?.email }}</p>
|
||||
<p class="text-xs font-semibold text-slate-700 truncate">{{ authStore.user?.username ?? authStore.user?.email }}</p>
|
||||
<div class="flex items-center gap-1 mt-0.5">
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-[hsl(var(--success))]"></div>
|
||||
<span class="text-[10px] text-white/45">Online</span>
|
||||
<div class="h-1.5 w-1.5 rounded-full bg-emerald-400"></div>
|
||||
<span class="text-[10px] text-slate-400 font-medium">Online</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ function toggleDark() {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<header class="h-14 border-b border-border bg-card/95 backdrop-blur-sm flex items-center px-4 gap-3 shrink-0 sticky top-0 z-10">
|
||||
<header class="h-14 border-b border-slate-200/80 bg-white/80 backdrop-blur-xl flex items-center px-4 gap-3 shrink-0 sticky top-0 z-10 shadow-sm shadow-slate-100/60">
|
||||
<!-- Mobile hamburger -->
|
||||
<button
|
||||
class="lg:hidden flex h-9 w-9 items-center justify-center rounded-lg text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
|
||||
|
|
@ -70,7 +70,7 @@ function toggleDark() {
|
|||
|
||||
<!-- User section -->
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="h-7 w-7 rounded-full bg-primary/15 ring-1 ring-primary/25 flex items-center justify-center text-[10px] font-bold text-primary shrink-0">
|
||||
<div class="h-7 w-7 rounded-full bg-gradient-to-br from-orange-400 to-orange-600 flex items-center justify-center text-[10px] font-bold text-white shrink-0 shadow-sm shadow-orange-200">
|
||||
{{ (authStore.user?.username ?? authStore.user?.email ?? '?').slice(0, 2).toUpperCase() }}
|
||||
</div>
|
||||
<span class="hidden sm:block text-xs font-medium text-foreground max-w-[120px] truncate">
|
||||
|
|
|
|||
250
web/src/components/ui/DataTable.vue
Normal file
250
web/src/components/ui/DataTable.vue
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, reactive, watch } from 'vue'
|
||||
|
||||
export interface TableColumn {
|
||||
key: string
|
||||
title: string
|
||||
width?: number
|
||||
minWidth?: number
|
||||
sortable?: boolean
|
||||
filterable?: boolean
|
||||
resizable?: boolean
|
||||
align?: 'left' | 'center' | 'right'
|
||||
className?: string
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
columns: TableColumn[]
|
||||
rows: Record<string, unknown>[]
|
||||
rowKey?: string
|
||||
}>(), {
|
||||
rowKey: 'id',
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
rowClick: [row: Record<string, unknown>]
|
||||
}>()
|
||||
|
||||
const sortKey = ref<string | null>(null)
|
||||
const sortDir = ref<'asc' | 'desc'>('asc')
|
||||
const filters = reactive<Record<string, string>>({})
|
||||
const colWidths = reactive<Record<string, number>>({})
|
||||
const showFilters = ref(false)
|
||||
|
||||
props.columns.forEach((col) => {
|
||||
if (col.width) colWidths[col.key] = col.width
|
||||
})
|
||||
|
||||
function getWidth(col: TableColumn): string {
|
||||
const w = colWidths[col.key] ?? col.width
|
||||
return w ? `${w}px` : 'auto'
|
||||
}
|
||||
|
||||
function toggleSort(col: TableColumn) {
|
||||
if (!col.sortable) return
|
||||
if (sortKey.value === col.key) {
|
||||
sortDir.value = sortDir.value === 'asc' ? 'desc' : 'asc'
|
||||
} else {
|
||||
sortKey.value = col.key
|
||||
sortDir.value = 'asc'
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = computed(() => {
|
||||
let rows = [...props.rows]
|
||||
for (const [k, v] of Object.entries(filters)) {
|
||||
if (!v) continue
|
||||
rows = rows.filter((row) => {
|
||||
const val = String(row[k] ?? '').toLowerCase()
|
||||
return val.includes(v.toLowerCase())
|
||||
})
|
||||
}
|
||||
return rows
|
||||
})
|
||||
|
||||
const displayed = computed(() => {
|
||||
if (!sortKey.value) return filtered.value
|
||||
const k = sortKey.value
|
||||
return [...filtered.value].sort((a, b) => {
|
||||
const av = a[k]
|
||||
const bv = b[k]
|
||||
if (av == null) return 1
|
||||
if (bv == null) return -1
|
||||
const cmp = String(av) < String(bv) ? -1 : String(av) > String(bv) ? 1 : 0
|
||||
return sortDir.value === 'asc' ? cmp : -cmp
|
||||
})
|
||||
})
|
||||
|
||||
const hasActiveFilters = computed(() => Object.values(filters).some(Boolean))
|
||||
|
||||
let resizing: { key: string; startX: number; startW: number } | null = null
|
||||
|
||||
function startResize(col: TableColumn, e: MouseEvent) {
|
||||
if (!col.resizable) return
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const currentW = colWidths[col.key] ?? col.width ?? 100
|
||||
resizing = { key: col.key, startX: e.clientX, startW: currentW }
|
||||
|
||||
function onMove(e: MouseEvent) {
|
||||
if (!resizing) return
|
||||
const delta = e.clientX - resizing.startX
|
||||
colWidths[resizing.key] = Math.max(col.minWidth ?? 60, resizing.startW + delta)
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
resizing = null
|
||||
window.removeEventListener('mousemove', onMove)
|
||||
window.removeEventListener('mouseup', onUp)
|
||||
}
|
||||
|
||||
window.addEventListener('mousemove', onMove)
|
||||
window.addEventListener('mouseup', onUp)
|
||||
}
|
||||
|
||||
function clearFilters() {
|
||||
for (const key of Object.keys(filters)) {
|
||||
filters[key] = ''
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full">
|
||||
<!-- Toolbar -->
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="text-xs text-muted-foreground">{{ displayed.length }} items</span>
|
||||
<div class="flex-1" />
|
||||
<button
|
||||
v-if="columns.some(c => c.filterable)"
|
||||
:class="[
|
||||
'flex items-center gap-1.5 px-2.5 py-1 rounded-lg text-xs font-medium border transition-colors',
|
||||
(showFilters || hasActiveFilters)
|
||||
? 'bg-primary/10 border-primary/20 text-primary'
|
||||
: 'border-border text-muted-foreground hover:text-foreground hover:bg-muted/40',
|
||||
]"
|
||||
@click="showFilters = !showFilters"
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2a1 1 0 01-.293.707L13 13.414V19a1 1 0 01-.553.894l-4 2A1 1 0 017 21v-7.586L3.293 6.707A1 1 0 013 6V4z" />
|
||||
</svg>
|
||||
Filter
|
||||
<span v-if="hasActiveFilters" class="h-1.5 w-1.5 rounded-full bg-primary" />
|
||||
</button>
|
||||
<button
|
||||
v-if="hasActiveFilters"
|
||||
class="text-xs text-muted-foreground hover:text-foreground"
|
||||
@click="clearFilters"
|
||||
>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Table wrapper with horizontal scroll -->
|
||||
<div class="overflow-x-auto rounded-xl border border-border">
|
||||
<table class="w-full table-fixed border-collapse" style="min-width: 600px">
|
||||
<colgroup>
|
||||
<col
|
||||
v-for="col in columns"
|
||||
:key="col.key"
|
||||
:style="{ width: getWidth(col) }"
|
||||
/>
|
||||
</colgroup>
|
||||
|
||||
<!-- Header -->
|
||||
<thead class="bg-muted/30">
|
||||
<tr>
|
||||
<th
|
||||
v-for="col in columns"
|
||||
:key="col.key"
|
||||
class="relative px-3 py-2 text-left group select-none"
|
||||
:class="[col.sortable ? 'cursor-pointer hover:bg-muted/50' : '']"
|
||||
@click="toggleSort(col)"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-1"
|
||||
:class="{
|
||||
'justify-center': col.align === 'center',
|
||||
'justify-end': col.align === 'right',
|
||||
}"
|
||||
>
|
||||
<span class="text-xs font-semibold text-muted-foreground tracking-wide uppercase">
|
||||
{{ col.title }}
|
||||
</span>
|
||||
<!-- Sort indicator -->
|
||||
<svg
|
||||
v-if="col.sortable"
|
||||
class="h-3 w-3 shrink-0 transition-opacity"
|
||||
:class="sortKey === col.key ? 'opacity-100 text-primary' : 'opacity-0 group-hover:opacity-40'"
|
||||
fill="none" stroke="currentColor" viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
:d="sortKey === col.key && sortDir === 'desc'
|
||||
? 'M19 9l-7 7-7-7'
|
||||
: 'M5 15l7-7 7 7'"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<!-- Resize handle -->
|
||||
<div
|
||||
v-if="col.resizable"
|
||||
class="absolute right-0 top-0 bottom-0 w-1 cursor-col-resize opacity-0 group-hover:opacity-100 hover:opacity-100 hover:bg-primary/30 transition-opacity"
|
||||
@mousedown.stop="startResize(col, $event)"
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
|
||||
<!-- Filter row -->
|
||||
<tr v-if="showFilters">
|
||||
<th
|
||||
v-for="col in columns"
|
||||
:key="`filter-${col.key}`"
|
||||
class="px-2 pb-2 pt-0"
|
||||
>
|
||||
<input
|
||||
v-if="col.filterable"
|
||||
v-model="filters[col.key]"
|
||||
type="text"
|
||||
class="w-full px-2 py-1 text-xs rounded-lg border border-border bg-background text-foreground placeholder:text-muted-foreground/60 focus:outline-none focus:ring-1 focus:ring-primary focus:border-primary"
|
||||
:placeholder="`Filter…`"
|
||||
@click.stop
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<!-- Body -->
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(row, idx) in displayed"
|
||||
:key="String(row[rowKey] ?? idx)"
|
||||
class="border-t border-border/50 hover:bg-muted/20 transition-colors cursor-pointer"
|
||||
@click="emit('rowClick', row)"
|
||||
>
|
||||
<td
|
||||
v-for="col in columns"
|
||||
:key="col.key"
|
||||
class="px-3 py-2"
|
||||
:class="[
|
||||
col.className,
|
||||
col.align === 'center' ? 'text-center' : col.align === 'right' ? 'text-right' : 'text-left',
|
||||
]"
|
||||
>
|
||||
<slot :name="`cell-${col.key}`" :row="row" :value="row[col.key]">
|
||||
<span class="text-sm text-foreground truncate block">{{ row[col.key] ?? '—' }}</span>
|
||||
</slot>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<tr v-if="displayed.length === 0">
|
||||
<td :colspan="columns.length" class="text-center py-10 text-sm text-muted-foreground">
|
||||
No items found
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -1,5 +1,4 @@
|
|||
@import url('https://api.fontshare.com/v2/css?f[]=satoshi@700,500,400&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap');
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
|
|
@ -7,9 +6,9 @@
|
|||
|
||||
@layer base {
|
||||
:root {
|
||||
/* ── Light theme ─────────────────────────────────────────────── */
|
||||
--background: 0 0% 98%;
|
||||
--foreground: 222 47% 11%;
|
||||
/* ── Soft light theme — warm white + amber accent ──────────────── */
|
||||
--background: 220 33% 96%; /* #F0F4FA */
|
||||
--foreground: 222 47% 11%; /* #0F172A */
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 222 47% 11%;
|
||||
|
|
@ -17,61 +16,51 @@
|
|||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 222 47% 11%;
|
||||
|
||||
/* Brand cyan */
|
||||
--primary: 191 91% 37%;
|
||||
/* Amber/orange primary */
|
||||
--primary: 25 95% 53%; /* #F97316 */
|
||||
--primary-foreground: 0 0% 100%;
|
||||
|
||||
--secondary: 210 40% 94%;
|
||||
--secondary-foreground: 222 47% 11%;
|
||||
/* Sky secondary */
|
||||
--secondary: 200 85% 50%; /* #0EA5E9 */
|
||||
--secondary-foreground: 0 0% 100%;
|
||||
|
||||
--muted: 210 40% 94%;
|
||||
--muted-foreground: 215 16% 47%;
|
||||
--muted: 220 20% 93%;
|
||||
--muted-foreground: 215 20% 48%;
|
||||
|
||||
--accent: 191 91% 92%;
|
||||
--accent-foreground: 191 91% 25%;
|
||||
--accent: 25 100% 96%;
|
||||
--accent-foreground: 25 95% 40%;
|
||||
|
||||
--destructive: 0 84% 60%;
|
||||
--destructive-foreground: 0 0% 100%;
|
||||
|
||||
--border: 214 32% 88%;
|
||||
--input: 214 32% 88%;
|
||||
--ring: 191 91% 37%;
|
||||
--border: 220 20% 88%;
|
||||
--input: 220 20% 88%;
|
||||
--ring: 25 95% 53%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
--radius: 0.75rem;
|
||||
|
||||
--sidebar-background: 210 40% 96%;
|
||||
--sidebar-foreground: 222 47% 11%;
|
||||
--sidebar-primary: 191 91% 37%;
|
||||
--sidebar-primary-foreground: 0 0% 100%;
|
||||
--sidebar-accent: 191 91% 92%;
|
||||
--sidebar-accent-foreground: 191 91% 25%;
|
||||
--sidebar-border: 214 32% 88%;
|
||||
--sidebar-ring: 191 91% 37%;
|
||||
|
||||
/* Success / Warning */
|
||||
--success: 158 64% 40%;
|
||||
--success: 142 71% 45%;
|
||||
--warning: 38 92% 50%;
|
||||
}
|
||||
|
||||
/* Dark theme — operational dashboard dark navy / cyan */
|
||||
/* Dark theme */
|
||||
.dark {
|
||||
--background: 226 49% 8%; /* #0b1020 */
|
||||
--foreground: 220 40% 92%; /* #dce4f4 */
|
||||
--background: 226 49% 8%;
|
||||
--foreground: 220 40% 92%;
|
||||
|
||||
--card: 220 44% 10%; /* #0f1629 panel L1 */
|
||||
--card: 220 44% 10%;
|
||||
--card-foreground: 220 40% 92%;
|
||||
|
||||
--popover: 220 44% 12%; /* #111c34 panel L2 */
|
||||
--popover: 220 44% 12%;
|
||||
--popover-foreground: 220 40% 92%;
|
||||
|
||||
/* Cyan accent #57c7ff */
|
||||
--primary: 200 100% 67%;
|
||||
--primary: 25 95% 60%;
|
||||
--primary-foreground: 226 49% 8%;
|
||||
|
||||
--secondary: 220 30% 14%;
|
||||
--secondary-foreground: 220 20% 75%;
|
||||
--secondary: 200 85% 55%;
|
||||
--secondary-foreground: 226 49% 8%;
|
||||
|
||||
--muted: 220 30% 12%;
|
||||
--muted: 220 30% 14%;
|
||||
--muted-foreground: 220 12% 52%;
|
||||
|
||||
--accent: 220 30% 14%;
|
||||
|
|
@ -82,9 +71,8 @@
|
|||
|
||||
--border: 220 28% 17%;
|
||||
--input: 220 28% 17%;
|
||||
--ring: 200 100% 67%;
|
||||
--ring: 25 95% 60%;
|
||||
|
||||
/* Success / Warning */
|
||||
--success: 158 64% 52%;
|
||||
--warning: 38 92% 60%;
|
||||
}
|
||||
|
|
@ -97,40 +85,76 @@
|
|||
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-family: 'Satoshi', 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
||||
font-feature-settings: "rlig" 1, "calt" 1;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
/* Monospace for numeric values */
|
||||
.tabular-nums,
|
||||
[data-value],
|
||||
.kpi-value {
|
||||
font-family: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
|
||||
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
/* Thin scrollbar */
|
||||
::-webkit-scrollbar { width: 4px; height: 4px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: hsl(220 28% 20%);
|
||||
background: hsl(220 20% 80%);
|
||||
border-radius: 9999px;
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: hsl(220 28% 30%);
|
||||
background: hsl(220 20% 68%);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Utility: panel glow on hover ─────────────────────────────────── */
|
||||
@layer utilities {
|
||||
/* Glass card effect */
|
||||
.glass-card {
|
||||
background: rgba(255, 255, 255, 0.72);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.85);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 8px 32px rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.panel-glow {
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06), 0 8px 32px rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.dark .panel-glow {
|
||||
box-shadow: 0 0 0 1px hsl(var(--border)), 0 4px 24px -4px hsl(226 49% 4% / 0.6);
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
.panel-glow-hover:hover {
|
||||
box-shadow: 0 0 0 1px hsl(200 100% 67% / 0.18), 0 8px 32px -4px hsl(200 100% 67% / 0.08);
|
||||
box-shadow: 0 4px 24px rgba(249, 115, 22, 0.12), 0 1px 3px rgba(0,0,0,0.06);
|
||||
border-color: rgba(249, 115, 22, 0.25);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.accent-glow {
|
||||
box-shadow: 0 0 16px -2px hsl(200 100% 67% / 0.35);
|
||||
box-shadow: 0 0 20px -2px rgba(249, 115, 22, 0.4);
|
||||
}
|
||||
|
||||
/* Smooth fade-in animation for page content */
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; transform: translateY(6px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
/* Slide up for cards */
|
||||
.slide-up {
|
||||
animation: slideUp 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,8 @@ import CardTitle from '@/components/ui/CardTitle.vue'
|
|||
import CardContent from '@/components/ui/CardContent.vue'
|
||||
import Button from '@/components/ui/Button.vue'
|
||||
import Spinner from '@/components/ui/Spinner.vue'
|
||||
import DataTable from '@/components/ui/DataTable.vue'
|
||||
import type { TableColumn } from '@/components/ui/DataTable.vue'
|
||||
import DevopsConnectForm from '@/components/devops/DevopsConnectForm.vue'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
|
|
@ -26,10 +28,22 @@ onMounted(async () => {
|
|||
})
|
||||
|
||||
const filteredWorkItems = computed(() => {
|
||||
if (stateFilter.value === 'All') return devopsStore.workItems
|
||||
return devopsStore.workItems.filter((wi) => wi.state === stateFilter.value)
|
||||
const items = stateFilter.value === 'All'
|
||||
? devopsStore.workItems
|
||||
: devopsStore.workItems.filter((wi) => wi.state === stateFilter.value)
|
||||
return items as unknown as Record<string, unknown>[]
|
||||
})
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ key: 'ado_id', title: '#', width: 70, minWidth: 50, sortable: true, resizable: true },
|
||||
{ key: 'title', title: 'Title', minWidth: 120, sortable: true, filterable: true, resizable: true },
|
||||
{ key: 'team_project', title: 'Project', width: 140, minWidth: 80, sortable: true, filterable: true, resizable: true },
|
||||
{ key: 'priority', title: 'P', width: 60, minWidth: 50, sortable: true, align: 'center', resizable: true },
|
||||
{ key: 'created_date', title: 'Created', width: 110, minWidth: 80, sortable: true, resizable: true },
|
||||
{ key: 'state', title: 'State', width: 110, minWidth: 80, sortable: true, filterable: true, resizable: true },
|
||||
{ key: 'id', title: 'Actions', width: 90, minWidth: 80, align: 'right' },
|
||||
]
|
||||
|
||||
async function syncNow() {
|
||||
try {
|
||||
await devopsStore.sync()
|
||||
|
|
@ -40,7 +54,8 @@ async function syncNow() {
|
|||
}
|
||||
}
|
||||
|
||||
async function cloneToTasks(wiId: string) {
|
||||
async function cloneToTasks(wiId: string, e: Event) {
|
||||
e.stopPropagation()
|
||||
if (cloningId.value) return
|
||||
cloningId.value = wiId
|
||||
try {
|
||||
|
|
@ -56,55 +71,64 @@ async function cloneToTasks(wiId: string) {
|
|||
cloningId.value = null
|
||||
}
|
||||
}
|
||||
|
||||
function priorityClass(p: number | null | undefined) {
|
||||
const v = p ?? 3
|
||||
if (v <= 1) return 'text-red-500 font-bold'
|
||||
if (v <= 2) return 'text-amber-500 font-semibold'
|
||||
return 'text-slate-400'
|
||||
}
|
||||
|
||||
function stateClass(s: string) {
|
||||
if (s === 'Active' || s === 'Doing' || s === 'In Progress') return 'bg-blue-50 text-blue-600 border border-blue-100'
|
||||
if (s === 'Resolved' || s === 'Done' || s === 'Closed') return 'bg-emerald-50 text-emerald-600 border border-emerald-100'
|
||||
if (s === 'New') return 'bg-slate-50 text-slate-500 border border-slate-200'
|
||||
return 'bg-slate-50 text-slate-500 border border-slate-200'
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-6 space-y-6">
|
||||
<div class="p-6 space-y-5">
|
||||
<div class="flex items-center justify-between gap-4 flex-wrap">
|
||||
<h2 class="text-lg font-semibold text-foreground">Azure DevOps</h2>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button
|
||||
v-if="devopsStore.integration"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:loading="devopsStore.syncing"
|
||||
@click="syncNow"
|
||||
>
|
||||
Sync Now
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
v-if="devopsStore.integration"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
:loading="devopsStore.syncing"
|
||||
@click="syncNow"
|
||||
>
|
||||
Sync Now
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Connection status -->
|
||||
<Card>
|
||||
<CardContent class="pt-4">
|
||||
<div v-if="devopsStore.loading && !devopsStore.integration" class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Spinner size="sm" />
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
<div v-else-if="devopsStore.integration" class="flex items-center gap-3">
|
||||
<div class="h-2 w-2 rounded-full bg-[hsl(var(--success))]" />
|
||||
<span class="text-sm text-foreground">
|
||||
Connected to
|
||||
<strong>{{ devopsStore.integration.organization }}</strong>
|
||||
/
|
||||
<strong>{{ devopsStore.integration.project }}</strong>
|
||||
</span>
|
||||
<span v-if="devopsStore.integration.last_synced_at" class="text-xs text-muted-foreground ml-2">
|
||||
Last synced: {{ new Date(devopsStore.integration.last_synced_at).toLocaleString() }}
|
||||
</span>
|
||||
</div>
|
||||
<div v-else class="flex items-center gap-3">
|
||||
<div class="h-2 w-2 rounded-full bg-muted-foreground" />
|
||||
<span class="text-sm text-muted-foreground">Not connected</span>
|
||||
</div>
|
||||
<p v-if="devopsStore.integration?.last_sync_error" class="text-xs text-destructive mt-2">
|
||||
Error: {{ devopsStore.integration.last_sync_error }}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div class="glass-card rounded-xl px-4 py-3 flex items-center gap-3 flex-wrap">
|
||||
<template v-if="devopsStore.loading && !devopsStore.integration">
|
||||
<Spinner size="sm" />
|
||||
<span class="text-sm text-muted-foreground">Loading…</span>
|
||||
</template>
|
||||
<template v-else-if="devopsStore.integration">
|
||||
<div class="h-2 w-2 rounded-full bg-emerald-400 shadow-sm shadow-emerald-200" />
|
||||
<span class="text-sm text-slate-700">
|
||||
Connected to <strong class="text-slate-800">{{ devopsStore.integration.organization }}</strong>
|
||||
<span class="text-slate-400 mx-1">·</span>
|
||||
<span class="text-slate-500 text-xs">all assigned work items</span>
|
||||
</span>
|
||||
<span v-if="devopsStore.integration.last_synced_at" class="text-xs text-slate-400 ml-auto">
|
||||
Last synced: {{ new Date(devopsStore.integration.last_synced_at).toLocaleString() }}
|
||||
</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="h-2 w-2 rounded-full bg-slate-300" />
|
||||
<span class="text-sm text-muted-foreground">Not connected</span>
|
||||
</template>
|
||||
<p v-if="devopsStore.integration?.last_sync_error" class="w-full text-xs text-destructive">
|
||||
Error: {{ devopsStore.integration.last_sync_error }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Connect form (shown when not connected) -->
|
||||
<!-- Connect form -->
|
||||
<Card v-if="!devopsStore.integration && !devopsStore.loading">
|
||||
<CardHeader>
|
||||
<CardTitle class="text-sm">Connect Azure DevOps</CardTitle>
|
||||
|
|
@ -114,89 +138,94 @@ async function cloneToTasks(wiId: string) {
|
|||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<!-- Work items list -->
|
||||
<Card v-if="devopsStore.integration">
|
||||
<CardHeader class="pb-2">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<CardTitle class="text-sm">Work Items</CardTitle>
|
||||
<!-- State filter -->
|
||||
<div class="flex items-center rounded-lg border border-border overflow-hidden bg-muted/30">
|
||||
<button
|
||||
v-for="state in (['All', 'Active', 'Resolved', 'Closed'] as const)"
|
||||
:key="state"
|
||||
:class="[
|
||||
'px-3 py-1 text-xs font-medium transition-colors',
|
||||
stateFilter === state
|
||||
? 'bg-primary text-primary-foreground'
|
||||
: 'text-muted-foreground hover:text-foreground hover:bg-muted/50',
|
||||
]"
|
||||
@click="stateFilter = state"
|
||||
>
|
||||
{{ state }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div v-if="devopsStore.loading" class="flex items-center justify-center py-8">
|
||||
<Spinner size="md" class="text-primary" />
|
||||
</div>
|
||||
<div v-else-if="filteredWorkItems.length === 0" class="text-center py-8 text-sm text-muted-foreground">
|
||||
No work items found
|
||||
</div>
|
||||
<div v-else class="space-y-0">
|
||||
<!-- Header row -->
|
||||
<div class="flex items-center gap-3 px-3 py-1.5 text-xs text-muted-foreground border-b border-border mb-1">
|
||||
<span class="w-10 shrink-0">#</span>
|
||||
<span class="flex-1">Title</span>
|
||||
<span class="w-28 shrink-0 hidden md:block">Project</span>
|
||||
<span class="w-8 shrink-0 text-center hidden sm:block">P</span>
|
||||
<span class="w-20 shrink-0 hidden lg:block">Created</span>
|
||||
<span class="w-20 shrink-0">State</span>
|
||||
<span class="w-14 shrink-0 text-right">Link</span>
|
||||
</div>
|
||||
<div
|
||||
v-for="wi in filteredWorkItems"
|
||||
:key="wi.id"
|
||||
class="flex items-center gap-3 px-3 py-2 rounded-lg hover:bg-muted/30 transition-colors"
|
||||
<!-- Work items table -->
|
||||
<div v-if="devopsStore.integration" class="space-y-3">
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<h3 class="text-sm font-semibold text-slate-700">Work Items</h3>
|
||||
<!-- State filter tabs -->
|
||||
<div class="flex items-center rounded-xl border border-slate-200 overflow-hidden bg-white shadow-sm">
|
||||
<button
|
||||
v-for="state in (['All', 'Active', 'Resolved', 'Closed'] as const)"
|
||||
:key="state"
|
||||
:class="[
|
||||
'px-3 py-1.5 text-xs font-medium transition-colors',
|
||||
stateFilter === state
|
||||
? 'bg-orange-500 text-white'
|
||||
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-50',
|
||||
]"
|
||||
@click="stateFilter = state"
|
||||
>
|
||||
<span class="text-xs font-mono text-muted-foreground w-10 shrink-0">#{{ wi.ado_id }}</span>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm text-foreground truncate">{{ wi.title }}</p>
|
||||
<p class="text-xs text-muted-foreground">{{ wi.type }}</p>
|
||||
{{ state }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="devopsStore.loading" class="flex items-center justify-center py-12">
|
||||
<Spinner size="md" class="text-primary" />
|
||||
</div>
|
||||
<div v-else class="bg-white rounded-xl shadow-sm border border-slate-200/80 overflow-hidden">
|
||||
<DataTable
|
||||
:columns="columns"
|
||||
:rows="filteredWorkItems"
|
||||
row-key="id"
|
||||
>
|
||||
<!-- # column -->
|
||||
<template #cell-ado_id="{ value }">
|
||||
<span class="text-xs font-mono text-slate-400">#{{ value }}</span>
|
||||
</template>
|
||||
|
||||
<!-- Title column -->
|
||||
<template #cell-title="{ row }">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-slate-800 truncate font-medium">{{ row.title }}</p>
|
||||
<p class="text-xs text-slate-400 truncate">{{ row.type }}</p>
|
||||
</div>
|
||||
<span class="text-xs text-muted-foreground w-28 shrink-0 truncate hidden md:block" :title="wi.team_project">
|
||||
{{ wi.team_project || '—' }}
|
||||
</template>
|
||||
|
||||
<!-- Project column -->
|
||||
<template #cell-team_project="{ value }">
|
||||
<span class="text-xs text-slate-500 truncate block" :title="String(value ?? '')">
|
||||
{{ value || '—' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- Priority column -->
|
||||
<template #cell-priority="{ value }">
|
||||
<span
|
||||
class="text-xs font-medium w-8 shrink-0 text-center hidden sm:block"
|
||||
:class="(wi.priority ?? 3) <= 1 ? 'text-red-400' : (wi.priority ?? 3) <= 2 ? 'text-amber-400' : 'text-muted-foreground'"
|
||||
:title="`Priority ${wi.priority}`"
|
||||
class="text-xs font-semibold"
|
||||
:class="priorityClass(value as number)"
|
||||
>
|
||||
P{{ wi.priority ?? 3 }}
|
||||
P{{ value ?? 3 }}
|
||||
</span>
|
||||
<span class="text-xs text-muted-foreground w-20 shrink-0 hidden lg:block">
|
||||
{{ wi.created_date ? new Date(wi.created_date).toLocaleDateString() : '—' }}
|
||||
</template>
|
||||
|
||||
<!-- Created column -->
|
||||
<template #cell-created_date="{ value }">
|
||||
<span class="text-xs text-slate-400">
|
||||
{{ value ? new Date(String(value)).toLocaleDateString() : '—' }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<!-- State column -->
|
||||
<template #cell-state="{ value }">
|
||||
<span
|
||||
:class="[
|
||||
'text-xs px-2 py-0.5 rounded-full shrink-0 w-20 text-center',
|
||||
wi.state === 'Active' ? 'bg-blue-500/10 text-blue-400' :
|
||||
wi.state === 'Resolved' ? 'bg-green-500/10 text-green-400' :
|
||||
wi.state === 'Closed' ? 'bg-muted text-muted-foreground' :
|
||||
'bg-muted text-muted-foreground',
|
||||
]"
|
||||
class="text-xs px-2 py-0.5 rounded-full font-medium"
|
||||
:class="stateClass(String(value ?? ''))"
|
||||
>
|
||||
{{ wi.state }}
|
||||
{{ value }}
|
||||
</span>
|
||||
<div class="flex items-center gap-1 shrink-0 w-14 justify-end">
|
||||
</template>
|
||||
|
||||
<!-- Actions column -->
|
||||
<template #cell-id="{ row }">
|
||||
<div class="flex items-center gap-1 justify-end">
|
||||
<button
|
||||
class="p-1 rounded text-muted-foreground hover:text-primary transition-colors"
|
||||
:class="{ 'opacity-50': cloningId === wi.id }"
|
||||
class="p-1.5 rounded-lg text-slate-400 hover:text-orange-500 hover:bg-orange-50 transition-colors"
|
||||
:class="{ 'opacity-50': cloningId === String(row.id) }"
|
||||
title="Clone to Tasks"
|
||||
@click.stop="cloneToTasks(wi.id)"
|
||||
@click="cloneToTasks(String(row.id), $event)"
|
||||
>
|
||||
<svg v-if="cloningId !== wi.id" class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<svg v-if="cloningId !== String(row.id)" class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
<svg v-else class="h-3.5 w-3.5 animate-spin" fill="none" viewBox="0 0 24 24">
|
||||
|
|
@ -205,15 +234,22 @@ async function cloneToTasks(wiId: string) {
|
|||
</svg>
|
||||
</button>
|
||||
<a
|
||||
v-if="wi.url"
|
||||
:href="wi.url"
|
||||
v-if="row.url"
|
||||
:href="String(row.url)"
|
||||
target="_blank"
|
||||
class="text-xs text-primary hover:underline"
|
||||
>→</a>
|
||||
rel="noopener"
|
||||
class="p-1.5 rounded-lg text-slate-400 hover:text-blue-500 hover:bg-blue-50 transition-colors"
|
||||
title="Open in Azure DevOps"
|
||||
@click.stop
|
||||
>
|
||||
<svg class="h-3.5 w-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</template>
|
||||
</DataTable>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ import { toast } from 'vue-sonner'
|
|||
import { formatDate, isoDateStr } from '@/lib/utils'
|
||||
import { marked } from 'marked'
|
||||
import type { AiReport } from '@/types'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const reports = ref<AiReport[]>([])
|
||||
const loading = ref(false)
|
||||
|
|
@ -52,6 +55,24 @@ function toggleExpand(id: string) {
|
|||
function renderMarkdown(md: string): string {
|
||||
return marked(md) as string
|
||||
}
|
||||
|
||||
async function downloadReport(reportId: string, fmt: 'md' | 'html') {
|
||||
try {
|
||||
const res = await fetch(`/cc-dashboard/api/reports/${reportId}/export?format=${fmt}`, {
|
||||
headers: { Authorization: `Bearer ${authStore.token}` },
|
||||
})
|
||||
if (!res.ok) { toast.error('Export failed'); return }
|
||||
const blob = await res.blob()
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `report.${fmt}`
|
||||
a.click()
|
||||
URL.revokeObjectURL(url)
|
||||
} catch {
|
||||
toast.error('Export failed')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -132,16 +153,14 @@ function renderMarkdown(md: string): string {
|
|||
<!-- Expanded content -->
|
||||
<div v-if="expandedId === report.id" class="mt-4 pt-4 border-t border-border">
|
||||
<div class="flex gap-2 mb-3">
|
||||
<a
|
||||
:href="`/cc-dashboard/api/reports/${report.id}/export?format=md`"
|
||||
download
|
||||
class="text-xs px-2 py-1 rounded border border-border text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors"
|
||||
>↓ Markdown</a>
|
||||
<a
|
||||
:href="`/cc-dashboard/api/reports/${report.id}/export?format=html`"
|
||||
download
|
||||
class="text-xs px-2 py-1 rounded border border-border text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors"
|
||||
>↓ HTML</a>
|
||||
<button
|
||||
class="text-xs px-2.5 py-1 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors"
|
||||
@click="downloadReport(report.id, 'md')"
|
||||
>↓ Markdown</button>
|
||||
<button
|
||||
class="text-xs px-2.5 py-1 rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-foreground/30 transition-colors"
|
||||
@click="downloadReport(report.id, 'html')"
|
||||
>↓ HTML</button>
|
||||
</div>
|
||||
<div
|
||||
class="prose prose-sm prose-invert max-w-none text-sm text-foreground break-words"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue