recive email + save draft and list draft + skeleton email detail

This commit is contained in:
hamid zarghami
2025-07-10 11:06:36 +03:30
parent 4258f57662
commit df6e74c632
14 changed files with 540 additions and 228 deletions
+2 -2
View File
@@ -1,6 +1,6 @@
VITE_TOKEN_NAME = 'dmail_token' VITE_TOKEN_NAME = 'dmail_token'
VITE_REFRESH_TOKEN_NAME = 'dmail_refresh_token' VITE_REFRESH_TOKEN_NAME = 'dmail_refresh_token'
VITE_DANAK_BASE_URL ='https://api.danakcorp.com' VITE_DANAK_BASE_URL ='https://api.danakcorp.com'
# VITE_BASE_URL = 'http://192.168.1.134:4000' VITE_BASE_URL = 'http://192.168.1.134:4000'
VITE_BASE_URL = 'https://dmail-api.danakcorp.com' # VITE_BASE_URL = 'https://dmail-api.danakcorp.com'
VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e' VITE_SERVICE_ID = 'e51afdc3-ea0b-49cf-8f49-2a6f131b024e'
+23 -90
View File
@@ -1,91 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!-- Generator: Adobe Illustrator 27.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> <svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 841.89 595.28">
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" <!-- Generator: Adobe Illustrator 29.5.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 141) -->
viewBox="0 0 961.6 222.5" style="enable-background:new 0 0 961.6 222.5;" xml:space="preserve"> <defs>
<style type="text/css"> <style>
.st0{fill:#1D1D1B;} .st0 {
</style> fill: #231f20;
<g> }
<g> </style>
<g> </defs>
<g> <path d="M366.35,300.52v12.08h-29.98v-12.08h29.98Z"/>
<path class="st0" d="M246,180.4h-8.7v24.3h-3.6v-24.3H225v-2.9h21V180.4z"/> <path d="M237.82,332.6h-9.97v-3.26c0-1.08.22-1.9.67-2.44.45-.54,1.11-.82,1.98-.82.47,0,.88.13,1.24.4.36.26.64.62.83,1.08.15-.54.44-.96.86-1.28.42-.31.93-.47,1.51-.47.89,0,1.6.29,2.11.87s.77,1.4.77,2.46v3.46ZM232.1,331.29v-1.98c0-.58-.14-1.04-.43-1.38-.29-.34-.68-.52-1.17-.52-.55,0-.95.16-1.2.48-.25.32-.37.81-.37,1.46v1.94h3.18ZM233.16,331.29h3.59v-2.17c0-.61-.16-1.09-.48-1.45-.32-.35-.75-.53-1.31-.53-1.2,0-1.8.65-1.8,1.96v2.19Z"/>
<path class="st0" d="M279.1,205c-2.7,0-5-0.9-6.7-2.7s-2.6-4.2-2.6-7.2v-0.6c0-2,0.4-3.8,1.1-5.4s1.8-2.8,3.2-3.7 <path d="M232.85,320.33l-5-2.6v-1.49l6.25,3.44h3.72v1.31h-3.72l-6.25,3.44v-1.51l5-2.59Z"/>
s2.9-1.3,4.5-1.3c2.6,0,4.7,0.9,6.1,2.6c1.5,1.7,2.2,4.2,2.2,7.4v1.4h-13.7c0,2,0.6,3.6,1.7,4.8s2.5,1.8,4.2,1.8 <path d="M237.58,310.17h-9.99v-2.82c0-.87.19-1.64.58-2.31.38-.67.93-1.18,1.64-1.55.71-.36,1.52-.55,2.44-.55h.64c.94,0,1.77.18,2.48.55s1.25.88,1.63,1.56c.38.67.57,1.46.58,2.36v2.77ZM228.67,308.85h7.84v-1.39c0-1.02-.32-1.81-.95-2.37-.63-.57-1.53-.85-2.7-.85h-.58c-1.14,0-2.02.27-2.65.8-.63.53-.95,1.29-.96,2.27v1.54Z"/>
c1.2,0,2.3-0.2,3.1-0.7s1.6-1.2,2.2-2l2.1,1.6C285,203.7,282.5,205,279.1,205z M278.7,186.9c-1.4,0-2.6,0.5-3.5,1.5 <path d="M237.58,282.61v1.32l-7.7,5.03h7.7v1.32h-9.99v-1.32l7.74-5.04h-7.74v-1.31h9.99Z"/>
s-1.5,2.4-1.8,4.3h10.1v-0.3c-0.1-1.8-0.6-3.1-1.4-4.1C281.3,187.4,280.1,186.9,278.7,186.9z"/> <path d="M232.93,267.02l1.27,1.22h3.38v1.32h-9.99v-1.32h4.94l-4.94-4.44v-1.59l4.41,3.93,5.58-4.24v1.58l-4.65,3.54Z"/>
<path class="st0" d="M320.4,202.2c1.2,0,2.3-0.4,3.2-1.1c0.9-0.7,1.4-1.7,1.5-2.8h3.3c-0.1,1.2-0.5,2.3-1.2,3.3 <polygon points="229.21 296.84 237.58 293.79 237.58 292.44 227.59 296.27 227.59 297.42 237.58 301.24 237.58 299.88 229.21 296.84"/>
s-1.7,1.9-2.9,2.5c-1.2,0.6-2.5,0.9-3.9,0.9c-2.8,0-4.9-0.9-6.6-2.8c-1.6-1.8-2.4-4.3-2.4-7.5v-0.6c0-2,0.4-3.7,1.1-5.2 <polygon points="229.21 276.17 237.58 273.12 237.58 271.77 227.59 275.6 227.59 276.75 237.58 280.57 237.58 279.21 229.21 276.17"/>
s1.8-2.7,3.1-3.6c1.4-0.8,2.9-1.3,4.8-1.3c2.3,0,4.1,0.7,5.6,2c1.5,1.4,2.3,3.1,2.4,5.3h-3.3c-0.1-1.3-0.6-2.4-1.5-3.2 <path class="st0" d="M414.84,314.81h.3l18.96-52.91h19.32v71.79h-14.81v-49.01l-.3-.05-18.25,49.06h-10.04l-18.25-49.16-.3.05v49.11h-14.8v-71.79h19.42l18.76,52.91Z"/>
s-2-1.3-3.3-1.3c-1.8,0-3.1,0.6-4.1,1.9s-1.4,3.1-1.4,5.5v0.7c0,2.3,0.5,4.1,1.4,5.4C317.2,201.6,318.6,202.2,320.4,202.2z"/> <path class="st0" d="M553.87,333.71h-15.05v-71.79h15.05v71.79Z"/>
<path class="st0" d="M357,186.9c1.5-1.9,3.5-2.8,6-2.8c4.3,0,6.4,2.4,6.5,7.2v13.4H366v-13.4c0-1.5-0.3-2.5-1-3.2s-1.7-1-3.1-1 <path class="st0" d="M580.35,322.65h34.83v11.05h-50.42v-71.79h15.6v60.75Z"/>
c-1.1,0-2.1,0.3-3,0.9s-1.5,1.4-2,2.4v14.4h-3.4v-28.7h3.5V186.9z"/> <polygon class="st0" points="503.2 261.9 486.88 261.9 460.07 333.69 476.72 333.69 482.07 318.21 482.09 318.21 486.02 306.92 486 306.92 490.09 295.23 490.09 295.23 490.09 295.23 494.85 281.62 495.17 281.62 502.88 303.78 502.88 303.78 502.88 303.78 503.96 306.92 503.96 306.92 509.79 323.79 509.79 323.79 513.25 333.69 529.9 333.69 503.2 261.9"/>
<path class="st0" d="M399.1,184.5l0.1,2.5c1.5-1.9,3.6-2.9,6.1-2.9c4.3,0,6.4,2.4,6.5,7.2v13.4h-3.5v-13.4c0-1.5-0.3-2.5-1-3.2 <path d="M290.8,261.9c-8.76,0-27.37.05-27.79.07,8.69,8.7,25.29,25.04,35.12,34.83-1.03,1.03-2.08,2.08-3.19,3.19-3.01,3.01-5.04,3.73-8.08.69-12.46-12.46-38.65-38.71-38.75-38.78h-5.28v71.79s11.97-.02,12.57-.02v-46.95c.06-.22,4.3,3.95,9.51,9.14l-.45,37.85c.65,0,18.09.02,25.65-.01,1.88,0,3.78-.11,5.65-.31,16.3-1.82,29.74-15.62,31.21-32.01,1.93-21.53-14.52-39.48-36.16-39.48ZM289.13,273.48c9.28-.86,17.69,3.92,21.54,10.81-1.8,1.8-3.48,3.47-5.12,5.1-4.95-4.8-13.48-13.06-16.42-15.91ZM291.13,321.97c-2.92.05-5.84.05-8.76.04h0s-7.68,0-7.68,0v-16.34c2.08,2.08,4,4.01,5.57,5.56,6.21,6.16,15.69,3.55,21.9-2.58,4.29-4.24,12.61-12.65,13.24-13.24.93,14.06-10.11,26.33-24.28,26.58Z"/>
s-1.7-1-3.1-1c-1.1,0-2.1,0.3-3,0.9s-1.5,1.4-2,2.4v14.4h-3.5v-20.2h3.4V184.5z"/>
<path class="st0" d="M437.3,194.4c0-2,0.4-3.8,1.2-5.3c0.8-1.6,1.9-2.8,3.2-3.7s3-1.3,4.8-1.3c2.8,0,5,1,6.7,2.9
s2.5,4.4,2.5,7.6v0.2c0,2-0.4,3.7-1.1,5.3s-1.8,2.8-3.2,3.7c-1.4,0.9-3,1.3-4.8,1.3c-2.7,0-5-1-6.7-2.9s-2.5-4.4-2.5-7.6v-0.2
H437.3z M440.8,194.8c0,2.2,0.5,4,1.6,5.4c1,1.4,2.4,2,4.2,2c1.8,0,3.1-0.7,4.2-2.1s1.6-3.3,1.6-5.8c0-2.2-0.5-4-1.6-5.4
s-2.5-2.1-4.2-2.1s-3.1,0.7-4.1,2C441.3,190.3,440.8,192.3,440.8,194.8z"/>
<path class="st0" d="M485.1,204.7h-3.5V176h3.5V204.7z"/>
<path class="st0" d="M511,194.4c0-2,0.4-3.8,1.2-5.3c0.8-1.5,1.9-2.8,3.2-3.7c1.4-0.9,3-1.3,4.8-1.3c2.8,0,5,1,6.7,2.9
s2.5,4.4,2.5,7.6v0.2c0,2-0.4,3.7-1.1,5.3s-1.8,2.8-3.2,3.7s-3,1.3-4.8,1.3c-2.7,0-5-1-6.7-2.9c-1.7-1.9-2.5-4.4-2.5-7.6v-0.2
H511z M514.4,194.8c0,2.2,0.5,4,1.6,5.4c1,1.4,2.4,2,4.2,2s3.2-0.7,4.2-2.1c1-1.4,1.5-3.3,1.5-5.8c0-2.2-0.5-4-1.6-5.4
s-2.5-2.1-4.2-2.1s-3.1,0.7-4.1,2C515,190.3,514.4,192.3,514.4,194.8z"/>
<path class="st0" d="M554.1,194.4c0-3.2,0.7-5.7,2.2-7.5s3.4-2.8,5.8-2.8c2.5,0,4.4,0.9,5.8,2.6l0.2-2.2h3.2v19.7
c0,2.6-0.8,4.7-2.3,6.2c-1.6,1.5-3.6,2.3-6.2,2.3c-1.5,0-2.9-0.3-4.3-0.9c-1.4-0.6-2.5-1.5-3.2-2.6l1.8-2.1
c1.5,1.8,3.3,2.7,5.4,2.7c1.7,0,3-0.5,3.9-1.4s1.4-2.3,1.4-4v-1.7c-1.4,1.6-3.3,2.4-5.7,2.4s-4.3-1-5.7-2.9
S554.1,197.7,554.1,194.4z M557.6,194.8c0,2.3,0.5,4.1,1.4,5.4c0.9,1.3,2.2,2,3.9,2c2.2,0,3.8-1,4.8-3V190
c-1.1-1.9-2.6-2.9-4.8-2.9c-1.7,0-3,0.7-3.9,2C558.1,190.3,557.6,192.2,557.6,194.8z"/>
<path class="st0" d="M604.3,199.6l4.7-15.1h3.7l-8.1,23.3c-1.3,3.4-3.3,5-6,5l-0.7-0.1l-1.3-0.2v-2.8l0.9,0.1
c1.2,0,2.1-0.2,2.7-0.7c0.7-0.5,1.2-1.3,1.6-2.6l0.8-2.1l-7.2-20h3.8L604.3,199.6z"/>
<path class="st0" d="M688.3,196c-0.3,2.9-1.4,5.1-3.2,6.7c-1.8,1.6-4.2,2.3-7.1,2.3c-3.2,0-5.8-1.2-7.7-3.5s-2.9-5.4-2.9-9.2
v-2.6c0-2.5,0.5-4.8,1.4-6.7s2.2-3.4,3.8-4.4s3.6-1.5,5.8-1.5c2.9,0,5.2,0.8,6.9,2.4c1.7,1.6,2.8,3.8,3,6.7h-3.6
c-0.3-2.2-1-3.7-2-4.7s-2.5-1.5-4.4-1.5c-2.3,0-4.1,0.8-5.4,2.5c-1.3,1.7-2,4.1-2,7.2v2.6c0,3,0.6,5.3,1.8,7s3,2.6,5.2,2.6
c2,0,3.5-0.5,4.6-1.4s1.8-2.5,2.1-4.7h3.7V196z"/>
<path class="st0" d="M713,194.4c0-2,0.4-3.8,1.2-5.3s1.9-2.8,3.2-3.7c1.4-0.9,3-1.3,4.8-1.3c2.8,0,5,1,6.7,2.9s2.5,4.4,2.5,7.6
v0.2c0,2-0.4,3.7-1.1,5.3s-1.8,2.8-3.2,3.7s-3,1.3-4.8,1.3c-2.7,0-5-1-6.7-2.9s-2.5-4.4-2.5-7.6v-0.2H713z M716.5,194.8
c0,2.2,0.5,4,1.6,5.4c1,1.4,2.4,2,4.2,2s3.2-0.7,4.2-2.1c1-1.4,1.5-3.3,1.5-5.8c0-2.2-0.5-4-1.6-5.4s-2.5-2.1-4.2-2.1
s-3.1,0.7-4.1,2C717,190.3,716.5,192.3,716.5,194.8z"/>
<path class="st0" d="M760.3,184.5l0.1,2.2c1.5-1.7,3.5-2.6,6-2.6c2.8,0,4.8,1.1,5.8,3.2c0.7-1,1.5-1.8,2.6-2.4
c1.1-0.6,2.4-0.9,3.8-0.9c4.4,0,6.7,2.3,6.8,7v13.5h-3.5v-13.3c0-1.4-0.3-2.5-1-3.2s-1.8-1.1-3.3-1.1c-1.3,0-2.3,0.4-3.2,1.1
c-0.8,0.8-1.3,1.8-1.5,3.1v13.4h-3.5v-13.2c0-2.9-1.4-4.4-4.3-4.4c-2.3,0-3.8,1-4.7,2.9v14.8h-3.5v-20.2h3.4V184.5z"/>
<path class="st0" d="M828.9,194.8c0,3.1-0.7,5.6-2.1,7.4c-1.4,1.9-3.3,2.8-5.7,2.8c-2.5,0-4.4-0.8-5.8-2.3v9.7h-3.5v-28h3.2
l0.2,2.2c1.4-1.7,3.4-2.6,5.9-2.6c2.4,0,4.3,0.9,5.8,2.7c1.4,1.8,2.1,4.4,2.1,7.6v0.5H828.9z M825.5,194.4
c0-2.3-0.5-4.1-1.5-5.4c-1-1.3-2.3-2-4-2c-2.1,0-3.7,0.9-4.7,2.8v9.7c1,1.8,2.6,2.8,4.7,2.8c1.7,0,3-0.7,4-2
S825.5,197,825.5,194.4z"/>
<path class="st0" d="M867,204.7c-0.2-0.4-0.4-1.1-0.5-2.1c-1.6,1.7-3.5,2.5-5.8,2.5c-2,0-3.6-0.6-4.9-1.7s-1.9-2.6-1.9-4.3
c0-2.1,0.8-3.7,2.4-4.9c1.6-1.2,3.9-1.7,6.8-1.7h3.4v-1.7c0-1.2-0.4-2.2-1.1-2.9c-0.7-0.7-1.8-1.1-3.2-1.1
c-1.2,0-2.3,0.3-3.1,0.9c-0.8,0.6-1.3,1.4-1.3,2.3h-3.5c0-1,0.4-2,1.1-2.9c0.7-0.9,1.7-1.7,2.9-2.2s2.6-0.8,4-0.8
c2.3,0,4.2,0.6,5.5,1.7c1.3,1.2,2,2.8,2.1,4.8v9.3c0,1.9,0.2,3.3,0.7,4.4v0.3H867V204.7z M861.3,202c1.1,0,2.1-0.3,3.1-0.8
c1-0.6,1.7-1.3,2.1-2.2v-4.1h-2.7c-4.2,0-6.4,1.2-6.4,3.7c0,1.1,0.4,1.9,1.1,2.5C859.2,201.7,860.2,202,861.3,202z"/>
<path class="st0" d="M899.9,184.5l0.1,2.5c1.5-1.9,3.6-2.9,6.1-2.9c4.3,0,6.4,2.4,6.5,7.2v13.4h-3.5v-13.4c0-1.5-0.3-2.5-1-3.2
s-1.7-1-3.1-1c-1.1,0-2.1,0.3-3,0.9s-1.5,1.4-2,2.4v14.4h-3.5v-20.2h3.4V184.5z"/>
<path class="st0" d="M945.6,199.6l4.7-15.1h3.7l-8.1,23.3c-1.3,3.4-3.3,5-6,5l-0.7-0.1l-1.3-0.2v-2.8l0.9,0.1
c1.2,0,2.1-0.2,2.7-0.7c0.7-0.5,1.2-1.3,1.6-2.6l0.8-2.1l-7.2-20h3.8L945.6,199.6z"/>
</g>
</g>
<path class="st0" d="M221.6,148.7V44.1h29.5c9.1,0,17.1,2,24.1,6s12.4,9.7,16.2,17.2c3.8,7.4,5.7,15.9,5.8,25.6v6.7
c0,9.9-1.9,18.5-5.7,25.9c-3.8,7.4-9.2,13.1-16.3,17.1c-7.1,4-15.3,6-24.7,6.1L221.6,148.7L221.6,148.7z M235.4,55.4v82h14.5
c10.6,0,18.9-3.3,24.8-9.9c5.9-6.6,8.9-16,8.9-28.3v-6.1c0-11.9-2.8-21.1-8.4-27.7s-13.5-9.9-23.7-10L235.4,55.4L235.4,55.4z"/>
<path class="st0" d="M627.1,148.7h-13.9l-52.7-80.6v80.6h-13.9V44.1h13.9l52.8,81v-81H627v104.6H627.1z"/>
<path class="st0" d="M900.9,100.1l-12.8,13.3v35.3h-13.8V44.1h13.8v51.7l46.5-51.7h16.7l-41.2,46.2l44.4,58.4H938L900.9,100.1z"/>
<polygon class="st0" points="423.2,61.1 455.1,148.7 469.2,148.7 429.2,44.1 417.1,44.1 377.2,148.7 391.4,148.7 "/>
<polygon class="st0" points="750.6,61.1 782.6,148.7 796.7,148.7 756.7,44.1 744.6,44.1 704.7,148.7 718.9,148.7 "/>
</g>
<path class="st0" d="M132.1,140.4c0,0-26.1,25.2-48,46.5v28.9c-2.2,1.7,77.6-75.4,77.6-75.4L84.1,59v31L132.1,140.4z"/>
<g>
<polygon class="st0" points="83.7,157.5 6.5,75.1 7.5,74.2 84.7,156.6 "/>
<polygon class="st0" points="83.7,149.8 13.6,75.2 14.6,74.3 84.6,148.9 "/>
<polygon class="st0" points="83.7,142.1 20.7,75.4 21.7,74.4 84.6,141.1 "/>
<polygon class="st0" points="83.6,134.3 27.8,75.5 28.7,74.6 84.6,133.4 "/>
<polygon class="st0" points="83.6,126.6 34.9,75.6 35.8,74.7 84.6,125.7 "/>
</g>
<g>
<polygon class="st0" points="8,75.5 7.1,74.5 83.7,0 84.6,0.9 "/>
<polygon class="st0" points="15.2,75.5 14.3,74.6 83.6,7 84.5,7.9 "/>
<polygon class="st0" points="22.4,75.6 21.4,74.7 83.6,14 84.5,14.9 "/>
<polygon class="st0" points="29.5,75.6 28.6,74.7 83.5,21 84.4,22 "/>
<polygon class="st0" points="36.7,75.7 35.7,74.8 83.5,28 84.4,29 "/>
</g>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

+2 -2
View File
@@ -85,7 +85,7 @@ const Table = <T extends RowDataType>({
if (data.length === 0) { if (data.length === 0) {
return ( return (
<div className={`bg-white ${roundedClass} py-6 md:py-8 text-center text-gray-500`}> <div className={`bg-white ${roundedClass} text-center text-gray-500`}>
{noDataMessage} {noDataMessage}
</div> </div>
); );
@@ -155,7 +155,7 @@ const Table = <T extends RowDataType>({
if (data.length === 0) { if (data.length === 0) {
return ( return (
<tr> <tr>
<td colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 py-6 md:py-8 text-center text-gray-500"> <td colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 pb-6 md:pb-8 text-center text-gray-500">
{noDataMessage} {noDataMessage}
</td> </td>
</tr> </tr>
+100 -52
View File
@@ -1,9 +1,9 @@
import { FC, useState, useEffect } from 'react' import { FC, useState, useEffect, useRef } from 'react'
import { CloseCircle } from 'iconsax-react' import { CloseCircle } from 'iconsax-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useSharedStore } from '@/shared/store/sharedStore' import { useSharedStore } from '@/shared/store/sharedStore'
import { useAuthStore } from '../../pages/auth/store/AuthStore' import { useAuthStore } from '../../pages/auth/store/AuthStore'
import { useSendEmail, useSaveDraft } from '../../pages/received/hooks/useEmailData' import { useSendEmail } from '../../pages/received/hooks/useEmailData'
import { SendEmailDto } from '../../pages/received/types/Types' import { SendEmailDto } from '../../pages/received/types/Types'
import Input from '../Input' import Input from '../Input'
import ReactQuill from 'react-quill-new'; import ReactQuill from 'react-quill-new';
@@ -11,6 +11,7 @@ import Button from '../Button'
import Select from '../Select' import Select from '../Select'
import UploadButton from '../UploadButton' import UploadButton from '../UploadButton'
import { toast } from '../Toast' import { toast } from '../Toast'
import { ErrorType } from '@/helpers/types'
const NewMessage: FC = () => { const NewMessage: FC = () => {
@@ -28,10 +29,15 @@ const NewMessage: FC = () => {
const [subject, setSubject] = useState('') const [subject, setSubject] = useState('')
const [priority, setPriority] = useState('') const [priority, setPriority] = useState('')
// Loading states
const [isSending, setIsSending] = useState(false)
const [isSavingDraft, setIsSavingDraft] = useState(false)
// Auto-add email functionality
const typingTimeoutRef = useRef<number | null>(null)
// API hooks // API hooks
const sendEmailMutation = useSendEmail() const sendEmailMutation = useSendEmail()
const saveDraftMutation = useSaveDraft()
useEffect(() => { useEffect(() => {
if (openNewMessage) { if (openNewMessage) {
@@ -70,13 +76,8 @@ const NewMessage: FC = () => {
return emailRegex.test(email) return emailRegex.test(email)
} }
const handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { const addEmailToList = (email: string) => {
if (e.key === 'Enter' && to.trim()) {
e.preventDefault()
const email = to.trim()
if (!isValidEmail(email)) { if (!isValidEmail(email)) {
toast('لطفا یک ایمیل معتبر وارد کنید', 'error')
return return
} }
@@ -88,45 +89,79 @@ const NewMessage: FC = () => {
setToEmails([...toEmails, email]) setToEmails([...toEmails, email])
setTo('') setTo('')
} }
const handleToChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value
setTo(newValue)
// Clear existing timeout
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
} }
// Set new timeout to auto-add email after user stops typing
if (newValue.trim()) {
typingTimeoutRef.current = window.setTimeout(() => {
if (isValidEmail(newValue.trim())) {
addEmailToList(newValue.trim())
}
}, 1500) // Wait 1.5 seconds after user stops typing
}
}
const handleToBlur = () => {
// Clear timeout when input loses focus
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
// Auto-add email if valid when input loses focus
if (to.trim() && isValidEmail(to.trim())) {
addEmailToList(to.trim())
}
}
const handleToKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter' && to.trim()) {
e.preventDefault()
// Clear timeout when Enter is pressed
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
addEmailToList(to.trim())
}
}
// Clean up timeout on component unmount
useEffect(() => {
return () => {
if (typingTimeoutRef.current) {
clearTimeout(typingTimeoutRef.current)
}
}
}, [])
const removeEmail = (emailToRemove: string) => { const removeEmail = (emailToRemove: string) => {
setToEmails(toEmails.filter(email => email !== emailToRemove)) setToEmails(toEmails.filter(email => email !== emailToRemove))
} }
const resetForm = () => {
const handleSend = async () => {
if (toEmails.length === 0 || !subject) {
toast('لطفا تمام فیلدهای مورد نیاز را پر کنید', 'error')
return
}
const emailData: SendEmailDto = {
from: { address: userEmail || 'sender@example.com' },
to: parseEmailAddresses(toEmails),
subject,
html: value,
text: value.replace(/<[^>]*>/g, ''), // Strip HTML for text version
}
try {
await sendEmailMutation.mutateAsync(emailData)
toast('ایمیل با موفقیت ارسال شد', 'success')
handleClose()
// Reset form
setTo('') setTo('')
setToEmails([]) setToEmails([])
setSubject('') setSubject('')
setValue('') setValue('')
setPriority('') setPriority('')
} catch {
toast('خطا در ارسال ایمیل', 'error')
}
} }
const handleSaveDraft = async () => { const handleEmailAction = async (isDraft: boolean = false) => {
if (toEmails.length === 0 && !subject && !value) { if (!isDraft && (toEmails.length === 0 || !subject)) {
toast('لطفا تمام فیلدهای مورد نیاز را پر کنید', 'error')
return
}
if (isDraft && toEmails.length === 0 && !subject && !value) {
toast('حداقل یکی از فیلدها باید پر باشد', 'error') toast('حداقل یکی از فیلدها باید پر باشد', 'error')
return return
} }
@@ -136,25 +171,37 @@ const NewMessage: FC = () => {
to: parseEmailAddresses(toEmails), to: parseEmailAddresses(toEmails),
subject, subject,
html: value, html: value,
text: value.replace(/<[^>]*>/g, ''), text: value.replace(/<[^>]*>/g, ''), // Strip HTML for text version
isDraft: true, ...(isDraft && { isDraft: true, uploadOnly: true }),
} }
try { // Set appropriate loading state
await saveDraftMutation.mutateAsync(emailData) if (isDraft) {
toast('پیش‌نویس ذخیره شد', 'success') setIsSavingDraft(true)
} else {
setIsSending(true)
}
await sendEmailMutation.mutateAsync(emailData, {
onSuccess: (data) => {
toast(data.message, 'success')
handleClose() handleClose()
// Reset form resetForm()
setTo('') },
setToEmails([]) onError: (error: ErrorType) => {
setSubject('') toast(error.response?.data?.error.message[0], 'error')
setValue('') },
setPriority('') onSettled: () => {
} catch { // Reset loading states
toast('خطا در ذخیره پیش‌نویس', 'error') setIsSending(false)
setIsSavingDraft(false)
} }
})
} }
const handleSend = () => handleEmailAction(false)
const handleSaveDraft = () => handleEmailAction(true)
const priorityOptions = [ const priorityOptions = [
{ value: 'high', label: 'بالا' }, { value: 'high', label: 'بالا' },
{ value: 'medium', label: 'متوسط' }, { value: 'medium', label: 'متوسط' },
@@ -170,7 +217,7 @@ const NewMessage: FC = () => {
}`} }`}
onClick={handleClose} onClick={handleClose}
/> />
<div className={`fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 xl:left-8 xl:right-auto xl:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0' <div className={`fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto transition-transform duration-300 ease-out ${isClosing ? 'translate-y-[120%]' : isOpening ? 'translate-y-full' : 'translate-y-0'
}`}> }`}>
{/* Header */} {/* Header */}
<div className='flex justify-between items-center mb-6 md:mb-8'> <div className='flex justify-between items-center mb-6 md:mb-8'>
@@ -209,7 +256,8 @@ const NewMessage: FC = () => {
{/* Input Field */} {/* Input Field */}
<input <input
value={to} value={to}
onChange={(e) => setTo(e.target.value)} onChange={handleToChange}
onBlur={handleToBlur}
onKeyDown={handleToKeyDown} onKeyDown={handleToKeyDown}
placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""} placeholder={toEmails.length === 0 ? "ایمیل را وارد کرده و Enter بزنید" : ""}
className="flex-1 min-w-32 bg-transparent outline-none" className="flex-1 min-w-32 bg-transparent outline-none"
@@ -263,13 +311,13 @@ const NewMessage: FC = () => {
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1' className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
label={t('new_message.draft')} label={t('new_message.draft')}
onClick={handleSaveDraft} onClick={handleSaveDraft}
loading={saveDraftMutation.isPending} loading={isSavingDraft}
/> />
<Button <Button
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2' className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
label={t('new_message.send')} label={t('new_message.send')}
onClick={handleSend} onClick={handleSend}
loading={sendEmailMutation.isPending} loading={isSending}
/> />
</div> </div>
</div> </div>
+14
View File
@@ -247,3 +247,17 @@ textarea::placeholder {
.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg { .ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg {
display: none; display: none;
} }
/* انیمیشن چرخش معکوس */
@keyframes spin-reverse {
from {
transform: rotate(360deg);
}
to {
transform: rotate(0deg);
}
}
.animate-spin-reverse {
animation: spin-reverse 1s linear infinite;
}
+98 -43
View File
@@ -5,42 +5,55 @@ import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes'; import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2, Trash, Edit } from 'iconsax-react'; import { InfoCircle, More, Refresh2, Trash, Edit } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown'; import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
interface Message extends Record<string, unknown> { import { useGetDrafts, useDeleteDraft } from './hooks/useDraftData';
id: string; import { DraftMessage } from './types/DraftTypes';
title: string; import { formatDate } from '@/config/func';
sender: string;
date: string;
read: boolean;
}
const List: FC = () => { const List: FC = () => {
const navigate = useNavigate();
const { t } = useTranslation(); const { t } = useTranslation();
const [isLoading] = useState(false); const [currentPage, setCurrentPage] = useState(1);
const [selectedMessages, setSelectedMessages] = useState<Message[]>([]); const [filters, setFilters] = useState<FilterValues>({});
const messageData: Message[] = [ const { data: draftData, isLoading, refetch, isFetching } = useGetDrafts({
{ id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true }, page: currentPage,
{ id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true }, limit: 10,
{ id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true }, ...filters
]; });
const columns: ColumnType<Message>[] = [ const deleteMutation = useDeleteDraft();
const [selectedMessages, setSelectedMessages] = useState<DraftMessage[]>([]);
const columns: ColumnType<DraftMessage>[] = [
{ {
key: 'title', key: 'title',
title: 'عنوان پیام', title: 'title',
render: (item) => ( render: () => (
<div className="flex items-center"> <div className="text-[#0038FF]">
{!item.read && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 mr-2" />} پیش نویس
<span className="truncate">{item.title}</span>
</div> </div>
) )
}, },
{ {
key: 'sender', key: 'subject',
title: 'فرستنده', title: 'عنوان پیام',
render: (item) => ( render: (item) => (
<span className="truncate">{item.sender}</span> <div className="flex items-center">
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
</div>
)
},
{
key: 'to',
title: 'گیرنده',
render: (item) => (
<span className="truncate">
{item.to && item.to.length > 0
? (item.to[0].name || item.to[0].address)
: 'بدون گیرنده'
}
</span>
) )
}, },
{ {
@@ -48,87 +61,129 @@ const List: FC = () => {
title: 'تاریخ', title: 'تاریخ',
align: 'center', align: 'center',
render: (item) => ( render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span> <span>{formatDate(item.date)}</span>
) )
}, },
]; ];
const tableActions = ( const tableActions = (
<div className='flex items-center gap-2 md:gap-4'> <div className='flex items-center gap-2 md:gap-4'>
<Refresh2 size={18} color='black' className="cursor-pointer" /> <Refresh2
size={18}
color='black'
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' /> <More size={18} color='black' className='rotate-90 cursor-pointer' />
</div> </div>
); );
const selectedActions = ( const selectedActions = (
<> <>
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" /> <Trash
<Edit size={18} color='black' onClick={() => handleEditSelected()} className="cursor-pointer" /> size={18}
color='black'
onClick={() => handleDeleteSelected()}
className="cursor-pointer"
/>
<Edit
size={18}
color='black'
onClick={() => handleEditSelected()}
className="cursor-pointer"
/>
</> </>
); );
const handleFilterChange = (newFilters: FilterValues) => { const handleFilterChange = (newFilters: FilterValues) => {
console.log('Applied filters:', newFilters); setFilters(newFilters);
setCurrentPage(1); // Reset to first page when filters change
}; };
const handleSelectionChange = (selectedRows: Message[]) => { const handleSelectionChange = (selectedRows: DraftMessage[]) => {
setSelectedMessages(selectedRows); setSelectedMessages(selectedRows);
}; };
const handleDeleteSelected = () => { const handleDeleteSelected = () => {
console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); selectedMessages.forEach(message => {
// اینجا منطق حذف را پیاده‌سازی کنید deleteMutation.mutate(message.id.toString());
});
setSelectedMessages([]);
}; };
const handleEditSelected = () => { const handleEditSelected = () => {
console.log('ویرایش پیام‌های انتخاب شده:', selectedMessages.map(m => m.id)); if (selectedMessages.length === 1) {
// اینجا منطق ویرایش را پیاده‌سازی کنید // Navigate to edit page for single draft
navigate(`/draft/edit/${selectedMessages[0].id}`);
} else {
console.log('ویرایش چندین پیش‌نویس انتخاب شده:', selectedMessages.map(m => m.id));
}
}; };
const getRowActions = (message: Message): RowActionItem[] => [ const handleDeleteDraft = (messageId: number) => {
deleteMutation.mutate(messageId.toString());
};
const handleEditDraft = (messageId: number) => {
navigate(`/draft/edit/${messageId}`);
};
const getRowActions = (message: DraftMessage): RowActionItem[] => [
{ {
label: 'ویرایش', label: 'ویرایش',
icon: <Edit size={16} color="black" />, icon: <Edit size={16} color="black" />,
onClick: () => console.log('ویرایش پیام', message.id), onClick: () => handleEditDraft(message.id),
}, },
{ {
label: 'حذف', label: 'حذف',
icon: <Trash size={16} color="red" />, icon: <Trash size={16} color="red" />,
onClick: () => console.log('حذف پیام', message.id), onClick: () => handleDeleteDraft(message.id),
}, },
]; ];
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const messages = draftData?.data?.draftsMail || [];
const pager = draftData?.data?.pager;
return ( return (
<div className='mt-2 md:mt-4 px-2 md:px-0'> <div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg mb-4 md:mb-0">{t('draft.title')}</h1> <h1 className="text-lg mb-4 md:mb-0">{t('draft.title')}</h1>
<Filters <Filters
fields={[ fields={[
{ type: 'date', name: 'from_date', placeholder: t('draft.from_date') }, { type: 'date', name: 'dateFrom', placeholder: t('draft.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('draft.to_date') }, { type: 'date', name: 'dateTo', placeholder: t('draft.to_date') },
{ type: 'input', name: 'search', placeholder: t('draft.search') } { type: 'input', name: 'search', placeholder: t('draft.search') }
]} ]}
onChange={handleFilterChange} onChange={handleFilterChange}
searchField="search" searchField="search"
/> />
<Table<Message> <Table<DraftMessage>
columns={columns} columns={columns}
data={messageData} data={messages}
isLoading={isLoading} isLoading={isLoading}
showHeader={false} showHeader={false}
actions={tableActions} actions={tableActions}
selectedActions={selectedActions} selectedActions={selectedActions}
onSelectionChange={handleSelectionChange} onSelectionChange={handleSelectionChange}
onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)} onRowClick={(id) => navigate(`/draft/edit/${id}`)}
noDataMessage={ noDataMessage={
<div className="flex flex-col items-center py-6"> <div className="flex flex-col items-center ">
<InfoCircle size={32} className="text-gray-300 mb-2" /> <InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">هیچ پیشنویسی یافت نشد</div> <div className="text-sm">هیچ پیشنویسی یافت نشد</div>
</div> </div>
} }
selectable={true} selectable={true}
rowActions={getRowActions} rowActions={getRowActions}
pagination={pager ? {
totalPages: pager.totalPages,
currentPage: pager.page,
onPageChange: handlePageChange
} : undefined}
/> />
</div> </div>
) )
+45
View File
@@ -0,0 +1,45 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import * as api from '../service/DraftService';
import { MessageListQueryDto, DraftUpdateDto } from '../types/DraftTypes';
export const useGetDrafts = (query: MessageListQueryDto) => {
return useQuery({
queryKey: ['drafts', query],
queryFn: () => api.getDrafts(query),
staleTime: 1000 * 60 * 5, // 5 minutes
});
};
export const useGetDraftDetail = (messageId: string, enabled: boolean = true) => {
return useQuery({
queryKey: ['draft', messageId],
queryFn: () => api.getDraftDetail(messageId),
enabled: !!messageId && enabled,
staleTime: 1000 * 60 * 10, // 10 minutes
});
};
export const useDeleteDraft = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (messageId: string) => api.deleteDraft(messageId),
onSuccess: () => {
// Invalidate drafts queries to refresh the list
queryClient.invalidateQueries({ queryKey: ['drafts'] });
},
});
};
export const useEditDraft = () => {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ messageId, draftData }: { messageId: string; draftData: DraftUpdateDto }) =>
api.editDraft(messageId, draftData),
onSuccess: () => {
// Invalidate drafts queries to refresh the list
queryClient.invalidateQueries({ queryKey: ['drafts'] });
},
});
};
+37
View File
@@ -0,0 +1,37 @@
import axios from "../../../config/axios";
import {
MessageListQueryDto,
InboxResponse,
InboxMessage,
DraftUpdateDto,
} from "../types/DraftTypes";
export const getDrafts = async (
query: MessageListQueryDto
): Promise<InboxResponse> => {
const { data } = await axios.get(`/email/messages/drafts`, { params: query });
return data;
};
export const deleteDraft = async (messageId: string): Promise<void> => {
const { data } = await axios.delete(`/email/messages/drafts/${messageId}`);
return data;
};
export const editDraft = async (
messageId: string,
draftData: DraftUpdateDto
): Promise<void> => {
const { data } = await axios.put(
`/email/messages/drafts/${messageId}`,
draftData
);
return data;
};
export const getDraftDetail = async (
messageId: string
): Promise<InboxMessage> => {
const { data } = await axios.get(`/email/messages/drafts/${messageId}`);
return data.data.message;
};
+107
View File
@@ -0,0 +1,107 @@
export interface EmailRecipientDto {
name?: string;
address: string;
}
export interface EmailHeaderDto {
key: string;
value: string;
}
export interface EmailAttachmentDto {
filename?: string;
contentType?: string;
encoding?: string;
contentTransferEncoding?: string;
contentDisposition?: "inline" | "attachment";
content: string;
cid?: string;
}
export interface MessageListQueryDto {
page?: number;
limit?: number;
order?: string;
search?: string;
from?: string;
subject?: string;
dateFrom?: string;
dateTo?: string;
hasAttachment?: boolean;
isRead?: boolean;
isFlagged?: boolean;
}
export interface DraftMessage extends Record<string, unknown> {
id: number;
mailbox: string;
thread: string;
from: {
address: string;
name: string;
};
to: Array<{
address: string;
name: string;
}>;
cc: Array<{
address: string;
name: string;
}>;
bcc: Array<{
address: string;
name: string;
}>;
messageId: string;
subject: string;
date: string;
idate: string;
intro: string;
attachments: boolean;
attachmentsList: EmailAttachmentDto[];
size: number;
seen: boolean;
deleted: boolean;
flagged: boolean;
draft: boolean;
answered: boolean;
forwarded: boolean;
references: string[];
contentType: {
value: string;
params: {
boundary: string;
};
};
}
export type InboxMessage = DraftMessage;
export interface InboxPager {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: boolean;
nextPage: boolean;
}
export interface InboxResponse {
statusCode: number;
success: boolean;
data: {
pager: InboxPager;
draftsMail: DraftMessage[];
};
}
export interface DraftUpdateDto {
from?: EmailRecipientDto;
to?: EmailRecipientDto[];
cc?: EmailRecipientDto[];
bcc?: EmailRecipientDto[];
subject?: string;
text?: string;
html?: string;
attachments?: EmailAttachmentDto[];
}
@@ -0,0 +1,67 @@
import { FC } from 'react'
const SkeletonDetail: FC = () => {
return (
<div className='bg-white rounded-4xl p-8'>
<div className='animate-pulse'>
{/* Header Skeleton */}
<div className='h-6 bg-gray-200 rounded w-48 mb-6'></div>
{/* Subject and Tags Section */}
<div className='mt-6 flex xl:flex-row flex-col justify-between xl:items-center'>
<div className='flex xl:flex-row flex-col-reverse gap-4 xl:items-center'>
<div className='flex gap-1 items-center'>
<div className='h-4 bg-gray-200 rounded w-12 hidden xl:block'></div>
<div className='h-5 bg-gray-200 rounded w-40'></div>
</div>
<div className='flex gap-2'>
<div className='h-6 xl:h-10 bg-gray-200 rounded-full w-20'></div>
<div className='h-6 xl:h-10 bg-gray-200 rounded-full w-24'></div>
</div>
</div>
<div className='h-4 bg-gray-200 rounded w-24'></div>
</div>
{/* Sender Info */}
<div className='mt-6 flex gap-4 items-center'>
<div className='size-10 bg-gray-200 rounded-full'></div>
<div className='h-4 bg-gray-200 rounded w-32'></div>
</div>
{/* Email Content */}
<div className='xl:mt-8 mt-3 space-y-3'>
<div className='h-4 bg-gray-200 rounded w-full'></div>
<div className='h-4 bg-gray-200 rounded w-full'></div>
<div className='h-4 bg-gray-200 rounded w-3/4'></div>
<div className='h-4 bg-gray-200 rounded w-full'></div>
<div className='h-4 bg-gray-200 rounded w-2/3'></div>
</div>
{/* Attachments Section */}
<div className='mt-10'>
<div className='h-4 bg-gray-200 rounded w-20 mb-4'></div>
<div className='flex border-b border-gray-200 pb-10 mt-4 flex-wrap gap-2.5 items-center'>
<div className='h-10 bg-gray-200 rounded-full w-36'></div>
</div>
</div>
{/* Action Buttons */}
<div className='flex mt-9 justify-end'>
<div className='flex gap-5'>
<div className='size-6 bg-gray-200 rounded'></div>
<div className='flex gap-2 items-center border-r border-gray-200 pr-5'>
<div className='w-[17px] h-[17px] bg-gray-200 rounded'></div>
<div className='h-4 bg-gray-200 rounded w-12'></div>
</div>
<div className='flex gap-2 items-center border-r border-gray-200 pr-5'>
<div className='w-[17px] h-[17px] bg-gray-200 rounded'></div>
<div className='h-4 bg-gray-200 rounded w-16'></div>
</div>
</div>
</div>
</div>
</div>
)
}
export default SkeletonDetail
+18 -24
View File
@@ -9,11 +9,12 @@ import AnswerIcon from '@/assets/images/answer.svg'
import Header from './Components/Header' import Header from './Components/Header'
import ReactQuill from 'react-quill-new'; import ReactQuill from 'react-quill-new';
import Select from '@/components/Select' import Select from '@/components/Select'
import { useGetMessageDetail, useSendEmail, useSaveDraft } from './hooks/useEmailData' import { useGetMessageDetail, useSendEmail } from './hooks/useEmailData'
import { useAuthStore } from '../auth/store/AuthStore' import { useAuthStore } from '../auth/store/AuthStore'
import { SendEmailDto } from './types/Types' import { SendEmailDto } from './types/Types'
import { toast } from '@/components/Toast' import { toast } from '@/components/Toast'
import { formatDate } from '@/config/func' import { formatDate } from '@/config/func'
import SkeletonDetail from './Components/SkeletonDetail'
const DetailEmail: FC = () => { const DetailEmail: FC = () => {
@@ -27,7 +28,7 @@ const DetailEmail: FC = () => {
// API hooks // API hooks
const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', !!id) const { data: messageDetail, isLoading, error } = useGetMessageDetail(id || '', !!id)
const sendEmailMutation = useSendEmail() const sendEmailMutation = useSendEmail()
const saveDraftMutation = useSaveDraft() // const saveDraftMutation = useSaveDraft()
const priorityOptions = [ const priorityOptions = [
{ value: 'high', label: 'بالا' }, { value: 'high', label: 'بالا' },
@@ -80,21 +81,21 @@ const DetailEmail: FC = () => {
return return
} }
const draftData: SendEmailDto = { // const draftData: SendEmailDto = {
from: { address: userEmail || 'sender@example.com' }, // from: { address: userEmail || 'sender@example.com' },
to: [{ address: messageDetail.from.address, name: messageDetail.from.name }], // to: [{ address: messageDetail.from.address, name: messageDetail.from.name }],
subject: `Re: ${messageDetail.subject}`, // subject: `Re: ${messageDetail.subject}`,
html: value, // html: value,
text: value.replace(/<[^>]*>/g, ''), // text: value.replace(/<[^>]*>/g, ''),
isDraft: true, // isDraft: true,
reference: { // reference: {
inReplyTo: messageDetail.messageId, // inReplyTo: messageDetail.messageId,
originalMessageId: messageDetail.id.toString() // originalMessageId: messageDetail.id.toString()
} // }
} // }
try { try {
await saveDraftMutation.mutateAsync(draftData) // await saveDraftMutation.mutateAsync(draftData)
toast('پیش‌نویس ذخیره شد', 'success') toast('پیش‌نویس ذخیره شد', 'success')
} catch { } catch {
toast('خطا در ذخیره پیش‌نویس', 'error') toast('خطا در ذخیره پیش‌نویس', 'error')
@@ -108,14 +109,7 @@ const DetailEmail: FC = () => {
if (isLoading) { if (isLoading) {
return ( return (
<div className='bg-white rounded-4xl p-8'> <SkeletonDetail />
<div className='animate-pulse'>
<div className='h-6 bg-gray-200 rounded mb-4'></div>
<div className='h-4 bg-gray-200 rounded mb-2'></div>
<div className='h-4 bg-gray-200 rounded mb-2'></div>
<div className='h-20 bg-gray-200 rounded'></div>
</div>
</div>
) )
} }
@@ -247,7 +241,7 @@ const DetailEmail: FC = () => {
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1' className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
label={t('new_message.draft')} label={t('new_message.draft')}
onClick={handleSaveDraft} onClick={handleSaveDraft}
loading={saveDraftMutation.isPending} // loading={saveDraftMutation.isPending}
/> />
<Button <Button
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2' className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
+2 -2
View File
@@ -16,7 +16,7 @@ const List: FC = () => {
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({}); const [filters, setFilters] = useState<FilterValues>({});
const { data: inboxData, isLoading, refetch } = useGetInbox({ const { data: inboxData, isLoading, refetch, isFetching } = useGetInbox({
page: currentPage, page: currentPage,
limit: 10, limit: 10,
...filters ...filters
@@ -57,7 +57,7 @@ const List: FC = () => {
<Refresh2 <Refresh2
size={18} size={18}
color='black' color='black'
className="cursor-pointer" className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()} onClick={() => refetch()}
/> />
<More size={18} color='black' className='rotate-90 cursor-pointer' /> <More size={18} color='black' className='rotate-90 cursor-pointer' />
@@ -14,6 +14,14 @@ export const useSaveDraft = () => {
}); });
}; };
export const useGetDraft = (query: MessageListQueryDto) => {
return useQuery({
queryKey: ['draft', query],
queryFn: () => api.getDraft(query),
staleTime: 1000 * 60 * 5, // 5 minutes
});
};
export const useGetInbox = (query: MessageListQueryDto) => { export const useGetInbox = (query: MessageListQueryDto) => {
return useQuery({ return useQuery({
queryKey: ['inbox', query], queryKey: ['inbox', query],
+8 -4
View File
@@ -13,10 +13,14 @@ export const sendEmail = async (params: SendEmailDto) => {
}; };
export const saveDraft = async (params: SendEmailDto) => { export const saveDraft = async (params: SendEmailDto) => {
const { data } = await axios.post(`/email/draft`, { const { data } = await axios.post(`/email/messages/drafts`, params);
...params, return data;
isDraft: true, };
});
export const getDraft = async (
query: MessageListQueryDto
): Promise<InboxResponse> => {
const { data } = await axios.get(`/email/messages/drafts`, { params: query });
return data; return data;
}; };