This commit is contained in:
2026-09-30 09:53:30 +03:30
parent 4d27da98fc
commit ebfbeb7a88
64 changed files with 1275 additions and 626 deletions
@@ -1 +1 @@
import{$ as e,g as t,k as n,l as r,st as i}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a}from"./http-BjLkjdQu.js";import{B as o}from"./index-pv0Ktkd1.js";import{t as s}from"./BaseSwitch-pJwnFBVo.js";function c(e=480){return new Promise(t=>{window.setTimeout(t,e)})}function l(t){let n=e(null),r=e(null),i=a();async function o(e,a){if(n.value)return a;let o=!a;if(!o&&t.confirmOff&&!await t.confirmOff())return a;n.value=e,r.value=null;let s=a;try{return await t.onPersist(o),i.success(t.successMessage?.(o)??(o?`وضعیت فعال شد.`:`وضعیت غیرفعال شد.`)),o}catch{return r.value=e,i.error(t.errorMessage??`به‌روزرسانی وضعیت انجام نشد.`),s}finally{n.value=null}}function s(e){return n.value===e}function c(e){return r.value===e}return{toggle:o,isLoading:s,hasError:c}}var u=t({__name:`ActiveSwitchCell`,props:{id:{},modelValue:{type:Boolean},entityName:{},entityType:{},persist:{type:Function}},emits:[`update:modelValue`],setup(e,{emit:t}){let a=e,u=t,{toggle:d,isLoading:f,hasError:p}=l({onPersist:async e=>{a.persist?await a.persist(a.id,e):await c()},successMessage:e=>e?`${a.entityType??`مورد`} «${a.entityName}» فعال شد.`:`${a.entityType??`مورد`} «${a.entityName}» غیرفعال شد.`});async function m(e){if(e===a.modelValue)return;let t=a.entityType??`مورد`;if(!await o({title:e?`فعال‌سازی ${t}`:`غیرفعال‌سازی ${t}`,message:e?`آیا از فعال کردن «${a.entityName}» مطمئن هستید؟`:`آیا از غیرفعال کردن «${a.entityName}» مطمئن هستید؟`,tone:e?`primary`:`danger`,confirmLabel:e?`فعال کردن`:`غیرفعال کردن`,cancelLabel:`انصراف`}))return;let n=await d(a.id,a.modelValue);u(`update:modelValue`,n)}return(t,a)=>(n(),r(s,{"model-value":e.modelValue,loading:i(f)(e.id),error:i(p)(e.id),"onUpdate:modelValue":m},null,8,[`model-value`,`loading`,`error`]))}});export{u as t}; import{$ as e,g as t,k as n,l as r,st as i}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a}from"./http-BjLkjdQu.js";import{V as o}from"./index-BzcByePp.js";import{t as s}from"./BaseSwitch-pJwnFBVo.js";function c(e=480){return new Promise(t=>{window.setTimeout(t,e)})}function l(t){let n=e(null),r=e(null),i=a();async function o(e,a){if(n.value)return a;let o=!a;if(!o&&t.confirmOff&&!await t.confirmOff())return a;n.value=e,r.value=null;let s=a;try{return await t.onPersist(o),i.success(t.successMessage?.(o)??(o?`وضعیت فعال شد.`:`وضعیت غیرفعال شد.`)),o}catch{return r.value=e,i.error(t.errorMessage??`به‌روزرسانی وضعیت انجام نشد.`),s}finally{n.value=null}}function s(e){return n.value===e}function c(e){return r.value===e}return{toggle:o,isLoading:s,hasError:c}}var u=t({__name:`ActiveSwitchCell`,props:{id:{},modelValue:{type:Boolean},entityName:{},entityType:{},persist:{type:Function}},emits:[`update:modelValue`],setup(e,{emit:t}){let a=e,u=t,{toggle:d,isLoading:f,hasError:p}=l({onPersist:async e=>{a.persist?await a.persist(a.id,e):await c()},successMessage:e=>e?`${a.entityType??`مورد`} «${a.entityName}» فعال شد.`:`${a.entityType??`مورد`} «${a.entityName}» غیرفعال شد.`});async function m(e){if(e===a.modelValue)return;let t=a.entityType??`مورد`;if(!await o({title:e?`فعال‌سازی ${t}`:`غیرفعال‌سازی ${t}`,message:e?`آیا از فعال کردن «${a.entityName}» مطمئن هستید؟`:`آیا از غیرفعال کردن «${a.entityName}» مطمئن هستید؟`,tone:e?`primary`:`danger`,confirmLabel:e?`فعال کردن`:`غیرفعال کردن`,cancelLabel:`انصراف`}))return;let n=await d(a.id,a.modelValue);u(`update:modelValue`,n)}return(t,a)=>(n(),r(s,{"model-value":e.modelValue,loading:i(f)(e.id),error:i(p)(e.id),"onUpdate:modelValue":m},null,8,[`model-value`,`loading`,`error`]))}});export{u as t};
@@ -1 +1 @@
import{Dt as e,d as t,g as n,k as r,s as i,t as a}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{c as o,r as s}from"./index-pv0Ktkd1.js";var c=[`id`,`value`,`placeholder`,`disabled`],l=a(n({__name:`AdminCurrencyInput`,props:{modelValue:{default:null},placeholder:{default:``},disabled:{type:Boolean,default:!1},id:{},inputClass:{default:`admin-input`},allowEmpty:{type:Boolean,default:!0}},emits:[`update:modelValue`],setup(n,{emit:a}){let l=n,u=a,d=i(()=>l.modelValue==null||!Number.isFinite(l.modelValue)?``:o(l.modelValue));function f(e){let t=e.target,n=s(t.value);if(!n){u(`update:modelValue`,l.allowEmpty?null:0),t.value=``;return}let r=Number(n);u(`update:modelValue`,r),t.value=o(r)}return(i,a)=>(r(),t(`input`,{id:n.id,type:`text`,inputmode:`numeric`,dir:`ltr`,class:e([`admin-currency-input`,n.inputClass]),value:d.value,placeholder:n.placeholder,disabled:n.disabled,onInput:f},null,42,c))}}),[[`__scopeId`,`data-v-56435fc8`]]);export{l as t}; import{Dt as e,d as t,g as n,k as r,s as i,t as a}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{c as o,r as s}from"./index-BzcByePp.js";var c=[`id`,`value`,`placeholder`,`disabled`],l=a(n({__name:`AdminCurrencyInput`,props:{modelValue:{default:null},placeholder:{default:``},disabled:{type:Boolean,default:!1},id:{},inputClass:{default:`admin-input`},allowEmpty:{type:Boolean,default:!0}},emits:[`update:modelValue`],setup(n,{emit:a}){let l=n,u=a,d=i(()=>l.modelValue==null||!Number.isFinite(l.modelValue)?``:o(l.modelValue));function f(e){let t=e.target,n=s(t.value);if(!n){u(`update:modelValue`,l.allowEmpty?null:0),t.value=``;return}let r=Number(n);u(`update:modelValue`,r),t.value=o(r)}return(i,a)=>(r(),t(`input`,{id:n.id,type:`text`,inputmode:`numeric`,dir:`ltr`,class:e([`admin-currency-input`,n.inputClass]),value:d.value,placeholder:n.placeholder,disabled:n.disabled,onInput:f},null,42,c))}}),[[`__scopeId`,`data-v-56435fc8`]]);export{l as t};
@@ -1 +1 @@
import{R as e,S as t,a as n,c as r,d as i,g as a,k as o,l as s,t as c,u as l}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{m as u}from"./http-BjLkjdQu.js";var d=[`src`,`alt`],f=c(a({__name:`AdminImageLightbox`,props:t({src:{},alt:{}},{modelValue:{type:Boolean,required:!0},modelModifiers:{}}),emits:[`update:modelValue`],setup(t){let a=e(t,`modelValue`);return(e,c)=>(o(),s(n,{to:`body`},[a.value?(o(),i(`div`,{key:0,class:`admin-image-lightbox`,onClick:c[1]||=u(e=>a.value=!1,[`self`])},[r(`button`,{type:`button`,class:`admin-image-lightbox__close`,onClick:c[0]||=e=>a.value=!1},` بستن `),r(`img`,{src:t.src,alt:t.alt??`پیش‌نمایش تصویر`},null,8,d)])):l(``,!0)]))}}),[[`__scopeId`,`data-v-65676449`]]);export{f as t}; import{R as e,S as t,a as n,c as r,d as i,g as a,k as o,l as s,t as c,u as l}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{m as u}from"./http-BjLkjdQu.js";var d=[`src`,`alt`],f=c(a({__name:`AdminImageLightbox`,props:t({src:{},alt:{}},{modelValue:{type:Boolean,required:!0},modelModifiers:{}}),emits:[`update:modelValue`],setup(t){let a=e(t,`modelValue`);return(e,c)=>(o(),s(n,{to:`body`},[a.value?(o(),i(`div`,{key:0,class:`admin-image-lightbox`,onClick:c[1]||=u(e=>a.value=!1,[`self`])},[r(`button`,{type:`button`,class:`admin-image-lightbox__close`,onClick:c[0]||=e=>a.value=!1},` بستن `),r(`img`,{src:t.src,alt:t.alt??`پیش‌نمایش تصویر`},null,8,d)])):l(``,!0)]))}}),[[`__scopeId`,`data-v-d52b23fb`]]);export{f as t};
@@ -1 +1 @@
.admin-image-lightbox[data-v-65676449]{z-index:300000;background:#000000d6;place-items:center;padding:20px;display:grid;position:fixed;inset:0}.admin-image-lightbox img[data-v-65676449]{object-fit:contain;border-radius:8px;max-width:min(900px,100%);max-height:88vh}.admin-image-lightbox__close[data-v-65676449]{top:14px;cursor:pointer;background:var(--color-surface);color:var(--color-text);font:inherit;border:0;border-radius:8px;padding:8px 12px;font-size:.875rem;font-weight:600;position:absolute;inset-inline-end:14px} .admin-image-lightbox[data-v-d52b23fb]{z-index:300000;background:#000000d6;place-items:center;padding:20px;display:grid;position:fixed;inset:0}.admin-image-lightbox img[data-v-d52b23fb]{object-fit:contain;border-radius:8px;max-width:min(900px,100%);max-height:88vh}.admin-image-lightbox__close[data-v-d52b23fb]{top:14px;cursor:pointer;background:var(--color-surface);color:var(--color-text);font:inherit;border:0;border-radius:8px;padding:8px 12px;font-size:.875rem;font-weight:600;position:absolute;inset-inline-end:14px}
-1
View File
@@ -1 +0,0 @@
.image-picker__label[data-v-05db5be7]{margin:0 0 8px;font-size:.8125rem;font-weight:600}.image-picker__row[data-v-05db5be7]{align-items:center;gap:12px;display:flex}.image-picker__preview[data-v-05db5be7]{border:1px solid var(--color-border);background:#e8eceb 50%/cover no-repeat;border-radius:10px;flex-shrink:0;width:88px;height:72px}.image-picker__preview--empty[data-v-05db5be7]{background:repeating-linear-gradient(45deg,#eef1f0,#eef1f0 8px,#e4e8e6 8px 16px)}.image-picker__actions[data-v-05db5be7]{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.image-picker__file[data-v-05db5be7]{display:none}.image-picker__error[data-v-05db5be7]{width:100%;color:var(--color-danger);margin:0;font-size:.75rem}.image-picker__ok[data-v-05db5be7]{width:100%;color:var(--color-success-dark);margin:0;font-size:.75rem;font-weight:600}
@@ -1 +1 @@
import{$ as e,At as t,Dt as n,c as r,d as i,g as a,k as o,kt as s,st as c,t as l,u}from"./_plugin-vue_export-helper-QrIvvHC8.js";var d=960,f=.82;function p(e){let t=e.trim();return t?`url("${t.replace(/"/g,`\\"`)}")`:``}async function m(e){if(!e.type.startsWith(`image/`))throw Error(`فقط فایل تصویری مجاز است.`);if(e.size>8388608)throw Error(`حجم تصویر نباید بیشتر از ۸ مگابایت باشد.`);return g(await h(e))}function h(e){return new Promise((t,n)=>{let r=new FileReader;r.onload=()=>t(String(r.result??``)),r.onerror=()=>n(Error(`خواندن تصویر ناموفق بود.`)),r.readAsDataURL(e)})}function g(e){return new Promise((t,n)=>{let r=new Image;r.onload=()=>{let e=Math.min(1,d/Math.max(r.width,r.height)),i=Math.max(1,Math.round(r.width*e)),a=Math.max(1,Math.round(r.height*e)),o=document.createElement(`canvas`);o.width=i,o.height=a;let s=o.getContext(`2d`);if(!s){n(Error(`پردازش تصویر ناموفق بود.`));return}s.drawImage(r,0,0,i,a),t(o.toDataURL(`image/jpeg`,f))},r.onerror=()=>n(Error(`تصویر نامعتبر است.`)),r.src=e})}var _={class:`image-picker`},v={key:0,class:`image-picker__label`},y={class:`image-picker__row`},b={class:`image-picker__actions`},x=[`disabled`],S=[`disabled`],C={key:1,class:`image-picker__error`},w={key:2,class:`image-picker__ok`},T=l(a({__name:`AdminImagePicker`,props:{modelValue:{},label:{}},emits:[`update:modelValue`],setup(a,{emit:l}){let d=a,f=l,h=e(null),g=e(!1),T=e(``),E=e(!1);function D(){h.value?.click()}async function O(e){let t=e.target,n=t.files?.[0];if(t.value=``,n){g.value=!0,T.value=``,E.value=!1;try{f(`update:modelValue`,await m(n)),E.value=!0}catch(e){T.value=e instanceof Error?e.message:`بارگذاری تصویر ناموفق بود.`}finally{g.value=!1}}}function k(){f(`update:modelValue`,null)}return(e,l)=>(o(),i(`div`,_,[a.label?(o(),i(`p`,v,t(a.label),1)):u(``,!0),r(`div`,y,[r(`div`,{class:n([`image-picker__preview`,{"image-picker__preview--empty":!d.modelValue}]),style:s(d.modelValue?{backgroundImage:c(p)(d.modelValue)}:void 0)},null,6),r(`div`,b,[r(`button`,{type:`button`,class:`admin-btn admin-btn--outline admin-btn--sm`,disabled:g.value,onClick:D},t(d.modelValue?`تغییر تصویر`:`افزودن تصویر`),9,x),d.modelValue?(o(),i(`button`,{key:0,type:`button`,class:`admin-btn admin-btn--outline admin-btn--sm`,disabled:g.value,onClick:k},` حذف `,8,S)):u(``,!0),T.value?(o(),i(`p`,C,t(T.value),1)):E.value?(o(),i(`p`,w,`تصویر انتخاب شد — برای ثبت نهایی «ذخیره» را بزنید.`)):u(``,!0)])]),r(`input`,{ref_key:`inputRef`,ref:h,type:`file`,accept:`image/*`,class:`image-picker__file`,onChange:O},null,544)]))}}),[[`__scopeId`,`data-v-05db5be7`]]);export{m as n,p as r,T as t}; import{$ as e,At as t,Dt as n,c as r,d as i,g as a,k as o,kt as s,st as c,t as l,u}from"./_plugin-vue_export-helper-QrIvvHC8.js";var d=960,f=.82;function p(e){let t=e.trim();return t?`url("${t.replace(/"/g,`\\"`)}")`:``}async function m(e){if(!e.type.startsWith(`image/`))throw Error(`فقط فایل تصویری مجاز است.`);if(e.size>8388608)throw Error(`حجم تصویر نباید بیشتر از ۸ مگابایت باشد.`);return g(await h(e))}function h(e){return new Promise((t,n)=>{let r=new FileReader;r.onload=()=>t(String(r.result??``)),r.onerror=()=>n(Error(`خواندن تصویر ناموفق بود.`)),r.readAsDataURL(e)})}function g(e){return new Promise((t,n)=>{let r=new Image;r.onload=()=>{let e=Math.min(1,d/Math.max(r.width,r.height)),i=Math.max(1,Math.round(r.width*e)),a=Math.max(1,Math.round(r.height*e)),o=document.createElement(`canvas`);o.width=i,o.height=a;let s=o.getContext(`2d`);if(!s){n(Error(`پردازش تصویر ناموفق بود.`));return}s.drawImage(r,0,0,i,a),t(o.toDataURL(`image/jpeg`,f))},r.onerror=()=>n(Error(`تصویر نامعتبر است.`)),r.src=e})}var _={class:`image-picker`},v={key:0,class:`image-picker__label`},y={class:`image-picker__row`},b={class:`image-picker__actions`},x=[`disabled`],S=[`disabled`],C={key:1,class:`image-picker__error`},w={key:2,class:`image-picker__ok`},T=l(a({__name:`AdminImagePicker`,props:{modelValue:{},label:{}},emits:[`update:modelValue`],setup(a,{emit:l}){let d=a,f=l,h=e(null),g=e(!1),T=e(``),E=e(!1);function D(){h.value?.click()}async function O(e){let t=e.target,n=t.files?.[0];if(t.value=``,n){g.value=!0,T.value=``,E.value=!1;try{f(`update:modelValue`,await m(n)),E.value=!0}catch(e){T.value=e instanceof Error?e.message:`بارگذاری تصویر ناموفق بود.`}finally{g.value=!1}}}function k(){f(`update:modelValue`,null)}return(e,l)=>(o(),i(`div`,_,[a.label?(o(),i(`p`,v,t(a.label),1)):u(``,!0),r(`div`,y,[r(`div`,{class:n([`image-picker__preview`,{"image-picker__preview--empty":!d.modelValue}]),style:s(d.modelValue?{backgroundImage:c(p)(d.modelValue)}:void 0)},null,6),r(`div`,b,[r(`button`,{type:`button`,class:`admin-btn admin-btn--outline admin-btn--sm`,disabled:g.value,onClick:D},t(d.modelValue?`تغییر تصویر`:`افزودن تصویر`),9,x),d.modelValue?(o(),i(`button`,{key:0,type:`button`,class:`admin-btn admin-btn--outline admin-btn--sm`,disabled:g.value,onClick:k},` حذف `,8,S)):u(``,!0),T.value?(o(),i(`p`,C,t(T.value),1)):E.value?(o(),i(`p`,w,`تصویر انتخاب شد — برای ثبت نهایی «ثبت» را بزنید.`)):u(``,!0)])]),r(`input`,{ref_key:`inputRef`,ref:h,type:`file`,accept:`image/*`,class:`image-picker__file`,onChange:O},null,544)]))}}),[[`__scopeId`,`data-v-ab0add26`]]);export{p as n,T as t};
+1
View File
@@ -0,0 +1 @@
.image-picker__label[data-v-ab0add26]{margin:0 0 8px;font-size:.8125rem;font-weight:600}.image-picker__row[data-v-ab0add26]{align-items:center;gap:12px;display:flex}.image-picker__preview[data-v-ab0add26]{border:1px solid var(--color-border);background:#e8eceb 50%/cover no-repeat;border-radius:10px;flex-shrink:0;width:88px;height:72px}.image-picker__preview--empty[data-v-ab0add26]{background:repeating-linear-gradient(45deg,#eef1f0,#eef1f0 8px,#e4e8e6 8px 16px)}.image-picker__actions[data-v-ab0add26]{flex-wrap:wrap;align-items:center;gap:8px;display:flex}.image-picker__file[data-v-ab0add26]{display:none}.image-picker__error[data-v-ab0add26]{width:100%;color:var(--color-danger);margin:0;font-size:.75rem}.image-picker__ok[data-v-ab0add26]{width:100%;color:var(--color-success-dark);margin:0;font-size:.75rem;font-weight:600}
@@ -1 +1 @@
import{$ as e,At as t,B as n,D as r,Dt as i,M as a,N as o,U as s,a as c,c as l,d as u,g as d,h as f,k as p,kt as m,l as h,r as g,s as _,t as v,u as y,w as b}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{f as x,m as S,p as C}from"./http-BjLkjdQu.js";import{L as w,M as T}from"./index-pv0Ktkd1.js";var E=[`id`],D=[`aria-label`,`aria-expanded`],O={class:`admin-select__trigger-content`},k={class:`admin-select__value`},A={class:`admin-select__chevron`,"aria-hidden":`true`},j=[`aria-label`],M={key:0,class:`admin-select__search`,role:`presentation`},N=[`placeholder`],P={key:1,class:`admin-select__empty`,role:`presentation`},F=[`aria-selected`,`onClick`],I={class:`admin-select__option-label`},L=v(d({__name:`AdminSelect`,props:{modelValue:{},options:{},placeholder:{default:`انتخاب کنید`},ariaLabel:{default:`انتخاب`},block:{type:Boolean,default:!1},searchable:{type:Boolean,default:!1},searchPlaceholder:{default:`جستجو…`},id:{default:void 0}},emits:[`update:modelValue`],setup(d,{emit:v}){let L=d,R=v,z=e(!1),B=e(``),V=e(null),H=e(null),U=e(null),W=e({});T(V,()=>{z.value=!1,B.value=``},{ignore:[H]});let G=_(()=>L.options.find(e=>e.value===L.modelValue)),K=_(()=>G.value?.label??L.placeholder),q=_(()=>{let e=B.value.trim().toLowerCase();return e?L.options.filter(t=>t.label.toLowerCase().includes(e)):L.options});function J(){let e=V.value;if(!e)return;let t=e.getBoundingClientRect(),n=Math.min(320,window.innerHeight*.5),r=window.innerHeight-t.bottom-6,i=t.top-6,a=r<Math.min(160,n)&&i>r,o=Math.max(120,Math.min(n,a?i:r));W.value={position:`fixed`,left:`${Math.max(8,Math.min(t.left,window.innerWidth-Math.max(t.width,220)-8))}px`,width:`${Math.max(t.width,220)}px`,zIndex:`1100`,maxHeight:`${o}px`,...a?{top:`auto`,bottom:`${window.innerHeight-t.top+6}px`}:{top:`${t.bottom+6}px`,bottom:`auto`}}}function Y(e){e?(window.addEventListener(`resize`,J),window.addEventListener(`scroll`,J,!0)):(window.removeEventListener(`resize`,J),window.removeEventListener(`scroll`,J,!0))}async function X(){z.value=!z.value,z.value&&(await b(),J(),L.searchable&&(B.value=``,await b(),U.value?.focus()))}function Z(e){R(`update:modelValue`,e),z.value=!1,B.value=``}return n(z,e=>{Y(e),e&&b(()=>J())}),r(()=>{Y(!1)}),(e,n)=>(p(),u(`div`,{id:d.id,ref_key:`rootRef`,ref:V,class:i([`admin-select`,{"admin-select--open":z.value,"admin-select--block":d.block}])},[l(`button`,{type:`button`,class:`admin-select__trigger`,"aria-label":d.ariaLabel,"aria-expanded":z.value,"aria-haspopup":`listbox`,onClick:X},[l(`span`,O,[o(e.$slots,`option-leading`,{option:G.value},void 0,!0),l(`span`,k,t(K.value),1)]),l(`span`,A,[f(w,{name:`chevron-down`,size:16})])],8,D),(p(),h(c,{to:`body`},[z.value?(p(),u(`ul`,{key:0,ref_key:`menuRef`,ref:H,class:`admin-select__menu`,role:`listbox`,"aria-label":d.ariaLabel,style:m(W.value)},[d.searchable?(p(),u(`li`,M,[s(l(`input`,{ref_key:`searchInputRef`,ref:U,"onUpdate:modelValue":n[0]||=e=>B.value=e,type:`search`,class:`admin-select__search-input`,placeholder:d.searchPlaceholder,onClick:n[1]||=S(()=>{},[`stop`]),onKeydown:n[2]||=C(S(e=>z.value=!1,[`stop`]),[`esc`])},null,40,N),[[x,B.value]])])):y(``,!0),q.value.length?y(``,!0):(p(),u(`li`,P,` موردی یافت نشد `)),(p(!0),u(g,null,a(q.value,n=>(p(),u(`li`,{key:n.value,role:`presentation`},[l(`button`,{type:`button`,class:i([`admin-select__option`,{"admin-select__option--active":n.value===d.modelValue}]),role:`option`,"aria-selected":n.value===d.modelValue,onClick:e=>Z(n.value)},[l(`span`,I,[o(e.$slots,`option-leading`,{option:n},void 0,!0),l(`span`,null,t(n.label),1)]),n.value===d.modelValue?(p(),h(w,{key:0,name:`check`,size:16,class:`admin-select__check`})):y(``,!0)],10,F)]))),128))],12,j)):y(``,!0)]))],10,E))}}),[[`__scopeId`,`data-v-4623e322`]]);export{L as t}; import{$ as e,At as t,B as n,D as r,Dt as i,M as a,N as o,U as s,a as c,c as l,d as u,g as d,h as f,k as p,kt as m,l as h,r as g,s as _,t as v,u as y,w as b}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{f as x,m as S,p as C}from"./http-BjLkjdQu.js";import{N as w,R as T}from"./index-BzcByePp.js";var E=[`id`],D=[`aria-label`,`aria-expanded`],O={class:`admin-select__trigger-content`},k={class:`admin-select__value`},A={class:`admin-select__chevron`,"aria-hidden":`true`},j=[`aria-label`],M={key:0,class:`admin-select__search`,role:`presentation`},N=[`placeholder`],P={key:1,class:`admin-select__empty`,role:`presentation`},F=[`aria-selected`,`onClick`],I={class:`admin-select__option-label`},L=v(d({__name:`AdminSelect`,props:{modelValue:{},options:{},placeholder:{default:`انتخاب کنید`},ariaLabel:{default:`انتخاب`},block:{type:Boolean,default:!1},searchable:{type:Boolean,default:!1},searchPlaceholder:{default:`جستجو…`},id:{default:void 0}},emits:[`update:modelValue`],setup(d,{emit:v}){let L=d,R=v,z=e(!1),B=e(``),V=e(null),H=e(null),U=e(null),W=e({});w(V,()=>{z.value=!1,B.value=``},{ignore:[H]});let G=_(()=>L.options.find(e=>e.value===L.modelValue)),K=_(()=>G.value?.label??L.placeholder),q=_(()=>{let e=B.value.trim().toLowerCase();return e?L.options.filter(t=>t.label.toLowerCase().includes(e)):L.options});function J(){let e=V.value;if(!e)return;let t=e.getBoundingClientRect(),n=Math.min(320,window.innerHeight*.5),r=window.innerHeight-t.bottom-6,i=t.top-6,a=r<Math.min(160,n)&&i>r,o=Math.max(120,Math.min(n,a?i:r));W.value={position:`fixed`,left:`${Math.max(8,Math.min(t.left,window.innerWidth-Math.max(t.width,220)-8))}px`,width:`${Math.max(t.width,220)}px`,zIndex:`1100`,maxHeight:`${o}px`,...a?{top:`auto`,bottom:`${window.innerHeight-t.top+6}px`}:{top:`${t.bottom+6}px`,bottom:`auto`}}}function Y(e){e?(window.addEventListener(`resize`,J),window.addEventListener(`scroll`,J,!0)):(window.removeEventListener(`resize`,J),window.removeEventListener(`scroll`,J,!0))}async function X(){z.value=!z.value,z.value&&(await b(),J(),L.searchable&&(B.value=``,await b(),U.value?.focus()))}function Z(e){R(`update:modelValue`,e),z.value=!1,B.value=``}return n(z,e=>{Y(e),e&&b(()=>J())}),r(()=>{Y(!1)}),(e,n)=>(p(),u(`div`,{id:d.id,ref_key:`rootRef`,ref:V,class:i([`admin-select`,{"admin-select--open":z.value,"admin-select--block":d.block}])},[l(`button`,{type:`button`,class:`admin-select__trigger`,"aria-label":d.ariaLabel,"aria-expanded":z.value,"aria-haspopup":`listbox`,onClick:X},[l(`span`,O,[o(e.$slots,`option-leading`,{option:G.value},void 0,!0),l(`span`,k,t(K.value),1)]),l(`span`,A,[f(T,{name:`chevron-down`,size:16})])],8,D),(p(),h(c,{to:`body`},[z.value?(p(),u(`ul`,{key:0,ref_key:`menuRef`,ref:H,class:`admin-select__menu`,role:`listbox`,"aria-label":d.ariaLabel,style:m(W.value)},[d.searchable?(p(),u(`li`,M,[s(l(`input`,{ref_key:`searchInputRef`,ref:U,"onUpdate:modelValue":n[0]||=e=>B.value=e,type:`search`,class:`admin-select__search-input`,placeholder:d.searchPlaceholder,onClick:n[1]||=S(()=>{},[`stop`]),onKeydown:n[2]||=C(S(e=>z.value=!1,[`stop`]),[`esc`])},null,40,N),[[x,B.value]])])):y(``,!0),q.value.length?y(``,!0):(p(),u(`li`,P,` موردی یافت نشد `)),(p(!0),u(g,null,a(q.value,n=>(p(),u(`li`,{key:n.value,role:`presentation`},[l(`button`,{type:`button`,class:i([`admin-select__option`,{"admin-select__option--active":n.value===d.modelValue}]),role:`option`,"aria-selected":n.value===d.modelValue,onClick:e=>Z(n.value)},[l(`span`,I,[o(e.$slots,`option-leading`,{option:n},void 0,!0),l(`span`,null,t(n.label),1)]),n.value===d.modelValue?(p(),h(T,{key:0,name:`check`,size:16,class:`admin-select__check`})):y(``,!0)],10,F)]))),128))],12,j)):y(``,!0)]))],10,E))}}),[[`__scopeId`,`data-v-4623e322`]]);export{L as t};
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{At as e,Dt as t,N as n,c as r,d as i,g as a,h as o,k as s,t as c,u as l}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{L as u}from"./index-pv0Ktkd1.js";var d={class:`base-empty__icon`,"aria-hidden":`true`},f={class:`base-empty__title`},p={key:0,class:`base-empty__description`},m={key:1,class:`base-empty__actions`},h=c(a({__name:`BaseEmptyState`,props:{icon:{default:`file`},title:{},description:{},compact:{type:Boolean}},emits:[`action`,`secondary`],setup(a){return(c,h)=>(s(),i(`div`,{class:t([`base-empty`,{"base-empty--compact":a.compact}])},[r(`div`,d,[o(u,{name:a.icon,size:a.compact?28:36},null,8,[`name`,`size`])]),r(`h3`,f,e(a.title),1),a.description?(s(),i(`p`,p,e(a.description),1)):l(``,!0),c.$slots.default||c.$slots.secondary?(s(),i(`div`,m,[n(c.$slots,`default`,{},void 0,!0),n(c.$slots,`secondary`,{},void 0,!0)])):l(``,!0)],2))}}),[[`__scopeId`,`data-v-47ea81e4`]]);export{h as t}; import{At as e,Dt as t,N as n,c as r,d as i,g as a,h as o,k as s,t as c,u as l}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{R as u}from"./index-BzcByePp.js";var d={class:`base-empty__icon`,"aria-hidden":`true`},f={class:`base-empty__title`},p={key:0,class:`base-empty__description`},m={key:1,class:`base-empty__actions`},h=c(a({__name:`BaseEmptyState`,props:{icon:{default:`file`},title:{},description:{},compact:{type:Boolean}},emits:[`action`,`secondary`],setup(a){return(c,h)=>(s(),i(`div`,{class:t([`base-empty`,{"base-empty--compact":a.compact}])},[r(`div`,d,[o(u,{name:a.icon,size:a.compact?28:36},null,8,[`name`,`size`])]),r(`h3`,f,e(a.title),1),a.description?(s(),i(`p`,p,e(a.description),1)):l(``,!0),c.$slots.default||c.$slots.secondary?(s(),i(`div`,m,[n(c.$slots,`default`,{},void 0,!0),n(c.$slots,`secondary`,{},void 0,!0)])):l(``,!0)],2))}}),[[`__scopeId`,`data-v-47ea81e4`]]);export{h as t};
-1
View File
@@ -1 +0,0 @@
.admin-page__head[data-v-88043806]{justify-content:space-between;margin-bottom:20px;display:flex}.admin-page__head h1[data-v-88043806]{margin:0 0 4px;font-size:1.25rem}.admin-page__head p[data-v-88043806]{color:var(--color-text-muted);margin:0;font-size:.875rem}.admin-table[data-v-88043806]{border-collapse:collapse;width:100%}.admin-table th[data-v-88043806],.admin-table td[data-v-88043806]{text-align:right;border-bottom:1px solid var(--color-border);padding:10px 8px}.field[data-v-88043806]{flex-direction:column;gap:6px;margin-bottom:12px;font-size:.875rem;display:flex}.link[data-v-88043806]{color:var(--color-primary);cursor:pointer;background:0 0;border:none;font-family:inherit}
@@ -1 +1 @@
import{$ as e,At as t,E as n,H as r,M as i,U as a,Z as o,c as s,d as c,g as l,h as u,k as d,l as f,m as p,r as m,s as h,t as g}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as _,f as v,t as y}from"./http-BjLkjdQu.js";import{V as b,p as x}from"./index-pv0Ktkd1.js";import{t as S}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as C}from"./BaseSwitch-pJwnFBVo.js";import{t as w}from"./BaseEmptyState-y-wpauXF.js";var T={class:`admin-page`},E={key:2,class:`admin-table`},D={dir:`ltr`},O=[`onClick`],k={class:`field`},A={class:`field`},j={class:`field`},M={class:`field`},N=[`disabled`],P=g(l({__name:`BrandsView`,setup(l){let g=e([]),P=e(!0),F=e(!1),I=e(null),L=_(),R=o({name:``,slug:``,description:``,displayOrder:0,isActive:!0}),z=h(()=>I.value?`ویرایش برند`:`افزودن برند`);async function B(){P.value=!0;try{g.value=await x.list()}catch(e){L.error(e instanceof y?e.message:`بارگذاری برندها ناموفق بود`)}finally{P.value=!1}}function V(){I.value=null,Object.assign(R,{name:``,slug:``,description:``,displayOrder:g.value.length+1,isActive:!0}),F.value=!0}function H(e){I.value=e.id,Object.assign(R,{name:e.name,slug:e.slug??``,description:e.description??``,displayOrder:e.displayOrder,isActive:e.isActive}),F.value=!0}async function U(){try{let e={name:R.name.trim(),slug:R.slug.trim()||void 0,description:R.description.trim()||void 0,displayOrder:R.displayOrder,isActive:R.isActive};I.value?await x.update(I.value,e):await x.create(e),F.value=!1,L.success(`ذخیره شد`),await B()}catch(e){L.error(e instanceof y?e.message:`ذخیره ناموفق بود`)}}return n(()=>{B()}),(e,n)=>(d(),c(`div`,T,[s(`header`,{class:`admin-page__head`},[n[6]||=s(`div`,null,[s(`h1`,null,`برندها`)],-1),s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,onClick:V},`افزودن برند`)]),P.value?(d(),f(S,{key:0,variant:`table`})):g.value.length?(d(),c(`table`,E,[n[7]||=s(`thead`,null,[s(`tr`,null,[s(`th`,null,`نام`),s(`th`,null,`شناسه لاتین`),s(`th`,null,`مشتریان`),s(`th`,null,`فعال`),s(`th`)])],-1),s(`tbody`,null,[(d(!0),c(m,null,i(g.value,e=>(d(),c(`tr`,{key:e.id},[s(`td`,null,t(e.name),1),s(`td`,D,t(e.slug||`—`),1),s(`td`,null,t(e.customersCount??0),1),s(`td`,null,t(e.isActive?`بله`:`خیر`),1),s(`td`,null,[s(`button`,{type:`button`,class:`link`,onClick:t=>H(e)},`ویرایش`,8,O)])]))),128))])])):(d(),f(w,{key:1,title:`برندی ثبت نشده`})),u(b,{open:F.value,title:z.value,onClose:n[5]||=e=>F.value=!1},{footer:r(()=>[s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,disabled:!R.name.trim(),onClick:U},`ذخیره`,8,N)]),default:r(()=>[s(`label`,k,[n[8]||=p(`نام`,-1),a(s(`input`,{"onUpdate:modelValue":n[0]||=e=>R.name=e,class:`admin-input`},null,512),[[v,R.name]])]),s(`label`,A,[n[9]||=p(`شناسه لاتین`,-1),a(s(`input`,{"onUpdate:modelValue":n[1]||=e=>R.slug=e,class:`admin-input`,dir:`ltr`,placeholder:`مثلاً nademi-silk`},null,512),[[v,R.slug]])]),s(`label`,j,[n[10]||=p(`توضیح`,-1),a(s(`textarea`,{"onUpdate:modelValue":n[2]||=e=>R.description=e,class:`admin-input`,rows:`3`},null,512),[[v,R.description]])]),s(`label`,M,[n[11]||=p(`ترتیب`,-1),a(s(`input`,{"onUpdate:modelValue":n[3]||=e=>R.displayOrder=e,type:`number`,class:`admin-input`},null,512),[[v,R.displayOrder,void 0,{number:!0}]])]),u(C,{modelValue:R.isActive,"onUpdate:modelValue":n[4]||=e=>R.isActive=e,label:`فعال`},null,8,[`modelValue`])]),_:1},8,[`open`,`title`])]))}}),[[`__scopeId`,`data-v-88043806`]]);export{P as default}; import{$ as e,At as t,E as n,H as r,M as i,U as a,Z as o,c as s,d as c,g as l,h as u,k as d,l as f,m as p,r as m,s as h,t as g}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as _,f as v,t as y}from"./http-BjLkjdQu.js";import{H as b,p as x}from"./index-BzcByePp.js";import{t as S}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as C}from"./BaseSwitch-pJwnFBVo.js";import{t as w}from"./BaseEmptyState-B7aEMvHg.js";var T={class:`admin-page`},E={key:2,class:`admin-table`},D={dir:`ltr`},O=[`onClick`],k={class:`field`},A={class:`field`},j={class:`field`},M={class:`field`},N=[`disabled`],P=g(l({__name:`BrandsView`,setup(l){let g=e([]),P=e(!0),F=e(!1),I=e(null),L=_(),R=o({name:``,slug:``,description:``,displayOrder:0,isActive:!0}),z=h(()=>I.value?`ویرایش برند`:`افزودن برند`);async function B(){P.value=!0;try{g.value=await x.list()}catch(e){L.error(e instanceof y?e.message:`بارگذاری برندها ناموفق بود`)}finally{P.value=!1}}function V(){I.value=null,Object.assign(R,{name:``,slug:``,description:``,displayOrder:g.value.length+1,isActive:!0}),F.value=!0}function H(e){I.value=e.id,Object.assign(R,{name:e.name,slug:e.slug??``,description:e.description??``,displayOrder:e.displayOrder,isActive:e.isActive}),F.value=!0}async function U(){try{let e={name:R.name.trim(),slug:R.slug.trim()||void 0,description:R.description.trim()||void 0,displayOrder:R.displayOrder,isActive:R.isActive};I.value?await x.update(I.value,e):await x.create(e),F.value=!1,L.success(`ذخیره شد`),await B()}catch(e){L.error(e instanceof y?e.message:`ذخیره ناموفق بود`)}}return n(()=>{B()}),(e,n)=>(d(),c(`div`,T,[s(`header`,{class:`admin-page__head`},[n[6]||=s(`div`,null,[s(`h1`,null,`برندها`)],-1),s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,onClick:V},`افزودن برند`)]),P.value?(d(),f(S,{key:0,variant:`table`})):g.value.length?(d(),c(`table`,E,[n[7]||=s(`thead`,null,[s(`tr`,null,[s(`th`,null,`نام`),s(`th`,null,`شناسه لاتین`),s(`th`,null,`محصولات`),s(`th`,null,`فعال`),s(`th`)])],-1),s(`tbody`,null,[(d(!0),c(m,null,i(g.value,e=>(d(),c(`tr`,{key:e.id},[s(`td`,null,t(e.name),1),s(`td`,D,t(e.slug||`—`),1),s(`td`,null,t(e.productsCount??0),1),s(`td`,null,t(e.isActive?`بله`:`خیر`),1),s(`td`,null,[s(`button`,{type:`button`,class:`link`,onClick:t=>H(e)},`ویرایش`,8,O)])]))),128))])])):(d(),f(w,{key:1,title:`برندی ثبت نشده`})),u(b,{open:F.value,title:z.value,onClose:n[5]||=e=>F.value=!1},{footer:r(()=>[s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,disabled:!R.name.trim(),onClick:U},`ذخیره`,8,N)]),default:r(()=>[s(`label`,k,[n[8]||=p(`نام`,-1),a(s(`input`,{"onUpdate:modelValue":n[0]||=e=>R.name=e,class:`admin-input`},null,512),[[v,R.name]])]),s(`label`,A,[n[9]||=p(`شناسه لاتین`,-1),a(s(`input`,{"onUpdate:modelValue":n[1]||=e=>R.slug=e,class:`admin-input`,dir:`ltr`,placeholder:`مثلاً nademi-silk`},null,512),[[v,R.slug]])]),s(`label`,j,[n[10]||=p(`توضیح`,-1),a(s(`textarea`,{"onUpdate:modelValue":n[2]||=e=>R.description=e,class:`admin-input`,rows:`3`},null,512),[[v,R.description]])]),s(`label`,M,[n[11]||=p(`ترتیب`,-1),a(s(`input`,{"onUpdate:modelValue":n[3]||=e=>R.displayOrder=e,type:`number`,class:`admin-input`},null,512),[[v,R.displayOrder,void 0,{number:!0}]])]),u(C,{modelValue:R.isActive,"onUpdate:modelValue":n[4]||=e=>R.isActive=e,label:`فعال`},null,8,[`modelValue`])]),_:1},8,[`open`,`title`])]))}}),[[`__scopeId`,`data-v-a6f53b6f`]]);export{P as default};
+1
View File
@@ -0,0 +1 @@
.admin-page__head[data-v-a6f53b6f]{justify-content:space-between;margin-bottom:20px;display:flex}.admin-page__head h1[data-v-a6f53b6f]{margin:0 0 4px;font-size:1.25rem}.admin-page__head p[data-v-a6f53b6f]{color:var(--color-text-muted);margin:0;font-size:.875rem}.admin-table[data-v-a6f53b6f]{border-collapse:collapse;width:100%}.admin-table th[data-v-a6f53b6f],.admin-table td[data-v-a6f53b6f]{text-align:right;border-bottom:1px solid var(--color-border);padding:10px 8px}.field[data-v-a6f53b6f]{flex-direction:column;gap:6px;margin-bottom:12px;font-size:.875rem;display:flex}.link[data-v-a6f53b6f]{color:var(--color-primary);cursor:pointer;background:0 0;border:none;font-family:inherit}
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
import{$ as e,At as t,E as n,M as r,c as i,d as a,g as o,h as s,k as c,l,m as u,r as d,st as f,t as p,u as m}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as h,t as g}from"./http-BjLkjdQu.js";import{h as _,o as v}from"./index-pv0Ktkd1.js";import{t as y}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as b}from"./BaseEmptyState-y-wpauXF.js";import{t as x}from"./AdminSelect-DFH4VaRE.js";var S={class:`admin-page`},C={class:`admin-page__head`},w={class:`request-card__top`},T={class:`muted`,dir:`ltr`},E={class:`badge`},D={key:0,class:`muted`},O={class:`admin-table`},k={dir:`ltr`},A={key:0,class:`movements`},j={class:`muted`},M=p(o({__name:`CatalogRequestsView`,setup(o){let p=[{value:``,label:`همه وضعیت‌ها`},{value:`new`,label:`درخواست جدید`},{value:`approved`,label:`تأیید شده`},{value:`ready_to_ship`,label:`آماده ارسال`},{value:`delivered`,label:`تحویل به مشتری`},{value:`returned`,label:`بازگردانده شده`},{value:`cancelled`,label:`لغو شده`}],M=[{value:`warehouse`,label:`انبار`},{value:`customer`,label:`مشتری`},{value:`agent`,label:`واسطه`}],N=Object.fromEntries(p.filter(e=>e.value).map(e=>[e.value,e.label])),P=Object.fromEntries(M.map(e=>[e.value,e.label])),F=e([]),I=e(``),L=e(!0),R=e(null),z=h();async function B(){L.value=!0;try{F.value=await _.list(I.value||void 0)}catch(e){z.error(e instanceof g?e.message:`بارگذاری انجام نشد`)}finally{L.value=!1}}async function V(e,t){try{await _.updateStatus(e,t),z.success(`وضعیت به‌روز شد`),await B()}catch(e){z.error(e instanceof g?e.message:`به‌روزرسانی ناموفق بود`)}}async function H(e,t){if(!R.value){R.value=e;try{await _.updateHolder(e,{holderType:t}),z.success(`محل نگهداری به‌روز شد`),await B()}catch(e){z.error(e instanceof g?e.message:`به‌روزرسانی محل ناموفق بود`)}finally{R.value=null}}}function U(e){return P[e]??e}return n(()=>{B()}),(e,n)=>(c(),a(`div`,S,[i(`header`,C,[n[1]||=i(`div`,null,[i(`h1`,null,`درخواست کالیته فیزیکی`),i(`p`,null,`بررسی، گردش وضعیت و محل نگهداری اقلام`)],-1),s(x,{modelValue:I.value,"onUpdate:modelValue":[n[0]||=e=>I.value=e,B],options:p},null,8,[`modelValue`])]),L.value?(c(),l(y,{key:0,variant:`cards`})):F.value.length?(c(!0),a(d,{key:2},r(F.value,e=>(c(),a(`div`,{key:e.id,class:`request-card`},[i(`div`,w,[i(`div`,null,[i(`strong`,null,t(e.customerName),1),i(`span`,T,t(e.customerPhone),1),i(`span`,E,t(f(N)[e.status]??e.status),1)]),s(x,{"model-value":e.status,options:p.filter(e=>e.value),"onUpdate:modelValue":t=>V(e.id,String(t))},null,8,[`model-value`,`options`,`onUpdate:modelValue`])]),e.note?(c(),a(`p`,D,t(e.note),1)):m(``,!0),i(`table`,O,[n[2]||=i(`thead`,null,[i(`tr`,null,[i(`th`,null,`محصول`),i(`th`,null,`کد`),i(`th`,null,`تعداد`),i(`th`,null,`محل`)])],-1),i(`tbody`,null,[(c(!0),a(d,null,r(e.items,e=>(c(),a(`tr`,{key:e.id},[i(`td`,null,t(e.productName),1),i(`td`,k,t(e.productCode),1),i(`td`,null,t(e.quantity),1),i(`td`,null,[s(x,{"model-value":e.holderType,options:M,"onUpdate:modelValue":t=>H(e.id,String(t))},null,8,[`model-value`,`onUpdate:modelValue`]),e.movements?.length?(c(),a(`ul`,A,[(c(!0),a(d,null,r(e.movements,e=>(c(),a(`li`,{key:e.id},[u(t(U(e.fromHolder))+` → `+t(U(e.toHolder))+` `,1),i(`span`,j,t(f(v)(e.movedAt)),1)]))),128))])):m(``,!0)])]))),128))])])]))),128)):(c(),l(b,{key:1,title:`درخواستی نیست`}))]))}}),[[`__scopeId`,`data-v-747d8f88`]]);export{M as default}; import{$ as e,At as t,E as n,M as r,c as i,d as a,g as o,h as s,k as c,l,m as u,r as d,st as f,t as p,u as m}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as h,t as g}from"./http-BjLkjdQu.js";import{h as _,o as v}from"./index-BzcByePp.js";import{t as y}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as b}from"./BaseEmptyState-B7aEMvHg.js";import{t as x}from"./AdminSelect-8XDtPu3U.js";var S={class:`admin-page`},C={class:`admin-page__head`},w={class:`request-card__top`},T={class:`muted`,dir:`ltr`},E={class:`badge`},D={key:0,class:`muted`},O={class:`admin-table`},k={dir:`ltr`},A={key:0,class:`movements`},j={class:`muted`},M=p(o({__name:`CatalogRequestsView`,setup(o){let p=[{value:``,label:`همه وضعیت‌ها`},{value:`new`,label:`درخواست جدید`},{value:`approved`,label:`تأیید شده`},{value:`ready_to_ship`,label:`آماده ارسال`},{value:`delivered`,label:`تحویل به مشتری`},{value:`returned`,label:`بازگردانده شده`},{value:`cancelled`,label:`لغو شده`}],M=[{value:`warehouse`,label:`انبار`},{value:`customer`,label:`مشتری`},{value:`agent`,label:`واسطه`}],N=Object.fromEntries(p.filter(e=>e.value).map(e=>[e.value,e.label])),P=Object.fromEntries(M.map(e=>[e.value,e.label])),F=e([]),I=e(``),L=e(!0),R=e(null),z=h();async function B(){L.value=!0;try{F.value=await _.list(I.value||void 0)}catch(e){z.error(e instanceof g?e.message:`بارگذاری انجام نشد`)}finally{L.value=!1}}async function V(e,t){try{await _.updateStatus(e,t),z.success(`وضعیت به‌روز شد`),await B()}catch(e){z.error(e instanceof g?e.message:`به‌روزرسانی ناموفق بود`)}}async function H(e,t){if(!R.value){R.value=e;try{await _.updateHolder(e,{holderType:t}),z.success(`محل نگهداری به‌روز شد`),await B()}catch(e){z.error(e instanceof g?e.message:`به‌روزرسانی محل ناموفق بود`)}finally{R.value=null}}}function U(e){return P[e]??e}return n(()=>{B()}),(e,n)=>(c(),a(`div`,S,[i(`header`,C,[n[1]||=i(`div`,null,[i(`h1`,null,`درخواست کالیته فیزیکی`),i(`p`,null,`بررسی، گردش وضعیت و محل نگهداری اقلام`)],-1),s(x,{modelValue:I.value,"onUpdate:modelValue":[n[0]||=e=>I.value=e,B],options:p},null,8,[`modelValue`])]),L.value?(c(),l(y,{key:0,variant:`cards`})):F.value.length?(c(!0),a(d,{key:2},r(F.value,e=>(c(),a(`div`,{key:e.id,class:`request-card`},[i(`div`,w,[i(`div`,null,[i(`strong`,null,t(e.customerName),1),i(`span`,T,t(e.customerPhone),1),i(`span`,E,t(f(N)[e.status]??e.status),1)]),s(x,{"model-value":e.status,options:p.filter(e=>e.value),"onUpdate:modelValue":t=>V(e.id,String(t))},null,8,[`model-value`,`options`,`onUpdate:modelValue`])]),e.note?(c(),a(`p`,D,t(e.note),1)):m(``,!0),i(`table`,O,[n[2]||=i(`thead`,null,[i(`tr`,null,[i(`th`,null,`محصول`),i(`th`,null,`کد`),i(`th`,null,`تعداد`),i(`th`,null,`محل`)])],-1),i(`tbody`,null,[(c(!0),a(d,null,r(e.items,e=>(c(),a(`tr`,{key:e.id},[i(`td`,null,t(e.productName),1),i(`td`,k,t(e.productCode),1),i(`td`,null,t(e.quantity),1),i(`td`,null,[s(x,{"model-value":e.holderType,options:M,"onUpdate:modelValue":t=>H(e.id,String(t))},null,8,[`model-value`,`onUpdate:modelValue`]),e.movements?.length?(c(),a(`ul`,A,[(c(!0),a(d,null,r(e.movements,e=>(c(),a(`li`,{key:e.id},[u(t(U(e.fromHolder))+` → `+t(U(e.toHolder))+` `,1),i(`span`,j,t(f(v)(e.movedAt)),1)]))),128))])):m(``,!0)])]))),128))])])]))),128)):(c(),l(b,{key:1,title:`درخواستی نیست`}))]))}}),[[`__scopeId`,`data-v-747d8f88`]]);export{M as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
.suggestions-panel[data-v-4182e847]{border-top:1px solid var(--color-border);margin-top:20px;padding-top:20px}.suggestions-panel__head[data-v-4182e847]{margin-bottom:12px}.suggestions-panel__title[data-v-4182e847]{margin:0 0 4px;font-size:.95rem}.suggestions-panel__hint[data-v-4182e847]{color:var(--color-text-muted);margin:0;font-size:.78rem;line-height:1.5}.suggestions-panel__loading[data-v-4182e847],.suggestions-panel__empty[data-v-4182e847]{color:var(--color-text-muted);font-size:.85rem}.suggestions-panel__add[data-v-4182e847]{flex-direction:column;gap:10px;display:flex}.field[data-v-4182e847]{flex-direction:column;gap:6px;font-size:.85rem;display:flex}.suggestions-panel__picked[data-v-4182e847]{flex-wrap:wrap;gap:6px;display:flex}.suggestions-panel__chip[data-v-4182e847]{background:var(--color-primary-soft);color:var(--color-primary);border-radius:999px;align-items:center;gap:6px;padding:4px 8px;font-size:.75rem;font-weight:700;display:inline-flex}.suggestions-panel__chip button[data-v-4182e847]{color:inherit;cursor:pointer;background:0 0;border:none;font-size:1rem;line-height:1}.suggestions-panel__sub[data-v-4182e847]{margin:16px 0 8px;font-size:.85rem}.suggestions-panel__list ul[data-v-4182e847]{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.suggestions-panel__row[data-v-4182e847]{background:var(--color-bg);border:1px solid var(--color-border);border-radius:10px;justify-content:space-between;align-items:flex-start;gap:12px;padding:10px 12px;display:flex}.suggestions-panel__actions[data-v-4182e847]{flex-direction:column;flex-shrink:0;align-items:flex-end;gap:4px;display:flex}.muted[data-v-4182e847]{color:var(--color-text-muted);margin-inline-start:4px;font-size:.75rem}.note[data-v-4182e847]{color:var(--color-text-muted);margin:4px 0 0;font-size:.75rem}.on[data-v-4182e847]{color:var(--color-success);font-size:.72rem;font-weight:700}.off[data-v-4182e847]{color:var(--color-text-muted);font-size:.72rem}.link[data-v-4182e847]{color:var(--color-primary);cursor:pointer;background:0 0;border:none;font-family:inherit;font-size:.78rem}.customers-page__tabs[data-v-db05c8d1]{gap:8px;margin-bottom:16px;display:flex}.customers-page__tab[data-v-db05c8d1]{border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-text-muted);font:inherit;cursor:pointer;border-radius:999px;padding:8px 16px;font-size:.875rem;font-weight:600}.customers-page__tab.is-active[data-v-db05c8d1]{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary)}.customers-page__search[data-v-db05c8d1]{color:var(--color-text-muted);align-items:center;gap:10px;margin-bottom:16px;padding:10px 14px;display:flex}.customers-page__search-input[data-v-db05c8d1]{background:0 0;border:none;outline:none;flex:1;font-size:.875rem}.customers-page__table[data-v-db05c8d1]{overflow:hidden}.customers-page__brands[data-v-db05c8d1]{flex-wrap:wrap;gap:10px 16px;display:flex}.customers-page__brand[data-v-db05c8d1]{cursor:pointer;align-items:center;gap:8px;font-size:.875rem;display:flex}.customers-page__brand input[data-v-db05c8d1]{width:16px;height:16px;accent-color:var(--color-primary)} .suggestions-panel[data-v-4182e847]{border-top:1px solid var(--color-border);margin-top:20px;padding-top:20px}.suggestions-panel__head[data-v-4182e847]{margin-bottom:12px}.suggestions-panel__title[data-v-4182e847]{margin:0 0 4px;font-size:.95rem}.suggestions-panel__hint[data-v-4182e847]{color:var(--color-text-muted);margin:0;font-size:.78rem;line-height:1.5}.suggestions-panel__loading[data-v-4182e847],.suggestions-panel__empty[data-v-4182e847]{color:var(--color-text-muted);font-size:.85rem}.suggestions-panel__add[data-v-4182e847]{flex-direction:column;gap:10px;display:flex}.field[data-v-4182e847]{flex-direction:column;gap:6px;font-size:.85rem;display:flex}.suggestions-panel__picked[data-v-4182e847]{flex-wrap:wrap;gap:6px;display:flex}.suggestions-panel__chip[data-v-4182e847]{background:var(--color-primary-soft);color:var(--color-primary);border-radius:999px;align-items:center;gap:6px;padding:4px 8px;font-size:.75rem;font-weight:700;display:inline-flex}.suggestions-panel__chip button[data-v-4182e847]{color:inherit;cursor:pointer;background:0 0;border:none;font-size:1rem;line-height:1}.suggestions-panel__sub[data-v-4182e847]{margin:16px 0 8px;font-size:.85rem}.suggestions-panel__list ul[data-v-4182e847]{flex-direction:column;gap:8px;margin:0;padding:0;list-style:none;display:flex}.suggestions-panel__row[data-v-4182e847]{background:var(--color-bg);border:1px solid var(--color-border);border-radius:10px;justify-content:space-between;align-items:flex-start;gap:12px;padding:10px 12px;display:flex}.suggestions-panel__actions[data-v-4182e847]{flex-direction:column;flex-shrink:0;align-items:flex-end;gap:4px;display:flex}.muted[data-v-4182e847]{color:var(--color-text-muted);margin-inline-start:4px;font-size:.75rem}.note[data-v-4182e847]{color:var(--color-text-muted);margin:4px 0 0;font-size:.75rem}.on[data-v-4182e847]{color:var(--color-success);font-size:.72rem;font-weight:700}.off[data-v-4182e847]{color:var(--color-text-muted);font-size:.72rem}.link[data-v-4182e847]{color:var(--color-primary);cursor:pointer;background:0 0;border:none;font-family:inherit;font-size:.78rem}.customers-page__tabs[data-v-2e37be8d]{gap:8px;margin-bottom:16px;display:flex}.customers-page__tab[data-v-2e37be8d]{border:1px solid var(--color-border);background:var(--color-surface);color:var(--color-text-muted);font:inherit;cursor:pointer;border-radius:999px;padding:8px 16px;font-size:.875rem;font-weight:600}.customers-page__tab.is-active[data-v-2e37be8d]{border-color:var(--color-primary);background:var(--color-primary-soft);color:var(--color-primary)}.customers-page__search[data-v-2e37be8d]{color:var(--color-text-muted);align-items:center;gap:10px;margin-bottom:16px;padding:10px 14px;display:flex}.customers-page__search-input[data-v-2e37be8d]{background:0 0;border:none;outline:none;flex:1;font-size:.875rem}.customers-page__table[data-v-2e37be8d]{overflow:hidden}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.admin-page-header__sub[data-v-413b6ea4]{max-width:52rem;color:var(--color-text-muted);margin:6px 0 0;font-size:.8125rem;line-height:1.6}.preview-cell[data-v-413b6ea4]{white-space:nowrap;text-overflow:ellipsis;max-width:280px;overflow:hidden}.link-cell[data-v-413b6ea4]{text-overflow:ellipsis;white-space:nowrap;max-width:160px;overflow:hidden}.field[data-v-413b6ea4]{flex-direction:column;gap:6px;margin-bottom:12px;font-size:.875rem;font-weight:600;display:flex}
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
import{$ as e,At as t,E as n,H as r,M as i,U as a,Z as o,c as s,d as c,g as l,h as u,k as d,l as f,m as p,r as m,s as h,t as g}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as _,f as v,t as y}from"./http-BjLkjdQu.js";import{V as b,x}from"./index-pv0Ktkd1.js";import{t as S}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as C}from"./BaseEmptyState-y-wpauXF.js";var w={class:`admin-page`},T={key:2,class:`admin-table`},E={dir:`ltr`},D=[`onClick`],O={class:`field`},k={class:`field`},A={class:`field`},j={class:`field`},M=[`disabled`],N=g(l({__name:`LevelsView`,setup(l){let g=e([]),N=e(!0),P=e(!1),F=e(null),I=_(),L=o({code:``,label:``,sortOrder:0,benefitsText:``}),R=h(()=>F.value?`ویرایش سطح`:`سطح جدید`);function z(e){return e.split(` import{$ as e,At as t,E as n,H as r,M as i,U as a,Z as o,c as s,d as c,g as l,h as u,k as d,l as f,m as p,r as m,s as h,t as g}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as _,f as v,t as y}from"./http-BjLkjdQu.js";import{H as b,S as x}from"./index-BzcByePp.js";import{t as S}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as C}from"./BaseEmptyState-B7aEMvHg.js";var w={class:`admin-page`},T={key:2,class:`admin-table`},E={dir:`ltr`},D=[`onClick`],O={class:`field`},k={class:`field`},A={class:`field`},j={class:`field`},M=[`disabled`],N=g(l({__name:`LevelsView`,setup(l){let g=e([]),N=e(!0),P=e(!1),F=e(null),I=_(),L=o({code:``,label:``,sortOrder:0,benefitsText:``}),R=h(()=>F.value?`ویرایش سطح`:`سطح جدید`);function z(e){return e.split(`
`).map(e=>e.trim()).filter(Boolean).map(e=>({title:e}))}async function B(){N.value=!0;try{g.value=await x.list()}catch(e){I.error(e instanceof y?e.message:`بارگذاری سطوح ناموفق بود`)}finally{N.value=!1}}function V(){F.value=null,Object.assign(L,{code:``,label:``,sortOrder:g.value.length+1,benefitsText:``}),P.value=!0}function H(e){F.value=e.id,Object.assign(L,{code:e.code,label:e.label,sortOrder:e.sortOrder,benefitsText:e.benefits.map(e=>e.title).join(` `).map(e=>e.trim()).filter(Boolean).map(e=>({title:e}))}async function B(){N.value=!0;try{g.value=await x.list()}catch(e){I.error(e instanceof y?e.message:`بارگذاری سطوح ناموفق بود`)}finally{N.value=!1}}function V(){F.value=null,Object.assign(L,{code:``,label:``,sortOrder:g.value.length+1,benefitsText:``}),P.value=!0}function H(e){F.value=e.id,Object.assign(L,{code:e.code,label:e.label,sortOrder:e.sortOrder,benefitsText:e.benefits.map(e=>e.title).join(`
`)}),P.value=!0}async function U(){try{let e={code:L.code.trim(),label:L.label.trim(),sortOrder:L.sortOrder,benefits:z(L.benefitsText)};F.value?await x.update(F.value,e):await x.create(e),P.value=!1,I.success(`ذخیره شد`),await B()}catch(e){I.error(e instanceof y?e.message:`ذخیره ناموفق بود`)}}return n(()=>{B()}),(e,n)=>(d(),c(`div`,w,[s(`header`,{class:`admin-page__head`},[n[5]||=s(`div`,null,[s(`h1`,null,`سطوح و مزایای مشتری`),s(`p`,null,`تعریف Level داخلی CRM و مزایای هر سطح (نمایش فقط در ادمین)`)],-1),s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,onClick:V},`سطح جدید`)]),N.value?(d(),f(S,{key:0,variant:`table`})):g.value.length?(d(),c(`table`,T,[n[6]||=s(`thead`,null,[s(`tr`,null,[s(`th`,null,`کد`),s(`th`,null,`عنوان`),s(`th`,null,`مزایا`),s(`th`,null,`مشتریان`),s(`th`)])],-1),s(`tbody`,null,[(d(!0),c(m,null,i(g.value,e=>(d(),c(`tr`,{key:e.id},[s(`td`,E,t(e.code),1),s(`td`,null,t(e.label),1),s(`td`,null,t(e.benefits.map(e=>e.title).join(`، `)||`—`),1),s(`td`,null,t(e.customersCount),1),s(`td`,null,[s(`button`,{type:`button`,class:`link`,onClick:t=>H(e)},`ویرایش`,8,D)])]))),128))])])):(d(),f(C,{key:1,title:`سطحی تعریف نشده`})),u(b,{open:P.value,title:R.value,onClose:n[4]||=e=>P.value=!1},{footer:r(()=>[s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,disabled:!L.code.trim()||!L.label.trim(),onClick:U},` ذخیره `,8,M)]),default:r(()=>[s(`label`,O,[n[7]||=p(`کد`,-1),a(s(`input`,{"onUpdate:modelValue":n[0]||=e=>L.code=e,class:`admin-input`,dir:`ltr`},null,512),[[v,L.code]])]),s(`label`,k,[n[8]||=p(`عنوان`,-1),a(s(`input`,{"onUpdate:modelValue":n[1]||=e=>L.label=e,class:`admin-input`},null,512),[[v,L.label]])]),s(`label`,A,[n[9]||=p(`ترتیب`,-1),a(s(`input`,{"onUpdate:modelValue":n[2]||=e=>L.sortOrder=e,type:`number`,class:`admin-input`},null,512),[[v,L.sortOrder,void 0,{number:!0}]])]),s(`label`,j,[n[10]||=p(` مزایا (هر خط یک مورد) `,-1),a(s(`textarea`,{"onUpdate:modelValue":n[3]||=e=>L.benefitsText=e,class:`admin-input`,rows:`4`},null,512),[[v,L.benefitsText]])])]),_:1},8,[`open`,`title`])]))}}),[[`__scopeId`,`data-v-44c7f808`]]);export{N as default}; `)}),P.value=!0}async function U(){try{let e={code:L.code.trim(),label:L.label.trim(),sortOrder:L.sortOrder,benefits:z(L.benefitsText)};F.value?await x.update(F.value,e):await x.create(e),P.value=!1,I.success(`ذخیره شد`),await B()}catch(e){I.error(e instanceof y?e.message:`ذخیره ناموفق بود`)}}return n(()=>{B()}),(e,n)=>(d(),c(`div`,w,[s(`header`,{class:`admin-page__head`},[n[5]||=s(`div`,null,[s(`h1`,null,`سطوح و مزایای مشتری`),s(`p`,null,`تعریف Level داخلی CRM و مزایای هر سطح (نمایش فقط در ادمین)`)],-1),s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,onClick:V},`سطح جدید`)]),N.value?(d(),f(S,{key:0,variant:`table`})):g.value.length?(d(),c(`table`,T,[n[6]||=s(`thead`,null,[s(`tr`,null,[s(`th`,null,`کد`),s(`th`,null,`عنوان`),s(`th`,null,`مزایا`),s(`th`,null,`مشتریان`),s(`th`)])],-1),s(`tbody`,null,[(d(!0),c(m,null,i(g.value,e=>(d(),c(`tr`,{key:e.id},[s(`td`,E,t(e.code),1),s(`td`,null,t(e.label),1),s(`td`,null,t(e.benefits.map(e=>e.title).join(`، `)||`—`),1),s(`td`,null,t(e.customersCount),1),s(`td`,null,[s(`button`,{type:`button`,class:`link`,onClick:t=>H(e)},`ویرایش`,8,D)])]))),128))])])):(d(),f(C,{key:1,title:`سطحی تعریف نشده`})),u(b,{open:P.value,title:R.value,onClose:n[4]||=e=>P.value=!1},{footer:r(()=>[s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,disabled:!L.code.trim()||!L.label.trim(),onClick:U},` ذخیره `,8,M)]),default:r(()=>[s(`label`,O,[n[7]||=p(`کد`,-1),a(s(`input`,{"onUpdate:modelValue":n[0]||=e=>L.code=e,class:`admin-input`,dir:`ltr`},null,512),[[v,L.code]])]),s(`label`,k,[n[8]||=p(`عنوان`,-1),a(s(`input`,{"onUpdate:modelValue":n[1]||=e=>L.label=e,class:`admin-input`},null,512),[[v,L.label]])]),s(`label`,A,[n[9]||=p(`ترتیب`,-1),a(s(`input`,{"onUpdate:modelValue":n[2]||=e=>L.sortOrder=e,type:`number`,class:`admin-input`},null,512),[[v,L.sortOrder,void 0,{number:!0}]])]),s(`label`,j,[n[10]||=p(` مزایا (هر خط یک مورد) `,-1),a(s(`textarea`,{"onUpdate:modelValue":n[3]||=e=>L.benefitsText=e,class:`admin-input`,rows:`4`},null,512),[[v,L.benefitsText]])])]),_:1},8,[`open`,`title`])]))}}),[[`__scopeId`,`data-v-44c7f808`]]);export{N as default};
@@ -1 +1 @@
import{$ as e,At as t,U as n,c as r,d as i,g as a,h as o,k as s,m as c,s as l,st as u,t as d,u as f}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as p,f as m,l as h,m as g,u as _}from"./http-BjLkjdQu.js";import{F as v,I as y,L as b,P as x,R as S,z as C}from"./index-pv0Ktkd1.js";var w={class:`login-page`},T={class:`login-page__form-wrap`},E={class:`login-page__card admin-card`},D={class:`login-page__header`},O={key:0,class:`login-page__mock-hint`},k={dir:`ltr`},A={dir:`ltr`},j={key:0,class:`admin-form__error`,role:`alert`},M={class:`admin-form__field`},N={class:`login-page__input-wrap`},P={class:`login-page__field-icon`,"aria-hidden":`true`},F={class:`admin-form__field`},I={class:`login-page__input-wrap login-page__input-wrap--password`},L={class:`login-page__field-icon`,"aria-hidden":`true`},R=[`type`],z=[`aria-label`],B={class:`login-page__remember`},V=[`disabled`],H={key:0,class:`admin-btn__spinner`,"aria-hidden":`true`},U=d(a({__name:`LoginView`,setup(a){let d=C(),U=S(),W=x(),G=p(),K=e(y()?v.phone:``),q=e(y()?v.password:``),J=e(!0),Y=e(!1),X=y(),Z=l(()=>K.value.trim().length>0&&q.value.length>0);function Q(e){return e.replace(/[۰-۹]/g,e=>String(`۰۱۲۳۴۵۶۷۸۹`.indexOf(e))).replace(/[٠-٩]/g,e=>String(`٠١٢٣٤٥٦٧٨٩`.indexOf(e)))}async function $(){if(!Z.value){W.error=`لطفاً شماره موبایل و رمز عبور را وارد کنید.`;return}if(!await W.login(Q(K.value.trim()),q.value,J.value))return;G.success(`خوش آمدید، ${W.fullName}`);let e=typeof U.query.redirect==`string`?U.query.redirect:`/`,t=`/admin/`.replace(/\/$/,``),n=e.startsWith(t)?e.slice(t.length)||`/`:e;await d.replace(n)}return(e,a)=>(s(),i(`div`,w,[r(`div`,T,[r(`div`,E,[r(`div`,D,[a[8]||=r(`h2`,{class:`login-page__title`},`ورود به پنل مدیریت`,-1),a[9]||=r(`p`,{class:`login-page__subtitle`},`برای ادامه، اطلاعات حساب کاربری خود را وارد کنید.`,-1),u(X)?(s(),i(`p`,O,[a[6]||=c(` حالت موک فعال است — `,-1),r(`span`,k,t(u(v).phone),1),a[7]||=c(` / `,-1),r(`span`,A,t(u(v).password),1)])):f(``,!0)]),r(`form`,{class:`admin-form login-page__form`,onSubmit:g($,[`prevent`])},[u(W).error?(s(),i(`p`,j,t(u(W).error),1)):f(``,!0),r(`div`,M,[a[10]||=r(`label`,{for:`login-phone`},`شماره موبایل`,-1),r(`div`,N,[r(`span`,P,[o(b,{name:`phone`,size:18})]),n(r(`input`,{id:`login-phone`,"onUpdate:modelValue":a[0]||=e=>K.value=e,type:`tel`,class:`admin-input login-page__input`,placeholder:`۰۹۱۲۱۱۱۱۲۲۲۲`,autocomplete:`username`,inputmode:`tel`,dir:`ltr`,onInput:a[1]||=e=>u(W).clearError()},null,544),[[m,K.value]])])]),r(`div`,F,[a[11]||=r(`label`,{for:`login-password`},`رمز عبور`,-1),r(`div`,I,[r(`span`,L,[o(b,{name:`lock`,size:18})]),n(r(`input`,{id:`login-password`,"onUpdate:modelValue":a[2]||=e=>q.value=e,type:Y.value?`text`:`password`,class:`admin-input login-page__input login-page__input--with-toggle`,placeholder:`رمز عبور`,autocomplete:`current-password`,onInput:a[3]||=e=>u(W).clearError()},null,40,R),[[_,q.value]]),r(`button`,{type:`button`,class:`login-page__toggle-password`,"aria-label":Y.value?`پنهان کردن رمز`:`نمایش رمز`,onClick:a[4]||=e=>Y.value=!Y.value},[o(b,{name:Y.value?`eye-off`:`eye`,size:18},null,8,[`name`])],8,z)])]),r(`label`,B,[n(r(`input`,{"onUpdate:modelValue":a[5]||=e=>J.value=e,type:`checkbox`},null,512),[[h,J.value]]),a[12]||=r(`span`,null,`مرا به خاطر بسپار`,-1)]),r(`button`,{type:`submit`,class:`admin-btn admin-btn--primary login-page__submit`,disabled:!Z.value||u(W).loading},[u(W).loading?(s(),i(`span`,H)):f(``,!0),a[13]||=c(` ورود به پنل `,-1)],8,V)],32)])]),r(`div`,{class:`login-page__brand`},[r(`div`,{class:`login-page__brand-inner`},[r(`img`,{src:`/admin/brand-logo.png`,alt:`نادمی تکس`,class:`login-page__brand-logo`}),a[14]||=r(`p`,{class:`login-page__brand-badge`},`پارچه`,-1),a[15]||=r(`h1`,{class:`login-page__brand-title`},`پنل مدیریت سفارش‌های عمده`,-1),a[16]||=r(`p`,{class:`login-page__brand-text`},` مدیریت سفارش‌ها، مشتریان، قیمت‌گذاری، چک‌ها و گزارش‌های فروش در یک محیط یکپارچه. `,-1)])])]))}}),[[`__scopeId`,`data-v-df8d196a`]]);export{U as default}; import{$ as e,At as t,U as n,c as r,d as i,g as a,h as o,k as s,m as c,s as l,st as u,t as d,u as f}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as p,f as m,l as h,m as g,u as _}from"./http-BjLkjdQu.js";import{B as v,F as y,I as b,L as x,R as S,z as C}from"./index-BzcByePp.js";var w={class:`login-page`},T={class:`login-page__form-wrap`},E={class:`login-page__card admin-card`},D={class:`login-page__header`},O={key:0,class:`login-page__mock-hint`},k={dir:`ltr`},A={dir:`ltr`},j={key:0,class:`admin-form__error`,role:`alert`},M={class:`admin-form__field`},N={class:`login-page__input-wrap`},P={class:`login-page__field-icon`,"aria-hidden":`true`},F={class:`admin-form__field`},I={class:`login-page__input-wrap login-page__input-wrap--password`},L={class:`login-page__field-icon`,"aria-hidden":`true`},R=[`type`],z=[`aria-label`],B={class:`login-page__remember`},V=[`disabled`],H={key:0,class:`admin-btn__spinner`,"aria-hidden":`true`},U=d(a({__name:`LoginView`,setup(a){let d=v(),U=C(),W=y(),G=p(),K=e(x()?b.phone:``),q=e(x()?b.password:``),J=e(!0),Y=e(!1),X=x(),Z=l(()=>K.value.trim().length>0&&q.value.length>0);function Q(e){return e.replace(/[۰-۹]/g,e=>String(`۰۱۲۳۴۵۶۷۸۹`.indexOf(e))).replace(/[٠-٩]/g,e=>String(`٠١٢٣٤٥٦٧٨٩`.indexOf(e)))}async function $(){if(!Z.value){W.error=`لطفاً شماره موبایل و رمز عبور را وارد کنید.`;return}if(!await W.login(Q(K.value.trim()),q.value,J.value))return;G.success(`خوش آمدید، ${W.fullName}`);let e=typeof U.query.redirect==`string`?U.query.redirect:`/`,t=`/admin/`.replace(/\/$/,``),n=e.startsWith(t)?e.slice(t.length)||`/`:e;await d.replace(n)}return(e,a)=>(s(),i(`div`,w,[r(`div`,T,[r(`div`,E,[r(`div`,D,[a[8]||=r(`h2`,{class:`login-page__title`},`ورود به پنل مدیریت`,-1),a[9]||=r(`p`,{class:`login-page__subtitle`},`برای ادامه، اطلاعات حساب کاربری خود را وارد کنید.`,-1),u(X)?(s(),i(`p`,O,[a[6]||=c(` حالت موک فعال است — `,-1),r(`span`,k,t(u(b).phone),1),a[7]||=c(` / `,-1),r(`span`,A,t(u(b).password),1)])):f(``,!0)]),r(`form`,{class:`admin-form login-page__form`,onSubmit:g($,[`prevent`])},[u(W).error?(s(),i(`p`,j,t(u(W).error),1)):f(``,!0),r(`div`,M,[a[10]||=r(`label`,{for:`login-phone`},`شماره موبایل`,-1),r(`div`,N,[r(`span`,P,[o(S,{name:`phone`,size:18})]),n(r(`input`,{id:`login-phone`,"onUpdate:modelValue":a[0]||=e=>K.value=e,type:`tel`,class:`admin-input login-page__input`,placeholder:`۰۹۱۲۱۱۱۱۲۲۲۲`,autocomplete:`username`,inputmode:`tel`,dir:`ltr`,onInput:a[1]||=e=>u(W).clearError()},null,544),[[m,K.value]])])]),r(`div`,F,[a[11]||=r(`label`,{for:`login-password`},`رمز عبور`,-1),r(`div`,I,[r(`span`,L,[o(S,{name:`lock`,size:18})]),n(r(`input`,{id:`login-password`,"onUpdate:modelValue":a[2]||=e=>q.value=e,type:Y.value?`text`:`password`,class:`admin-input login-page__input login-page__input--with-toggle`,placeholder:`رمز عبور`,autocomplete:`current-password`,onInput:a[3]||=e=>u(W).clearError()},null,40,R),[[_,q.value]]),r(`button`,{type:`button`,class:`login-page__toggle-password`,"aria-label":Y.value?`پنهان کردن رمز`:`نمایش رمز`,onClick:a[4]||=e=>Y.value=!Y.value},[o(S,{name:Y.value?`eye-off`:`eye`,size:18},null,8,[`name`])],8,z)])]),r(`label`,B,[n(r(`input`,{"onUpdate:modelValue":a[5]||=e=>J.value=e,type:`checkbox`},null,512),[[h,J.value]]),a[12]||=r(`span`,null,`مرا به خاطر بسپار`,-1)]),r(`button`,{type:`submit`,class:`admin-btn admin-btn--primary login-page__submit`,disabled:!Z.value||u(W).loading},[u(W).loading?(s(),i(`span`,H)):f(``,!0),a[13]||=c(` ورود به پنل `,-1)],8,V)],32)])]),r(`div`,{class:`login-page__brand`},[r(`div`,{class:`login-page__brand-inner`},[r(`img`,{src:`/admin/brand-logo.png`,alt:`نادمی تکس`,class:`login-page__brand-logo`}),a[14]||=r(`p`,{class:`login-page__brand-badge`},`پارچه`,-1),a[15]||=r(`h1`,{class:`login-page__brand-title`},`پنل مدیریت سفارش‌های عمده`,-1),a[16]||=r(`p`,{class:`login-page__brand-text`},` مدیریت سفارش‌ها، مشتریان، قیمت‌گذاری، چک‌ها و گزارش‌های فروش در یک محیط یکپارچه. `,-1)])])]))}}),[[`__scopeId`,`data-v-df8d196a`]]);export{U as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.products-page__filters[data-v-e062996d]{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:16px;padding:12px 14px;display:flex}.products-page__search[data-v-e062996d]{min-width:200px;color:var(--color-text-muted);flex:1;align-items:center;gap:10px;display:flex}.products-page__search-input[data-v-e062996d]{background:0 0;border:none;outline:none;flex:1;font-size:.875rem}.products-page__catalog-filter[data-v-e062996d]{width:min(260px,100%)}.products-page__table[data-v-e062996d]{overflow:hidden}.product-thumb[data-v-e062996d]{background:#e8eceb 50%/cover no-repeat;border-radius:8px;width:44px;height:36px;display:block}.product-thumb--empty[data-v-e062996d]{background:#eef1f0}.color-variants[data-v-e062996d]{flex-direction:column;gap:14px;display:flex}.color-variants__hint[data-v-e062996d]{color:var(--color-text-muted);margin:0 0 10px;font-size:.75rem;line-height:1.6}.color-variant[data-v-e062996d]{border:1px solid var(--color-border);background:var(--color-bg,#f7f8f7);border-radius:12px;flex-wrap:wrap;align-items:center;gap:12px;padding:10px;display:flex}.color-variant__fields[data-v-e062996d]{flex-direction:column;flex:1;align-items:flex-start;gap:8px;min-width:180px;display:flex}
-1
View File
@@ -1 +0,0 @@
.products-page__filters[data-v-761ea8ca]{flex-wrap:wrap;align-items:center;gap:12px;margin-bottom:16px;padding:12px 14px;display:flex}.products-page__search[data-v-761ea8ca]{min-width:200px;color:var(--color-text-muted);flex:1;align-items:center;gap:10px;display:flex}.products-page__search-input[data-v-761ea8ca]{background:0 0;border:none;outline:none;flex:1;font-size:.875rem}.products-page__catalog-filter[data-v-761ea8ca]{width:min(260px,100%)}.products-page__table[data-v-761ea8ca]{overflow:hidden}.product-thumb[data-v-761ea8ca]{background:#e8eceb 50%/cover no-repeat;border-radius:8px;width:44px;height:36px;display:block}.product-thumb--empty[data-v-761ea8ca]{background:#eef1f0}.color-variants[data-v-761ea8ca]{flex-direction:column;gap:14px;display:flex}.color-variants__hint[data-v-761ea8ca]{color:var(--color-text-muted);margin:0 0 10px;font-size:.75rem;line-height:1.6}.color-variant[data-v-761ea8ca]{border:1px solid var(--color-border);background:var(--color-bg,#f7f8f7);border-radius:12px;flex-wrap:wrap;align-items:center;gap:12px;padding:10px;display:flex}.color-variant__fields[data-v-761ea8ca]{flex-direction:column;flex:1;align-items:flex-start;gap:8px;min-width:180px;display:flex}.product-images[data-v-761ea8ca]{flex-wrap:wrap;gap:10px;display:flex}.product-images__item[data-v-761ea8ca]{flex-direction:column;gap:6px;width:92px;display:flex}.product-images__thumb[data-v-761ea8ca]{border:1px solid var(--color-border);background:#e8eceb 50%/cover no-repeat;border-radius:10px;width:92px;height:72px;display:block}.product-images__remove[data-v-761ea8ca],.product-images__add[data-v-761ea8ca]{border:1px dashed var(--color-border);cursor:pointer;text-align:center;background:0 0;border-radius:8px;padding:6px 8px;font-size:.75rem}.product-images__add[data-v-761ea8ca]{min-width:92px;min-height:72px;color:var(--color-text-muted);place-items:center;display:grid}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.admin-multi-select[data-v-9a22912b]{min-width:200px;max-width:100%;position:relative}.admin-multi-select--block[data-v-9a22912b]{width:100%;min-width:0}.admin-multi-select__trigger[data-v-9a22912b]{width:100%;min-height:var(--control-height,40px);border:1px solid var(--color-border);border-radius:var(--radius-sm);background:var(--color-surface);color:var(--color-text);cursor:pointer;transition:border-color var(--transition-fast), box-shadow var(--transition-fast), background var(--transition-fast);justify-content:space-between;align-items:center;gap:10px;padding:0 12px 0 10px;font-family:inherit;font-size:.875rem;font-weight:500;display:flex}.admin-multi-select--disabled .admin-multi-select__trigger[data-v-9a22912b]{opacity:.55;cursor:not-allowed}.admin-multi-select__trigger[data-v-9a22912b]:hover:not(:disabled){border-color:var(--color-primary);background:var(--color-elevated,var(--color-surface))}.admin-multi-select--open .admin-multi-select__trigger[data-v-9a22912b],.admin-multi-select__trigger[data-v-9a22912b]:focus-visible:not(:disabled){border-color:var(--color-primary);box-shadow:var(--focus-ring);outline:none}.admin-multi-select__value[data-v-9a22912b]{text-align:start;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0;overflow:hidden}.admin-multi-select__chevron[data-v-9a22912b]{color:var(--color-text-muted);transition:transform var(--transition-fast);flex-shrink:0;display:inline-flex}.admin-multi-select--open .admin-multi-select__chevron[data-v-9a22912b]{color:var(--color-primary);transform:rotate(180deg)}.admin-multi-select__chips[data-v-9a22912b]{flex-wrap:wrap;gap:6px;margin:8px 0 0;padding:0;list-style:none;display:flex}.admin-multi-select__chip[data-v-9a22912b]{background:var(--color-primary-soft);max-width:100%;color:var(--color-primary);border-radius:999px;align-items:center;gap:4px;padding:4px 8px;font-size:.75rem;font-weight:600;display:inline-flex}.admin-multi-select__chip-label[data-v-9a22912b]{text-overflow:ellipsis;white-space:nowrap;max-width:220px;overflow:hidden}.admin-multi-select__chip-remove[data-v-9a22912b]{color:inherit;cursor:pointer;background:0 0;border:none;padding:0 2px;font-size:1rem;line-height:1}.admin-multi-select__menu[data-v-9a22912b]{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-card,0 12px 28px #0f172a1f);box-sizing:border-box;flex-direction:column;gap:4px;margin:0;padding:8px;list-style:none;display:flex;overflow-y:auto}.admin-multi-select__search[data-v-9a22912b]{z-index:1;background:var(--color-surface);padding-bottom:4px;position:sticky;top:0}.admin-multi-select__search-input[data-v-9a22912b]{border:1px solid var(--color-border);background:var(--color-bg);width:100%;height:36px;color:var(--color-text);font:inherit;border-radius:10px;padding:0 12px;font-size:.8125rem}.admin-multi-select__search-input[data-v-9a22912b]:focus{border-color:var(--color-primary);box-shadow:var(--focus-ring);outline:none}.admin-multi-select__empty[data-v-9a22912b]{color:var(--color-text-muted);text-align:center;padding:12px;font-size:.8125rem}.admin-multi-select__option[data-v-9a22912b]{width:100%;color:var(--color-text);text-align:start;cursor:pointer;transition:background var(--transition-fast), border-color var(--transition-fast), color var(--transition-fast);background:0 0;border:1px solid #0000;border-radius:12px;justify-content:space-between;align-items:center;gap:10px;padding:10px 12px;font-family:inherit;font-size:.875rem;font-weight:500;display:flex}.admin-multi-select__option[data-v-9a22912b]:hover{background:var(--color-bg);border-color:var(--color-border)}.admin-multi-select__option--active[data-v-9a22912b]{background:var(--color-primary-soft);color:var(--color-primary);border-color:#0000;font-weight:600}.admin-multi-select__check[data-v-9a22912b]{color:var(--color-primary);flex-shrink:0}.settings-page__grid[data-v-e84a2579]{grid-template-columns:repeat(2,1fr);gap:16px;display:grid}.settings-page__section[data-v-e84a2579]{padding:20px}.settings-page__title[data-v-e84a2579]{margin:0 0 16px;font-size:1rem;font-weight:700}.settings-page__checks[data-v-e84a2579]{flex-direction:column;gap:12px;display:flex}.settings-page__check[data-v-e84a2579]{cursor:pointer;align-items:center;gap:10px;font-size:.875rem;display:flex}.settings-page__check input[data-v-e84a2579]{width:16px;height:16px;accent-color:var(--color-primary)}.settings-page__check--block[data-v-e84a2579]{margin-bottom:12px}.settings-page__broadcast-submit[data-v-e84a2579]{margin-top:4px}.settings-page__hint[data-v-e84a2579]{color:var(--color-text-muted);margin:6px 0 0;font-size:.75rem;line-height:1.5}@media (width<=1279px){.settings-page__grid[data-v-e84a2579]{grid-template-columns:1fr}}
File diff suppressed because one or more lines are too long
-1
View File
@@ -1 +0,0 @@
.settings-page__grid[data-v-85da5885]{grid-template-columns:repeat(2,1fr);gap:16px;display:grid}.settings-page__section[data-v-85da5885]{padding:20px}.settings-page__title[data-v-85da5885]{margin:0 0 16px;font-size:1rem;font-weight:700}.settings-page__checks[data-v-85da5885]{flex-direction:column;gap:12px;display:flex}.settings-page__check[data-v-85da5885]{cursor:pointer;align-items:center;gap:10px;font-size:.875rem;display:flex}.settings-page__check input[data-v-85da5885]{width:16px;height:16px;accent-color:var(--color-primary)}.settings-page__customers[data-v-85da5885]{flex-direction:column;gap:8px;max-height:220px;padding:8px 0;display:flex;overflow:auto}.settings-page__phone[data-v-85da5885]{color:var(--color-text-muted);margin-right:6px;font-size:.8125rem}.settings-page__hint[data-v-85da5885]{color:var(--color-text-muted);margin:6px 0 0;font-size:.75rem;line-height:1.5}@media (width<=1279px){.settings-page__grid[data-v-85da5885]{grid-template-columns:1fr}}
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{$ as e,At as t,E as n,M as r,c as i,d as a,g as o,k as s,l as c,m as l,r as u,t as d}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as f,t as p}from"./http-BjLkjdQu.js";import{A as m}from"./index-pv0Ktkd1.js";import{t as h}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as g}from"./BaseEmptyState-y-wpauXF.js";var _={class:`admin-page`},v={key:2,class:`admin-table`},y={class:`muted`,dir:`ltr`},b={class:`muted`,dir:`ltr`},x=[`onClick`],S=d(o({__name:`StockAlertsView`,setup(o){let d=e([]),S=e(!0),C=f();async function w(){S.value=!0;try{d.value=await m.list()}catch(e){C.error(e instanceof p?e.message:`بارگذاری ناموفق بود`)}finally{S.value=!1}}async function T(e){try{await m.notify(e),C.success(`اعلان موجودی ارسال شد`),await w()}catch(e){C.error(e instanceof p?e.message:`ارسال ناموفق بود`)}}return n(()=>{w()}),(e,n)=>(s(),a(`div`,_,[n[1]||=i(`header`,{class:`admin-page__head`},[i(`div`,null,[i(`h1`,null,`درخواست خبر موجودی`),i(`p`,null,`مشتریانی که برای محصول ناموجود درخواست اطلاع داده‌اند`)])],-1),S.value?(s(),c(h,{key:0,variant:`table`})):d.value.length?(s(),a(`table`,v,[n[0]||=i(`thead`,null,[i(`tr`,null,[i(`th`,null,`مشتری`),i(`th`,null,`محصول`),i(`th`,null,`موجودی فعلی`),i(`th`)])],-1),i(`tbody`,null,[(s(!0),a(u,null,r(d.value,e=>(s(),a(`tr`,{key:e.id},[i(`td`,null,[l(t(e.customerName)+` `,1),i(`span`,y,t(e.customerPhone),1)]),i(`td`,null,[l(t(e.productName)+` `,1),i(`span`,b,`(`+t(e.productCode)+`)`,1)]),i(`td`,null,t(e.stock??`—`),1),i(`td`,null,[i(`button`,{type:`button`,class:`admin-btn admin-btn--primary admin-btn--sm`,onClick:t=>T(e.id)},`ارسال اعلان`,8,x)])]))),128))])])):(s(),c(g,{key:1,title:`درخواستی فعال نیست`}))]))}}),[[`__scopeId`,`data-v-61131544`]]);export{S as default}; import{$ as e,At as t,E as n,M as r,c as i,d as a,g as o,k as s,l as c,m as l,r as u,t as d}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as f,t as p}from"./http-BjLkjdQu.js";import{j as m}from"./index-BzcByePp.js";import{t as h}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as g}from"./BaseEmptyState-B7aEMvHg.js";var _={class:`admin-page`},v={key:2,class:`admin-table`},y={class:`muted`,dir:`ltr`},b={class:`muted`,dir:`ltr`},x=[`onClick`],S=d(o({__name:`StockAlertsView`,setup(o){let d=e([]),S=e(!0),C=f();async function w(){S.value=!0;try{d.value=await m.list()}catch(e){C.error(e instanceof p?e.message:`بارگذاری ناموفق بود`)}finally{S.value=!1}}async function T(e){try{await m.notify(e),C.success(`اعلان موجودی ارسال شد`),await w()}catch(e){C.error(e instanceof p?e.message:`ارسال ناموفق بود`)}}return n(()=>{w()}),(e,n)=>(s(),a(`div`,_,[n[1]||=i(`header`,{class:`admin-page__head`},[i(`div`,null,[i(`h1`,null,`درخواست خبر موجودی`),i(`p`,null,`مشتریانی که برای محصول ناموجود درخواست اطلاع داده‌اند`)])],-1),S.value?(s(),c(h,{key:0,variant:`table`})):d.value.length?(s(),a(`table`,v,[n[0]||=i(`thead`,null,[i(`tr`,null,[i(`th`,null,`مشتری`),i(`th`,null,`محصول`),i(`th`,null,`موجودی فعلی`),i(`th`)])],-1),i(`tbody`,null,[(s(!0),a(u,null,r(d.value,e=>(s(),a(`tr`,{key:e.id},[i(`td`,null,[l(t(e.customerName)+` `,1),i(`span`,y,t(e.customerPhone),1)]),i(`td`,null,[l(t(e.productName)+` `,1),i(`span`,b,`(`+t(e.productCode)+`)`,1)]),i(`td`,null,t(e.stock??`—`),1),i(`td`,null,[i(`button`,{type:`button`,class:`admin-btn admin-btn--primary admin-btn--sm`,onClick:t=>T(e.id)},`ارسال اعلان`,8,x)])]))),128))])])):(s(),c(g,{key:1,title:`درخواستی فعال نیست`}))]))}}),[[`__scopeId`,`data-v-61131544`]]);export{S as default};
@@ -1 +1 @@
import{$ as e,At as t,E as n,H as r,M as i,U as a,Z as o,c as s,d as c,g as l,h as u,k as d,l as f,m as p,r as m,s as h,t as g}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as _,f as v,t as y}from"./http-BjLkjdQu.js";import{T as b,V as x,j as S,y as C}from"./index-pv0Ktkd1.js";import{t as w}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as T}from"./BaseEmptyState-y-wpauXF.js";import{t as E}from"./AdminSelect-DFH4VaRE.js";var D={class:`admin-page`},O={class:`admin-page__head`},k={key:2,class:`admin-table`},A={class:`muted`,dir:`ltr`},j=[`onClick`],M={class:`field`},N={class:`field`},P={class:`field`},F=[`disabled`],I=g(l({__name:`SuggestionsView`,setup(l){let g=e([]),I=e(!0),L=e([]),R=e([]),z=e(!1),B=_(),V=o({customerId:``,productId:``,note:``}),H=h(()=>L.value.map(e=>({value:e.id,label:e.name?.trim()||e.phone}))),U=h(()=>R.value.map(e=>({value:e.id,label:`${e.name} (${e.productCode})`})));async function W(){I.value=!0;try{let[e,t,n]=await Promise.all([S.list(),C.list(),b.list()]);g.value=e,L.value=t,R.value=n}catch(e){B.error(e instanceof y?e.message:`بارگذاری ناموفق بود`)}finally{I.value=!1}}async function G(){try{await S.create({customerId:V.customerId,productId:V.productId,note:V.note.trim()||void 0}),z.value=!1,V.note=``,B.success(`پیشنهاد ثبت شد`),await W()}catch(e){B.error(e instanceof y?e.message:`ثبت ناموفق بود`)}}async function K(e){try{await S.setActive(e.id,!e.isActive),await W()}catch(e){B.error(e instanceof y?e.message:`به‌روزرسانی ناموفق بود`)}}return n(()=>{W()}),(e,n)=>(d(),c(`div`,D,[s(`header`,O,[n[5]||=s(`div`,null,[s(`h1`,null,`پیشنهاد محصول`),s(`p`,null,`پیشنهاد دستی محصول به مشتری`)],-1),s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,onClick:n[0]||=e=>z.value=!0},`پیشنهاد جدید`)]),I.value?(d(),f(w,{key:0,variant:`table`})):g.value.length?(d(),c(`table`,k,[n[6]||=s(`thead`,null,[s(`tr`,null,[s(`th`,null,`مشتری`),s(`th`,null,`محصول`),s(`th`,null,`یادداشت`),s(`th`,null,`وضعیت`),s(`th`)])],-1),s(`tbody`,null,[(d(!0),c(m,null,i(g.value,e=>(d(),c(`tr`,{key:e.id},[s(`td`,null,t(e.customerName),1),s(`td`,null,[p(t(e.productName)+` `,1),s(`span`,A,`(`+t(e.productCode)+`)`,1)]),s(`td`,null,t(e.note||`—`),1),s(`td`,null,t(e.isActive?`فعال`:`غیرفعال`),1),s(`td`,null,[s(`button`,{type:`button`,class:`link`,onClick:t=>K(e)},t(e.isActive?`غیرفعال`:`فعال`),9,j)])]))),128))])])):(d(),f(T,{key:1,title:`پیشنهادی ثبت نشده`})),u(x,{open:z.value,title:`پیشنهاد محصول`,onClose:n[4]||=e=>z.value=!1},{footer:r(()=>[s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,disabled:!V.customerId||!V.productId,onClick:G},` ثبت `,8,F)]),default:r(()=>[s(`label`,M,[n[7]||=p(`مشتری `,-1),u(E,{modelValue:V.customerId,"onUpdate:modelValue":n[1]||=e=>V.customerId=e,options:H.value},null,8,[`modelValue`,`options`])]),s(`label`,N,[n[8]||=p(`محصول `,-1),u(E,{modelValue:V.productId,"onUpdate:modelValue":n[2]||=e=>V.productId=e,options:U.value},null,8,[`modelValue`,`options`])]),s(`label`,P,[n[9]||=p(`یادداشت`,-1),a(s(`textarea`,{"onUpdate:modelValue":n[3]||=e=>V.note=e,class:`admin-input`,rows:`3`},null,512),[[v,V.note]])])]),_:1},8,[`open`])]))}}),[[`__scopeId`,`data-v-56e3e728`]]);export{I as default}; import{$ as e,At as t,E as n,H as r,M as i,U as a,Z as o,c as s,d as c,g as l,h as u,k as d,l as f,m as p,r as m,s as h,t as g}from"./_plugin-vue_export-helper-QrIvvHC8.js";import{a as _,f as v,t as y}from"./http-BjLkjdQu.js";import{E as b,H as x,M as S,y as C}from"./index-BzcByePp.js";import{t as w}from"./AdminPageSkeleton-oWhHYjl4.js";import{t as T}from"./BaseEmptyState-B7aEMvHg.js";import{t as E}from"./AdminSelect-8XDtPu3U.js";var D={class:`admin-page`},O={class:`admin-page__head`},k={key:2,class:`admin-table`},A={class:`muted`,dir:`ltr`},j=[`onClick`],M={class:`field`},N={class:`field`},P={class:`field`},F=[`disabled`],I=g(l({__name:`SuggestionsView`,setup(l){let g=e([]),I=e(!0),L=e([]),R=e([]),z=e(!1),B=_(),V=o({customerId:``,productId:``,note:``}),H=h(()=>L.value.map(e=>({value:e.id,label:e.name?.trim()||e.phone}))),U=h(()=>R.value.map(e=>({value:e.id,label:`${e.name} (${e.productCode})`})));async function W(){I.value=!0;try{let[e,t,n]=await Promise.all([S.list(),C.list(),b.list()]);g.value=e,L.value=t,R.value=n}catch(e){B.error(e instanceof y?e.message:`بارگذاری ناموفق بود`)}finally{I.value=!1}}async function G(){try{await S.create({customerId:V.customerId,productId:V.productId,note:V.note.trim()||void 0}),z.value=!1,V.note=``,B.success(`پیشنهاد ثبت شد`),await W()}catch(e){B.error(e instanceof y?e.message:`ثبت ناموفق بود`)}}async function K(e){try{await S.setActive(e.id,!e.isActive),await W()}catch(e){B.error(e instanceof y?e.message:`به‌روزرسانی ناموفق بود`)}}return n(()=>{W()}),(e,n)=>(d(),c(`div`,D,[s(`header`,O,[n[5]||=s(`div`,null,[s(`h1`,null,`پیشنهاد محصول`),s(`p`,null,`پیشنهاد دستی محصول به مشتری`)],-1),s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,onClick:n[0]||=e=>z.value=!0},`پیشنهاد جدید`)]),I.value?(d(),f(w,{key:0,variant:`table`})):g.value.length?(d(),c(`table`,k,[n[6]||=s(`thead`,null,[s(`tr`,null,[s(`th`,null,`مشتری`),s(`th`,null,`محصول`),s(`th`,null,`یادداشت`),s(`th`,null,`وضعیت`),s(`th`)])],-1),s(`tbody`,null,[(d(!0),c(m,null,i(g.value,e=>(d(),c(`tr`,{key:e.id},[s(`td`,null,t(e.customerName),1),s(`td`,null,[p(t(e.productName)+` `,1),s(`span`,A,`(`+t(e.productCode)+`)`,1)]),s(`td`,null,t(e.note||`—`),1),s(`td`,null,t(e.isActive?`فعال`:`غیرفعال`),1),s(`td`,null,[s(`button`,{type:`button`,class:`link`,onClick:t=>K(e)},t(e.isActive?`غیرفعال`:`فعال`),9,j)])]))),128))])])):(d(),f(T,{key:1,title:`پیشنهادی ثبت نشده`})),u(x,{open:z.value,title:`پیشنهاد محصول`,onClose:n[4]||=e=>z.value=!1},{footer:r(()=>[s(`button`,{type:`button`,class:`admin-btn admin-btn--primary`,disabled:!V.customerId||!V.productId,onClick:G},` ثبت `,8,F)]),default:r(()=>[s(`label`,M,[n[7]||=p(`مشتری `,-1),u(E,{modelValue:V.customerId,"onUpdate:modelValue":n[1]||=e=>V.customerId=e,options:H.value},null,8,[`modelValue`,`options`])]),s(`label`,N,[n[8]||=p(`محصول `,-1),u(E,{modelValue:V.productId,"onUpdate:modelValue":n[2]||=e=>V.productId=e,options:U.value},null,8,[`modelValue`,`options`])]),s(`label`,P,[n[9]||=p(`یادداشت`,-1),a(s(`textarea`,{"onUpdate:modelValue":n[3]||=e=>V.note=e,class:`admin-input`,rows:`3`},null,512),[[v,V.note]])])]),_:1},8,[`open`])]))}}),[[`__scopeId`,`data-v-56e3e728`]]);export{I as default};
File diff suppressed because one or more lines are too long
+382
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-382
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -11,10 +11,10 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes"> <meta name="apple-mobile-web-app-capable" content="yes">
<title>پنل مدیریت | پارچه</title> <title>پنل مدیریت | پارچه</title>
<script type="module" crossorigin src="/admin/assets/index-pv0Ktkd1.js"></script> <script type="module" crossorigin src="/admin/assets/index-BzcByePp.js"></script>
<link rel="modulepreload" crossorigin href="/admin/assets/_plugin-vue_export-helper-QrIvvHC8.js"> <link rel="modulepreload" crossorigin href="/admin/assets/_plugin-vue_export-helper-QrIvvHC8.js">
<link rel="modulepreload" crossorigin href="/admin/assets/http-BjLkjdQu.js"> <link rel="modulepreload" crossorigin href="/admin/assets/http-BjLkjdQu.js">
<link rel="stylesheet" crossorigin href="/admin/assets/index-Ddm1RcgL.css"> <link rel="stylesheet" crossorigin href="/admin/assets/index-CrGJrAMi.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
+14 -3
View File
@@ -7,7 +7,7 @@ import BasicIcon from '@/components/BasicPacks/BasicIcon.vue'
import BaseModal from '@/components/ui/BaseModal.vue' import BaseModal from '@/components/ui/BaseModal.vue'
import { useSmsWalletSummary } from '@/composables/useSmsWalletSummary' import { useSmsWalletSummary } from '@/composables/useSmsWalletSummary'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { dashboardApi, notificationsApi } from '@/services/api' import { dashboardApi, internalMessagesApi, notificationsApi } from '@/services/api'
import { formatDateTime, formatNumber, toPersianDigits } from '@/utils/format' import { formatDateTime, formatNumber, toPersianDigits } from '@/utils/format'
import { resolveAdminInboxLink } from '@/utils/inboxLinks' import { resolveAdminInboxLink } from '@/utils/inboxLinks'
import { formatCardNumber, formatSheba } from '@/utils/smsFormat' import { formatCardNumber, formatSheba } from '@/utils/smsFormat'
@@ -78,7 +78,11 @@ function closePanel() {
} }
function markMessageRead(message: InternalMessage) { function markMessageRead(message: InternalMessage) {
if (message.isRead) return
message.isRead = true message.isRead = true
void internalMessagesApi.markRead(message.id).catch(() => {
message.isRead = false
})
} }
function markNotificationRead(notification: AdminNotification) { function markNotificationRead(notification: AdminNotification) {
@@ -90,9 +94,16 @@ function markNotificationRead(notification: AdminNotification) {
} }
function markAllMessagesRead() { function markAllMessagesRead() {
messageList.value.forEach((m) => { const unread = messageList.value.filter((m) => !m.isRead)
if (!unread.length) return
unread.forEach((m) => {
m.isRead = true m.isRead = true
}) })
void internalMessagesApi.markAllRead().catch(() => {
unread.forEach((m) => {
m.isRead = false
})
})
} }
function markAllNotificationsRead() { function markAllNotificationsRead() {
@@ -118,7 +129,7 @@ function openMessage(message: InternalMessage) {
markMessageRead(message) markMessageRead(message)
navigateInboxTarget( navigateInboxTarget(
resolveAdminInboxLink({ resolveAdminInboxLink({
link: message.link, link: message.link || undefined,
orderId: message.orderId, orderId: message.orderId,
title: message.subject, title: message.subject,
body: message.preview, body: message.preview,
+1 -1
View File
@@ -66,7 +66,7 @@ function remove() {
حذف حذف
</button> </button>
<p v-if="error" class="image-picker__error">{{ error }}</p> <p v-if="error" class="image-picker__error">{{ error }}</p>
<p v-else-if="picked" class="image-picker__ok">تصویر انتخاب شد — برای ثبت نهایی «ذخیره» را بزنید.</p> <p v-else-if="picked" class="image-picker__ok">تصویر انتخاب شد — برای ثبت نهایی «ثبت» را بزنید.</p>
</div> </div>
</div> </div>
<input <input
+436
View File
@@ -0,0 +1,436 @@
<script setup lang="ts">
import { computed, nextTick, onUnmounted, ref, watch } from 'vue'
import { onClickOutside } from '@vueuse/core'
import AdminIcon from '@/components/icons/AdminIcon.vue'
export type AdminMultiSelectOption = {
value: string
label: string
}
const props = withDefaults(
defineProps<{
modelValue: string[]
options: AdminMultiSelectOption[]
placeholder?: string
ariaLabel?: string
block?: boolean
searchable?: boolean
searchPlaceholder?: string
disabled?: boolean
id?: string
}>(),
{
placeholder: 'انتخاب کنید',
ariaLabel: 'انتخاب چندگانه',
block: false,
searchable: true,
searchPlaceholder: 'جستجو…',
disabled: false,
id: undefined,
},
)
const emit = defineEmits<{
'update:modelValue': [value: string[]]
}>()
const open = ref(false)
const searchQuery = ref('')
const rootRef = ref<HTMLElement | null>(null)
const menuRef = ref<HTMLElement | null>(null)
const searchInputRef = ref<HTMLInputElement | null>(null)
const menuStyle = ref<Record<string, string>>({})
onClickOutside(
rootRef,
() => {
open.value = false
searchQuery.value = ''
},
{ ignore: [menuRef] },
)
const selectedSet = computed(() => new Set(props.modelValue))
const selectedOptions = computed(() =>
props.options.filter((option) => selectedSet.value.has(option.value)),
)
const triggerLabel = computed(() => {
const n = props.modelValue.length
if (!n) return props.placeholder
if (n === 1) return selectedOptions.value[0]?.label ?? props.placeholder
return `${n} مورد انتخاب شده`
})
const filteredOptions = computed(() => {
const q = searchQuery.value.trim().toLowerCase()
if (!q) return props.options
return props.options.filter((option) => option.label.toLowerCase().includes(q))
})
function updateMenuPosition() {
const el = rootRef.value
if (!el) return
const rect = el.getBoundingClientRect()
const gap = 6
const maxMenu = Math.min(320, window.innerHeight * 0.5)
const spaceBelow = window.innerHeight - rect.bottom - gap
const spaceAbove = rect.top - gap
const openUp = spaceBelow < Math.min(160, maxMenu) && spaceAbove > spaceBelow
const maxHeight = Math.max(120, Math.min(maxMenu, openUp ? spaceAbove : spaceBelow))
menuStyle.value = {
position: 'fixed',
left: `${Math.max(8, Math.min(rect.left, window.innerWidth - Math.max(rect.width, 220) - 8))}px`,
width: `${Math.max(rect.width, 220)}px`,
zIndex: '1100',
maxHeight: `${maxHeight}px`,
...(openUp
? {
top: 'auto',
bottom: `${window.innerHeight - rect.top + gap}px`,
}
: {
top: `${rect.bottom + gap}px`,
bottom: 'auto',
}),
}
}
function bindPositionListeners(active: boolean) {
if (active) {
window.addEventListener('resize', updateMenuPosition)
window.addEventListener('scroll', updateMenuPosition, true)
} else {
window.removeEventListener('resize', updateMenuPosition)
window.removeEventListener('scroll', updateMenuPosition, true)
}
}
async function toggle() {
if (props.disabled) return
open.value = !open.value
if (open.value) {
await nextTick()
updateMenuPosition()
if (props.searchable) {
searchQuery.value = ''
await nextTick()
searchInputRef.value?.focus()
}
}
}
function toggleOption(value: string) {
const next = new Set(props.modelValue)
if (next.has(value)) next.delete(value)
else next.add(value)
emit('update:modelValue', [...next])
}
function removeChip(value: string) {
emit(
'update:modelValue',
props.modelValue.filter((id) => id !== value),
)
}
watch(open, (isOpen) => {
bindPositionListeners(isOpen)
if (isOpen) {
void nextTick(() => updateMenuPosition())
}
})
onUnmounted(() => {
bindPositionListeners(false)
})
</script>
<template>
<div
:id="id"
ref="rootRef"
class="admin-multi-select"
:class="{
'admin-multi-select--open': open,
'admin-multi-select--block': block,
'admin-multi-select--disabled': disabled,
}"
>
<button
type="button"
class="admin-multi-select__trigger"
:aria-label="ariaLabel"
:aria-expanded="open"
aria-haspopup="listbox"
:disabled="disabled"
@click="toggle"
>
<span class="admin-multi-select__value">{{ triggerLabel }}</span>
<span class="admin-multi-select__chevron" aria-hidden="true">
<AdminIcon name="chevron-down" :size="16" />
</span>
</button>
<ul v-if="selectedOptions.length && !disabled" class="admin-multi-select__chips">
<li v-for="option in selectedOptions" :key="option.value" class="admin-multi-select__chip">
<span class="admin-multi-select__chip-label">{{ option.label }}</span>
<button
type="button"
class="admin-multi-select__chip-remove"
aria-label="حذف"
@click="removeChip(option.value)"
>
×
</button>
</li>
</ul>
<Teleport to="body">
<ul
v-if="open"
ref="menuRef"
class="admin-multi-select__menu"
role="listbox"
:aria-label="ariaLabel"
:style="menuStyle"
aria-multiselectable="true"
>
<li v-if="searchable" class="admin-multi-select__search" role="presentation">
<input
ref="searchInputRef"
v-model="searchQuery"
type="search"
class="admin-multi-select__search-input"
:placeholder="searchPlaceholder"
@click.stop
@keydown.esc.stop="open = false"
/>
</li>
<li v-if="!filteredOptions.length" class="admin-multi-select__empty" role="presentation">
موردی یافت نشد
</li>
<li v-for="option in filteredOptions" :key="option.value" role="presentation">
<button
type="button"
class="admin-multi-select__option"
:class="{ 'admin-multi-select__option--active': selectedSet.has(option.value) }"
role="option"
:aria-selected="selectedSet.has(option.value)"
@click="toggleOption(option.value)"
>
<span class="admin-multi-select__option-label">{{ option.label }}</span>
<AdminIcon
v-if="selectedSet.has(option.value)"
name="check"
:size="16"
class="admin-multi-select__check"
/>
</button>
</li>
</ul>
</Teleport>
</div>
</template>
<style scoped>
.admin-multi-select {
position: relative;
min-width: 200px;
max-width: 100%;
}
.admin-multi-select--block {
width: 100%;
min-width: 0;
}
.admin-multi-select__trigger {
width: 100%;
min-height: var(--control-height, 40px);
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 0 12px 0 10px;
border: 1px solid var(--color-border);
border-radius: var(--radius-sm);
background: var(--color-surface);
color: var(--color-text);
font-family: inherit;
font-size: 0.875rem;
font-weight: 500;
cursor: pointer;
transition:
border-color var(--transition-fast),
box-shadow var(--transition-fast),
background var(--transition-fast);
}
.admin-multi-select--disabled .admin-multi-select__trigger {
opacity: 0.55;
cursor: not-allowed;
}
.admin-multi-select__trigger:hover:not(:disabled) {
border-color: var(--color-primary);
background: var(--color-elevated, var(--color-surface));
}
.admin-multi-select--open .admin-multi-select__trigger,
.admin-multi-select__trigger:focus-visible:not(:disabled) {
outline: none;
border-color: var(--color-primary);
box-shadow: var(--focus-ring);
}
.admin-multi-select__value {
flex: 1;
min-width: 0;
text-align: start;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.admin-multi-select__chevron {
display: inline-flex;
flex-shrink: 0;
color: var(--color-text-muted);
transition: transform var(--transition-fast);
}
.admin-multi-select--open .admin-multi-select__chevron {
transform: rotate(180deg);
color: var(--color-primary);
}
.admin-multi-select__chips {
list-style: none;
margin: 8px 0 0;
padding: 0;
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.admin-multi-select__chip {
display: inline-flex;
align-items: center;
gap: 4px;
max-width: 100%;
padding: 4px 8px;
border-radius: 999px;
background: var(--color-primary-soft);
color: var(--color-primary);
font-size: 0.75rem;
font-weight: 600;
}
.admin-multi-select__chip-label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
max-width: 220px;
}
.admin-multi-select__chip-remove {
border: none;
background: transparent;
color: inherit;
font-size: 1rem;
line-height: 1;
cursor: pointer;
padding: 0 2px;
}
.admin-multi-select__menu {
margin: 0;
padding: 8px;
list-style: none;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius-md);
box-shadow: var(--shadow-card, 0 12px 28px rgba(15, 23, 42, 0.12));
display: flex;
flex-direction: column;
gap: 4px;
overflow-y: auto;
box-sizing: border-box;
}
.admin-multi-select__search {
position: sticky;
top: 0;
z-index: 1;
background: var(--color-surface);
padding-bottom: 4px;
}
.admin-multi-select__search-input {
width: 100%;
height: 36px;
padding: 0 12px;
border: 1px solid var(--color-border);
border-radius: 10px;
background: var(--color-bg);
color: var(--color-text);
font: inherit;
font-size: 0.8125rem;
}
.admin-multi-select__search-input:focus {
outline: none;
border-color: var(--color-primary);
box-shadow: var(--focus-ring);
}
.admin-multi-select__empty {
padding: 12px;
font-size: 0.8125rem;
color: var(--color-text-muted);
text-align: center;
}
.admin-multi-select__option {
width: 100%;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 10px 12px;
border: 1px solid transparent;
border-radius: 12px;
background: transparent;
color: var(--color-text);
font-family: inherit;
font-size: 0.875rem;
font-weight: 500;
text-align: start;
cursor: pointer;
transition:
background var(--transition-fast),
border-color var(--transition-fast),
color var(--transition-fast);
}
.admin-multi-select__option:hover {
background: var(--color-bg);
border-color: var(--color-border);
}
.admin-multi-select__option--active {
background: var(--color-primary-soft);
border-color: transparent;
color: var(--color-primary);
font-weight: 600;
}
.admin-multi-select__check {
flex-shrink: 0;
color: var(--color-primary);
}
</style>
+6
View File
@@ -92,6 +92,12 @@ const router = createRouter({
component: () => import('@/views/banners/BannersView.vue'), component: () => import('@/views/banners/BannersView.vue'),
meta: { title: 'بنرها' }, meta: { title: 'بنرها' },
}, },
{
path: 'internal-messages',
name: 'internal-messages',
component: () => import('@/views/internal-messages/InternalMessagesView.vue'),
meta: { title: 'پیام‌های داخلی' },
},
{ {
path: 'catalogs', path: 'catalogs',
name: 'catalogs', name: 'catalogs',
+32 -4
View File
@@ -152,6 +152,7 @@ export const catalogsApi = {
export type CreateProductPayload = { export type CreateProductPayload = {
companyId: string companyId: string
catalogId: string catalogId: string
brandId?: string | null
name: string name: string
code?: string code?: string
productCode?: string productCode?: string
@@ -213,7 +214,6 @@ export type CreateCustomerPayload = {
assignedAgentId?: string | null assignedAgentId?: string | null
appRole?: 'customer' | 'agent' appRole?: 'customer' | 'agent'
agentCode?: string | null agentCode?: string | null
brandIds?: string[]
} }
export type UpdateCustomerPayload = Partial<CreateCustomerPayload> export type UpdateCustomerPayload = Partial<CreateCustomerPayload>
@@ -478,8 +478,36 @@ export const notificationsApi = {
apiRequest<{ ok: boolean }>(`/admin/notifications/${id}/read`, { method: 'PATCH' }), apiRequest<{ ok: boolean }>(`/admin/notifications/${id}/read`, { method: 'PATCH' }),
markAllRead: () => markAllRead: () =>
apiRequest<{ ok: boolean }>('/admin/notifications/read-all', { method: 'PATCH' }), apiRequest<{ ok: boolean }>('/admin/notifications/read-all', { method: 'PATCH' }),
broadcast: (body: { title: string; body: string; customerIds?: string[]; tone?: string }) => broadcast: (body: {
apiRequest<{ sent: number }>('/admin/notifications/broadcast', { method: 'POST', body }), title: string
body: string
allActive?: boolean
customerIds?: string[]
tone?: string
}) => apiRequest<{ sent: number }>('/admin/notifications/broadcast', { method: 'POST', body }),
}
/* ── Internal messages (admin inbox) ─────────────────────── */
export type InternalMessagePayload = {
senderName: string
subject: string
preview: string
link?: string | null
}
export const internalMessagesApi = {
list: () => apiRequest<InternalMessage[]>('/admin/internal-messages'),
create: (body: InternalMessagePayload) =>
apiRequest<InternalMessage>('/admin/internal-messages', { body }),
update: (id: string, body: Partial<InternalMessagePayload>) =>
apiRequest<InternalMessage>(`/admin/internal-messages/${id}`, { method: 'PATCH', body }),
remove: (id: string) =>
apiRequest<{ ok: boolean }>(`/admin/internal-messages/${id}`, { method: 'DELETE' }),
markRead: (id: string) =>
apiRequest<InternalMessage>(`/admin/internal-messages/${id}/read`, { method: 'PATCH' }),
markAllRead: () =>
apiRequest<{ ok: boolean }>('/admin/internal-messages/read-all', { method: 'PATCH' }),
} }
/* ── Catalog requests (physical) ──────────────────────────── */ /* ── Catalog requests (physical) ──────────────────────────── */
@@ -539,7 +567,7 @@ export type Brand = {
description?: string description?: string
isActive: boolean isActive: boolean
displayOrder: number displayOrder: number
customersCount?: number productsCount?: number
} }
export type AppBanner = { export type AppBanner = {
+2 -2
View File
@@ -212,8 +212,6 @@ export interface Customer {
levelLabel?: string levelLabel?: string
assignedAgentId?: string | null assignedAgentId?: string | null
assignedAgentName?: string assignedAgentName?: string
brandIds?: string[]
brands?: { id: string; name: string }[]
} }
export interface Catalog { export interface Catalog {
@@ -234,6 +232,8 @@ export interface Product {
catalogId: string catalogId: string
catalogCode: string catalogCode: string
companyName: string companyName: string
brandId?: string | null
brandName?: string | null
name: string name: string
color: string color: string
productCode: string productCode: string
+1
View File
@@ -7,6 +7,7 @@ export const ADMIN_NAV_ITEMS: NavItem[] = [
{ name: 'products', label: 'محصولات', path: '/products', icon: 'products' }, { name: 'products', label: 'محصولات', path: '/products', icon: 'products' },
{ name: 'categories', label: 'دسته‌بندی‌ها', path: '/categories', icon: 'catalogs' }, { name: 'categories', label: 'دسته‌بندی‌ها', path: '/categories', icon: 'catalogs' },
{ name: 'banners', label: 'بنرها', path: '/banners', icon: 'image' }, { name: 'banners', label: 'بنرها', path: '/banners', icon: 'image' },
{ name: 'internal-messages', label: 'پیام‌های داخلی', path: '/internal-messages', icon: 'message' },
{ name: 'catalogs', label: 'کالیته‌ها', path: '/catalogs', icon: 'catalogs' }, { name: 'catalogs', label: 'کالیته‌ها', path: '/catalogs', icon: 'catalogs' },
{ {
name: 'catalog-requests', name: 'catalog-requests',
+2 -2
View File
@@ -86,7 +86,7 @@ onMounted(() => {
<tr> <tr>
<th>نام</th> <th>نام</th>
<th>شناسه لاتین</th> <th>شناسه لاتین</th>
<th>مشتریان</th> <th>محصولات</th>
<th>فعال</th> <th>فعال</th>
<th></th> <th></th>
</tr> </tr>
@@ -95,7 +95,7 @@ onMounted(() => {
<tr v-for="b in list" :key="b.id"> <tr v-for="b in list" :key="b.id">
<td>{{ b.name }}</td> <td>{{ b.name }}</td>
<td dir="ltr">{{ b.slug || '—' }}</td> <td dir="ltr">{{ b.slug || '—' }}</td>
<td>{{ b.customersCount ?? 0 }}</td> <td>{{ b.productsCount ?? 0 }}</td>
<td>{{ b.isActive ? 'بله' : 'خیر' }}</td> <td>{{ b.isActive ? 'بله' : 'خیر' }}</td>
<td><button type="button" class="link" @click="openEdit(b)">ویرایش</button></td> <td><button type="button" class="link" @click="openEdit(b)">ویرایش</button></td>
</tr> </tr>
+2 -49
View File
@@ -7,7 +7,7 @@ import BaseEmptyState from '@/components/ui/BaseEmptyState.vue'
import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue' import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue'
import AdminIcon from '@/components/icons/AdminIcon.vue' import AdminIcon from '@/components/icons/AdminIcon.vue'
import AdminSelect from '@/components/ui/AdminSelect.vue' import AdminSelect from '@/components/ui/AdminSelect.vue'
import { brandsApi, customersApi, levelsApi, type Brand, type CustomerLevel } from '@/services/api' import { customersApi, levelsApi, type CustomerLevel } from '@/services/api'
import CustomerSuggestionsPanel from './CustomerSuggestionsPanel.vue' import CustomerSuggestionsPanel from './CustomerSuggestionsPanel.vue'
import { ApiError } from '@/services/http' import { ApiError } from '@/services/http'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
@@ -18,7 +18,6 @@ import type { Customer, CustomerType } from '@/types'
const customerList = ref<Customer[]>([]) const customerList = ref<Customer[]>([])
const levels = ref<CustomerLevel[]>([]) const levels = ref<CustomerLevel[]>([])
const agents = ref<Customer[]>([]) const agents = ref<Customer[]>([])
const brands = ref<Brand[]>([])
const searchQuery = ref('') const searchQuery = ref('')
const roleTab = ref<'customer' | 'agent'>('customer') const roleTab = ref<'customer' | 'agent'>('customer')
const loading = ref(true) const loading = ref(true)
@@ -84,7 +83,6 @@ const emptyForm = () => ({
isActive: true, isActive: true,
levelDefId: '' as string, levelDefId: '' as string,
assignedAgentId: '' as string, assignedAgentId: '' as string,
brandIds: [] as string[],
appRole: 'customer' as 'customer' | 'agent', appRole: 'customer' as 'customer' | 'agent',
agentCode: '', agentCode: '',
}) })
@@ -109,14 +107,6 @@ const filteredCustomers = computed(() => {
) )
}) })
function toggleBrand(brandId: string, checked: boolean) {
if (checked) {
if (!form.brandIds.includes(brandId)) form.brandIds.push(brandId)
} else {
form.brandIds = form.brandIds.filter((id) => id !== brandId)
}
}
async function loadCustomers() { async function loadCustomers() {
loading.value = true loading.value = true
try { try {
@@ -143,14 +133,12 @@ function setRoleTab(tab: 'customer' | 'agent') {
async function loadLookups() { async function loadLookups() {
try { try {
const [levelsData, agentsData, brandsData] = await Promise.all([ const [levelsData, agentsData] = await Promise.all([
levelsApi.list(), levelsApi.list(),
customersApi.list(undefined, 'agent'), customersApi.list(undefined, 'agent'),
brandsApi.list(),
]) ])
levels.value = levelsData levels.value = levelsData
agents.value = agentsData agents.value = agentsData
brands.value = brandsData
} catch (err) { } catch (err) {
toast.error(err instanceof ApiError ? err.message : 'بارگذاری اطلاعات کمکی انجام نشد.') toast.error(err instanceof ApiError ? err.message : 'بارگذاری اطلاعات کمکی انجام نشد.')
} }
@@ -180,7 +168,6 @@ function openEditModal(customer: Customer) {
form.isActive = customer.isActive form.isActive = customer.isActive
form.levelDefId = customer.levelDefId ?? '' form.levelDefId = customer.levelDefId ?? ''
form.assignedAgentId = customer.assignedAgentId ?? '' form.assignedAgentId = customer.assignedAgentId ?? ''
form.brandIds = [...(customer.brandIds ?? [])]
form.appRole = customer.appRole === 'agent' ? 'agent' : 'customer' form.appRole = customer.appRole === 'agent' ? 'agent' : 'customer'
form.agentCode = customer.agentCode ?? '' form.agentCode = customer.agentCode ?? ''
formError.value = '' formError.value = ''
@@ -207,7 +194,6 @@ async function submitCustomer() {
isActive: form.isActive, isActive: form.isActive,
levelDefId: form.levelDefId || null, levelDefId: form.levelDefId || null,
assignedAgentId: form.assignedAgentId || null, assignedAgentId: form.assignedAgentId || null,
brandIds: [...form.brandIds],
appRole: form.appRole, appRole: form.appRole,
agentCode: form.appRole === 'agent' ? form.agentCode.trim() || null : null, agentCode: form.appRole === 'agent' ? form.agentCode.trim() || null : null,
} }
@@ -483,20 +469,6 @@ function clearSearch() {
</div> </div>
</div> </div>
<div v-if="brands.length" class="admin-form__field">
<label>برندها</label>
<div class="customers-page__brands">
<label v-for="brand in brands" :key="brand.id" class="customers-page__brand">
<input
type="checkbox"
:checked="form.brandIds.includes(brand.id)"
@change="toggleBrand(brand.id, ($event.target as HTMLInputElement).checked)"
/>
<span>{{ brand.name }}</span>
</label>
</div>
</div>
<div class="admin-form__field"> <div class="admin-form__field">
<BaseSwitch v-model="form.isActive" label="وضعیت" /> <BaseSwitch v-model="form.isActive" label="وضعیت" />
</div> </div>
@@ -570,23 +542,4 @@ function clearSearch() {
overflow: hidden; overflow: hidden;
} }
.customers-page__brands {
display: flex;
flex-wrap: wrap;
gap: 10px 16px;
}
.customers-page__brand {
display: flex;
align-items: center;
gap: 8px;
font-size: 0.875rem;
cursor: pointer;
}
.customers-page__brand input {
width: 16px;
height: 16px;
accent-color: var(--color-primary);
}
</style> </style>
@@ -0,0 +1,232 @@
<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue'
import BaseModal from '@/components/ui/BaseModal.vue'
import BaseEmptyState from '@/components/ui/BaseEmptyState.vue'
import { internalMessagesApi } from '@/services/api'
import { ApiError } from '@/services/http'
import { useToast } from '@/composables/useToast'
import { useAuthStore } from '@/stores/auth'
import { formatDateTime } from '@/utils/format'
import type { InternalMessage } from '@/types'
const auth = useAuthStore()
const list = ref<InternalMessage[]>([])
const loading = ref(true)
const saving = ref(false)
const modalOpen = ref(false)
const editingId = ref<string | null>(null)
const toast = useToast()
const form = reactive({
senderName: '',
subject: '',
preview: '',
link: '',
})
const modalTitle = computed(() =>
editingId.value ? 'ویرایش پیام داخلی' : 'پیام داخلی جدید',
)
async function load() {
loading.value = true
try {
list.value = await internalMessagesApi.list()
} catch (err) {
toast.error(err instanceof ApiError ? err.message : 'بارگذاری پیام‌ها ناموفق بود.')
} finally {
loading.value = false
}
}
function openAdd() {
editingId.value = null
Object.assign(form, {
senderName: auth.fullName || 'پشتیبانی',
subject: '',
preview: '',
link: '',
})
modalOpen.value = true
}
function openEdit(row: InternalMessage) {
editingId.value = row.id
Object.assign(form, {
senderName: row.senderName,
subject: row.subject,
preview: row.preview,
link: row.link || '',
})
modalOpen.value = true
}
async function save() {
if (!form.senderName.trim() || !form.subject.trim() || !form.preview.trim()) {
toast.error('فرستنده، موضوع و متن پیام الزامی است.')
return
}
saving.value = true
const payload = {
senderName: form.senderName.trim(),
subject: form.subject.trim(),
preview: form.preview.trim(),
link: form.link.trim() || null,
}
try {
if (editingId.value) {
await internalMessagesApi.update(editingId.value, payload)
toast.success('پیام به‌روزرسانی شد.')
} else {
await internalMessagesApi.create(payload)
toast.success('پیام برای همه اپراتورها ارسال شد.')
}
modalOpen.value = false
await load()
} catch (err) {
toast.error(err instanceof ApiError ? err.message : 'ذخیره پیام ناموفق بود.')
} finally {
saving.value = false
}
}
async function remove(row: InternalMessage) {
if (!window.confirm(`پیام «${row.subject}» حذف شود؟`)) return
try {
await internalMessagesApi.remove(row.id)
toast.success('پیام حذف شد.')
await load()
} catch (err) {
toast.error(err instanceof ApiError ? err.message : 'حذف پیام ناموفق بود.')
}
}
onMounted(() => {
void load()
})
</script>
<template>
<div class="internal-messages-page">
<div class="admin-page-header">
<div>
<h1 class="admin-page-header__title">پیام‌های داخلی</h1>
<p class="admin-page-header__sub">
پیام برای تیم پشتیبانی در آیکن نامه بالای پنل نمایش داده می‌شود. با کلیک روی پیام، در صورت
تعیین لینک به همان صفحه می‌روید.
</p>
</div>
<button type="button" class="admin-btn admin-btn--primary" @click="openAdd">پیام جدید</button>
</div>
<AdminPageSkeleton v-if="loading" variant="table" />
<BaseEmptyState
v-else-if="!list.length"
title="پیامی ثبت نشده"
description="اولین پیام داخلی را برای تیم ارسال کنید."
>
<button type="button" class="admin-btn admin-btn--primary admin-btn--sm" @click="openAdd">
پیام جدید
</button>
</BaseEmptyState>
<div v-else class="admin-card">
<table class="admin-table">
<thead>
<tr>
<th>موضوع</th>
<th>فرستنده</th>
<th>خلاصه</th>
<th>لینک</th>
<th>زمان</th>
<th>خوانده‌شده</th>
<th class="actions">عملیات</th>
</tr>
</thead>
<tbody>
<tr v-for="row in list" :key="row.id">
<td><strong>{{ row.subject }}</strong></td>
<td>{{ row.senderName }}</td>
<td class="preview-cell">{{ row.preview }}</td>
<td dir="ltr" class="link-cell">{{ row.link || '—' }}</td>
<td>{{ formatDateTime(row.sentAt) }}</td>
<td>{{ row.isRead ? 'بله' : 'خیر' }}</td>
<td class="actions">
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="openEdit(row)">
ویرایش
</button>
<button type="button" class="admin-btn admin-btn--ghost admin-btn--xs" @click="remove(row)">
حذف
</button>
</td>
</tr>
</tbody>
</table>
</div>
<BaseModal :open="modalOpen" :title="modalTitle" size="md" @close="modalOpen = false">
<label class="field">
نام فرستنده
<input v-model="form.senderName" type="text" class="admin-input" />
</label>
<label class="field">
موضوع
<input v-model="form.subject" type="text" class="admin-input" />
</label>
<label class="field">
متن پیام (خلاصه در لیست نامه)
<textarea v-model="form.preview" class="admin-input" rows="4" />
</label>
<label class="field">
لینک (اختیاری)
<input
v-model="form.link"
type="text"
class="admin-input"
dir="ltr"
placeholder="/orders یا /settings"
/>
</label>
<template #footer>
<button type="button" class="admin-btn admin-btn--outline" @click="modalOpen = false">انصراف</button>
<button type="button" class="admin-btn admin-btn--primary" :disabled="saving" @click="save">
{{ saving ? 'در حال ذخیره…' : 'ذخیره' }}
</button>
</template>
</BaseModal>
</div>
</template>
<style scoped>
.admin-page-header__sub {
margin: 6px 0 0;
max-width: 52rem;
font-size: 0.8125rem;
color: var(--color-text-muted);
line-height: 1.6;
}
.preview-cell {
max-width: 280px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.link-cell {
max-width: 160px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.field {
display: flex;
flex-direction: column;
gap: 6px;
margin-bottom: 12px;
font-size: 0.875rem;
font-weight: 600;
}
</style>
+58 -97
View File
@@ -8,16 +8,17 @@ import BaseEmptyState from '@/components/ui/BaseEmptyState.vue'
import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue' import ActiveSwitchCell from '@/components/ui/ActiveSwitchCell.vue'
import AdminIcon from '@/components/icons/AdminIcon.vue' import AdminIcon from '@/components/icons/AdminIcon.vue'
import AdminSelect from '@/components/ui/AdminSelect.vue' import AdminSelect from '@/components/ui/AdminSelect.vue'
import { catalogsApi, productsApi, settingsApi } from '@/services/api' import { brandsApi, catalogsApi, productsApi, settingsApi, type Brand } from '@/services/api'
import { ApiError } from '@/services/http' import { ApiError } from '@/services/http'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { formatCurrency, formatNumber } from '@/utils/format' import { formatCurrency, formatNumber } from '@/utils/format'
import type { Catalog, Product } from '@/types' import type { Catalog, Product } from '@/types'
import AdminCurrencyInput from '@/components/ui/AdminCurrencyInput.vue' import AdminCurrencyInput from '@/components/ui/AdminCurrencyInput.vue'
import { fileToCatalogImage, toCssBackgroundUrl } from '@/utils/catalogImage' import { toCssBackgroundUrl } from '@/utils/catalogImage'
const productList = ref<Product[]>([]) const productList = ref<Product[]>([])
const catalogs = ref<Catalog[]>([]) const catalogs = ref<Catalog[]>([])
const brands = ref<Brand[]>([])
const usageOptions = ref<Array<{ value: string; label: string }>>([ const usageOptions = ref<Array<{ value: string; label: string }>>([
{ value: 'trousers', label: 'شلوار' }, { value: 'trousers', label: 'شلوار' },
{ value: 'suit', label: 'کت‌وشلوار' }, { value: 'suit', label: 'کت‌وشلوار' },
@@ -53,8 +54,16 @@ const catalogFormOptions = computed(() =>
})), })),
) )
const brandFormOptions = computed(() => [
{ value: '', label: 'بدون برند' },
...brands.value
.filter((b) => b.isActive)
.map((b) => ({ value: b.id, label: b.name })),
])
const emptyForm = () => ({ const emptyForm = () => ({
catalogId: catalogs.value[0]?.id ?? '', catalogId: catalogs.value[0]?.id ?? '',
brandId: '',
name: '', name: '',
color: '', color: '',
productCode: '', productCode: '',
@@ -70,8 +79,11 @@ const emptyForm = () => ({
subtitle: '', subtitle: '',
rollMeters: 50, rollMeters: 50,
isNew: false, isNew: false,
colorRows: [{ name: '', imageUrl: '' }] as Array<{ name: string; imageUrl: string }>, colorRows: [{ id: crypto.randomUUID(), name: '', imageUrl: '' }] as Array<{
extraImageUrls: [] as string[], id: string
name: string
imageUrl: string
}>,
}) })
const form = reactive(emptyForm()) const form = reactive(emptyForm())
@@ -104,13 +116,15 @@ const filteredProducts = computed(() => {
async function loadData() { async function loadData() {
loading.value = true loading.value = true
try { try {
const [productsData, catalogsData, settings] = await Promise.all([ const [productsData, catalogsData, brandsData, settings] = await Promise.all([
productsApi.list(), productsApi.list(),
catalogsApi.list(), catalogsApi.list(),
brandsApi.list(),
settingsApi.get().catch(() => null), settingsApi.get().catch(() => null),
]) ])
productList.value = productsData productList.value = productsData
catalogs.value = catalogsData catalogs.value = catalogsData
brands.value = brandsData
if (settings?.categoryDefsJson?.usage?.length) { if (settings?.categoryDefsJson?.usage?.length) {
usageOptions.value = [ usageOptions.value = [
...settings.categoryDefsJson.usage.map((item) => ({ ...settings.categoryDefsJson.usage.map((item) => ({
@@ -152,6 +166,7 @@ function openEditModal(product: Product) {
modalMode.value = 'edit' modalMode.value = 'edit'
editingId.value = product.id editingId.value = product.id
form.catalogId = product.catalogId form.catalogId = product.catalogId
form.brandId = product.brandId ?? ''
form.name = product.name form.name = product.name
form.color = product.color form.color = product.color
form.productCode = product.productCode form.productCode = product.productCode
@@ -172,53 +187,47 @@ function openEditModal(product: Product) {
const gallery = [...(product.imageUrls ?? [])] const gallery = [...(product.imageUrls ?? [])]
if (colors.length) { if (colors.length) {
form.colorRows = colors.map((name, index) => ({ form.colorRows = colors.map((name, index) => ({
id: crypto.randomUUID(),
name, name,
imageUrl: colorImages[index] || gallery[index] || '', imageUrl: colorImages[index] || gallery[index] || '',
})) }))
const used = new Set(form.colorRows.map((row) => row.imageUrl).filter(Boolean)) const used = new Set(form.colorRows.map((row) => row.imageUrl).filter(Boolean))
form.extraImageUrls = gallery.filter((url) => !used.has(url)) for (const url of gallery) {
if (!used.has(url)) {
form.colorRows.push({
id: crypto.randomUUID(),
name: `رنگ ${form.colorRows.length + 1}`,
imageUrl: url,
})
used.add(url)
}
}
} else if (gallery.length) {
form.colorRows = gallery.map((url, index) => ({
id: crypto.randomUUID(),
name: index === 0 ? product.color || '' : `رنگ ${index + 1}`,
imageUrl: url,
}))
} else { } else {
form.colorRows = [{ name: product.color || '', imageUrl: gallery[0] || '' }] form.colorRows = [
form.extraImageUrls = gallery.slice(1) { id: crypto.randomUUID(), name: product.color || '', imageUrl: '' },
]
} }
formError.value = '' formError.value = ''
} }
function addColorRow() { function addColorRow() {
form.colorRows.push({ name: '', imageUrl: '' }) form.colorRows.push({ id: crypto.randomUUID(), name: '', imageUrl: '' })
} }
function removeColorRow(index: number) { function removeColorRow(index: number) {
if (form.colorRows.length <= 1) { if (form.colorRows.length <= 1) {
form.colorRows[0] = { name: '', imageUrl: '' } form.colorRows[0] = { id: form.colorRows[0]?.id ?? crypto.randomUUID(), name: '', imageUrl: '' }
return return
} }
form.colorRows.splice(index, 1) form.colorRows.splice(index, 1)
} }
const imageBusy = ref(false)
async function addProductImages(event: Event) {
const input = event.target as HTMLInputElement
const files = [...(input.files ?? [])]
input.value = ''
if (!files.length) return
imageBusy.value = true
formError.value = ''
try {
const next = await Promise.all(files.map((file) => fileToCatalogImage(file)))
form.extraImageUrls = [...form.extraImageUrls, ...next]
} catch (e) {
formError.value = e instanceof Error ? e.message : 'بارگذاری تصویر ناموفق بود.'
} finally {
imageBusy.value = false
}
}
function removeProductImage(index: number) {
form.extraImageUrls.splice(index, 1)
}
function buildProductMedia() { function buildProductMedia() {
const colorEntries: Array<{ name: string; imageUrl?: string }> = [] const colorEntries: Array<{ name: string; imageUrl?: string }> = []
const imageUrls: string[] = [] const imageUrls: string[] = []
@@ -244,8 +253,6 @@ function buildProductMedia() {
pushImage(imageUrl) pushImage(imageUrl)
}) })
form.extraImageUrls.forEach((url) => pushImage(url))
return { colorEntries, imageUrls } return { colorEntries, imageUrls }
} }
@@ -294,6 +301,7 @@ async function submitProduct() {
price: form.price, price: form.price,
internalStock: form.internalStock, internalStock: form.internalStock,
isActive: form.isActive, isActive: form.isActive,
brandId: form.brandId || null,
...catalogPayload, ...catalogPayload,
}) })
const index = productList.value.findIndex((p) => p.id === editingId.value) const index = productList.value.findIndex((p) => p.id === editingId.value)
@@ -312,6 +320,7 @@ async function submitProduct() {
price: form.price, price: form.price,
internalStock: form.internalStock, internalStock: form.internalStock,
isActive: form.isActive, isActive: form.isActive,
brandId: form.brandId || null,
...catalogPayload, ...catalogPayload,
}) })
productList.value.push(created) productList.value.push(created)
@@ -396,6 +405,7 @@ function clearFilters() {
<th>کد محصول</th> <th>کد محصول</th>
<th>نام</th> <th>نام</th>
<th>رنگ</th> <th>رنگ</th>
<th>برند</th>
<th>کالیته</th> <th>کالیته</th>
<th>شرکت</th> <th>شرکت</th>
<th class="numeric">حداقل متراژ</th> <th class="numeric">حداقل متراژ</th>
@@ -418,6 +428,7 @@ function clearFilters() {
<td>{{ product.productCode }}</td> <td>{{ product.productCode }}</td>
<td>{{ product.name }}</td> <td>{{ product.name }}</td>
<td>{{ product.color }}</td> <td>{{ product.color }}</td>
<td>{{ product.brandName || '—' }}</td>
<td>{{ product.catalogCode }}</td> <td>{{ product.catalogCode }}</td>
<td>{{ product.companyName }}</td> <td>{{ product.companyName }}</td>
<td class="numeric">{{ formatNumber(product.minMeterage) }} متر</td> <td class="numeric">{{ formatNumber(product.minMeterage) }} متر</td>
@@ -485,6 +496,17 @@ function clearFilters() {
/> />
</div> </div>
<div class="admin-form__field">
<label>برند</label>
<AdminSelect
v-model="form.brandId"
block
aria-label="برند محصول"
placeholder="انتخاب برند"
:options="brandFormOptions"
/>
</div>
<div class="admin-form__row"> <div class="admin-form__row">
<div class="admin-form__field"> <div class="admin-form__field">
<label for="product-name">نام محصول <span class="admin-form__required">*</span></label> <label for="product-name">نام محصول <span class="admin-form__required">*</span></label>
@@ -571,7 +593,7 @@ function clearFilters() {
تصویر با زدن «ذخیره تغییرات» روی سرور ثبت می‌شود. تصویر با زدن «ذخیره تغییرات» روی سرور ثبت می‌شود.
</p> </p>
<div class="color-variants"> <div class="color-variants">
<div v-for="(row, index) in form.colorRows" :key="index" class="color-variant"> <div v-for="(row, index) in form.colorRows" :key="row.id" class="color-variant">
<AdminImagePicker <AdminImagePicker
:model-value="row.imageUrl || null" :model-value="row.imageUrl || null"
@update:model-value="row.imageUrl = $event ?? ''" @update:model-value="row.imageUrl = $event ?? ''"
@@ -594,27 +616,6 @@ function clearFilters() {
</div> </div>
</div> </div>
<div class="admin-form__field">
<label>تصاویر عمومی محصول (اختیاری)</label>
<p class="color-variants__hint">پس از انتخاب فایل، حتماً دکمه ذخیره را بزنید.</p>
<div class="product-images">
<div
v-for="(src, index) in form.extraImageUrls"
:key="`${index}-${src.slice(0, 24)}`"
class="product-images__item"
>
<span class="product-images__thumb" :style="{ backgroundImage: toCssBackgroundUrl(src) }" />
<button type="button" class="product-images__remove" @click="removeProductImage(index)">
حذف
</button>
</div>
<label class="product-images__add">
<input type="file" accept="image/*" multiple hidden :disabled="imageBusy" @change="addProductImages" />
{{ imageBusy ? '...' : '+ افزودن تصویر' }}
</label>
</div>
</div>
<div class="admin-form__field"> <div class="admin-form__field">
<BaseSwitch v-model="form.isNew" label="محصول جدید" /> <BaseSwitch v-model="form.isNew" label="محصول جدید" />
</div> </div>
@@ -714,44 +715,4 @@ function clearFilters() {
align-items: flex-start; align-items: flex-start;
} }
.product-images {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.product-images__item {
display: flex;
flex-direction: column;
gap: 6px;
width: 92px;
}
.product-images__thumb {
display: block;
width: 92px;
height: 72px;
border-radius: 10px;
background: #e8eceb center / cover no-repeat;
border: 1px solid var(--color-border);
}
.product-images__remove,
.product-images__add {
border: 1px dashed var(--color-border);
background: transparent;
border-radius: 8px;
padding: 6px 8px;
font-size: 0.75rem;
cursor: pointer;
text-align: center;
}
.product-images__add {
display: grid;
place-items: center;
min-width: 92px;
min-height: 72px;
color: var(--color-text-muted);
}
</style> </style>
+66 -47
View File
@@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue' import { computed, onMounted, reactive, ref, watch } from 'vue'
import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue' import AdminPageSkeleton from '@/components/ui/AdminPageSkeleton.vue'
import AdminMultiSelect from '@/components/ui/AdminMultiSelect.vue'
import { useToast } from '@/composables/useToast' import { useToast } from '@/composables/useToast'
import { import {
customersApi, customersApi,
@@ -34,25 +35,23 @@ const broadcast = reactive({ title: '', body: '' })
const broadcasting = ref(false) const broadcasting = ref(false)
const customers = ref<Customer[]>([]) const customers = ref<Customer[]>([])
const selectedCustomerIds = ref<string[]>([]) const selectedCustomerIds = ref<string[]>([])
const sendToAllActive = ref(false)
const loading = ref(true) const loading = ref(true)
const saving = ref(false) const saving = ref(false)
const broadcastButtonLabel = computed(() => { const activeCustomerOptions = computed(() =>
if (broadcasting.value) return 'در حال ارسال…' customers.value
if (selectedCustomerIds.value.length) { .filter((c) => c.isActive)
return `ارسال به ${selectedCustomerIds.value.length} مشتری انتخاب‌شده` .map((c) => ({
} value: c.id,
return 'ارسال به همه مشتریان فعال' label: `${c.name} · ${c.phone}${c.appRole === 'agent' ? ' (واسطه)' : ''}`,
}) })),
)
function toggleCustomer(id: string, checked: boolean) { watch(sendToAllActive, (all) => {
if (checked) { if (all) selectedCustomerIds.value = []
if (!selectedCustomerIds.value.includes(id)) selectedCustomerIds.value.push(id) })
} else {
selectedCustomerIds.value = selectedCustomerIds.value.filter((cid) => cid !== id)
}
}
onMounted(async () => { onMounted(async () => {
loading.value = true loading.value = true
@@ -97,21 +96,41 @@ async function saveSettings() {
} }
async function sendBroadcast() { async function sendBroadcast() {
if (!broadcast.title.trim() || !broadcast.body.trim() || broadcasting.value) return if (broadcasting.value) return
const title = broadcast.title.trim()
const body = broadcast.body.trim()
if (!title || !body) {
toast.error('عنوان و متن اعلان را وارد کنید.')
return
}
if (!sendToAllActive.value && !selectedCustomerIds.value.length) {
toast.error('حداقل یک مشتری انتخاب کنید یا «ارسال به همه مشتریان فعال» را فعال کنید.')
return
}
broadcasting.value = true broadcasting.value = true
try { try {
const payload: { title: string; body: string; customerIds?: string[] } = { const payload: {
title: broadcast.title.trim(), title: string
body: broadcast.body.trim(), body: string
} allActive?: boolean
if (selectedCustomerIds.value.length) { customerIds?: string[]
} = { title, body }
if (sendToAllActive.value) {
payload.allActive = true
} else {
payload.customerIds = [...selectedCustomerIds.value] payload.customerIds = [...selectedCustomerIds.value]
} }
const res = await notificationsApi.broadcast(payload) const res = await notificationsApi.broadcast(payload)
if (!res.sent) {
toast.warning('هیچ مشتری فعالی برای ارسال پیدا نشد.')
return
}
toast.success(`اعلان برای ${res.sent} مشتری ارسال شد.`) toast.success(`اعلان برای ${res.sent} مشتری ارسال شد.`)
broadcast.title = '' broadcast.title = ''
broadcast.body = '' broadcast.body = ''
selectedCustomerIds.value = [] selectedCustomerIds.value = []
sendToAllActive.value = false
} catch (e) { } catch (e) {
toast.error(e instanceof Error ? e.message : 'ارسال اعلان ناموفق بود.') toast.error(e instanceof Error ? e.message : 'ارسال اعلان ناموفق بود.')
} finally { } finally {
@@ -262,26 +281,33 @@ async function sendBroadcast() {
<label for="broadcast-body">متن</label> <label for="broadcast-body">متن</label>
<textarea id="broadcast-body" v-model="broadcast.body" class="admin-input" rows="4" /> <textarea id="broadcast-body" v-model="broadcast.body" class="admin-input" rows="4" />
</div> </div>
<div v-if="customers.length" class="admin-form__field"> <label class="settings-page__check settings-page__check--block">
<label>گیرندگان (بدون انتخاب = همه)</label> <input v-model="sendToAllActive" type="checkbox" />
<div class="settings-page__customers"> <span>ارسال به همه مشتریان فعال (نقش مشتری در اپ)</span>
<label v-for="customer in customers" :key="customer.id" class="settings-page__check"> </label>
<input <div class="admin-form__field">
type="checkbox" <label for="broadcast-recipients">گیرندگان</label>
:checked="selectedCustomerIds.includes(customer.id)" <AdminMultiSelect
@change="toggleCustomer(customer.id, ($event.target as HTMLInputElement).checked)" id="broadcast-recipients"
/> v-model="selectedCustomerIds"
<span>{{ customer.name }} <span class="settings-page__phone" dir="ltr">{{ customer.phone }}</span></span> :options="activeCustomerOptions"
</label> :disabled="sendToAllActive || !activeCustomerOptions.length"
</div> block
placeholder="انتخاب مشتریان…"
aria-label="گیرندگان اعلان"
/>
<p class="settings-page__hint">
با غیرفعال بودن «ارسال به همه»، یک یا چند مشتری فعال را انتخاب کنید. اعلان در بخش
اعلان‌های اپ مشتری نمایش داده می‌شود.
</p>
</div> </div>
<button <button
type="button" type="button"
class="admin-btn admin-btn--outline" class="admin-btn admin-btn--primary settings-page__broadcast-submit"
:disabled="broadcasting || !broadcast.title.trim() || !broadcast.body.trim()" :disabled="broadcasting"
@click="sendBroadcast" @click="sendBroadcast"
> >
{{ broadcastButtonLabel }} {{ broadcasting ? 'در حال ارسال…' : 'ثبت و ارسال اعلان' }}
</button> </button>
</section> </section>
</div> </div>
@@ -325,19 +351,12 @@ async function sendBroadcast() {
accent-color: var(--color-primary); accent-color: var(--color-primary);
} }
.settings-page__customers { .settings-page__check--block {
max-height: 220px; margin-bottom: 12px;
overflow: auto;
display: flex;
flex-direction: column;
gap: 8px;
padding: 8px 0;
} }
.settings-page__phone { .settings-page__broadcast-submit {
color: var(--color-text-muted); margin-top: 4px;
font-size: 0.8125rem;
margin-right: 6px;
} }
.settings-page__hint { .settings-page__hint {