Compare commits
3
Commits
1e0b638a19
..
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0998a1ec37 | ||
|
|
4979aa8b75 | ||
|
|
34d3689448 |
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 246.7 46.59">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #10897d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #16c1a8;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #43f7dd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="cls-1" d="M199.15,23.8v19.86c0,1.48-1.2,2.67-2.67,2.67h-8.09c-1.48,0-2.67-1.2-2.67-2.67V15.92c0-.91.47-1.76,1.23-2.25l12.85-8.23c1.13-.72,2.63-.5,3.49.53l11.02,13.14c.18.21.49.22.68.03l8.04-8.21c1.04-1.06,2.75-1.07,3.8-.02l5.99,5.99c1.05,1.05,1.05,2.74,0,3.78l-16.98,16.98c-1.1,1.1-2.92,1.03-3.93-.15l-11.96-14.01c-.28-.33-.81-.13-.81.3h0Z"/>
|
||||
<circle class="cls-3" cx="237.24" cy="6.48" r="6.48"/>
|
||||
<path class="cls-3" d="M242.84,16.26h0c2.13,0,3.86,1.73,3.86,3.86v22.61c0,2.13-1.73,3.86-3.86,3.86h0c-2.13,0-3.86-1.73-3.86-3.86v-22.61c0-2.13,1.73-3.86,3.86-3.86Z"/>
|
||||
<path class="cls-2" d="M233.49,25.66h0c2.13,0,3.86,1.73,3.86,3.86v13.21c0,2.13-1.73,3.86-3.86,3.86h0c-2.13,0-3.86-1.73-3.86-3.86v-13.21c0-2.13,1.73-3.86,3.86-3.86Z"/>
|
||||
<rect class="cls-1" x="220.08" y="35.24" width="7.72" height="11.35" rx="3.86" ry="3.86"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-1" d="M132.19,17.19v2.66c0,1.27,1.03,2.29,2.29,2.29h2.66c1.27,0,2.29-1.03,2.29-2.29v-2.66c0-1.27-1.03-2.29-2.29-2.29h-2.66c-1.27,0-2.29,1.03-2.29,2.29Z"/>
|
||||
<path class="cls-1" d="M102.53,17.19v2.66c0,1.27,1.03,2.29,2.29,2.29h12.91c1.27,0,2.29-1.03,2.29-2.29v-2.66c0-1.27-1.03-2.29-2.29-2.29h-12.91c-1.27,0-2.29,1.03-2.29,2.29Z"/>
|
||||
<path class="cls-1" d="M13.73,20.2v2.66c0,1.27,1.03,2.29,2.29,2.29h12.91c1.27,0,2.29-1.03,2.29-2.29v-2.66c0-1.27-1.03-2.29-2.29-2.29h-12.91c-1.27,0-2.29,1.03-2.29,2.29Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M173.05,30.25c-.52-1.2-1.24-2.25-2.14-3.16-.91-.9-1.96-1.62-3.16-2.14-1.2-.52-2.49-.78-3.88-.78h-3.25c-1.12,0-2.2.19-3.24.58-1.04.39-1.98.92-2.81,1.59-.83.68-1.54,1.49-2.11,2.43-.58.95-.97,1.98-1.16,3.1l-1.16,5.97c-.16.66-.47,1.18-.96,1.56-.48.39-1.05.58-1.71.58h-6.49c-.74,0-1.31-.26-1.74-.78-.43-.52-.56-1.15-.41-1.88l1.35-6.87c.25-1.25-.58-2.47-1.84-2.69l-2.52-.46c-1.22-.22-2.4.57-2.65,1.79l-1.81,8.7c-.12.66-.43,1.19-.93,1.59-.5.4-1.08.61-1.74.61h-10.08c-.74,0-1.31-.26-1.74-.78-.43-.52-.56-1.15-.41-1.88l1.35-6.87c.25-1.25-.58-2.47-1.84-2.69l-2.52-.46c-1.22-.22-2.4.57-2.65,1.79l-1.81,8.7c-.12.66-.43,1.19-.93,1.59-.5.4-1.08.61-1.74.61h-9.5c-.66,0-1.22-.22-1.68-.67-.46-.44-.69-.99-.69-1.65v-3.53c0-1.39-.26-2.69-.78-3.91-.52-1.22-1.24-2.28-2.14-3.19-.91-.91-1.96-1.62-3.16-2.14-1.2-.52-2.49-.78-3.88-.78h-21.78l9.58-11.3c.79-.94.71-2.33-.19-3.17l-1.79-1.65c-.96-.88-2.45-.79-3.29.2l-14.66,17.17c-.79.92-.72,2.3.16,3.14l3.59,3.43h29.14c.54,0,1.02.2,1.45.61.43.4.64.9.64,1.48v4.11c0,.54-.19.99-.58,1.33-.39.35-.79.52-1.22.52h-38.23c-.66,0-1.21-.22-1.65-.67-.44-.44-.67-.99-.67-1.65v-8.54c0-1.27-1.03-2.29-2.29-2.29h-2.6c-1.27,0-2.29,1.03-2.29,2.29v8.6c0,.66-.22,1.2-.67,1.62-.44.43-.99.64-1.65.64H11.53c-1.2,0-2.22-.4-3.07-1.21-.85-.81-1.27-1.83-1.27-3.07v-8.25c0-1.27-1.03-2.29-2.29-2.29h-2.6c-1.27,0-2.29,1.03-2.29,2.29v6.47c0,1.66.32,3.24.96,4.72.64,1.49,1.51,2.79,2.61,3.91,1.1,1.12,2.39,2,3.88,2.63,1.49.64,3.06.96,4.72.96h22.01c1.39,0,2.69-.25,3.91-.75,1.22-.5,2.33-1.33,3.33-2.49,1.93,2.16,4.29,3.24,7.07,3.24h35.8c1.24,0,2.44-.23,3.62-.69,1.18-.46,2.25-1.08,3.22-1.85.93.77,1.98,1.39,3.16,1.85,1.18.46,2.44.69,3.79.69h7.59c1.23,0,2.48-.29,3.74-.87,1.25-.58,2.25-1.37,2.98-2.38.58,1,1.46,1.8,2.64,2.38,1.18.58,2.36.87,3.56.87h9.44c1.23,0,2.48-.29,3.74-.87,1.25-.58,2.25-1.37,2.98-2.38.58,1,1.46,1.8,2.64,2.38,1.18.58,2.37.87,3.56.87h6.03c1.24,0,2.39-.23,3.47-.69,1.08-.46,2.07-1.29,2.95-2.49.89,1.24,1.86,2.08,2.93,2.52,1.06.44,2.15.67,3.27.67h6.6c1.04,0,2.03-.2,2.95-.61.93-.4,1.74-.96,2.43-1.65s1.24-1.51,1.65-2.43c.4-.93.61-1.91.61-2.95v-4.36c0-1.39-.26-2.68-.78-3.88ZM166.65,39.98h-7.99c-.43,0-.77-.15-1.04-.46-.27-.31-.37-.67-.29-1.1l.93-4.64c.08-.42.27-.77.58-1.04.31-.27.67-.4,1.1-.4h4.98c.5,0,.92.17,1.24.52.33.35.49.75.49,1.22v5.91Z"/>
|
||||
<path class="cls-3" d="M166.7,16.1c-.65.12-1.24.25-1.77.37-.53.12-1.12.26-1.79.4-.67.14-1.34.29-2.02.44-1.3.29-2.73.61-4.28.94-.68.15-1.34-.31-1.45-1l-.24-1.57c-.1-.63.31-1.23.94-1.36l4.01-.88c-.09-.34-.16-.61-.21-.81-.05-.2-.09-.38-.12-.54-.03-.15-.07-.33-.12-.54-.05-.2-.09-.47-.12-.81-.06-.59,0-1.13.21-1.63.2-.5.48-.95.84-1.35.36-.4.78-.73,1.26-.98.48-.25.95-.4,1.42-.47l2.42-.42c.56-.09,1.11-.05,1.65.14.54.19,1.02.46,1.44.81s.78.78,1.07,1.28c.29.5.49,1.01.58,1.54l.23,1.4c.12.62.09,1.21-.09,1.77-.19.56-.47,1.05-.84,1.49-.37.43-.82.81-1.35,1.12-.53.31-1.09.53-1.68.65ZM166.79,11.54s-.02-.1-.05-.21c-.03-.11-.06-.24-.09-.4-.06-.16-.12-.34-.19-.56-.16-.31-.39-.42-.7-.33l-1.44.37c-.37.06-.53.26-.47.6l.51,1.63,1.86-.47c.5-.09.68-.31.56-.65Z"/>
|
||||
<path class="cls-3" d="M105.49,6.57l11.59-2.48c.64-.14,1.28.27,1.43.91l.27,1.15c.15.66-.26,1.31-.93,1.45l-11.66,2.46c-.67.14-1.32-.3-1.44-.97l-.2-1.13c-.11-.64.3-1.25.94-1.39Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 197.7 151.02">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #10897d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #16c1a8;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #43f7dd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path class="cls-1" d="M43.57,77.15v64.37c0,4.79-3.88,8.67-8.67,8.67H8.67c-4.79,0-8.67-3.88-8.67-8.67V51.62c0-2.96,1.51-5.71,3.99-7.3l41.67-26.69c3.67-2.35,8.52-1.61,11.32,1.73l35.74,42.59c.57.68,1.6.72,2.22.09l26.07-26.6c3.38-3.45,8.92-3.47,12.33-.06l19.42,19.42c3.39,3.39,3.39,8.88,0,12.26l-55.06,55.06c-3.58,3.58-9.45,3.35-12.73-.5l-38.76-45.42c-.9-1.06-2.64-.42-2.64.97Z"/>
|
||||
<circle class="cls-3" cx="167.06" cy="21.02" r="21.02"/>
|
||||
<rect class="cls-3" x="172.68" y="52.7" width="25.02" height="98.32" rx="12.51" ry="12.51"/>
|
||||
<rect class="cls-2" x="142.38" y="83.17" width="25.02" height="67.85" rx="12.51" ry="12.51"/>
|
||||
<rect class="cls-1" x="111.4" y="114.24" width="25.02" height="36.78" rx="12.51" ry="12.51"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
+1
-1
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>منتکت | Montact CRM</title>
|
||||
<link rel="icon" type="image/png" href="/brand/logo.png" />
|
||||
<link rel="icon" type="image/svg+xml" href="/brand/logo.svg" />
|
||||
<style>
|
||||
/* چیدمان اولیه؛ استایل کامپوننت بعداً همین را دقیقتر میکند */
|
||||
.app-frame {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 197.7 151.02">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #10897d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #16c1a8;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #43f7dd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<path class="cls-1" d="M43.57,77.15v64.37c0,4.79-3.88,8.67-8.67,8.67H8.67c-4.79,0-8.67-3.88-8.67-8.67V51.62c0-2.96,1.51-5.71,3.99-7.3l41.67-26.69c3.67-2.35,8.52-1.61,11.32,1.73l35.74,42.59c.57.68,1.6.72,2.22.09l26.07-26.6c3.38-3.45,8.92-3.47,12.33-.06l19.42,19.42c3.39,3.39,3.39,8.88,0,12.26l-55.06,55.06c-3.58,3.58-9.45,3.35-12.73-.5l-38.76-45.42c-.9-1.06-2.64-.42-2.64.97Z"/>
|
||||
<circle class="cls-3" cx="167.06" cy="21.02" r="21.02"/>
|
||||
<rect class="cls-3" x="172.68" y="52.7" width="25.02" height="98.32" rx="12.51" ry="12.51"/>
|
||||
<rect class="cls-2" x="142.38" y="83.17" width="25.02" height="67.85" rx="12.51" ry="12.51"/>
|
||||
<rect class="cls-1" x="111.4" y="114.24" width="25.02" height="36.78" rx="12.51" ry="12.51"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" data-name="Layer 2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 246.7 46.59">
|
||||
<defs>
|
||||
<style>
|
||||
.cls-1 {
|
||||
fill: #10897d;
|
||||
}
|
||||
|
||||
.cls-2 {
|
||||
fill: #16c1a8;
|
||||
}
|
||||
|
||||
.cls-3 {
|
||||
fill: #43f7dd;
|
||||
}
|
||||
</style>
|
||||
</defs>
|
||||
<g id="Layer_1-2" data-name="Layer 1">
|
||||
<g>
|
||||
<path class="cls-1" d="M199.15,23.8v19.86c0,1.48-1.2,2.67-2.67,2.67h-8.09c-1.48,0-2.67-1.2-2.67-2.67V15.92c0-.91.47-1.76,1.23-2.25l12.85-8.23c1.13-.72,2.63-.5,3.49.53l11.02,13.14c.18.21.49.22.68.03l8.04-8.21c1.04-1.06,2.75-1.07,3.8-.02l5.99,5.99c1.05,1.05,1.05,2.74,0,3.78l-16.98,16.98c-1.1,1.1-2.92,1.03-3.93-.15l-11.96-14.01c-.28-.33-.81-.13-.81.3h0Z"/>
|
||||
<circle class="cls-3" cx="237.24" cy="6.48" r="6.48"/>
|
||||
<path class="cls-3" d="M242.84,16.26h0c2.13,0,3.86,1.73,3.86,3.86v22.61c0,2.13-1.73,3.86-3.86,3.86h0c-2.13,0-3.86-1.73-3.86-3.86v-22.61c0-2.13,1.73-3.86,3.86-3.86Z"/>
|
||||
<path class="cls-2" d="M233.49,25.66h0c2.13,0,3.86,1.73,3.86,3.86v13.21c0,2.13-1.73,3.86-3.86,3.86h0c-2.13,0-3.86-1.73-3.86-3.86v-13.21c0-2.13,1.73-3.86,3.86-3.86Z"/>
|
||||
<rect class="cls-1" x="220.08" y="35.24" width="7.72" height="11.35" rx="3.86" ry="3.86"/>
|
||||
</g>
|
||||
<g>
|
||||
<path class="cls-1" d="M132.19,17.19v2.66c0,1.27,1.03,2.29,2.29,2.29h2.66c1.27,0,2.29-1.03,2.29-2.29v-2.66c0-1.27-1.03-2.29-2.29-2.29h-2.66c-1.27,0-2.29,1.03-2.29,2.29Z"/>
|
||||
<path class="cls-1" d="M102.53,17.19v2.66c0,1.27,1.03,2.29,2.29,2.29h12.91c1.27,0,2.29-1.03,2.29-2.29v-2.66c0-1.27-1.03-2.29-2.29-2.29h-12.91c-1.27,0-2.29,1.03-2.29,2.29Z"/>
|
||||
<path class="cls-1" d="M13.73,20.2v2.66c0,1.27,1.03,2.29,2.29,2.29h12.91c1.27,0,2.29-1.03,2.29-2.29v-2.66c0-1.27-1.03-2.29-2.29-2.29h-12.91c-1.27,0-2.29,1.03-2.29,2.29Z"/>
|
||||
<g>
|
||||
<path class="cls-1" d="M173.05,30.25c-.52-1.2-1.24-2.25-2.14-3.16-.91-.9-1.96-1.62-3.16-2.14-1.2-.52-2.49-.78-3.88-.78h-3.25c-1.12,0-2.2.19-3.24.58-1.04.39-1.98.92-2.81,1.59-.83.68-1.54,1.49-2.11,2.43-.58.95-.97,1.98-1.16,3.1l-1.16,5.97c-.16.66-.47,1.18-.96,1.56-.48.39-1.05.58-1.71.58h-6.49c-.74,0-1.31-.26-1.74-.78-.43-.52-.56-1.15-.41-1.88l1.35-6.87c.25-1.25-.58-2.47-1.84-2.69l-2.52-.46c-1.22-.22-2.4.57-2.65,1.79l-1.81,8.7c-.12.66-.43,1.19-.93,1.59-.5.4-1.08.61-1.74.61h-10.08c-.74,0-1.31-.26-1.74-.78-.43-.52-.56-1.15-.41-1.88l1.35-6.87c.25-1.25-.58-2.47-1.84-2.69l-2.52-.46c-1.22-.22-2.4.57-2.65,1.79l-1.81,8.7c-.12.66-.43,1.19-.93,1.59-.5.4-1.08.61-1.74.61h-9.5c-.66,0-1.22-.22-1.68-.67-.46-.44-.69-.99-.69-1.65v-3.53c0-1.39-.26-2.69-.78-3.91-.52-1.22-1.24-2.28-2.14-3.19-.91-.91-1.96-1.62-3.16-2.14-1.2-.52-2.49-.78-3.88-.78h-21.78l9.58-11.3c.79-.94.71-2.33-.19-3.17l-1.79-1.65c-.96-.88-2.45-.79-3.29.2l-14.66,17.17c-.79.92-.72,2.3.16,3.14l3.59,3.43h29.14c.54,0,1.02.2,1.45.61.43.4.64.9.64,1.48v4.11c0,.54-.19.99-.58,1.33-.39.35-.79.52-1.22.52h-38.23c-.66,0-1.21-.22-1.65-.67-.44-.44-.67-.99-.67-1.65v-8.54c0-1.27-1.03-2.29-2.29-2.29h-2.6c-1.27,0-2.29,1.03-2.29,2.29v8.6c0,.66-.22,1.2-.67,1.62-.44.43-.99.64-1.65.64H11.53c-1.2,0-2.22-.4-3.07-1.21-.85-.81-1.27-1.83-1.27-3.07v-8.25c0-1.27-1.03-2.29-2.29-2.29h-2.6c-1.27,0-2.29,1.03-2.29,2.29v6.47c0,1.66.32,3.24.96,4.72.64,1.49,1.51,2.79,2.61,3.91,1.1,1.12,2.39,2,3.88,2.63,1.49.64,3.06.96,4.72.96h22.01c1.39,0,2.69-.25,3.91-.75,1.22-.5,2.33-1.33,3.33-2.49,1.93,2.16,4.29,3.24,7.07,3.24h35.8c1.24,0,2.44-.23,3.62-.69,1.18-.46,2.25-1.08,3.22-1.85.93.77,1.98,1.39,3.16,1.85,1.18.46,2.44.69,3.79.69h7.59c1.23,0,2.48-.29,3.74-.87,1.25-.58,2.25-1.37,2.98-2.38.58,1,1.46,1.8,2.64,2.38,1.18.58,2.36.87,3.56.87h9.44c1.23,0,2.48-.29,3.74-.87,1.25-.58,2.25-1.37,2.98-2.38.58,1,1.46,1.8,2.64,2.38,1.18.58,2.37.87,3.56.87h6.03c1.24,0,2.39-.23,3.47-.69,1.08-.46,2.07-1.29,2.95-2.49.89,1.24,1.86,2.08,2.93,2.52,1.06.44,2.15.67,3.27.67h6.6c1.04,0,2.03-.2,2.95-.61.93-.4,1.74-.96,2.43-1.65s1.24-1.51,1.65-2.43c.4-.93.61-1.91.61-2.95v-4.36c0-1.39-.26-2.68-.78-3.88ZM166.65,39.98h-7.99c-.43,0-.77-.15-1.04-.46-.27-.31-.37-.67-.29-1.1l.93-4.64c.08-.42.27-.77.58-1.04.31-.27.67-.4,1.1-.4h4.98c.5,0,.92.17,1.24.52.33.35.49.75.49,1.22v5.91Z"/>
|
||||
<path class="cls-3" d="M166.7,16.1c-.65.12-1.24.25-1.77.37-.53.12-1.12.26-1.79.4-.67.14-1.34.29-2.02.44-1.3.29-2.73.61-4.28.94-.68.15-1.34-.31-1.45-1l-.24-1.57c-.1-.63.31-1.23.94-1.36l4.01-.88c-.09-.34-.16-.61-.21-.81-.05-.2-.09-.38-.12-.54-.03-.15-.07-.33-.12-.54-.05-.2-.09-.47-.12-.81-.06-.59,0-1.13.21-1.63.2-.5.48-.95.84-1.35.36-.4.78-.73,1.26-.98.48-.25.95-.4,1.42-.47l2.42-.42c.56-.09,1.11-.05,1.65.14.54.19,1.02.46,1.44.81s.78.78,1.07,1.28c.29.5.49,1.01.58,1.54l.23,1.4c.12.62.09,1.21-.09,1.77-.19.56-.47,1.05-.84,1.49-.37.43-.82.81-1.35,1.12-.53.31-1.09.53-1.68.65ZM166.79,11.54s-.02-.1-.05-.21c-.03-.11-.06-.24-.09-.4-.06-.16-.12-.34-.19-.56-.16-.31-.39-.42-.7-.33l-1.44.37c-.37.06-.53.26-.47.6l.51,1.63,1.86-.47c.5-.09.68-.31.56-.65Z"/>
|
||||
<path class="cls-3" d="M105.49,6.57l11.59-2.48c.64-.14,1.28.27,1.43.91l.27,1.15c.15.66-.26,1.31-.93,1.45l-11.66,2.46c-.67.14-1.32-.3-1.44-.97l-.2-1.13c-.11-.64.3-1.25.94-1.39Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
@@ -3,10 +3,8 @@ import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
import axios from 'axios';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { USER_ROLE_LABELS } from '@/lib/roles';
|
||||
import { BUSINESS_FIELDS, BUSINESS_FIELD_LABELS } from '@/lib/business-fields';
|
||||
import type { BusinessField, OrganizationMembershipDto } from '@mento/shared';
|
||||
import type { OrganizationMembershipDto } from '@mento/shared';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import UiModal from '@/components/ui/UiModal.vue';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -28,12 +26,8 @@ const auth = useAuthStore();
|
||||
|
||||
const orgs = ref<OrganizationMembershipDto[]>([]);
|
||||
const open = ref(false);
|
||||
const createOpen = ref(false);
|
||||
const loading = ref(false);
|
||||
const creating = ref(false);
|
||||
const error = ref('');
|
||||
const newName = ref('');
|
||||
const newField = ref<BusinessField>('personal');
|
||||
const rootEl = ref<HTMLElement | null>(null);
|
||||
const menuEl = ref<HTMLElement | null>(null);
|
||||
const menuStyle = ref<Record<string, string>>({});
|
||||
@@ -60,7 +54,7 @@ const currentInitial = computed(() => {
|
||||
|
||||
const canSwitch = computed(() => orgs.value.length > 1);
|
||||
const isWorkspace = computed(() => props.variant === 'workspace');
|
||||
/** در حالت workspace منو همیشه باز میشود (لیست org + محیط جدید)؛ در حالت پیشفرض فقط وقتی بیش از یکی باشد */
|
||||
/** در حالت workspace منو همیشه باز میشود؛ در حالت پیشفرض فقط وقتی بیش از یکی باشد */
|
||||
const menuEnabled = computed(() => isWorkspace.value || canSwitch.value);
|
||||
|
||||
function apiError(e: unknown, fallback: string) {
|
||||
@@ -148,34 +142,6 @@ async function choose(org: OrganizationMembershipDto) {
|
||||
}
|
||||
}
|
||||
|
||||
function openCreate() {
|
||||
closeMenu();
|
||||
newName.value = '';
|
||||
newField.value = 'personal';
|
||||
error.value = '';
|
||||
createOpen.value = true;
|
||||
}
|
||||
|
||||
async function submitCreate() {
|
||||
const name = newName.value.trim();
|
||||
if (name.length < 2) {
|
||||
error.value = 'نام محیط باید حداقل دو حرف باشد.';
|
||||
return;
|
||||
}
|
||||
creating.value = true;
|
||||
error.value = '';
|
||||
try {
|
||||
await auth.createOrganization({
|
||||
name,
|
||||
businessField: newField.value,
|
||||
});
|
||||
window.location.assign('/');
|
||||
} catch (e) {
|
||||
error.value = apiError(e, 'ساخت محیط ناموفق بود');
|
||||
creating.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function onDocClick(event: MouseEvent) {
|
||||
if (!open.value || Date.now() < ignoreDocClickUntil) return;
|
||||
const target = event.target;
|
||||
@@ -260,36 +226,9 @@ onUnmounted(() => {
|
||||
:size="16"
|
||||
/>
|
||||
</button>
|
||||
<button type="button" class="create" :disabled="loading" @click="openCreate">
|
||||
محیط جدید
|
||||
</button>
|
||||
<p v-if="error && !createOpen" class="err">{{ error }}</p>
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
</div>
|
||||
</Teleport>
|
||||
|
||||
<UiModal :open="createOpen" title="محیط جدید" @close="createOpen = false">
|
||||
<form class="form" @submit.prevent="submitCreate">
|
||||
<label>
|
||||
نام محیط
|
||||
<input v-model="newName" required minlength="2" placeholder="مثلاً شرکت یا کار روزمره" />
|
||||
</label>
|
||||
<label>
|
||||
زمینه فعالیت
|
||||
<select v-model="newField">
|
||||
<option v-for="field in BUSINESS_FIELDS" :key="field" :value="field">
|
||||
{{ BUSINESS_FIELD_LABELS[field] }}
|
||||
</option>
|
||||
</select>
|
||||
</label>
|
||||
<p v-if="error" class="err">{{ error }}</p>
|
||||
</form>
|
||||
<template #footer>
|
||||
<button type="button" class="ghost" @click="createOpen = false">انصراف</button>
|
||||
<button type="button" class="primary" :disabled="creating" @click="submitCreate">
|
||||
{{ creating ? 'در حال ساخت…' : 'ساخت و ورود' }}
|
||||
</button>
|
||||
</template>
|
||||
</UiModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -585,61 +524,9 @@ onUnmounted(() => {
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.create {
|
||||
margin-top: 0.15rem;
|
||||
padding: 0.7rem 0.65rem 0.55rem;
|
||||
border-top: 1px solid #eef2f7;
|
||||
border-radius: 0 0 12px 12px;
|
||||
color: var(--accent-deep);
|
||||
font-weight: 700;
|
||||
font-size: 0.86rem;
|
||||
}
|
||||
|
||||
.menu.org-ws .create:hover {
|
||||
background: #f8fafc;
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.err {
|
||||
margin: 0.2rem 0 0;
|
||||
color: #b91c1c;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.form {
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.form label {
|
||||
display: grid;
|
||||
gap: 0.35rem;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
.form input,
|
||||
.form select {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
padding: 0.65rem 0.75rem;
|
||||
}
|
||||
|
||||
.ghost,
|
||||
.primary {
|
||||
border-radius: 10px;
|
||||
padding: 0.55rem 0.9rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.ghost {
|
||||
border: 1px solid var(--line);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.primary {
|
||||
border: 0;
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { SearchOutlined, SendOutlined } from '@/icons/ant-bridge';
|
||||
import { SendOutlined } from '@/icons/ant-bridge';
|
||||
import {
|
||||
Button as AButton,
|
||||
Input as AInput,
|
||||
@@ -57,17 +57,15 @@ const statusMessage = computed(() => {
|
||||
<div class="home-hero__row">
|
||||
<div class="home-hero__greet">
|
||||
<div class="home-hero__identity" :aria-label="firstName">
|
||||
<div class="home-hero__frame">
|
||||
<img
|
||||
v-if="hasCustomPhoto"
|
||||
:src="auth.avatarSrc"
|
||||
:alt="firstName"
|
||||
class="home-hero__photo"
|
||||
/>
|
||||
<span v-else class="home-hero__monogram" aria-hidden="true">
|
||||
{{ userInitial }}
|
||||
</span>
|
||||
</div>
|
||||
<img
|
||||
v-if="hasCustomPhoto"
|
||||
:src="auth.avatarSrc"
|
||||
:alt="firstName"
|
||||
class="home-hero__photo"
|
||||
/>
|
||||
<span v-else class="home-hero__monogram" aria-hidden="true">
|
||||
{{ userInitial }}
|
||||
</span>
|
||||
<span class="home-hero__online" title="آنلاین" aria-hidden="true" />
|
||||
</div>
|
||||
<div class="home-hero__copy">
|
||||
@@ -82,9 +80,6 @@ const statusMessage = computed(() => {
|
||||
</div>
|
||||
|
||||
<div class="home-hero__ask">
|
||||
<span class="home-hero__ask-ico" aria-hidden="true">
|
||||
<SearchOutlined />
|
||||
</span>
|
||||
<a-input
|
||||
id="home-hero-ask"
|
||||
:value="askQuery"
|
||||
@@ -99,13 +94,16 @@ const statusMessage = computed(() => {
|
||||
<a-button
|
||||
type="primary"
|
||||
shape="round"
|
||||
class="home-hero__ask-btn"
|
||||
:disabled="asking"
|
||||
:loading="asking"
|
||||
aria-label="ارسال"
|
||||
@click="emit('ask')"
|
||||
>
|
||||
<template #icon><SendOutlined /></template>
|
||||
<span class="home-hero__ask-label">بپرس</span>
|
||||
<span class="home-hero__ask-btn-inner">
|
||||
<SendOutlined />
|
||||
<span class="home-hero__ask-label">بپرس</span>
|
||||
</span>
|
||||
</a-button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -143,7 +141,7 @@ const statusMessage = computed(() => {
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
border-radius: 1rem;
|
||||
border: 1px solid rgb(110 231 183 / 0.5);
|
||||
border: 1px solid rgb(16 137 125 / 0.28);
|
||||
box-shadow: 0 1px 2px rgb(15 23 42 / 0.04);
|
||||
}
|
||||
|
||||
@@ -163,10 +161,10 @@ const statusMessage = computed(() => {
|
||||
background:
|
||||
linear-gradient(
|
||||
115deg,
|
||||
#ecfdf5 0%,
|
||||
#d1fae5 28%,
|
||||
#ccfbf1 55%,
|
||||
#f0fdfa 78%,
|
||||
#eef8f7 0%,
|
||||
#d0ebe8 28%,
|
||||
#c5e6e2 55%,
|
||||
#eef8f7 78%,
|
||||
#ffffff 100%
|
||||
);
|
||||
}
|
||||
@@ -174,14 +172,14 @@ const statusMessage = computed(() => {
|
||||
.home-hero__pattern {
|
||||
opacity: 0.45;
|
||||
background-image:
|
||||
radial-gradient(circle at 1px 1px, rgba(16, 185, 129, 0.28) 1px, transparent 0),
|
||||
radial-gradient(circle at 1px 1px, rgba(16, 137, 125, 0.28) 1px, transparent 0),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(20, 184, 166, 0.08) 25%,
|
||||
rgba(16, 137, 125, 0.08) 25%,
|
||||
transparent 25%,
|
||||
transparent 50%,
|
||||
rgba(20, 184, 166, 0.08) 50%,
|
||||
rgba(20, 184, 166, 0.08) 75%,
|
||||
rgba(16, 137, 125, 0.08) 50%,
|
||||
rgba(16, 137, 125, 0.08) 75%,
|
||||
transparent 75%,
|
||||
transparent
|
||||
);
|
||||
@@ -201,7 +199,7 @@ const statusMessage = computed(() => {
|
||||
top: -6rem;
|
||||
width: 13rem;
|
||||
height: 13rem;
|
||||
background: rgb(52 211 153 / 0.2);
|
||||
background: rgb(22 193 168 / 0.22);
|
||||
}
|
||||
|
||||
.home-hero__orb--end {
|
||||
@@ -209,7 +207,7 @@ const statusMessage = computed(() => {
|
||||
bottom: -5rem;
|
||||
width: 12rem;
|
||||
height: 12rem;
|
||||
background: rgb(20 184 166 / 0.15);
|
||||
background: rgb(16 137 125 / 0.16);
|
||||
}
|
||||
|
||||
.home-hero__row {
|
||||
@@ -237,19 +235,8 @@ const statusMessage = computed(() => {
|
||||
.home-hero__identity {
|
||||
position: relative;
|
||||
flex-shrink: 0;
|
||||
width: 3.25rem;
|
||||
height: 3.25rem;
|
||||
}
|
||||
|
||||
.home-hero__frame {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 1rem;
|
||||
padding: 2px;
|
||||
background: linear-gradient(145deg, #34d399 0%, #0d9488 48%, #059669 100%);
|
||||
box-shadow:
|
||||
0 0 0 3px rgba(255, 255, 255, 0.85),
|
||||
0 8px 18px -10px rgba(4, 120, 87, 0.55);
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.home-hero__photo,
|
||||
@@ -257,33 +244,30 @@ const statusMessage = computed(() => {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: calc(1rem - 2px);
|
||||
border-radius: 0.85rem;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.home-hero__monogram {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.15rem;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: #ecfdf5;
|
||||
background:
|
||||
radial-gradient(circle at 30% 25%, rgba(255, 255, 255, 0.22), transparent 45%),
|
||||
linear-gradient(160deg, #10b981 0%, #0f766e 100%);
|
||||
color: #eef8f7;
|
||||
background: linear-gradient(160deg, #16c1a8 0%, #10897d 100%);
|
||||
letter-spacing: -0.02em;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.home-hero__online {
|
||||
position: absolute;
|
||||
inset-inline-start: -1px;
|
||||
bottom: -1px;
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
inset-inline-start: 0;
|
||||
bottom: 0;
|
||||
width: 0.7rem;
|
||||
height: 0.7rem;
|
||||
border-radius: 999px;
|
||||
background: #10b981;
|
||||
background: #10897d;
|
||||
border: 2px solid #fff;
|
||||
box-shadow: 0 0 0 1px rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
|
||||
.home-hero__copy {
|
||||
@@ -340,22 +324,10 @@ const statusMessage = computed(() => {
|
||||
}
|
||||
|
||||
.home-hero__ask:focus-within {
|
||||
border-color: #6ee7b7;
|
||||
border-color: #16c1a8;
|
||||
box-shadow:
|
||||
0 0 0 2px rgb(167 243 208 / 0.7),
|
||||
0 4px 12px rgb(6 78 59 / 0.05);
|
||||
}
|
||||
|
||||
.home-hero__ask-ico {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.75rem;
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
0 0 0 2px rgb(16 137 125 / 0.18),
|
||||
0 4px 12px rgb(10 92 84 / 0.06);
|
||||
}
|
||||
|
||||
.home-hero__ask-input {
|
||||
@@ -377,6 +349,20 @@ const statusMessage = computed(() => {
|
||||
font-family: ui-sans-serif, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.home-hero__ask-btn {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.home-hero__ask-btn-inner {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.35rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.home-hero__ask-label {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useAuthStore } from '@/stores/auth';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatJalaliDateTime, formatJalaliShort, toFaDigits } from '@/lib/jalali';
|
||||
import UiIcon from '@/components/UiIcon.vue';
|
||||
import UserAvatar from '@/components/UserAvatar.vue';
|
||||
import MentoLogo from '@/components/brand/MentoLogo.vue';
|
||||
import QuickCreate from '@/components/QuickCreate.vue';
|
||||
import OrgSwitcher from '@/components/OrgSwitcher.vue';
|
||||
@@ -13,13 +14,15 @@ import NavHubPanel from '@/components/nav/NavHubPanel.vue';
|
||||
import GamificationToaster from '@/components/gamification/GamificationToaster.vue';
|
||||
import SmsHeaderWallet from '@/components/SmsHeaderWallet.vue';
|
||||
import { NAV_HUBS, type NavHubDef, type NavHubKey } from '@/lib/nav-hubs';
|
||||
import type {
|
||||
ActivitySummaryDto,
|
||||
AuthUserDto,
|
||||
NotificationDto,
|
||||
NotificationsListDto,
|
||||
import {
|
||||
brand,
|
||||
type ActivitySummaryDto,
|
||||
type AuthUserDto,
|
||||
type NotificationDto,
|
||||
type NotificationsListDto,
|
||||
type OrgUserDto,
|
||||
} from '@mento/shared';
|
||||
import { canManageSettings, canViewAllRecords } from '@/lib/roles';
|
||||
import { USER_ROLE_LABELS, canManageSettings, canViewAllRecords } from '@/lib/roles';
|
||||
import { hasSectionPermission, sectionPermissionForPath } from '@/lib/section-permissions';
|
||||
import { refreshUnreadTotal, useOrgChat } from '@/lib/useOrgChat';
|
||||
|
||||
@@ -38,8 +41,12 @@ const canSettings = computed(() => canManageSettings(auth.user?.role));
|
||||
const showSettingsNav = computed(
|
||||
() => canSettings.value || canOpenTeamWhileViewing.value,
|
||||
);
|
||||
/** فقط وقتی بهجای نقش دیگری وارد شدهاید؛ در پنل/دسترسی خود کاربر نشان داده نمیشود */
|
||||
const showOwnRecordsBanner = computed(
|
||||
() => Boolean(auth.user?.role) && !canViewAllRecords(auth.user?.role),
|
||||
() =>
|
||||
Boolean(auth.user?.viewAs) &&
|
||||
Boolean(auth.user?.role) &&
|
||||
!canViewAllRecords(auth.user?.role),
|
||||
);
|
||||
const todayLabel = computed(() => formatJalaliShort(new Date()));
|
||||
|
||||
@@ -260,12 +267,79 @@ const hubCanBack = computed(() => hubStack.value.length > 0);
|
||||
|
||||
const profileFullName = computed(() => auth.user?.fullName?.trim() || '');
|
||||
const profileDisplayName = computed(() => profileFullName.value);
|
||||
const profileSecondary = computed(
|
||||
() => auth.user?.email?.trim() || auth.user?.mobile?.trim() || '',
|
||||
const profileSecondary = computed(() => {
|
||||
const mobile = auth.user?.mobile?.trim();
|
||||
if (mobile) return mobile;
|
||||
const email = auth.user?.email?.trim() || '';
|
||||
const fromSynthetic = email.match(/^(\d+)@users\.mento\.local$/i);
|
||||
return fromSynthetic?.[1] || '';
|
||||
});
|
||||
|
||||
const roleUsers = ref<OrgUserDto[]>([]);
|
||||
const roleBusy = ref(false);
|
||||
const roleError = ref('');
|
||||
|
||||
/** سوئیچ نقش فقط برای مدیر/مالک همین محیط (یا وقتی مالک در حال مشاهده است) */
|
||||
const canSwitchRoles = computed(
|
||||
() => canManageSettings(auth.user?.role) || canOpenTeamWhileViewing.value,
|
||||
);
|
||||
|
||||
function roleUserLabel(user: OrgUserDto) {
|
||||
return user.orgRoleName || USER_ROLE_LABELS[user.role] || user.role;
|
||||
}
|
||||
|
||||
const currentRoleLabel = computed(() => {
|
||||
const current = roleUsers.value.find((user) => user.id === auth.user?.id);
|
||||
if (current) return roleUserLabel(current);
|
||||
if (auth.user?.role) return USER_ROLE_LABELS[auth.user.role];
|
||||
return profileSecondary.value;
|
||||
});
|
||||
|
||||
const sortedRoleUsers = computed(() => {
|
||||
if (!canSwitchRoles.value) return [];
|
||||
const me = auth.user?.id;
|
||||
return [...roleUsers.value].sort((a, b) => {
|
||||
if (a.id === me) return -1;
|
||||
if (b.id === me) return 1;
|
||||
return roleUserLabel(a).localeCompare(roleUserLabel(b), 'fa');
|
||||
});
|
||||
});
|
||||
|
||||
async function loadRoleUsers() {
|
||||
if (!canSwitchRoles.value) {
|
||||
roleUsers.value = [];
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { data } = await api.get<OrgUserDto[]>('/users');
|
||||
// فقط کاربران فعال همین محیط (اسکوپ توکن)
|
||||
roleUsers.value = data.filter((user) => user.isActive !== false);
|
||||
} catch {
|
||||
roleUsers.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function switchRoleUser(user: OrgUserDto) {
|
||||
if (!canSwitchRoles.value || roleBusy.value) return;
|
||||
if (user.id === auth.user?.id) {
|
||||
closeUserMenu();
|
||||
return;
|
||||
}
|
||||
roleBusy.value = true;
|
||||
roleError.value = '';
|
||||
try {
|
||||
await auth.viewAs(user.id);
|
||||
closeUserMenu();
|
||||
window.location.reload();
|
||||
} catch {
|
||||
roleError.value = 'تغییر نقش ناموفق بود.';
|
||||
roleBusy.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function closeUserMenu() {
|
||||
userMenuOpen.value = false;
|
||||
roleError.value = '';
|
||||
}
|
||||
|
||||
function closeOrgSwitcher() {
|
||||
@@ -289,6 +363,9 @@ async function toggleUserMenu() {
|
||||
userMenuOpen.value = true;
|
||||
await nextTick();
|
||||
placeUserMenu();
|
||||
await loadRoleUsers();
|
||||
await nextTick();
|
||||
placeUserMenu();
|
||||
}
|
||||
|
||||
function onOrgMenuOpen() {
|
||||
@@ -656,6 +733,9 @@ onUnmounted(() => {
|
||||
<div class="shell" :class="{ collapsed, 'hub-open': Boolean(openHub), 'more-open': moreOpen }">
|
||||
|
||||
<aside id="app-sidebar" class="sidebar" :class="{ collapsed }">
|
||||
<div v-if="!collapsed" class="sidebar-logo">
|
||||
<img :src="brand.logoWordmark" :alt="brand.nameFa" width="118" height="22" />
|
||||
</div>
|
||||
<div class="brand-row">
|
||||
<div class="brand-cluster">
|
||||
<OrgSwitcher
|
||||
@@ -951,7 +1031,7 @@ onUnmounted(() => {
|
||||
:style="userMenuStyle"
|
||||
role="menu"
|
||||
>
|
||||
<div class="user-popover-head">
|
||||
<div class="user-popover-head divided">
|
||||
<span class="avatar-wrap head-avatar">
|
||||
<span class="avatar">
|
||||
<img
|
||||
@@ -964,9 +1044,55 @@ onUnmounted(() => {
|
||||
</span>
|
||||
<span class="user-popover-meta">
|
||||
<strong class="user-popover-name">{{ profileDisplayName }}</strong>
|
||||
<small class="user-popover-sub">{{ planLabel || profileSecondary }}</small>
|
||||
<small class="user-popover-sub">{{ currentRoleLabel || profileSecondary }}</small>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
v-if="sortedRoleUsers.length > 1"
|
||||
class="user-role-list"
|
||||
:class="{ collapsible: sortedRoleUsers.length > 3 }"
|
||||
role="group"
|
||||
aria-label="نقشها"
|
||||
>
|
||||
<p class="user-role-hint">نقشها</p>
|
||||
<div class="user-role-scroll">
|
||||
<button
|
||||
v-for="user in sortedRoleUsers"
|
||||
:key="user.id"
|
||||
type="button"
|
||||
class="user-role-item"
|
||||
:class="{ on: user.id === auth.user?.id }"
|
||||
:disabled="roleBusy"
|
||||
role="menuitem"
|
||||
:aria-current="user.id === auth.user?.id ? 'true' : undefined"
|
||||
@click.stop="switchRoleUser(user)"
|
||||
>
|
||||
<UserAvatar
|
||||
class="user-role-avatar"
|
||||
:name="user.fullName"
|
||||
:user-id="user.id"
|
||||
:has-avatar="user.hasAvatar"
|
||||
:size="28"
|
||||
/>
|
||||
<span class="user-role-copy">
|
||||
<strong>{{ roleUserLabel(user) }}</strong>
|
||||
<small>{{ user.fullName }}</small>
|
||||
</span>
|
||||
<UiIcon
|
||||
v-if="user.id === auth.user?.id"
|
||||
class="user-role-check"
|
||||
name="check"
|
||||
:size="15"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="roleError" class="user-role-err">{{ roleError }}</p>
|
||||
<div
|
||||
v-if="sortedRoleUsers.length > 1 || roleError"
|
||||
class="user-popover-sep"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<RouterLink
|
||||
class="user-popover-item"
|
||||
:to="{ name: 'profile' }"
|
||||
@@ -1445,6 +1571,24 @@ onUnmounted(() => {
|
||||
gap: 0.85rem;
|
||||
}
|
||||
|
||||
.sidebar-logo {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0.2rem 0.5rem 0.55rem;
|
||||
margin-bottom: 0.2rem;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
}
|
||||
|
||||
.sidebar-logo img {
|
||||
display: block;
|
||||
width: auto;
|
||||
max-width: 100%;
|
||||
height: 22px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.brand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1501,12 +1645,13 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.58rem 0.85rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
color: #475569;
|
||||
text-decoration: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.88rem;
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
@@ -1515,6 +1660,7 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.nav-item.router-link-active {
|
||||
overflow: hidden;
|
||||
background: #f3f4f6;
|
||||
color: #0f172a;
|
||||
box-shadow: none;
|
||||
@@ -1523,6 +1669,7 @@ onUnmounted(() => {
|
||||
.nav-item.router-link-active::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
inset-inline-start: 0.2rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
@@ -1532,6 +1679,36 @@ onUnmounted(() => {
|
||||
background: var(--accent);
|
||||
}
|
||||
|
||||
.nav-item.router-link-active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
opacity: 0.45;
|
||||
border-radius: inherit;
|
||||
background-image:
|
||||
radial-gradient(circle at 1px 1px, rgba(16, 137, 125, 0.28) 1px, transparent 0),
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(16, 137, 125, 0.08) 25%,
|
||||
transparent 25%,
|
||||
transparent 50%,
|
||||
rgba(16, 137, 125, 0.08) 50%,
|
||||
rgba(16, 137, 125, 0.08) 75%,
|
||||
transparent 75%,
|
||||
transparent
|
||||
);
|
||||
background-size:
|
||||
18px 18px,
|
||||
28px 28px;
|
||||
mask-image: linear-gradient(to left, rgba(0, 0, 0, 0.85), rgba(0, 0, 0, 0.25));
|
||||
}
|
||||
|
||||
.nav-item.router-link-active > * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.nav-item.router-link-active :deep(.ico) {
|
||||
color: var(--accent-deep);
|
||||
}
|
||||
@@ -1542,7 +1719,8 @@ onUnmounted(() => {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.shell.hub-open .nav-item.router-link-active:not(.hub-on)::before {
|
||||
.shell.hub-open .nav-item.router-link-active:not(.hub-on)::before,
|
||||
.shell.hub-open .nav-item.router-link-active:not(.hub-on)::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1785,7 +1963,7 @@ onUnmounted(() => {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
direction: ltr;
|
||||
text-align: start;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.user-chev {
|
||||
@@ -1832,7 +2010,11 @@ onUnmounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.4rem 0.5rem 0.55rem;
|
||||
padding: 0.4rem 0.5rem 0.35rem;
|
||||
}
|
||||
|
||||
.user-popover-head.divided {
|
||||
padding-bottom: 0.55rem;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
@@ -1870,6 +2052,126 @@ onUnmounted(() => {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-role-list {
|
||||
display: grid;
|
||||
gap: 0.1rem;
|
||||
padding: 0.2rem 0.1rem 0.1rem;
|
||||
}
|
||||
|
||||
.user-role-hint {
|
||||
margin: 0;
|
||||
padding: 0.2rem 0.55rem 0.15rem;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.user-role-scroll {
|
||||
display: grid;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.user-role-list.collapsible .user-role-scroll {
|
||||
/* دقیقاً جای ۳ ردیف؛ بقیه در همین فضا اسکرول میخورند */
|
||||
max-height: calc(3 * (28px + 0.9rem) + 2 * 0.15rem);
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.user-role-list.collapsible .user-role-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.user-role-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
width: 100%;
|
||||
min-height: calc(28px + 0.9rem);
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 12px;
|
||||
padding: 0.45rem 0.5rem;
|
||||
color: #334155;
|
||||
font: inherit;
|
||||
cursor: pointer;
|
||||
text-align: start;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.user-role-item:hover:not(:disabled) {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.user-role-item.on {
|
||||
background: #f0fdfa;
|
||||
}
|
||||
|
||||
.user-role-item:disabled {
|
||||
opacity: 0.65;
|
||||
cursor: wait;
|
||||
}
|
||||
|
||||
.user-role-mark {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 9px;
|
||||
flex-shrink: 0;
|
||||
background: #ecfdf8;
|
||||
color: #0f766e;
|
||||
font-size: 0.78rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.user-role-avatar {
|
||||
flex-shrink: 0;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.user-role-copy {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
display: grid;
|
||||
gap: 0.05rem;
|
||||
}
|
||||
|
||||
.user-role-copy strong {
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
line-height: 1.25;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-role-copy small {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
color: #94a3b8;
|
||||
line-height: 1.2;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.user-role-check {
|
||||
color: #0f766e;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.user-role-err {
|
||||
margin: 0;
|
||||
padding: 0.15rem 0.55rem 0.25rem;
|
||||
font-size: 0.72rem;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.user-plan-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -2210,7 +2512,6 @@ onUnmounted(() => {
|
||||
}
|
||||
|
||||
.sidebar.collapsed .nav-item.router-link-active {
|
||||
background: #f3f4f6;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,12 +24,12 @@
|
||||
--line-light: #f1f5f9;
|
||||
--line-strong: #cbd5e1;
|
||||
|
||||
/* Accent / Primary (Teal) */
|
||||
--accent: #14b8a6;
|
||||
--accent-hover: #0d9488;
|
||||
--accent-deep: #0f766e;
|
||||
--accent-soft: #ccfbf1;
|
||||
--accent-subtle: #f0fdfa;
|
||||
/* Accent / Primary (Montact brand) */
|
||||
--accent: #10897d;
|
||||
--accent-hover: #0d7369;
|
||||
--accent-deep: #0a5c54;
|
||||
--accent-soft: #d0ebe8;
|
||||
--accent-subtle: #eef8f7;
|
||||
--accent-contrast: #ffffff;
|
||||
|
||||
/* Semantics */
|
||||
|
||||
@@ -14,7 +14,7 @@ const GRAY_200 = '#e5e7eb';
|
||||
|
||||
export const antdTheme = {
|
||||
token: {
|
||||
colorPrimary: '#10b981',
|
||||
colorPrimary: '#10897d',
|
||||
fontFamily: '"IRANSans", Tahoma, sans-serif',
|
||||
|
||||
/** Page / subtle surface fill */
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, ref } from 'vue';
|
||||
import { Spin as ASpin } from 'ant-design-vue';
|
||||
import { Alert as AAlert, Spin as ASpin } from 'ant-design-vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import { api } from '@/lib/api';
|
||||
import { formatMoney } from '@/lib/money';
|
||||
@@ -13,8 +13,6 @@ import HomeAiAssistant from '@/components/dashboard/HomeAiAssistant.vue';
|
||||
import HomeRecentActivities from '@/components/dashboard/HomeRecentActivities.vue';
|
||||
import HomeMiniPipeline from '@/components/dashboard/HomeMiniPipeline.vue';
|
||||
import HomeTopCustomers from '@/components/dashboard/HomeTopCustomers.vue';
|
||||
import { Alert as AAlert } from 'ant-design-vue';
|
||||
|
||||
import axios from 'axios';
|
||||
import type {
|
||||
ActivityDto,
|
||||
@@ -24,21 +22,11 @@ import type {
|
||||
PaginatedResponse,
|
||||
PipelineBoardDto,
|
||||
} from '@mento/shared';
|
||||
import {
|
||||
HOME_ACTIVITIES_MOCK,
|
||||
HOME_DASHBOARD_MOCK,
|
||||
HOME_PIPELINE_MOCK,
|
||||
isHomeDashboardEmpty,
|
||||
} from './home-dashboard-mock';
|
||||
|
||||
/** پیشنمایش UI با دیتای ماک — برای اتصال به API واقعی بگذارید false */
|
||||
const FORCE_HOME_MOCK = true;
|
||||
|
||||
const auth = useAuthStore();
|
||||
|
||||
const loading = ref(true);
|
||||
const usingMock = ref(false);
|
||||
const showMockBanner = ref(true);
|
||||
const loadError = ref('');
|
||||
const home = ref<HomeDashboardDto | null>(null);
|
||||
const activities = ref<ActivityDto[]>([]);
|
||||
const board = ref<PipelineBoardDto | null>(null);
|
||||
@@ -153,21 +141,10 @@ function onAskHotkey(e: KeyboardEvent) {
|
||||
document.getElementById('home-hero-ask')?.focus();
|
||||
}
|
||||
|
||||
function applyHomeMock() {
|
||||
home.value = HOME_DASHBOARD_MOCK;
|
||||
activities.value = HOME_ACTIVITIES_MOCK;
|
||||
board.value = HOME_PIPELINE_MOCK;
|
||||
usingMock.value = true;
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
window.addEventListener('keydown', onAskHotkey);
|
||||
loading.value = true;
|
||||
if (FORCE_HOME_MOCK) {
|
||||
applyHomeMock();
|
||||
loading.value = false;
|
||||
return;
|
||||
}
|
||||
loadError.value = '';
|
||||
try {
|
||||
const [homeRes, actRes, boardRes] = await Promise.all([
|
||||
api.get<HomeDashboardDto>('/reports/home'),
|
||||
@@ -176,16 +153,16 @@ onMounted(async () => {
|
||||
}),
|
||||
api.get<PipelineBoardDto>('/pipelines/board'),
|
||||
]);
|
||||
if (isHomeDashboardEmpty(homeRes.data)) {
|
||||
applyHomeMock();
|
||||
} else {
|
||||
home.value = homeRes.data;
|
||||
activities.value = actRes.data.items;
|
||||
board.value = boardRes.data;
|
||||
usingMock.value = false;
|
||||
}
|
||||
} catch {
|
||||
applyHomeMock();
|
||||
home.value = homeRes.data;
|
||||
activities.value = actRes.data.items;
|
||||
board.value = boardRes.data;
|
||||
} catch (e) {
|
||||
loadError.value = axios.isAxiosError(e)
|
||||
? String(
|
||||
(e.response?.data as { message?: string })?.message ??
|
||||
'بارگذاری داشبورد ناموفق بود.',
|
||||
)
|
||||
: 'بارگذاری داشبورد ناموفق بود.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -211,13 +188,11 @@ onUnmounted(() => {
|
||||
/>
|
||||
|
||||
<a-alert
|
||||
v-if="usingMock && showMockBanner"
|
||||
type="info"
|
||||
closable
|
||||
v-if="loadError"
|
||||
type="error"
|
||||
show-icon
|
||||
class="rounded-xl"
|
||||
message="پیشنمایش با دیتای ماک"
|
||||
description="برای دیدن دادهٔ واقعی، FORCE_HOME_MOCK را در HomeView روی false بگذارید."
|
||||
@close="showMockBanner = false"
|
||||
:message="loadError"
|
||||
/>
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center min-h-[120px]">
|
||||
|
||||
@@ -170,349 +170,6 @@ const chartFillStyle = { height: '100%', width: '100%' };
|
||||
type PeriodKey = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'custom';
|
||||
type PageMode = 'dashboard' | 'builder';
|
||||
|
||||
/** Rich mock for layout review. Disable with ?mock=0 */
|
||||
function buildMockReportsDashboard(): ReportsDashboardDto {
|
||||
const days = Array.from({ length: 13 }, (_, i) => {
|
||||
const d = i + 1;
|
||||
const incoming = 8 + ((i * 3) % 7);
|
||||
const won = Math.max(1, Math.round(incoming * (0.22 + (i % 5) * 0.04)));
|
||||
return {
|
||||
key: `1405-08-${String(d).padStart(2, '0')}`,
|
||||
label: `${toFaDigits(d)} آبان`,
|
||||
incomingCount: incoming,
|
||||
wonCount: won,
|
||||
conversionPercent: Math.round((won / incoming) * 100),
|
||||
};
|
||||
});
|
||||
const trend = days.map((d, i) => ({
|
||||
key: d.key,
|
||||
label: d.label,
|
||||
value: 18_000_000 + i * 4_500_000 + (i % 3) * 2_000_000,
|
||||
}));
|
||||
return {
|
||||
from: '2026-10-23',
|
||||
to: '2026-11-04',
|
||||
currency: 'IRR',
|
||||
kpis: [
|
||||
{
|
||||
key: 'new_customers',
|
||||
label: 'مشتریان جدید',
|
||||
value: 42,
|
||||
format: 'number',
|
||||
changePercent: 18,
|
||||
changeLabel: 'نسبت به دوره قبل',
|
||||
},
|
||||
{
|
||||
key: 'invoice_count',
|
||||
label: 'تعداد فاکتور',
|
||||
value: 67,
|
||||
format: 'number',
|
||||
changePercent: 9,
|
||||
changeLabel: 'نسبت به دوره قبل',
|
||||
},
|
||||
{
|
||||
key: 'sales_amount',
|
||||
label: 'مبلغ فروش',
|
||||
value: 485_000_000,
|
||||
format: 'money',
|
||||
changePercent: 14,
|
||||
changeLabel: 'نسبت به دوره قبل',
|
||||
},
|
||||
{
|
||||
key: 'avg_invoice',
|
||||
label: 'میانگین فاکتور',
|
||||
value: 7_240_000,
|
||||
format: 'money',
|
||||
changePercent: -3,
|
||||
changeLabel: 'نسبت به دوره قبل',
|
||||
},
|
||||
{
|
||||
key: 'avg_sale',
|
||||
label: 'میانگین فروش',
|
||||
value: 12_100_000,
|
||||
format: 'money',
|
||||
changePercent: 6,
|
||||
changeLabel: 'نسبت به دوره قبل',
|
||||
},
|
||||
],
|
||||
salesTrend: trend,
|
||||
monthlyComparison: [
|
||||
{ key: 'm1', label: 'مرداد', value: 320_000_000 },
|
||||
{ key: 'm2', label: 'شهریور', value: 410_000_000 },
|
||||
{ key: 'm3', label: 'مهر', value: 455_000_000 },
|
||||
{ key: 'm4', label: 'آبان', value: 485_000_000 },
|
||||
],
|
||||
customerGrowth: [
|
||||
{ key: 'g1', label: 'مرداد', value: 18 },
|
||||
{ key: 'g2', label: 'شهریور', value: 24 },
|
||||
{ key: 'g3', label: 'مهر', value: 31 },
|
||||
{ key: 'g4', label: 'آبان', value: 42 },
|
||||
],
|
||||
sources: [
|
||||
{ label: 'اینستاگرام', count: 28, percent: 34, amount: 120_000_000 },
|
||||
{ label: 'معرفی', count: 19, percent: 23, amount: 150_000_000 },
|
||||
{ label: 'سایت', count: 16, percent: 20, amount: 95_000_000 },
|
||||
{ label: 'تبلیغات', count: 12, percent: 15, amount: 70_000_000 },
|
||||
{ label: 'سایر', count: 7, percent: 8, amount: 50_000_000 },
|
||||
],
|
||||
lostReasons: [
|
||||
{ label: 'قیمت', count: 11, percent: 40 },
|
||||
{ label: 'رقیب', count: 8, percent: 29 },
|
||||
{ label: 'زمانبندی', count: 5, percent: 18 },
|
||||
{ label: 'عدم نیاز', count: 4, percent: 13 },
|
||||
],
|
||||
salesByCategory: [
|
||||
{ label: 'نرمافزار', count: 22, percent: 45, amount: 220_000_000 },
|
||||
{ label: 'سرویس', count: 15, percent: 30, amount: 145_000_000 },
|
||||
{ label: 'سختافزار', count: 12, percent: 25, amount: 120_000_000 },
|
||||
],
|
||||
topProducts: [
|
||||
{ name: 'پلن حرفهای', quantity: 18, amount: 180_000_000 },
|
||||
{ name: 'پلن پایه', quantity: 24, amount: 96_000_000 },
|
||||
{ name: 'پشتیبانی سالانه', quantity: 12, amount: 72_000_000 },
|
||||
{ name: 'ماژول پیامک', quantity: 9, amount: 45_000_000 },
|
||||
{ name: 'آموزش', quantity: 7, amount: 28_000_000 },
|
||||
],
|
||||
customerStatus: [
|
||||
{ label: 'فعال', count: 86, percent: 58 },
|
||||
{ label: 'در حال پیگیری', count: 34, percent: 23 },
|
||||
{ label: 'راکد', count: 28, percent: 19 },
|
||||
],
|
||||
activities: {
|
||||
open: 24,
|
||||
done: 58,
|
||||
overdue: 7,
|
||||
byType: [
|
||||
{ type: 'call', label: 'تماس', count: 32 },
|
||||
{ type: 'meeting', label: 'جلسه', count: 18 },
|
||||
{ type: 'task', label: 'وظیفه', count: 21 },
|
||||
{ type: 'email', label: 'ایمیل', count: 11 },
|
||||
{ type: 'note', label: 'یادداشت', count: 7 },
|
||||
],
|
||||
},
|
||||
team: [
|
||||
{
|
||||
userId: 'u1',
|
||||
fullName: 'مدیر نمونه',
|
||||
wonAmount: 165_000_000,
|
||||
wonCount: 9,
|
||||
activitiesDone: 22,
|
||||
},
|
||||
{
|
||||
userId: 'u2',
|
||||
fullName: 'کارشناس فروش نمونه',
|
||||
wonAmount: 142_000_000,
|
||||
wonCount: 11,
|
||||
activitiesDone: 31,
|
||||
},
|
||||
{
|
||||
userId: 'u3',
|
||||
fullName: 'آقای بنی لوحی',
|
||||
wonAmount: 98_000_000,
|
||||
wonCount: 6,
|
||||
activitiesDone: 14,
|
||||
},
|
||||
{
|
||||
userId: 'u4',
|
||||
fullName: 'مدیر سازمانی نمونه',
|
||||
wonAmount: 80_000_000,
|
||||
wonCount: 5,
|
||||
activitiesDone: 10,
|
||||
},
|
||||
],
|
||||
insights: [
|
||||
'نرخ تبدیل اینستاگرام ۱۸٪ بالاتر از میانگین منابع است؛ بودجه تبلیغ را اینجا نگه دارید.',
|
||||
'مرحله «قیمت و نهایی کردن» بیشترین ریزش را دارد؛ اسکریپت تخفیف کنترلشده پیشنهاد میشود.',
|
||||
'دو فروشنده بالای میانگین تیم هستند؛ جلسات کپیروش فروش را با آنها برگزار کنید.',
|
||||
],
|
||||
sales: {
|
||||
openCount: 38,
|
||||
wonCount: 31,
|
||||
lostCount: 28,
|
||||
openAmount: 620_000_000,
|
||||
wonAmount: 485_000_000,
|
||||
lostAmount: 210_000_000,
|
||||
winRate: 52,
|
||||
},
|
||||
funnel: [
|
||||
{
|
||||
stageId: 's1',
|
||||
stageName: 'لید جدید',
|
||||
pipelineId: 'p1',
|
||||
pipelineName: 'مسیر فروش اصلی',
|
||||
count: 24,
|
||||
amount: 180_000_000,
|
||||
conversionFromPrevPercent: null,
|
||||
shareOfPipelinePercent: 35,
|
||||
},
|
||||
{
|
||||
stageId: 's2',
|
||||
stageName: 'اولین تماس',
|
||||
pipelineId: 'p1',
|
||||
pipelineName: 'مسیر فروش اصلی',
|
||||
count: 16,
|
||||
amount: 140_000_000,
|
||||
conversionFromPrevPercent: 67,
|
||||
shareOfPipelinePercent: 24,
|
||||
},
|
||||
{
|
||||
stageId: 's3',
|
||||
stageName: 'مذاکرات اولیه',
|
||||
pipelineId: 'p1',
|
||||
pipelineName: 'مسیر فروش اصلی',
|
||||
count: 12,
|
||||
amount: 160_000_000,
|
||||
conversionFromPrevPercent: 75,
|
||||
shareOfPipelinePercent: 18,
|
||||
},
|
||||
{
|
||||
stageId: 's4',
|
||||
stageName: 'پیشنهاد قیمت',
|
||||
pipelineId: 'p1',
|
||||
pipelineName: 'مسیر فروش اصلی',
|
||||
count: 9,
|
||||
amount: 190_000_000,
|
||||
conversionFromPrevPercent: 75,
|
||||
shareOfPipelinePercent: 13,
|
||||
},
|
||||
{
|
||||
stageId: 's5',
|
||||
stageName: 'قیمت و نهایی کردن',
|
||||
pipelineId: 'p1',
|
||||
pipelineName: 'مسیر فروش اصلی',
|
||||
count: 7,
|
||||
amount: 210_000_000,
|
||||
conversionFromPrevPercent: 78,
|
||||
shareOfPipelinePercent: 10,
|
||||
},
|
||||
],
|
||||
conversionRate: {
|
||||
incomingCount: 94,
|
||||
wonCount: 31,
|
||||
conversionPercent: 33,
|
||||
previousIncomingCount: 81,
|
||||
previousWonCount: 22,
|
||||
previousConversionPercent: 27,
|
||||
changePercent: 6,
|
||||
byTime: days,
|
||||
byUser: [
|
||||
{
|
||||
key: 'u2',
|
||||
label: 'کارشناس فروش نمونه',
|
||||
incomingCount: 34,
|
||||
wonCount: 14,
|
||||
conversionPercent: 41,
|
||||
},
|
||||
{
|
||||
key: 'u1',
|
||||
label: 'مدیر نمونه',
|
||||
incomingCount: 28,
|
||||
wonCount: 9,
|
||||
conversionPercent: 32,
|
||||
},
|
||||
{
|
||||
key: 'u3',
|
||||
label: 'آقای بنی لوحی',
|
||||
incomingCount: 20,
|
||||
wonCount: 5,
|
||||
conversionPercent: 25,
|
||||
},
|
||||
{
|
||||
key: 'u4',
|
||||
label: 'مدیر سازمانی نمونه',
|
||||
incomingCount: 12,
|
||||
wonCount: 3,
|
||||
conversionPercent: 25,
|
||||
},
|
||||
],
|
||||
bySource: [
|
||||
{
|
||||
key: 'ig',
|
||||
label: 'اینستاگرام',
|
||||
incomingCount: 30,
|
||||
wonCount: 12,
|
||||
conversionPercent: 40,
|
||||
},
|
||||
{
|
||||
key: 'ref',
|
||||
label: 'معرفی',
|
||||
incomingCount: 22,
|
||||
wonCount: 9,
|
||||
conversionPercent: 41,
|
||||
},
|
||||
{
|
||||
key: 'web',
|
||||
label: 'سایت',
|
||||
incomingCount: 24,
|
||||
wonCount: 6,
|
||||
conversionPercent: 25,
|
||||
},
|
||||
{
|
||||
key: 'ads',
|
||||
label: 'تبلیغات',
|
||||
incomingCount: 18,
|
||||
wonCount: 4,
|
||||
conversionPercent: 22,
|
||||
},
|
||||
],
|
||||
},
|
||||
periodCompare: [
|
||||
{
|
||||
key: 'won_amount',
|
||||
label: 'مبلغ فروش موفق',
|
||||
format: 'money',
|
||||
current: 485_000_000,
|
||||
previous: 410_000_000,
|
||||
changePercent: 18,
|
||||
drillKind: 'period_won',
|
||||
},
|
||||
{
|
||||
key: 'won_count',
|
||||
label: 'تعداد فروش موفق',
|
||||
format: 'number',
|
||||
current: 31,
|
||||
previous: 26,
|
||||
changePercent: 19,
|
||||
drillKind: 'period_won',
|
||||
},
|
||||
{
|
||||
key: 'open_amount',
|
||||
label: 'معاملات باز',
|
||||
format: 'money',
|
||||
current: 620_000_000,
|
||||
previous: 540_000_000,
|
||||
changePercent: 15,
|
||||
},
|
||||
{
|
||||
key: 'win_rate',
|
||||
label: 'نرخ برد',
|
||||
format: 'percent',
|
||||
current: 52,
|
||||
previous: 47,
|
||||
changePercent: 5,
|
||||
},
|
||||
{
|
||||
key: 'new_people',
|
||||
label: 'مشتریان جدید',
|
||||
format: 'number',
|
||||
current: 42,
|
||||
previous: 35,
|
||||
changePercent: 20,
|
||||
},
|
||||
],
|
||||
tickets: {
|
||||
open: 6,
|
||||
resolved: 14,
|
||||
closed: 9,
|
||||
slaBreached: 1,
|
||||
avgCsat: 4.2,
|
||||
csatCount: 11,
|
||||
},
|
||||
peopleCreated: 42,
|
||||
invoiceCount: 67,
|
||||
invoiceAmount: 485_000_000,
|
||||
};
|
||||
}
|
||||
|
||||
const PERIODS: Array<{ key: PeriodKey; label: string }> = [
|
||||
{ key: 'today', label: 'امروز' },
|
||||
@@ -1201,14 +858,7 @@ onMounted(() => {
|
||||
async function load() {
|
||||
loading.value = true;
|
||||
error.value = '';
|
||||
const useMock = route.query.mock !== '0';
|
||||
try {
|
||||
if (useMock) {
|
||||
const orgRes = await api.get<OrganizationDto>('/organizations/current');
|
||||
org.value = orgRes.data;
|
||||
data.value = buildMockReportsDashboard();
|
||||
return;
|
||||
}
|
||||
const [dash, orgRes] = await Promise.all([
|
||||
api.get<ReportsDashboardDto>('/reports/dashboard', {
|
||||
params: {
|
||||
@@ -1221,11 +871,6 @@ async function load() {
|
||||
data.value = dash.data;
|
||||
org.value = orgRes.data;
|
||||
} catch (e) {
|
||||
if (useMock) {
|
||||
data.value = buildMockReportsDashboard();
|
||||
error.value = '';
|
||||
return;
|
||||
}
|
||||
error.value = axios.isAxiosError(e)
|
||||
? String(
|
||||
(e.response?.data as { message?: string })?.message ??
|
||||
|
||||
@@ -1,358 +0,0 @@
|
||||
import type {
|
||||
ActivityDto,
|
||||
HomeDashboardDto,
|
||||
PipelineBoardDto,
|
||||
} from '@mento/shared';
|
||||
|
||||
/** پیشنمایش داشبورد خانه وقتی API خالی است یا FORCE روشن باشد */
|
||||
export const HOME_DASHBOARD_MOCK: HomeDashboardDto = {
|
||||
currency: 'IRR',
|
||||
kpis: [
|
||||
{
|
||||
key: 'sales',
|
||||
label: 'فروش این ماه',
|
||||
value: 186_500_000,
|
||||
changePercent: 18.4,
|
||||
changeLabel: 'نسبت به ماه قبل',
|
||||
},
|
||||
{
|
||||
key: 'customers',
|
||||
label: 'مشتریان جدید',
|
||||
value: 42,
|
||||
changePercent: 12.5,
|
||||
changeLabel: 'نسبت به ماه قبل',
|
||||
},
|
||||
{
|
||||
key: 'open-deals',
|
||||
label: 'معاملات باز',
|
||||
value: 27,
|
||||
changePercent: -4.2,
|
||||
changeLabel: 'نسبت به هفته قبل',
|
||||
},
|
||||
{
|
||||
key: 'followups',
|
||||
label: 'پیگیری امروز',
|
||||
value: 9,
|
||||
changePercent: 25,
|
||||
changeLabel: 'نسبت به دیروز',
|
||||
},
|
||||
{
|
||||
key: 'win-rate',
|
||||
label: 'نرخ برد',
|
||||
value: 58.3,
|
||||
changePercent: null,
|
||||
changeLabel: '۱۲ ماه اخیر',
|
||||
},
|
||||
{
|
||||
key: 'overdue',
|
||||
label: 'عقبافتاده',
|
||||
value: 6,
|
||||
changePercent: null,
|
||||
changeLabel: 'فعالیت باز',
|
||||
},
|
||||
],
|
||||
alerts: [
|
||||
{
|
||||
key: 'stale-deal',
|
||||
tone: 'urgent',
|
||||
text: 'شرکت نمونه آکمه در مرحله تصمیمگیری است — امروز تماس بگیرید',
|
||||
link: '/pipeline',
|
||||
},
|
||||
{
|
||||
key: 'stale-count',
|
||||
tone: 'warning',
|
||||
text: '۵ معامله بیش از ۵ روز پیگیری نشده',
|
||||
link: '/pipeline',
|
||||
},
|
||||
{
|
||||
key: 'overdue-acts',
|
||||
tone: 'warning',
|
||||
text: '۶ پیگیری عقبافتاده دارید — همین امروز ببندید',
|
||||
link: '/activities',
|
||||
},
|
||||
{
|
||||
key: 'target-close',
|
||||
tone: 'info',
|
||||
text: 'به تارگت ماه نزدیک شدهاید',
|
||||
link: '/sales/forecast',
|
||||
},
|
||||
{
|
||||
key: 'big-deal',
|
||||
tone: 'info',
|
||||
text: 'پیشنهاد ۱۱۲ میلیون تومانی سپهر در انتظار تأیید است',
|
||||
link: '/pipeline',
|
||||
},
|
||||
{
|
||||
key: 'win-streak',
|
||||
tone: 'success',
|
||||
text: 'این هفته ۳ معامله برنده بستهاید',
|
||||
link: '/reports',
|
||||
},
|
||||
{
|
||||
key: 'meeting-today',
|
||||
tone: 'urgent',
|
||||
text: 'جلسه دمو با پارسنوین ساعت ۱۴ — آمادهسازی پیشنهاد',
|
||||
link: '/activities',
|
||||
},
|
||||
{
|
||||
key: 'quote-expire',
|
||||
tone: 'warning',
|
||||
text: 'پیشفاکتور صنایع سپهر تا فردا منقضی میشود',
|
||||
link: '/pipeline',
|
||||
},
|
||||
{
|
||||
key: 'new-lead',
|
||||
tone: 'info',
|
||||
text: '۴ لید جدید از فرم وبسایت بدون مالک ماندهاند',
|
||||
link: '/contacts',
|
||||
},
|
||||
{
|
||||
key: 'renewal',
|
||||
tone: 'info',
|
||||
text: 'قرارداد تمدید سارا محمدی این ماه سررسید میشود',
|
||||
link: '/pipeline',
|
||||
},
|
||||
],
|
||||
salesTrend: [
|
||||
{ monthKey: '1403-07', monthLabel: 'مهر', amount: 64_000_000 },
|
||||
{ monthKey: '1403-08', monthLabel: 'آبان', amount: 71_500_000 },
|
||||
{ monthKey: '1403-09', monthLabel: 'آذر', amount: 68_200_000 },
|
||||
{ monthKey: '1403-10', monthLabel: 'دی', amount: 79_800_000 },
|
||||
{ monthKey: '1403-11', monthLabel: 'بهمن', amount: 88_400_000 },
|
||||
{ monthKey: '1403-12', monthLabel: 'اسفند', amount: 95_000_000 },
|
||||
{ monthKey: '1404-01', monthLabel: 'فروردین', amount: 82_600_000 },
|
||||
{ monthKey: '1404-02', monthLabel: 'اردیبهشت', amount: 92_000_000 },
|
||||
{ monthKey: '1404-03', monthLabel: 'خرداد', amount: 118_500_000 },
|
||||
{ monthKey: '1404-04', monthLabel: 'تیر', amount: 141_200_000 },
|
||||
{ monthKey: '1404-05', monthLabel: 'مرداد', amount: 157_800_000 },
|
||||
{ monthKey: '1404-06', monthLabel: 'شهریور', amount: 186_500_000 },
|
||||
],
|
||||
customerGrowth: [
|
||||
{ monthKey: '1403-07', monthLabel: 'مهر', value: 11 },
|
||||
{ monthKey: '1403-08', monthLabel: 'آبان', value: 14 },
|
||||
{ monthKey: '1403-09', monthLabel: 'آذر', value: 13 },
|
||||
{ monthKey: '1403-10', monthLabel: 'دی', value: 16 },
|
||||
{ monthKey: '1403-11', monthLabel: 'بهمن', value: 19 },
|
||||
{ monthKey: '1403-12', monthLabel: 'اسفند', value: 22 },
|
||||
{ monthKey: '1404-01', monthLabel: 'فروردین', value: 15 },
|
||||
{ monthKey: '1404-02', monthLabel: 'اردیبهشت', value: 18 },
|
||||
{ monthKey: '1404-03', monthLabel: 'خرداد', value: 24 },
|
||||
{ monthKey: '1404-04', monthLabel: 'تیر', value: 31 },
|
||||
{ monthKey: '1404-05', monthLabel: 'مرداد', value: 28 },
|
||||
{ monthKey: '1404-06', monthLabel: 'شهریور', value: 42 },
|
||||
],
|
||||
sources: [
|
||||
{ label: 'فرم لید', count: 38, percent: 32.2 },
|
||||
{ label: 'معرفی دوستان', count: 29, percent: 24.6 },
|
||||
{ label: 'وبسایت', count: 21, percent: 17.8 },
|
||||
{ label: 'اکسل', count: 16, percent: 13.6 },
|
||||
{ label: 'تلفن', count: 8, percent: 6.8 },
|
||||
{ label: 'سایر', count: 6, percent: 5 },
|
||||
],
|
||||
topCustomers: [
|
||||
{ id: 'mock-c1', name: 'شرکت آکمه', dealCount: 7, rank: 1 },
|
||||
{ id: 'mock-c2', name: 'گروه پارسنوین', dealCount: 5, rank: 2 },
|
||||
{ id: 'mock-c3', name: 'سارا محمدی', dealCount: 4, rank: 3 },
|
||||
{ id: 'mock-c4', name: 'صنایع سپهر', dealCount: 3, rank: 4 },
|
||||
{ id: 'mock-c5', name: 'رضا کریمی', dealCount: 3, rank: 5 },
|
||||
],
|
||||
salesSummary: {
|
||||
openCount: 27,
|
||||
wonCount: 35,
|
||||
lostCount: 25,
|
||||
openAmount: 412_000_000,
|
||||
wonAmount: 696_000_000,
|
||||
lostAmount: 218_000_000,
|
||||
winRate: 58.3,
|
||||
},
|
||||
funnel: [
|
||||
{ stageId: 's1', stageName: 'تماس اول', count: 11, amount: 95_000_000 },
|
||||
{ stageId: 's2', stageName: 'نیازسنجی', count: 8, amount: 128_000_000 },
|
||||
{ stageId: 's3', stageName: 'پیشنهاد', count: 5, amount: 112_000_000 },
|
||||
{ stageId: 's4', stageName: 'مذاکره', count: 3, amount: 77_000_000 },
|
||||
],
|
||||
lostReasons: [
|
||||
{ label: 'قیمت', count: 9, percent: 36, amount: 82_000_000 },
|
||||
{ label: 'رقیب', count: 7, percent: 28, amount: 61_000_000 },
|
||||
{ label: 'زمانبندی', count: 5, percent: 20, amount: 44_000_000 },
|
||||
{ label: 'بدون دلیل ثبتشده', count: 4, percent: 16, amount: 31_000_000 },
|
||||
],
|
||||
activityByType: [
|
||||
{ type: 'call', label: 'تماس', count: 14 },
|
||||
{ type: 'meeting', label: 'جلسه', count: 7 },
|
||||
{ type: 'task', label: 'کار', count: 11 },
|
||||
{ type: 'note', label: 'یادداشت', count: 4 },
|
||||
],
|
||||
};
|
||||
|
||||
function hoursFromNow(hours: number) {
|
||||
const d = new Date();
|
||||
d.setHours(d.getHours() + hours);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
export const HOME_ACTIVITIES_MOCK = [
|
||||
{
|
||||
id: 'mock-act-1',
|
||||
type: 'call',
|
||||
status: 'open',
|
||||
priority: 'high',
|
||||
title: 'تماس پیگیری پیشنهاد آکمه',
|
||||
description: null,
|
||||
dueAt: hoursFromNow(3),
|
||||
durationMinutes: 30,
|
||||
reminderAt: null,
|
||||
reminderNotifyApp: false,
|
||||
reminderNotifySms: false,
|
||||
reminderSentAt: null,
|
||||
completedAt: null,
|
||||
tags: [],
|
||||
person: { id: 'p1', fullName: 'علی رضایی' },
|
||||
company: { id: 'c1', name: 'شرکت آکمه' },
|
||||
deal: null,
|
||||
assignee: null,
|
||||
assignees: [],
|
||||
createdBy: null,
|
||||
createdAt: hoursFromNow(-20),
|
||||
updatedAt: hoursFromNow(-2),
|
||||
},
|
||||
{
|
||||
id: 'mock-act-2',
|
||||
type: 'meeting',
|
||||
status: 'open',
|
||||
priority: 'medium',
|
||||
title: 'جلسه دمو با پارسنوین',
|
||||
description: null,
|
||||
dueAt: hoursFromNow(26),
|
||||
durationMinutes: 60,
|
||||
reminderAt: null,
|
||||
reminderNotifyApp: false,
|
||||
reminderNotifySms: false,
|
||||
reminderSentAt: null,
|
||||
completedAt: null,
|
||||
tags: [],
|
||||
person: { id: 'p2', fullName: 'مریم احمدی' },
|
||||
company: { id: 'c2', name: 'گروه پارسنوین' },
|
||||
deal: null,
|
||||
assignee: null,
|
||||
assignees: [],
|
||||
createdBy: null,
|
||||
createdAt: hoursFromNow(-48),
|
||||
updatedAt: hoursFromNow(-10),
|
||||
},
|
||||
{
|
||||
id: 'mock-act-3',
|
||||
type: 'task',
|
||||
status: 'done',
|
||||
priority: 'low',
|
||||
title: 'ارسال پیشفاکتور سپهر',
|
||||
description: null,
|
||||
dueAt: hoursFromNow(-5),
|
||||
durationMinutes: null,
|
||||
reminderAt: null,
|
||||
reminderNotifyApp: false,
|
||||
reminderNotifySms: false,
|
||||
reminderSentAt: null,
|
||||
completedAt: hoursFromNow(-4),
|
||||
tags: [],
|
||||
person: null,
|
||||
company: { id: 'c3', name: 'صنایع سپهر' },
|
||||
deal: null,
|
||||
assignee: null,
|
||||
assignees: [],
|
||||
createdBy: null,
|
||||
createdAt: hoursFromNow(-72),
|
||||
updatedAt: hoursFromNow(-4),
|
||||
},
|
||||
] as unknown as ActivityDto[];
|
||||
|
||||
export const HOME_PIPELINE_MOCK = {
|
||||
pipeline: {
|
||||
id: 'mock-pipe',
|
||||
name: 'فروش اصلی',
|
||||
isDefault: true,
|
||||
createdAt: hoursFromNow(-1000),
|
||||
updatedAt: hoursFromNow(-1),
|
||||
},
|
||||
stages: [
|
||||
{
|
||||
id: 's1',
|
||||
name: 'تماس اول',
|
||||
position: 0,
|
||||
isWon: false,
|
||||
isLost: false,
|
||||
totalAmount: 95_000_000,
|
||||
deals: [
|
||||
{
|
||||
id: 'd1',
|
||||
title: 'لایسنس سازمانی',
|
||||
amount: 45_000_000,
|
||||
currency: 'IRR',
|
||||
company: { id: 'c1', name: 'شرکت آکمه' },
|
||||
person: null,
|
||||
updatedAt: hoursFromNow(-30),
|
||||
createdAt: hoursFromNow(-200),
|
||||
},
|
||||
{
|
||||
id: 'd2',
|
||||
title: 'پکیج پشتیبانی',
|
||||
amount: 22_000_000,
|
||||
currency: 'IRR',
|
||||
company: null,
|
||||
person: { id: 'p3', fullName: 'سارا محمدی' },
|
||||
updatedAt: hoursFromNow(-8),
|
||||
createdAt: hoursFromNow(-90),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 's2',
|
||||
name: 'نیازسنجی',
|
||||
position: 1,
|
||||
isWon: false,
|
||||
isLost: false,
|
||||
totalAmount: 128_000_000,
|
||||
deals: [
|
||||
{
|
||||
id: 'd3',
|
||||
title: 'ارتقای CRM',
|
||||
amount: 68_000_000,
|
||||
currency: 'IRR',
|
||||
company: { id: 'c2', name: 'گروه پارسنوین' },
|
||||
person: null,
|
||||
updatedAt: hoursFromNow(-12),
|
||||
createdAt: hoursFromNow(-140),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 's3',
|
||||
name: 'پیشنهاد',
|
||||
position: 2,
|
||||
isWon: false,
|
||||
isLost: false,
|
||||
totalAmount: 112_000_000,
|
||||
deals: [
|
||||
{
|
||||
id: 'd4',
|
||||
title: 'پروژه یکپارچهسازی',
|
||||
amount: 112_000_000,
|
||||
currency: 'IRR',
|
||||
company: { id: 'c3', name: 'صنایع سپهر' },
|
||||
person: null,
|
||||
updatedAt: hoursFromNow(-2),
|
||||
createdAt: hoursFromNow(-60),
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as unknown as PipelineBoardDto;
|
||||
|
||||
export function isHomeDashboardEmpty(data: HomeDashboardDto | null | undefined): boolean {
|
||||
if (!data) return true;
|
||||
const salesFlat = (data.salesTrend ?? []).every((p) => !p.amount);
|
||||
const growthFlat = (data.customerGrowth ?? []).every((p) => !p.value);
|
||||
const noSources = !(data.sources ?? []).length;
|
||||
const kpisFlat = (data.kpis ?? []).every((k) => !k.value);
|
||||
return (salesFlat && growthFlat && noSources) || kpisFlat;
|
||||
}
|
||||
@@ -45,11 +45,8 @@ import type {
|
||||
} from 'echarts/components';
|
||||
import VChart from 'vue-echarts';
|
||||
import { api } from '@/lib/api';
|
||||
import axios from 'axios';
|
||||
import type { TreasuryDashboardDto } from '@mento/shared';
|
||||
import {
|
||||
TREASURY_DASHBOARD_MOCK,
|
||||
isTreasuryDashboardEmpty,
|
||||
} from './treasury-dashboard-mock';
|
||||
import { toman } from './treasury-format';
|
||||
|
||||
use([CanvasRenderer, BarChart, PieChart, GridComponent, TooltipComponent, LegendComponent]);
|
||||
@@ -92,7 +89,6 @@ type MetricCard = {
|
||||
const data = ref<TreasuryDashboardDto | null>(null);
|
||||
const error = ref('');
|
||||
const loading = ref(true);
|
||||
const usingMock = ref(false);
|
||||
|
||||
const heroCards: MetricCard[] = [
|
||||
{
|
||||
@@ -288,19 +284,18 @@ const mixChartOption = computed<ChartOption>(() => {
|
||||
|
||||
onMounted(async () => {
|
||||
loading.value = true;
|
||||
usingMock.value = false;
|
||||
error.value = '';
|
||||
try {
|
||||
const res = await api.get<TreasuryDashboardDto>('/treasury/dashboard');
|
||||
if (isTreasuryDashboardEmpty(res.data)) {
|
||||
data.value = TREASURY_DASHBOARD_MOCK;
|
||||
usingMock.value = true;
|
||||
} else {
|
||||
data.value = res.data;
|
||||
}
|
||||
} catch {
|
||||
data.value = TREASURY_DASHBOARD_MOCK;
|
||||
usingMock.value = true;
|
||||
error.value = '';
|
||||
data.value = res.data;
|
||||
} catch (e) {
|
||||
data.value = null;
|
||||
error.value = axios.isAxiosError(e)
|
||||
? String(
|
||||
(e.response?.data as { message?: string })?.message ??
|
||||
'بارگذاری داشبورد خزانه ناموفق بود.',
|
||||
)
|
||||
: 'بارگذاری داشبورد خزانه ناموفق بود.';
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
@@ -311,13 +306,6 @@ onMounted(async () => {
|
||||
<div>
|
||||
<a-flex vertical :gap="token.margin">
|
||||
<a-alert v-if="error" type="error" show-icon :message="error" />
|
||||
<a-alert
|
||||
v-if="usingMock"
|
||||
type="info"
|
||||
show-icon
|
||||
message="در حال نمایش دادهٔ نمونه (ماک) برای پیشنمایش داشبورد هستید."
|
||||
description="وقتی سند و حساب واقعی ثبت شود، همین صفحه دادهٔ واقعی API را نشان میدهد."
|
||||
/>
|
||||
|
||||
<CrmSkeleton :loading="loading">
|
||||
<a-row :gutter="[token.marginSM, token.marginSM]">
|
||||
|
||||
@@ -1,190 +0,0 @@
|
||||
import type {
|
||||
FinancialDocumentDto,
|
||||
FinancialInstallmentDto,
|
||||
TreasuryDashboardDto,
|
||||
} from '@mento/shared';
|
||||
|
||||
function daysFromNow(days: number) {
|
||||
const d = new Date();
|
||||
d.setHours(12, 0, 0, 0);
|
||||
d.setDate(d.getDate() + days);
|
||||
return d.toISOString();
|
||||
}
|
||||
|
||||
function doc(
|
||||
partial: Pick<
|
||||
FinancialDocumentDto,
|
||||
'id' | 'kind' | 'title' | 'amount' | 'settledAmount' | 'remaining' | 'status' | 'categoryName' | 'partyName'
|
||||
> &
|
||||
Partial<FinancialDocumentDto>,
|
||||
): FinancialDocumentDto {
|
||||
return {
|
||||
bookedAt: daysFromNow(-3),
|
||||
dueAt: daysFromNow(10),
|
||||
categoryId: null,
|
||||
personId: null,
|
||||
accountId: null,
|
||||
accountName: 'صندوق اصلی',
|
||||
method: 'transfer',
|
||||
reference: null,
|
||||
notes: null,
|
||||
createdAt: daysFromNow(-2),
|
||||
...partial,
|
||||
partyKind: partial.partyKind ?? null,
|
||||
companyId: partial.companyId ?? null,
|
||||
userId: partial.userId ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
function installment(
|
||||
partial: FinancialInstallmentDto,
|
||||
): FinancialInstallmentDto {
|
||||
return partial;
|
||||
}
|
||||
|
||||
/** دیتای نمایشی برای پیشنمایش داشبورد وقتی API خالی است */
|
||||
export const TREASURY_DASHBOARD_MOCK: TreasuryDashboardDto = {
|
||||
bookedIncome: '185000000',
|
||||
actualReceipts: '142500000',
|
||||
bookedExpense: '98000000',
|
||||
actualPayments: '87500000',
|
||||
operatingProfit: '87000000',
|
||||
receivables: '42500000',
|
||||
payables: '18500000',
|
||||
totalBalance: '256000000',
|
||||
upcomingInstallments: [
|
||||
installment({
|
||||
id: 'mock-inst-1',
|
||||
position: 2,
|
||||
dueDate: daysFromNow(-2),
|
||||
amount: '12000000',
|
||||
paidAmount: '0',
|
||||
remaining: '12000000',
|
||||
status: 'overdue',
|
||||
}),
|
||||
installment({
|
||||
id: 'mock-inst-2',
|
||||
position: 3,
|
||||
dueDate: daysFromNow(5),
|
||||
amount: '12000000',
|
||||
paidAmount: '4000000',
|
||||
remaining: '8000000',
|
||||
status: 'partial',
|
||||
}),
|
||||
installment({
|
||||
id: 'mock-inst-3',
|
||||
position: 4,
|
||||
dueDate: daysFromNow(35),
|
||||
amount: '12000000',
|
||||
paidAmount: '0',
|
||||
remaining: '12000000',
|
||||
status: 'open',
|
||||
}),
|
||||
],
|
||||
upcomingPayables: [
|
||||
doc({
|
||||
id: 'mock-pay-1',
|
||||
kind: 'expense',
|
||||
title: 'اجاره دفتر — مهر',
|
||||
amount: '45000000',
|
||||
settledAmount: '30000000',
|
||||
remaining: '15000000',
|
||||
status: 'partial',
|
||||
categoryName: 'اجاره',
|
||||
partyName: 'موجر ساختمان',
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-pay-2',
|
||||
kind: 'expense',
|
||||
title: 'بیمه تکمیلی کارکنان',
|
||||
amount: '8500000',
|
||||
settledAmount: '5000000',
|
||||
remaining: '3500000',
|
||||
status: 'partial',
|
||||
categoryName: 'بیمه',
|
||||
partyName: 'بیمه ایران',
|
||||
}),
|
||||
],
|
||||
recent: [
|
||||
doc({
|
||||
id: 'mock-doc-1',
|
||||
kind: 'income',
|
||||
title: 'پیشپرداخت پروژه آلفا',
|
||||
amount: '75000000',
|
||||
settledAmount: '75000000',
|
||||
remaining: '0',
|
||||
status: 'settled',
|
||||
categoryName: 'فروش خدمات',
|
||||
partyName: 'شرکت آریا',
|
||||
bookedAt: daysFromNow(-1),
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-doc-2',
|
||||
kind: 'expense',
|
||||
title: 'خرید تجهیزات شبکه',
|
||||
amount: '18500000',
|
||||
settledAmount: '18500000',
|
||||
remaining: '0',
|
||||
status: 'settled',
|
||||
categoryName: 'تجهیزات',
|
||||
partyName: 'فروشگاه سختافزار',
|
||||
bookedAt: daysFromNow(-2),
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-doc-3',
|
||||
kind: 'income',
|
||||
title: 'قسط قرارداد بتا',
|
||||
amount: '12000000',
|
||||
settledAmount: '0',
|
||||
remaining: '12000000',
|
||||
status: 'open',
|
||||
categoryName: 'فروش خدمات',
|
||||
partyName: 'گروه سپهر',
|
||||
bookedAt: daysFromNow(-4),
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-doc-4',
|
||||
kind: 'expense',
|
||||
title: 'حقوق مهر — تیم فروش',
|
||||
amount: '42000000',
|
||||
settledAmount: '42000000',
|
||||
remaining: '0',
|
||||
status: 'settled',
|
||||
categoryName: 'حقوق',
|
||||
partyName: null,
|
||||
bookedAt: daysFromNow(-5),
|
||||
}),
|
||||
doc({
|
||||
id: 'mock-doc-5',
|
||||
kind: 'income',
|
||||
title: 'فروش پکیج پشتیبانی',
|
||||
amount: '28000000',
|
||||
settledAmount: '10000000',
|
||||
remaining: '18000000',
|
||||
status: 'partial',
|
||||
categoryName: 'پشتیبانی',
|
||||
partyName: 'کارخانه نوین',
|
||||
bookedAt: daysFromNow(-6),
|
||||
}),
|
||||
],
|
||||
};
|
||||
|
||||
export function isTreasuryDashboardEmpty(data: TreasuryDashboardDto | null | undefined) {
|
||||
if (!data) return true;
|
||||
const keys = [
|
||||
data.bookedIncome,
|
||||
data.actualReceipts,
|
||||
data.bookedExpense,
|
||||
data.actualPayments,
|
||||
data.totalBalance,
|
||||
data.receivables,
|
||||
data.payables,
|
||||
] as const;
|
||||
const allZero = keys.every((v) => !Number(v));
|
||||
return (
|
||||
allZero &&
|
||||
data.recent.length === 0 &&
|
||||
data.upcomingInstallments.length === 0 &&
|
||||
data.upcomingPayables.length === 0
|
||||
);
|
||||
}
|
||||
+1025
File diff suppressed because one or more lines are too long
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* تنها منبع نام و لوگوی محصول.
|
||||
* برای تغییر برند همین فایل را عوض کن؛ برای لوگو فایل
|
||||
* `apps/web/public/brand/logo.png` را جایگزین کن یا مسیر `logo` را اینجا تغییر بده.
|
||||
* `apps/web/public/brand/logo.svg` را جایگزین کن یا مسیر `logo` را اینجا تغییر بده.
|
||||
*/
|
||||
export const brand = {
|
||||
id: 'montact',
|
||||
@@ -10,7 +10,10 @@ export const brand = {
|
||||
fullName: 'Montact CRM',
|
||||
company: 'Mentech',
|
||||
taglineFa: 'نرمافزار مدیریت ارتباط با مشتری',
|
||||
logo: '/brand/logo.png',
|
||||
/** لوگوی اصلی علامت (Asset 26) — همهٔ مصرفها از این مسیر */
|
||||
logo: '/brand/logo.svg',
|
||||
/** لوگو افقی با متن (Asset 24) — فقط سایدبار */
|
||||
logoWordmark: '/brand/montact-wordmark.svg',
|
||||
askPlaceholderFa: 'از مُنتَکت بپرس...',
|
||||
askLabelFa: 'از مُنتَکت بپرس',
|
||||
memoryEn: 'Montact Memory',
|
||||
|
||||
Reference in New Issue
Block a user