email actions + sent list + action list + archive list + trash list

This commit is contained in:
hamid zarghami
2025-07-12 16:01:17 +03:30
parent fac9159ec0
commit 663dd1a9db
45 changed files with 1947 additions and 326 deletions
+8 -1
View File
@@ -11,7 +11,7 @@ import { I18nextProvider } from 'react-i18next'
import NewMessage from './components/newMessage/NewMessage';
import { QueryCache, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { IApiErrorRepsonse } from './types/error.types';
import ToastContainer from './components/Toast';
import ToastContainer, { toast } from './components/Toast';
import { getRefreshToken, getToken, removeRefreshToken, removeToken, setRefreshToken, setToken } from './config/func';
import { refreshToken } from './pages/auth/service/AuthService';
import { Paths } from './utils/Paths';
@@ -37,6 +37,13 @@ i18next.init({
const queryClient = new QueryClient({
queryCache: new QueryCache({
onError: async (error: IApiErrorRepsonse) => {
// نمایش پیغام خطا از API
const errorMessage = error?.response?.error?.message?.[0] || undefined;
if (errorMessage) {
toast(errorMessage, 'error');
}
if (error?.response?.status === 401) {
try {
// Use a flag to track if refresh is in progress
+2 -12
View File
@@ -1,13 +1,3 @@
<svg width="39" height="54" viewBox="0 0 39 54" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M31.1559 34.8412C31.1559 34.8412 24.6651 41.094 19.2416 46.3804V53.5924C18.6935 54.0179 38.4905 34.8845 38.4905 34.8845L19.2416 14.6909V22.3573L31.1559 34.8412Z" fill="#1D1D1B"/>
<path d="M19.1479 39.0818L0 18.643L0.237992 18.405L19.3931 38.8583L19.1479 39.0818Z" fill="#1D1D1B"/>
<path d="M19.1407 37.1635L1.75977 18.6719L1.99776 18.4412L19.3787 36.9399L19.1407 37.1635Z" fill="#1D1D1B"/>
<path d="M19.1334 35.2523L3.52661 18.708L3.7574 18.47L19.3786 35.0215L19.1334 35.2523Z" fill="#1D1D1B"/>
<path d="M19.1262 33.3337L5.28638 18.7366L5.50995 18.5059L19.3714 33.1029L19.1262 33.3337Z" fill="#1D1D1B"/>
<path d="M19.1191 31.4155L7.05347 18.7729L7.26982 18.5349L19.3571 31.1848L19.1191 31.4155Z" fill="#1D1D1B"/>
<path d="M0.375071 18.7368L0.144287 18.4988L19.1407 0L19.3715 0.237996L0.375071 18.7368Z" fill="#1D1D1B"/>
<path d="M2.15645 18.7442L1.91846 18.5134L19.1263 1.73828L19.3571 1.97628L2.15645 18.7442Z" fill="#1D1D1B"/>
<path d="M3.93049 18.7587L3.69971 18.5279L19.1117 3.48364L19.3425 3.71443L3.93049 18.7587Z" fill="#1D1D1B"/>
<path d="M5.71187 18.7658L5.47388 18.5422L19.0973 5.22168L19.3281 5.45968L5.71187 18.7658Z" fill="#1D1D1B"/>
<path d="M7.49326 18.7799L7.24805 18.5564L19.0901 6.95947L19.3137 7.19747L7.49326 18.7799Z" fill="#1D1D1B"/>
<svg width="85" height="72" viewBox="0 0 85 72" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M47.97 0C39.21 0 20.6 0.0500073 20.18 0.0700073C28.87 8.77001 45.47 25.11 55.3 34.9C54.27 35.93 53.22 36.98 52.11 38.09C49.1 41.1 47.07 41.82 44.03 38.78C31.57 26.32 5.38 0.07 5.28 0H0V71.79C0 71.79 11.97 71.77 12.57 71.77V24.82C12.63 24.6 16.87 28.77 22.08 33.96L21.63 71.81C22.28 71.81 39.72 71.83 47.28 71.8C49.16 71.8 51.06 71.69 52.93 71.49C69.23 69.67 82.67 55.87 84.14 39.48C86.07 17.95 69.62 0 47.98 0H47.97ZM46.3 11.58C55.58 10.72 63.99 15.5 67.84 22.39C66.04 24.19 64.36 25.86 62.72 27.49C57.77 22.69 49.24 14.43 46.3 11.58ZM48.3 60.07C45.38 60.12 42.46 60.12 39.54 60.11H31.86V43.77C33.94 45.85 35.86 47.78 37.43 49.33C43.64 55.49 53.12 52.88 59.33 46.75C63.62 42.51 71.94 34.1 72.57 33.51C73.5 47.57 62.46 59.84 48.29 60.09L48.3 60.07Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 876 B

+15 -24
View File
@@ -1,24 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" version="1.1" viewBox="0 0 841.89 595.28">
<!-- Generator: Adobe Illustrator 29.5.1, SVG Export Plug-In . SVG Version: 2.1.0 Build 141) -->
<defs>
<style>
.st0 {
fill: #231f20;
}
</style>
</defs>
<path d="M366.35,300.52v12.08h-29.98v-12.08h29.98Z"/>
<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 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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<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"/>
<path class="st0" d="M553.87,333.71h-15.05v-71.79h15.05v71.79Z"/>
<path class="st0" d="M580.35,322.65h34.83v11.05h-50.42v-71.79h15.6v60.75Z"/>
<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 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"/>
</svg>
<svg width="389" height="73" viewBox="0 0 389 73" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M139.35 39.5195V51.5995H109.37V39.5195H139.35Z" fill="black"/>
<path d="M10.8196 71.6006H0.849609V68.3405C0.849609 67.2605 1.06961 66.4405 1.51961 65.9005C1.96961 65.3605 2.6296 65.0805 3.4996 65.0805C3.9696 65.0805 4.37961 65.2106 4.73961 65.4806C5.09961 65.7406 5.37961 66.1005 5.56961 66.5605C5.71961 66.0205 6.0096 65.6005 6.4296 65.2805C6.8496 64.9705 7.35961 64.8105 7.93961 64.8105C8.82961 64.8105 9.53961 65.1005 10.0496 65.6805C10.5596 66.2605 10.8196 67.0806 10.8196 68.1406V71.6006ZM5.09961 70.2906V68.3105C5.09961 67.7305 4.9596 67.2705 4.6696 66.9305C4.3796 66.5905 3.9896 66.4106 3.4996 66.4106C2.9496 66.4106 2.54961 66.5706 2.29961 66.8906C2.04961 67.2106 1.9296 67.7006 1.9296 68.3506V70.2906H5.1096H5.09961ZM6.15961 70.2906H9.7496V68.1205C9.7496 67.5105 9.58961 67.0306 9.26961 66.6706C8.94961 66.3206 8.51961 66.1406 7.95961 66.1406C6.75961 66.1406 6.15961 66.7906 6.15961 68.1006V70.2906Z" fill="black"/>
<path d="M5.84961 59.3302L0.849609 56.7303V55.2402L7.09961 58.6802H10.8196V59.9902H7.09961L0.849609 63.4302V61.9203L5.84961 59.3302Z" fill="black"/>
<path d="M10.5798 49.1695H0.589844V46.3495C0.589844 45.4795 0.779846 44.7095 1.16985 44.0395C1.54985 43.3695 2.09984 42.8594 2.80984 42.4894C3.51984 42.1294 4.32985 41.9395 5.24985 41.9395H5.88985C6.82985 41.9395 7.65984 42.1194 8.36984 42.4894C9.07984 42.8594 9.61985 43.3694 9.99985 44.0494C10.3798 44.7194 10.5698 45.5095 10.5798 46.4095V49.1794V49.1695ZM1.66985 47.8495H9.50984V46.4594C9.50984 45.4394 9.18984 44.6494 8.55984 44.0894C7.92984 43.5194 7.02985 43.2394 5.85985 43.2394H5.27985C4.13985 43.2394 3.25985 43.5095 2.62985 44.0395C1.99985 44.5695 1.67985 45.3294 1.66985 46.3094V47.8495Z" fill="black"/>
<path d="M10.5798 21.6094V22.9294L2.87985 27.9594H10.5798V29.2794H0.589844V27.9594L8.32985 22.9194H0.589844V21.6094H10.5798Z" fill="black"/>
<path d="M5.92984 6.02039L7.19984 7.24039H10.5798V8.56039H0.589844V7.24039H5.52985L0.589844 2.80038V1.21039L4.99985 5.14041L10.5798 0.900391V2.48041L5.92984 6.02039Z" fill="black"/>
<path d="M2.20985 35.8394L10.5798 32.7895V31.4395L0.589844 35.2694V36.4195L10.5798 40.2394V38.8795L2.20985 35.8394Z" fill="black"/>
<path d="M2.20985 15.1696L10.5798 12.1195V10.7695L0.589844 14.5995V15.7495L10.5798 19.5695V18.2095L2.20985 15.1696Z" fill="black"/>
<path d="M187.84 53.8104H188.14L207.1 0.900391H226.42V72.6904H211.61V23.6804L211.31 23.6304L193.06 72.6904H183.02L164.77 23.5304L164.47 23.5804V72.6904H149.67V0.900391H169.09L187.85 53.8104H187.84Z" fill="#231F20"/>
<path d="M326.87 72.7099H311.82V0.919922H326.87V72.7099Z" fill="#231F20"/>
<path d="M353.35 61.6501H388.18V72.7002H337.76V0.910156H353.36V61.6602L353.35 61.6501Z" fill="#231F20"/>
<path d="M276.2 0.900391H259.88L233.07 72.6904H249.72L255.07 57.2104H255.09L259.02 45.9204H259L263.09 34.2304L267.85 20.6204H268.17L275.88 42.7804L276.96 45.9204L282.79 62.7904L286.25 72.6904H302.9L276.2 0.900391Z" fill="#231F20"/>
<path d="M63.8001 0.900391C55.0401 0.900391 36.4301 0.950398 36.0101 0.970398C44.7001 9.6704 61.3001 26.0104 71.1301 35.8004C70.1001 36.8304 69.0501 37.8804 67.9401 38.9904C64.9301 42.0004 62.9001 42.7204 59.8601 39.6804C47.4001 27.2204 21.2101 0.970391 21.1101 0.900391H15.8301V72.6904C15.8301 72.6904 27.8001 72.6704 28.4001 72.6704V25.7204C28.4601 25.5004 32.7001 29.6704 37.9101 34.8604L37.4601 72.7104C38.1101 72.7104 55.5501 72.7304 63.1101 72.7004C64.9901 72.7004 66.8901 72.5904 68.7601 72.3904C85.0601 70.5704 98.5001 56.7704 99.9701 40.3804C101.9 18.8504 85.4501 0.900391 63.8101 0.900391H63.8001ZM62.1301 12.4804C71.4101 11.6204 79.8201 16.4004 83.6701 23.2904C81.8701 25.0904 80.1901 26.7604 78.5501 28.3904C73.6001 23.5904 65.0701 15.3304 62.1301 12.4804ZM64.1301 60.9704C61.2101 61.0204 58.2901 61.0204 55.3701 61.0104H47.6901V44.6704C49.7701 46.7504 51.6901 48.6804 53.2601 50.2304C59.4701 56.3904 68.9501 53.7804 75.1601 47.6504C79.4501 43.4104 87.7701 35.0004 88.4001 34.4104C89.3301 48.4704 78.2901 60.7404 64.1201 60.9904L64.1301 60.9704Z" fill="black"/>
</svg>

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 4.0 KiB

+4
View File
@@ -0,0 +1,4 @@
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0027 21.004H3.10616C2.82828 21.0176 2.55063 20.973 2.29094 20.8731C2.03125 20.7732 1.79525 20.6201 1.59803 20.4237C1.40081 20.2273 1.2467 19.9918 1.14557 19.7324C1.04444 19.4729 0.99851 19.1952 1.01071 18.917C1.01071 15.4902 1.01071 12.0635 1.01071 8.63674C0.998636 8.13071 1.11629 7.63002 1.35244 7.18244C1.58859 6.73486 1.93537 6.35532 2.35969 6.08003C4.35382 4.6887 6.33907 3.28313 8.31543 1.86333C9.10548 1.29743 10.0526 0.993164 11.024 0.993164C11.9955 0.993164 12.9426 1.29743 13.7327 1.86333C15.7321 3.30448 17.7582 4.70827 19.7684 6.13341C20.1468 6.38212 20.4575 6.72112 20.6725 7.11991C20.8875 7.51869 21.0001 7.96474 21.0001 8.4179C21.0001 11.9567 21.0001 15.4902 21.0001 19.0237C20.9902 19.548 20.7742 20.0473 20.3988 20.4131C20.0234 20.7788 19.5191 20.9816 18.9953 20.9773L11.0027 21.004ZM1.8105 9.368V18.8796C1.79028 19.06 1.8108 19.2426 1.87056 19.414C1.93032 19.5854 2.02778 19.7412 2.15574 19.8698C2.28371 19.9984 2.43891 20.0966 2.60987 20.1571C2.78084 20.2176 2.9632 20.2389 3.14348 20.2193H18.8726C19.0534 20.239 19.2363 20.2177 19.4077 20.1569C19.5791 20.0961 19.7346 19.9974 19.8627 19.8681C19.9907 19.7389 20.0881 19.5824 20.1474 19.4104C20.2068 19.2383 20.2267 19.0551 20.2056 18.8743V8.66343C20.2162 8.52308 20.2162 8.38212 20.2056 8.24176C20.1507 7.92244 20.0269 7.61896 19.8427 7.35252C19.6585 7.08608 19.4184 6.86312 19.1392 6.69919C17.1131 5.27939 15.0976 3.84358 13.0715 2.42911C12.4738 2.02204 11.7693 1.80081 11.0465 1.79322C10.3236 1.78562 9.61466 1.99199 9.00858 2.38641C6.87582 3.87026 4.74305 5.39148 2.61029 6.87533C2.21336 7.17388 1.94157 7.60958 1.84782 8.09765C1.81967 8.17803 1.82205 8.26598 1.8545 8.34472C1.88695 8.42347 1.9472 8.48751 2.02378 8.52465L8.88595 13.3872C9.51179 13.8235 10.2561 14.0573 11.0187 14.0573C11.7813 14.0573 12.5256 13.8235 13.1515 13.3872L17.8062 10.0779C17.8723 10.0285 17.9418 9.98388 18.0142 9.94446C18.0557 9.91433 18.1031 9.89324 18.1533 9.88254C18.2035 9.87184 18.2554 9.87177 18.3056 9.88232C18.3558 9.89288 18.4032 9.91383 18.4449 9.94384C18.4865 9.97384 18.5214 10.0122 18.5474 10.0565C18.5806 10.0941 18.6058 10.1381 18.6215 10.1858C18.6372 10.2334 18.6429 10.2838 18.6384 10.3338C18.6339 10.3838 18.6193 10.4323 18.5954 10.4764C18.5715 10.5205 18.5388 10.5593 18.4994 10.5903C18.4384 10.6477 18.3723 10.6995 18.3021 10.7451C17.0918 11.5991 15.8814 12.4585 14.6657 13.3071C14.4898 13.4299 14.4631 13.4993 14.6284 13.6701C16.1214 15.2233 17.609 16.7926 19.0966 18.3512C19.3738 18.6394 19.4111 18.8849 19.2139 19.0611C19.0166 19.2372 18.7926 19.1945 18.51 18.8956C16.9887 17.305 15.4726 15.7037 13.9619 14.0918C13.834 13.9637 13.754 13.9423 13.594 14.0437C12.8436 14.5687 11.9502 14.8502 11.0347 14.8502C10.1192 14.8502 9.22582 14.5687 8.47539 14.0437C8.3101 13.9423 8.23012 13.953 8.09149 14.0971C6.58256 15.6984 5.05763 17.2997 3.54337 18.901C3.48822 18.9633 3.42755 19.0204 3.36209 19.0718C3.32632 19.107 3.28364 19.1344 3.23675 19.1523C3.18985 19.1702 3.13976 19.1781 3.08964 19.1756C3.03952 19.1731 2.99047 19.1602 2.94559 19.1377C2.9007 19.1153 2.86097 19.0837 2.8289 19.0451C2.75261 18.9799 2.70528 18.8871 2.69728 18.787C2.68928 18.687 2.72128 18.5878 2.78624 18.5113C2.83354 18.4523 2.88523 18.397 2.94087 18.3458C4.44091 16.773 5.94096 15.2002 7.441 13.6274C7.57963 13.4833 7.56897 13.4246 7.40901 13.3125C6.28397 12.5279 5.16427 11.7112 4.04457 10.9373L1.8105 9.368Z" fill="black" stroke="black" stroke-width="0.7"/>
<path d="M20.5172 8.52215L17.9827 10.4782" stroke="black" stroke-width="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

+129 -83
View File
@@ -22,11 +22,23 @@ const Table = <T extends RowDataType>({
selectable = false,
selectedActions = null,
onSelectionChange,
selectedRows: externalSelectedRows,
clearSelection = false,
rowActions,
pagination,
}: TableProps<T>): React.ReactElement => {
const [selectedRows, setSelectedRows] = useState<T[]>([]);
const [internalSelectedRows, setInternalSelectedRows] = useState<T[]>([]);
// استفاده از selectedRows خارجی اگر موجود باشد، در غیر این صورت از internal استفاده کن
const selectedRows = externalSelectedRows !== undefined ? externalSelectedRows : internalSelectedRows;
// پاک کردن انتخاب‌ها وقتی clearSelection تغییر کند
React.useEffect(() => {
if (clearSelection && externalSelectedRows === undefined) {
setInternalSelectedRows([]);
}
}, [clearSelection, externalSelectedRows]);
const getRowClassName = (item: T, index: number): string => {
if (typeof rowClassName === 'function') {
@@ -48,9 +60,18 @@ const Table = <T extends RowDataType>({
const handleSelectAll = () => {
const newSelectedRows = selectedRows.length === data.length ? [] : [...data];
setSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
if (externalSelectedRows !== undefined) {
// اگر selectedRows از خارج کنترل می‌شود، فقط callback را فراخوانی کن
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
} else {
// در غیر این صورت internal state را به‌روزرسانی کن
setInternalSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
}
};
@@ -58,9 +79,18 @@ const Table = <T extends RowDataType>({
const newSelectedRows = checked
? [...selectedRows, item]
: selectedRows.filter((row) => row.id !== item.id);
setSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
if (externalSelectedRows !== undefined) {
// اگر selectedRows از خارج کنترل می‌شود، فقط callback را فراخوانی کن
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
} else {
// در غیر این صورت internal state را به‌روزرسانی کن
setInternalSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
}
};
@@ -93,52 +123,60 @@ const Table = <T extends RowDataType>({
return (
<div className={`bg-white ${roundedClass} overflow-hidden`}>
{data.map((item, rowIndex) => (
<div
key={item.id}
className={`w-full min-h-[48px] hover:bg-[#F4F5F9] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
<div
className="flex-shrink-0 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/>
</div>
)}
{data.map((item, rowIndex) => {
// Gmail-style: خوانده نشده = پس‌زمینه سفید، خوانده شده = پس‌زمینه خاکستری
const isUnread = 'seen' in item && !item.seen;
const bgColor = isUnread ? 'bg-white' : 'bg-gray-50';
const hoverColor = isUnread ? 'hover:bg-gray-50' : 'hover:bg-gray-100';
const fontWeight = isUnread ? 'font-semibold' : 'font-normal';
<div className="flex-1 min-w-0">
{/* محتوای اصلی - ستون اول */}
<div className="flex items-center mb-0.5 text-sm font-medium">
{columns[0]?.render ? columns[0].render(item) : (item[columns[0]?.key] as React.ReactNode)}
return (
<div
key={item.id}
className={`w-full min-h-[48px] ${bgColor} ${hoverColor} ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)} px-4 py-3 flex items-center gap-3 ${rowIndex !== 0 ? 'border-t border-[#EAEDF5]' : ''}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
<div
className="flex-shrink-0 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/>
</div>
)}
<div className="flex-1 min-w-0">
{/* محتوای اصلی - ستون اول */}
<div className={`flex items-center mb-0.5 text-sm ${fontWeight}`}>
{columns[0]?.render ? columns[0].render(item) : (item[columns[0]?.key] as React.ReactNode)}
</div>
{/* اطلاعات اضافی - سه ستون آخر در یک خط */}
{columns.length > 1 && (
<div className={`flex items-center gap-3 text-xs text-gray-500 ${fontWeight}`}>
{columns.slice(1).map((col) => (
<div key={col.key} className="truncate">
{col.render ? col.render(item) : (item[col.key] as React.ReactNode)}
</div>
))}
</div>
)}
</div>
{/* اطلاعات اضافی - سه ستون آخر در یک خط */}
{columns.length > 1 && (
<div className="flex items-center gap-3 text-xs text-gray-500">
{columns.slice(1).map((col) => (
<div key={col.key} className="truncate">
{col.render ? col.render(item) : (item[col.key] as React.ReactNode)}
</div>
))}
{rowActions && (
<div
className="flex-shrink-0 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<RowActionsDropdown actions={rowActions(item)} />
</div>
)}
</div>
{rowActions && (
<div
className="flex-shrink-0 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<RowActionsDropdown actions={rowActions(item)} />
</div>
)}
</div>
))}
);
})}
</div>
);
};
@@ -161,42 +199,50 @@ const Table = <T extends RowDataType>({
</tr>
);
}
return data.map((item, rowIndex) => (
<tr
key={item.id}
className={`w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5] hover:bg-[#F4F5F9] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
<td
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/>
</td>
)}
{columns.map((col) => (
<td
key={`${item.id}-${col.key}`}
className={getCellClassName(col)}
style={{ width: col.width }}
>
{col.render ? col.render(item) : item[col.key] as React.ReactNode}
</td>
))}
{rowActions && (
<td
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<RowActionsDropdown actions={rowActions(item)} />
</td>
)}
</tr>
));
return data.map((item, rowIndex) => {
// Gmail-style: خوانده نشده = پس‌زمینه سفید، خوانده شده = پس‌زمینه خاکستری
const isUnread = 'seen' in item && !item.seen;
const bgColor = isUnread ? 'bg-white' : 'bg-gray-50';
const hoverColor = isUnread ? 'hover:bg-gray-50' : 'hover:bg-gray-100';
const fontWeight = isUnread ? 'font-semibold' : 'font-normal';
return (
<tr
key={item.id}
className={`w-full h-[64px] md:h-[74px] ${bgColor} border-t border-[#EAEDF5] ${hoverColor} ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
<td
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<Checkbox
checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/>
</td>
)}
{columns.map((col) => (
<td
key={`${item.id}-${col.key}`}
className={`${getCellClassName(col)} ${fontWeight}`}
style={{ width: col.width }}
>
{col.render ? col.render(item) : item[col.key] as React.ReactNode}
</td>
))}
{rowActions && (
<td
className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]"
onClick={(e) => e.stopPropagation()}
>
<RowActionsDropdown actions={rowActions(item)} />
</td>
)}
</tr>
);
});
};
const renderHeader = () => {
+1 -4
View File
@@ -11,7 +11,7 @@ import Button from '../Button'
import Select from '../Select'
import UploadButton from '../UploadButton'
import { toast } from '../Toast'
import { ErrorType } from '@/helpers/types'
const NewMessage: FC = () => {
@@ -188,9 +188,6 @@ const NewMessage: FC = () => {
handleClose()
resetForm()
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
},
onSettled: () => {
// Reset loading states
setIsSending(false)
+2
View File
@@ -36,6 +36,8 @@ export interface TableProps<T extends RowDataType = RowDataType> {
selectable?: boolean;
selectedActions?: ReactNode;
onSelectionChange?: (selectedRows: T[]) => void;
selectedRows?: T[];
clearSelection?: boolean;
rowActions?: (item: T) => RowActionItem[];
pagination?: PaginationConfig;
}
+201
View File
@@ -0,0 +1,201 @@
import { useMutation, useQueryClient } from '@tanstack/react-query';
import {
markMessageAsSeen,
moveMessageToArchive,
moveMessageToFavorite,
moveMessageToTrash,
deleteMessagePermanently,
sendEmail,
updateDraft,
deleteDraft,
sendDraft,
bulkAction,
unfavoriteMessage,
unarchiveMessage,
restoreMessage,
} from '../services/EmailActionsService';
import { BulkActionType } from '../pages/received/types/Types';
import { toast } from '../components/Toast';
export const useEmailActions = (customRefetch?: () => void) => {
const queryClient = useQueryClient();
// تمام query key های موجود در سیستم
const invalidateAllQueries = () => {
// صفحه‌های اصلی
queryClient.invalidateQueries({ queryKey: ['inbox'] });
queryClient.invalidateQueries({ queryKey: ['drafts'] });
queryClient.invalidateQueries({ queryKey: ['archive-messages'] });
queryClient.invalidateQueries({ queryKey: ['favorite-messages'] });
queryClient.invalidateQueries({ queryKey: ['spam-messages'] });
queryClient.invalidateQueries({ queryKey: ['trash-messages'] });
queryClient.invalidateQueries({ queryKey: ['search-messages'] });
// پیام‌های تکی
queryClient.invalidateQueries({ queryKey: ['message'] });
queryClient.invalidateQueries({ queryKey: ['draft'] });
// صفحه‌هایی که از React Query استفاده نمی‌کنند (مثل sent)
if (customRefetch) {
customRefetch();
}
};
const bulkActionMutation = useMutation({
mutationFn: bulkAction,
onSuccess: (data) => {
toast(data?.data?.message || 'عملیات گروهی با موفقیت انجام شد', 'success');
invalidateAllQueries();
},
});
const markAsSeenMutation = useMutation({
mutationFn: markMessageAsSeen,
onSuccess: (data) => {
toast(data?.data?.message || 'پیام به عنوان خوانده شده علامت‌گذاری شد', 'success');
invalidateAllQueries();
},
});
const archiveMutation = useMutation({
mutationFn: moveMessageToArchive,
onSuccess: (data) => {
toast(data?.data?.message || 'پیام به آرشیو منتقل شد', 'success');
invalidateAllQueries();
},
});
const favoriteMutation = useMutation({
mutationFn: moveMessageToFavorite,
onSuccess: (data) => {
toast(data?.data?.message || 'پیام به علاقه‌مندی‌ها اضافه شد', 'success');
invalidateAllQueries();
},
});
const moveToTrashMutation = useMutation({
mutationFn: moveMessageToTrash,
onSuccess: (data) => {
toast(data?.data?.message || 'پیام به سطل زباله منتقل شد', 'success');
invalidateAllQueries();
},
});
const deletePermanentlyMutation = useMutation({
mutationFn: deleteMessagePermanently,
onSuccess: (data) => {
toast(data?.data?.message || 'پیام به طور کامل حذف شد', 'success');
invalidateAllQueries();
},
});
const sendEmailMutation = useMutation({
mutationFn: sendEmail,
onSuccess: (data) => {
toast(data?.data?.message || 'ایمیل با موفقیت ارسال شد', 'success');
invalidateAllQueries();
},
});
const updateDraftMutation = useMutation({
mutationFn: ({ messageId, draftData }: { messageId: number; draftData: unknown }) =>
updateDraft(messageId, draftData),
onSuccess: (data) => {
toast(data?.data?.message || 'پیش‌نویس به‌روزرسانی شد', 'success');
invalidateAllQueries();
},
});
const deleteDraftMutation = useMutation({
mutationFn: deleteDraft,
onSuccess: (data) => {
toast(data?.data?.message || 'پیش‌نویس حذف شد', 'success');
invalidateAllQueries();
},
});
const sendDraftMutation = useMutation({
mutationFn: sendDraft,
onSuccess: (data) => {
toast(data?.data?.message || 'پیش‌نویس ارسال شد', 'success');
invalidateAllQueries();
},
});
const unfavoriteMutation = useMutation({
mutationFn: unfavoriteMessage,
onSuccess: (data) => {
toast(data?.data?.message || 'پیام از علاقه‌مندی‌ها حذف شد', 'success');
invalidateAllQueries();
},
});
const unarchiveMutation = useMutation({
mutationFn: unarchiveMessage,
onSuccess: (data) => {
toast(data?.data?.message || 'پیام از آرشیو خارج شد', 'success');
invalidateAllQueries();
},
});
const restoreMutation = useMutation({
mutationFn: restoreMessage,
onSuccess: (data) => {
toast(data?.data?.message || 'پیام بازگردانی شد', 'success');
invalidateAllQueries();
},
});
const isLoading =
bulkActionMutation.isPending ||
markAsSeenMutation.isPending ||
archiveMutation.isPending ||
favoriteMutation.isPending ||
moveToTrashMutation.isPending ||
deletePermanentlyMutation.isPending ||
sendEmailMutation.isPending ||
updateDraftMutation.isPending ||
deleteDraftMutation.isPending ||
sendDraftMutation.isPending ||
unfavoriteMutation.isPending ||
unarchiveMutation.isPending ||
restoreMutation.isPending;
return {
// Bulk actions
bulkAction: bulkActionMutation.mutate,
bulkMarkAsSeen: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.SEEN }),
bulkArchive: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.ARCHIVE }),
bulkFavorite: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.FAVORITE }),
bulkMoveToTrash: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.TRASH }),
bulkDelete: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.DELETE }),
bulkJunk: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.JUNK }),
bulkUnfavorite: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.UNFAVORITE }),
bulkUnarchive: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.UNARCHIVE }),
bulkRestore: (messageIds: number[]) =>
bulkActionMutation.mutate({ messageIds, action: BulkActionType.RESTORE }),
// Single actions (keeping for backward compatibility)
markAsSeen: markAsSeenMutation.mutate,
archive: archiveMutation.mutate,
favorite: favoriteMutation.mutate,
moveToTrash: moveToTrashMutation.mutate,
deletePermanently: deletePermanentlyMutation.mutate,
sendEmail: sendEmailMutation.mutate,
updateDraft: updateDraftMutation.mutate,
deleteDraft: deleteDraftMutation.mutate,
sendDraft: sendDraftMutation.mutate,
unfavorite: unfavoriteMutation.mutate,
unarchive: unarchiveMutation.mutate,
restore: restoreMutation.mutate,
isLoading,
};
};
+1 -1
View File
@@ -180,7 +180,7 @@
"phonen_number": "شماره تماس",
"enter_phone_number": "شماره تماس خود را وارد کنید",
"password": "رمز عبور",
"enter_password": "رمز عبور انتخابی خود را وارد کنید",
"enter_password": "رمز عبور خود را وارد کنید",
"next": "ادامه",
"before_registered": "آیا قبلا ثبت نام کردید؟",
"login": "ورود",
+73 -51
View File
@@ -1,46 +1,45 @@
import Filters, { FilterValues } from '../../components/Filters';
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2, Trash, RecoveryConvert } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
interface Message extends Record<string, unknown> {
id: string;
title: string;
sender: string;
date: string;
read: boolean;
}
import { useNavigate } from 'react-router-dom';
import { useGetTrashMessages } from './hooks/useTrashData';
import { TrashMessage } from './types/TrashTypes';
import { formatDate } from '@/config/func';
import { useEmailActions } from '@/hooks/useEmailActions';
const List: FC = () => {
const { t } = useTranslation();
const [isLoading] = useState(false);
const [selectedMessages, setSelectedMessages] = useState<Message[]>([]);
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const messageData: Message[] = [
{ id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true },
{ id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true },
{ id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true },
];
const { data: trashData, isLoading, refetch, isFetching } = useGetTrashMessages({
page: currentPage,
limit: 10,
...filters
});
const columns: ColumnType<Message>[] = [
const [selectedMessages, setSelectedMessages] = useState<TrashMessage[]>([]);
const columns: ColumnType<TrashMessage>[] = [
{
key: 'title',
key: 'subject',
title: 'عنوان پیام',
render: (item) => (
<div className="flex items-center">
{!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>
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
)
},
{
key: 'sender',
key: 'from',
title: 'فرستنده',
render: (item) => (
<span className="truncate">{item.sender}</span>
<div className="flex items-center">
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
<span className="truncate">{item.from.name || item.from.address}</span>
</div>
)
},
{
@@ -48,14 +47,19 @@ const List: FC = () => {
title: 'تاریخ',
align: 'center',
render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span>
<span>{formatDate(item.date)}</span>
)
},
];
const tableActions = (
<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' />
</div>
);
@@ -68,77 +72,95 @@ const List: FC = () => {
);
const handleFilterChange = (newFilters: FilterValues) => {
console.log('Applied filters:', newFilters);
setFilters(newFilters);
setCurrentPage(1);
};
const handleSelectionChange = (selectedRows: Message[]) => {
const handleSelectionChange = (selectedRows: TrashMessage[]) => {
setSelectedMessages(selectedRows);
};
const handleDeleteSelected = () => {
console.log('حذف دائم پیام‌های انتخاب شده:', selectedMessages.map(m => m.id));
// اینجا منطق حذف دائم را پیاده‌سازی کنید
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkDelete(messageIds);
setSelectedMessages([]);
};
const handleRestoreSelected = () => {
console.log('بازگردانی پیام‌های انتخاب شده:', selectedMessages.map(m => m.id));
// اینجا منطق بازگردانی را پیاده‌سازی کنید
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkRestore(messageIds);
setSelectedMessages([]);
};
const getRowActions = (message: Message): RowActionItem[] => [
const getRowActions = (message: TrashMessage): RowActionItem[] => [
{
label: 'بازگردانی',
icon: <RecoveryConvert size={16} color="black" />,
onClick: () => console.log('بازگردانی پیام', message.id),
onClick: () => emailActions.restore(message.id),
},
{
label: 'حذف دائم',
icon: <Trash size={16} color="red" />,
onClick: () => console.log('حذف دائم پیام', message.id),
onClick: () => emailActions.deletePermanently(message.id),
},
];
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const messages = trashData?.data?.trashMails || [];
const pager = trashData?.data?.pager;
return (
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg mb-4 md:mb-0">{t('trash.title')}</h1>
<h1 className="text-lg mb-4 md:mb-0">سطل آشغال</h1>
<Filters
fields={[
{ type: 'date', name: 'from_date', placeholder: t('trash.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('trash.to_date') },
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
{
type: 'select',
name: 'status',
placeholder: t('trash.all'),
name: 'isRead',
placeholder: 'همه',
options: [
{ value: 'all', label: t('trash.all') },
{ value: 'read', label: t('trash.read') },
{ value: 'unread', label: t('trash.unread') }
{ value: '', label: 'همه' },
{ value: 'true', label: 'خوانده شده' },
{ value: 'false', label: 'خوانده نشده' }
]
},
{ type: 'input', name: 'search', placeholder: t('trash.search') }
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
]}
onChange={handleFilterChange}
searchField="search"
/>
<Table<Message>
<Table<TrashMessage>
columns={columns}
data={messageData}
isLoading={isLoading}
data={messages}
isLoading={isLoading || emailActions.isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)}
selectedRows={selectedMessages}
onRowClick={(id) => navigate(`/mail/${id}`)}
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" />
<div className="text-sm">سطل آشغال خالی است</div>
<div className="text-sm">هیچ پیامی یافت نشد</div>
</div>
}
selectable={true}
rowActions={getRowActions}
pagination={pager ? {
totalPages: pager.totalPages,
currentPage: pager.page,
onPageChange: handlePageChange
} : undefined}
/>
</div>
)
+13
View File
@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { getTrashMessages } from '../service/TrashService';
import { MessageListQueryDto } from '../types/TrashTypes';
export const useGetTrashMessages = (query: MessageListQueryDto) => {
return useQuery({
queryKey: ['trash-messages', query],
queryFn: () => getTrashMessages(query),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
// Note: useRestoreMessage is now handled by useEmailActions hook
+11
View File
@@ -0,0 +1,11 @@
import axios from "../../../config/axios";
import { MessageListQueryDto, TrashResponse } from "../types/TrashTypes";
export const getTrashMessages = async (
query: MessageListQueryDto
): Promise<TrashResponse> => {
const { data } = await axios.get(`/email/messages/trash`, { params: query });
return data;
};
// Note: deleteMessagePermanently and restoreMessage are now handled by EmailActionsService
+61
View File
@@ -0,0 +1,61 @@
export interface EmailAddress {
address: string;
name: string;
}
export interface TrashMessage extends Record<string, unknown> {
id: number;
mailbox: string;
thread: string;
from: EmailAddress;
to: EmailAddress[];
cc: EmailAddress[];
bcc: EmailAddress[];
messageId: string;
subject: string;
date: string;
idate: string;
intro: string;
attachments: boolean;
attachmentsList: unknown[];
size: number;
seen: boolean;
deleted: boolean;
flagged: boolean;
draft: boolean;
answered: boolean;
forwarded: boolean;
references: string[];
contentType: {
value: string;
params: {
boundary: string;
};
};
}
export interface MessageListQueryDto {
page?: number;
limit?: number;
order?: "asc" | "desc";
search?: string;
dateFrom?: string;
dateTo?: string;
isRead?: string;
}
export interface TrashResponse {
statusCode: number;
success: boolean;
data: {
trashMails: TrashMessage[];
pager: {
page: number;
totalPages: number;
totalItems: number;
limit: number;
prevPage: boolean;
nextPage: boolean;
};
};
}
+71 -51
View File
@@ -1,46 +1,45 @@
import Filters, { FilterValues } from '../../components/Filters';
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2, Trash, ArchiveSlash } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
interface Message extends Record<string, unknown> {
id: string;
title: string;
sender: string;
date: string;
read: boolean;
}
import { useNavigate } from 'react-router-dom';
import { useGetArchiveMessages } from './hooks/useArchiveData';
import { ArchiveMessage } from './types/ArchiveTypes';
import { formatDate } from '@/config/func';
import { useEmailActions } from '@/hooks/useEmailActions';
const List: FC = () => {
const { t } = useTranslation();
const [isLoading] = useState(false);
const [selectedMessages, setSelectedMessages] = useState<Message[]>([]);
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const messageData: Message[] = [
{ id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true },
{ id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true },
{ id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true },
];
const { data: archiveData, isLoading, refetch, isFetching } = useGetArchiveMessages({
page: currentPage,
limit: 10,
...filters
});
const columns: ColumnType<Message>[] = [
const [selectedMessages, setSelectedMessages] = useState<ArchiveMessage[]>([]);
const columns: ColumnType<ArchiveMessage>[] = [
{
key: 'title',
key: 'subject',
title: 'عنوان پیام',
render: (item) => (
<div className="flex items-center">
{!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>
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
)
},
{
key: 'sender',
key: 'from',
title: 'فرستنده',
render: (item) => (
<span className="truncate">{item.sender}</span>
<div className="flex items-center">
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
<span className="truncate">{item.from.name || item.from.address}</span>
</div>
)
},
{
@@ -48,14 +47,19 @@ const List: FC = () => {
title: 'تاریخ',
align: 'center',
render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span>
<span>{formatDate(item.date)}</span>
)
},
];
const tableActions = (
<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' />
</div>
);
@@ -68,77 +72,93 @@ const List: FC = () => {
);
const handleFilterChange = (newFilters: FilterValues) => {
console.log('Applied filters:', newFilters);
setFilters(newFilters);
setCurrentPage(1);
};
const handleSelectionChange = (selectedRows: Message[]) => {
const handleSelectionChange = (selectedRows: ArchiveMessage[]) => {
setSelectedMessages(selectedRows);
};
const handleDeleteSelected = () => {
console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id));
// اینجا منطق حذف را پیاده‌سازی کنید
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkMoveToTrash(messageIds);
setSelectedMessages([]);
};
const handleUnarchiveSelected = () => {
console.log('خروج از آرشیو پیام‌های انتخاب شده:', selectedMessages.map(m => m.id));
// اینجا منطق خروج از آرشیو را پیاده‌سازی کنید
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkUnarchive(messageIds);
setSelectedMessages([]);
};
const getRowActions = (message: Message): RowActionItem[] => [
const getRowActions = (message: ArchiveMessage): RowActionItem[] => [
{
label: 'خروج از آرشیو',
icon: <ArchiveSlash size={16} color="black" />,
onClick: () => console.log('خروج از آرشیو', message.id),
onClick: () => emailActions.unarchive(message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => console.log('حذف پیام', message.id),
onClick: () => emailActions.moveToTrash(message.id),
},
];
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const messages = archiveData?.data?.archiveMails || [];
const pager = archiveData?.data?.pager;
return (
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg mb-4 md:mb-0">{t('archive.title')}</h1>
<h1 className="text-lg mb-4 md:mb-0">آرشیو</h1>
<Filters
fields={[
{ type: 'date', name: 'from_date', placeholder: t('archive.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('archive.to_date') },
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
{
type: 'select',
name: 'status',
placeholder: t('archive.all'),
name: 'isRead',
placeholder: 'همه',
options: [
{ value: 'all', label: t('archive.all') },
{ value: 'read', label: t('archive.read') },
{ value: 'unread', label: t('archive.unread') }
{ value: '', label: 'همه' },
{ value: 'true', label: 'خوانده شده' },
{ value: 'false', label: 'خوانده نشده' }
]
},
{ type: 'input', name: 'search', placeholder: t('archive.search') }
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
]}
onChange={handleFilterChange}
searchField="search"
/>
<Table<Message>
<Table<ArchiveMessage>
columns={columns}
data={messageData}
isLoading={isLoading}
data={messages}
isLoading={isLoading || emailActions.isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)}
selectedRows={selectedMessages}
onRowClick={(id) => navigate(`/mail/${id}`)}
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" />
<div className="text-sm">هیچ پیام آرشیو شدهای یافت نشد</div>
<div className="text-sm">هیچ پیامی یافت نشد</div>
</div>
}
selectable={true}
rowActions={getRowActions}
pagination={pager ? {
totalPages: pager.totalPages,
currentPage: pager.page,
onPageChange: handlePageChange
} : undefined}
/>
</div>
)
@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { getArchiveMessages } from '../service/ArchiveService';
import { MessageListQueryDto } from '../types/ArchiveTypes';
export const useGetArchiveMessages = (query: MessageListQueryDto) => {
return useQuery({
queryKey: ['archive-messages', query],
queryFn: () => getArchiveMessages(query),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
// Note: useUnarchiveMessage is now handled by useEmailActions hook
@@ -0,0 +1,13 @@
import axios from "../../../config/axios";
import { MessageListQueryDto, ArchiveResponse } from "../types/ArchiveTypes";
export const getArchiveMessages = async (
query: MessageListQueryDto
): Promise<ArchiveResponse> => {
const { data } = await axios.get(`/email/messages/archive`, {
params: query,
});
return data;
};
// Note: moveMessageToArchive and unarchiveMessage are now handled by EmailActionsService
+61
View File
@@ -0,0 +1,61 @@
export interface EmailAddress {
address: string;
name: string;
}
export interface ArchiveMessage extends Record<string, unknown> {
id: number;
mailbox: string;
thread: string;
from: EmailAddress;
to: EmailAddress[];
cc: EmailAddress[];
bcc: EmailAddress[];
messageId: string;
subject: string;
date: string;
idate: string;
intro: string;
attachments: boolean;
attachmentsList: unknown[];
size: number;
seen: boolean;
deleted: boolean;
flagged: boolean;
draft: boolean;
answered: boolean;
forwarded: boolean;
references: string[];
contentType: {
value: string;
params: {
boundary: string;
};
};
}
export interface MessageListQueryDto {
page?: number;
limit?: number;
order?: "asc" | "desc";
search?: string;
dateFrom?: string;
dateTo?: string;
isRead?: string;
}
export interface ArchiveResponse {
statusCode: number;
success: boolean;
data: {
archiveMails: ArchiveMessage[];
pager: {
page: number;
totalPages: number;
totalItems: number;
limit: number;
prevPage: boolean;
nextPage: boolean;
};
};
}
+1 -7
View File
@@ -9,7 +9,7 @@ import OTPInput from 'react-otp-input'
import { useCountDown } from '../../../hooks/useCountDown'
// import ArrowLeftIcon from '../../../assets/images/arrow-left.svg'
import { useLoginWithOtp, useOtpVerify } from '../hooks/useAuthData'
import { ErrorType } from '../../../helpers/types'
import { setRefreshToken } from '../../../config/func'
import { setToken } from '../../../config/func'
import { getRedirectUrl } from '../../../config/func'
@@ -49,9 +49,6 @@ const LoginStep2: FC = () => {
}
window.location.href = Paths.received
},
onError(error: ErrorType) {
toast(error?.response?.data?.error?.message[0], 'error')
},
})
},
})
@@ -83,9 +80,6 @@ const LoginStep2: FC = () => {
reset()
toast(t('auth.toast_resend_code'), 'success')
},
onError: (error: ErrorType) => {
toast(error.response?.data?.error.message[0], 'error')
}
})
}
+33 -10
View File
@@ -3,18 +3,20 @@ import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2, Trash, Edit } from 'iconsax-react';
import { InfoCircle, More, Refresh2, Trash, Edit, Send } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
import { useGetDrafts, useDeleteDraft } from './hooks/useDraftData';
import { useGetDrafts } from './hooks/useDraftData';
import { DraftMessage } from './types/DraftTypes';
import { formatDate } from '@/config/func';
import { useEmailActions } from '@/hooks/useEmailActions';
const List: FC = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const { data: draftData, isLoading, refetch, isFetching } = useGetDrafts({
page: currentPage,
@@ -22,7 +24,6 @@ const List: FC = () => {
...filters
});
const deleteMutation = useDeleteDraft();
const [selectedMessages, setSelectedMessages] = useState<DraftMessage[]>([]);
const columns: ColumnType<DraftMessage>[] = [
@@ -80,10 +81,10 @@ const List: FC = () => {
const selectedActions = (
<>
<Trash
<Send
size={18}
color='black'
onClick={() => handleDeleteSelected()}
onClick={() => handleSendSelected()}
className="cursor-pointer"
/>
<Edit
@@ -92,12 +93,18 @@ const List: FC = () => {
onClick={() => handleEditSelected()}
className="cursor-pointer"
/>
<Trash
size={18}
color='black'
onClick={() => handleDeleteSelected()}
className="cursor-pointer"
/>
</>
);
const handleFilterChange = (newFilters: FilterValues) => {
setFilters(newFilters);
setCurrentPage(1); // Reset to first page when filters change
setCurrentPage(1);
};
const handleSelectionChange = (selectedRows: DraftMessage[]) => {
@@ -106,14 +113,20 @@ const List: FC = () => {
const handleDeleteSelected = () => {
selectedMessages.forEach(message => {
deleteMutation.mutate(message.id.toString());
emailActions.deleteDraft(message.id);
});
setSelectedMessages([]);
};
const handleSendSelected = () => {
selectedMessages.forEach(message => {
emailActions.sendDraft(message.id);
});
setSelectedMessages([]);
};
const handleEditSelected = () => {
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));
@@ -121,7 +134,11 @@ const List: FC = () => {
};
const handleDeleteDraft = (messageId: number) => {
deleteMutation.mutate(messageId.toString());
emailActions.deleteDraft(messageId);
};
const handleSendDraft = (messageId: number) => {
emailActions.sendDraft(messageId);
};
const handleEditDraft = (messageId: number) => {
@@ -129,6 +146,11 @@ const List: FC = () => {
};
const getRowActions = (message: DraftMessage): RowActionItem[] => [
{
label: 'ارسال',
icon: <Send size={16} color="black" />,
onClick: () => handleSendDraft(message.id),
},
{
label: 'ویرایش',
icon: <Edit size={16} color="black" />,
@@ -165,11 +187,12 @@ const List: FC = () => {
<Table<DraftMessage>
columns={columns}
data={messages}
isLoading={isLoading}
isLoading={isLoading || emailActions.isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
selectedRows={selectedMessages}
onRowClick={(id) => navigate(`/draft/edit/${id}`)}
noDataMessage={
<div className="flex flex-col items-center ">
+179
View File
@@ -0,0 +1,179 @@
import Filters, { FilterValues } from '../../components/Filters';
import { FC, useState } from 'react'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2, Trash, Star, Archive } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
import { useGetFavoriteMessages } from './hooks/useFavoriteData';
import { FavoriteMessage } from './types/FavoriteTypes';
import { formatDate } from '@/config/func';
import { useEmailActions } from '@/hooks/useEmailActions';
const List: FC = () => {
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const { data: favoriteData, isLoading, refetch, isFetching } = useGetFavoriteMessages({
page: currentPage,
limit: 10,
...filters
});
const [selectedMessages, setSelectedMessages] = useState<FavoriteMessage[]>([]);
const columns: ColumnType<FavoriteMessage>[] = [
{
key: 'subject',
title: 'عنوان پیام',
render: (item) => (
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
)
},
{
key: 'from',
title: 'فرستنده',
render: (item) => (
<div className="flex items-center">
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
<span className="truncate">{item.from.name || item.from.address}</span>
</div>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span>{formatDate(item.date)}</span>
)
},
];
const tableActions = (
<div className='flex items-center gap-2 md:gap-4'>
<Refresh2
size={18}
color='black'
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<Star size={18} color='black' onClick={() => handleUnfavoriteSelected()} className="cursor-pointer" />
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
const handleFilterChange = (newFilters: FilterValues) => {
setFilters(newFilters);
setCurrentPage(1);
};
const handleSelectionChange = (selectedRows: FavoriteMessage[]) => {
setSelectedMessages(selectedRows);
};
const handleDeleteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkMoveToTrash(messageIds);
setSelectedMessages([]);
};
const handleArchiveSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkArchive(messageIds);
setSelectedMessages([]);
};
const handleUnfavoriteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkUnfavorite(messageIds);
setSelectedMessages([]);
};
const getRowActions = (message: FavoriteMessage): RowActionItem[] => [
{
label: 'حذف از علاقه‌مندی‌ها',
icon: <Star size={16} color="black" />,
onClick: () => emailActions.unfavorite(message.id),
},
{
label: 'بایگانی',
icon: <Archive size={16} color="black" />,
onClick: () => emailActions.archive(message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => emailActions.moveToTrash(message.id),
},
];
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const messages = favoriteData?.data?.favoriteMails || [];
const pager = favoriteData?.data?.pager;
return (
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg mb-4 md:mb-0">علاقهمندیها</h1>
<Filters
fields={[
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
{
type: 'select',
name: 'isRead',
placeholder: 'همه',
options: [
{ value: '', label: 'همه' },
{ value: 'true', label: 'خوانده شده' },
{ value: 'false', label: 'خوانده نشده' }
]
},
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
]}
onChange={handleFilterChange}
searchField="search"
/>
<Table<FavoriteMessage>
columns={columns}
data={messages}
isLoading={isLoading || emailActions.isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
selectedRows={selectedMessages}
onRowClick={(id) => navigate(`/mail/${id}`)}
noDataMessage={
<div className="flex flex-col items-center">
<InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">هیچ پیامی یافت نشد</div>
</div>
}
selectable={true}
rowActions={getRowActions}
pagination={pager ? {
totalPages: pager.totalPages,
currentPage: pager.page,
onPageChange: handlePageChange
} : undefined}
/>
</div>
)
}
export default List
@@ -0,0 +1,13 @@
import { useQuery } from '@tanstack/react-query';
import { getFavoriteMessages } from '../service/FavoriteService';
import { MessageListQueryDto } from '../types/FavoriteTypes';
export const useGetFavoriteMessages = (query: MessageListQueryDto) => {
return useQuery({
queryKey: ['favorite-messages', query],
queryFn: () => getFavoriteMessages(query),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
// Note: useUnfavoriteMessage is now handled by useEmailActions hook
@@ -0,0 +1,13 @@
import axios from "../../../config/axios";
import { MessageListQueryDto, FavoriteResponse } from "../types/FavoriteTypes";
export const getFavoriteMessages = async (
query: MessageListQueryDto
): Promise<FavoriteResponse> => {
const { data } = await axios.get(`/email/messages/favorite`, {
params: query,
});
return data;
};
// Note: moveMessageToFavorite and unfavoriteMessage are now handled by EmailActionsService
+61
View File
@@ -0,0 +1,61 @@
export interface EmailAddress {
address: string;
name: string;
}
export interface FavoriteMessage extends Record<string, unknown> {
id: number;
mailbox: string;
thread: string;
from: EmailAddress;
to: EmailAddress[];
cc: EmailAddress[];
bcc: EmailAddress[];
messageId: string;
subject: string;
date: string;
idate: string;
intro: string;
attachments: boolean;
attachmentsList: unknown[];
size: number;
seen: boolean;
deleted: boolean;
flagged: boolean;
draft: boolean;
answered: boolean;
forwarded: boolean;
references: string[];
contentType: {
value: string;
params: {
boundary: string;
};
};
}
export interface MessageListQueryDto {
page?: number;
limit?: number;
order?: "asc" | "desc";
search?: string;
dateFrom?: string;
dateTo?: string;
isRead?: string;
}
export interface FavoriteResponse {
statusCode: number;
success: boolean;
data: {
favoriteMails: FavoriteMessage[];
pager: {
page: number;
totalPages: number;
totalItems: number;
limit: number;
prevPage: boolean;
nextPage: boolean;
};
};
}
+60 -22
View File
@@ -3,18 +3,21 @@ import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2, Trash, Edit, Archive } from 'iconsax-react';
import { InfoCircle, More, Refresh2, Trash, Edit, Archive, ArchiveTick, Star1 } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
import { useGetInbox } from './hooks/useEmailData';
import { InboxMessage } from './types/Types';
import { formatDate } from '@/config/func';
import MarkAsRead from '@/assets/images/mark_as_read.svg'
import { useEmailActions } from '@/hooks/useEmailActions';
const List: FC = () => {
const navigate = useNavigate();
const { t } = useTranslation();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const { data: inboxData, isLoading, refetch, isFetching } = useGetInbox({
page: currentPage,
@@ -29,17 +32,16 @@ const List: FC = () => {
key: 'subject',
title: 'عنوان پیام',
render: (item) => (
<div className="flex items-center">
{/* {!item.seen && <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.subject || 'بدون موضوع'}</span>
</div>
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
)
},
{
key: 'from',
title: 'فرستنده',
render: (item) => (
<span className="truncate">{item.from.name || item.from.address}</span>
<div className="flex items-center">
<span className="truncate">{item.from.name || item.from.address}</span>
</div>
)
},
{
@@ -60,21 +62,42 @@ const List: FC = () => {
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Edit size={18} color='black' onClick={() => handleMarkAsReadSelected()} className="cursor-pointer" />
<img
src={MarkAsRead}
className='w-[18px] cursor-pointer'
onClick={() => handleMarkAsReadSelected()}
/>
<ArchiveTick
size={18}
color='black'
onClick={() => handleArchiveSelected()}
className="cursor-pointer"
/>
<InfoCircle size={18} color='black' className="cursor-pointer" />
<Star1
size={18}
color='black'
className="cursor-pointer"
onClick={() => handleFavoriteSelected()}
/>
<Trash
size={18}
color='#CD0000'
onClick={() => handleDeleteSelected()}
className="cursor-pointer"
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</>
);
const handleFilterChange = (newFilters: FilterValues) => {
setFilters(newFilters);
setCurrentPage(1); // Reset to first page when filters change
setCurrentPage(1);
};
const handleSelectionChange = (selectedRows: InboxMessage[]) => {
@@ -82,35 +105,49 @@ const List: FC = () => {
};
const handleDeleteSelected = () => {
console.log('حذف پیام‌های انتخاب شده:', selectedMessages.map(m => m.id));
// اینجا منطق حذف را پیاده‌سازی کنید
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkMoveToTrash(messageIds);
setSelectedMessages([]);
};
const handleArchiveSelected = () => {
console.log('بایگانی پیام‌های انتخاب شده:', selectedMessages.map(m => m.id));
// اینجا منطق بایگانی را پیاده‌سازی کنید
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkArchive(messageIds);
setSelectedMessages([]);
};
const handleMarkAsReadSelected = () => {
console.log('علامت‌گذاری به عنوان خوانده شده:', selectedMessages.map(m => m.id));
// اینجا منطق علامت‌گذاری را پیاده‌سازی کنید
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkMarkAsSeen(messageIds);
setSelectedMessages([]);
};
const handleFavoriteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkFavorite(messageIds);
setSelectedMessages([]);
};
const getRowActions = (message: InboxMessage): RowActionItem[] => [
{
label: message.seen ? 'علامت‌گذاری به عنوان خوانده نشده' : 'علامت‌گذاری به عنوان خوانده شده',
icon: <Edit size={16} color="black" />,
onClick: () => console.log('تغییر وضعیت خوانده شده', message.id),
onClick: () => emailActions.markAsSeen(message.id),
},
{
label: 'اضافه به علاقه‌مندی‌ها',
icon: <Star1 size={16} color="black" />,
onClick: () => emailActions.favorite(message.id),
},
{
label: 'بایگانی',
icon: <Archive size={16} color="black" />,
onClick: () => console.log('بایگانی پیام', message.id),
onClick: () => emailActions.archive(message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => console.log('حذف پیام', message.id),
onClick: () => emailActions.moveToTrash(message.id),
},
];
@@ -148,14 +185,15 @@ const List: FC = () => {
<Table<InboxMessage>
columns={columns}
data={messages}
isLoading={isLoading}
isLoading={isLoading || emailActions.isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
selectedRows={selectedMessages}
onRowClick={(id) => navigate(`/mail/${id}`)}
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" />
<div className="text-sm">هیچ پیامی یافت نشد</div>
</div>
+2 -2
View File
@@ -35,12 +35,12 @@ export const getMessageDetail = async (
messageId: string
): Promise<MessageDetail> => {
const { data }: { data: MessageDetailResponse } = await axios.get(
`/email/messages/inbox/${messageId}`
`/email/messages/${messageId}`
);
return data.data.message;
};
export const deleteMessage = async (messageId: string): Promise<void> => {
const { data } = await axios.delete(`/email/messages/inbox/${messageId}`);
const { data } = await axios.delete(`/email/messages/${messageId}`);
return data;
};
+18
View File
@@ -286,3 +286,21 @@ export interface MessageDetail {
};
};
}
export enum BulkActionType {
SEEN = "seen",
DELETE = "delete",
ARCHIVE = "archive",
FAVORITE = "favorite",
UNFAVORITE = "unfavorite",
UNARCHIVE = "unarchive",
RESTORE = "restore",
JUNK = "junk",
TRASH = "trash",
}
// Interface for bulk action request
export interface BulkActionRequest {
messageIds: number[];
action: BulkActionType;
}
+192
View File
@@ -0,0 +1,192 @@
import Filters, { FilterValues } from '../../components/Filters';
import { FC, useState } from 'react'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2, Trash, Archive, Star } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
import { useSearchMessages } from './hooks/useSearchData';
import { SearchMessage } from './types/SearchTypes';
import { formatDate } from '@/config/func';
import { useEmailActions } from '@/hooks/useEmailActions';
const List: FC = () => {
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const { data: searchData, isLoading, refetch, isFetching } = useSearchMessages({
page: currentPage,
limit: 10,
...filters
});
const [selectedMessages, setSelectedMessages] = useState<SearchMessage[]>([]);
const columns: ColumnType<SearchMessage>[] = [
{
key: 'subject',
title: 'عنوان پیام',
render: (item) => (
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
)
},
{
key: 'from',
title: 'فرستنده',
render: (item) => (
<div className="flex items-center">
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
<span className="truncate">{item.from.name || item.from.address}</span>
</div>
)
},
{
key: 'mailbox',
title: 'پوشه',
render: (item) => (
<span className="text-xs bg-gray-100 px-2 py-1 rounded">{item.mailbox}</span>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span>{formatDate(item.date)}</span>
)
},
];
const tableActions = (
<div className='flex items-center gap-2 md:gap-4'>
<Refresh2
size={18}
color='black'
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<Star size={18} color='black' onClick={() => handleFavoriteSelected()} className="cursor-pointer" />
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
const handleFilterChange = (newFilters: FilterValues) => {
setFilters(newFilters);
setCurrentPage(1);
};
const handleSelectionChange = (selectedRows: SearchMessage[]) => {
setSelectedMessages(selectedRows);
};
const handleDeleteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkMoveToTrash(messageIds);
setSelectedMessages([]);
};
const handleArchiveSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkArchive(messageIds);
setSelectedMessages([]);
};
const handleFavoriteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkFavorite(messageIds);
setSelectedMessages([]);
};
const getRowActions = (message: SearchMessage): RowActionItem[] => [
{
label: 'اضافه به علاقه‌مندی‌ها',
icon: <Star size={16} color="black" />,
onClick: () => emailActions.favorite(message.id),
},
{
label: 'بایگانی',
icon: <Archive size={16} color="black" />,
onClick: () => emailActions.archive(message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => emailActions.moveToTrash(message.id),
},
];
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const messages = searchData?.data?.results || [];
const pager = searchData?.data?.pager;
return (
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg mb-4 md:mb-0">جستجو در پیامها</h1>
<Filters
fields={[
{ type: 'input', name: 'q', placeholder: 'جستجو در عنوان، متن یا فرستنده...' },
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
{
type: 'select',
name: 'mailbox',
placeholder: 'تمام پوشه‌ها',
options: [
{ value: '', label: 'تمام پوشه‌ها' },
{ value: 'INBOX', label: 'دریافتی' },
{ value: 'Sent', label: 'ارسال شده' },
{ value: 'Drafts', label: 'پیش‌نویس' },
{ value: 'Trash', label: 'سطل آشغال' },
{ value: 'Archive', label: 'بایگانی' },
{ value: 'Junk', label: 'اسپم' },
]
}
]}
onChange={handleFilterChange}
searchField="q"
/>
<Table<SearchMessage>
columns={columns}
data={messages}
isLoading={isLoading || emailActions.isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
selectedRows={selectedMessages}
onRowClick={(id) => navigate(`/mail/${id}`)}
noDataMessage={
<div className="flex flex-col items-center">
<InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">
{filters.q ? 'هیچ نتیجه‌ای یافت نشد' : 'برای جستجو کلمه مورد نظر را وارد کنید'}
</div>
</div>
}
selectable={true}
rowActions={getRowActions}
pagination={pager ? {
totalPages: pager.totalPages,
currentPage: pager.page,
onPageChange: handlePageChange
} : undefined}
/>
</div>
)
}
export default List
+12
View File
@@ -0,0 +1,12 @@
import { useQuery } from '@tanstack/react-query';
import { searchMessages } from '../service/SearchService';
import { SearchQueryDto } from '../types/SearchTypes';
export const useSearchMessages = (query: SearchQueryDto) => {
return useQuery({
queryKey: ['search-messages', query],
queryFn: () => searchMessages(query),
staleTime: 5 * 60 * 1000, // 5 minutes
enabled: !!query.q, // Only run query if search term exists
});
};
@@ -0,0 +1,9 @@
import axios from "../../../config/axios";
import { SearchQueryDto, SearchResponse } from "../types/SearchTypes";
export const searchMessages = async (
query: SearchQueryDto
): Promise<SearchResponse> => {
const { data } = await axios.get(`/email/search`, { params: query });
return data;
};
+59
View File
@@ -0,0 +1,59 @@
export interface EmailAddress {
address: string;
name: string;
}
export interface SearchMessage extends Record<string, unknown> {
id: number;
mailbox: string;
thread: string;
from: EmailAddress;
to: EmailAddress[];
cc: EmailAddress[];
bcc: EmailAddress[];
messageId: string;
subject: string;
date: string;
idate: string;
intro: string;
attachments: boolean;
attachmentsList: unknown[];
size: number;
seen: boolean;
deleted: boolean;
flagged: boolean;
draft: boolean;
answered: boolean;
forwarded: boolean;
references: string[];
contentType: {
value: string;
params: {
boundary: string;
};
};
}
export interface SearchQueryDto {
page?: number;
limit?: number;
q?: string;
dateFrom?: string;
dateTo?: string;
mailbox?: string;
}
export interface SearchResponse {
success: boolean;
data: {
results: SearchMessage[];
pager: {
page: number;
totalPages: number;
totalItems: number;
limit: number;
prevPage: boolean;
nextPage: boolean;
};
};
}
+129 -44
View File
@@ -1,105 +1,190 @@
import Filters, { FilterValues } from '../../components/Filters';
import { FC, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2 } from 'iconsax-react';
interface Message extends Record<string, unknown> {
id: string;
title: string;
sender: string;
date: string;
read: boolean;
}
import { InfoCircle, More, Refresh2, AttachSquare, Trash, Archive, Star } from 'iconsax-react';
import { useGetSentMessages } from './hooks/useSentData';
import { InboxMessage } from './types/SentTypes';
import { useNavigate } from 'react-router-dom';
import { Paths } from '../../utils/Paths';
import { useEmailActions } from '@/hooks/useEmailActions';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { formatDate } from '@/config/func';
const List: FC = () => {
const { t } = useTranslation();
const [isLoading] = useState(false);
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const messageData: Message[] = [
{ id: '1', title: 'اولین پیام', sender: 'علی محمدی', date: '1402/05/12', read: true },
{ id: '2', title: 'دومین پیام', sender: 'زهرا احمدی', date: '1402/05/13', read: true },
{ id: '3', title: 'جلسه هفتگی', sender: 'مدیریت', date: '1402/05/14', read: true },
];
const { data: sentData, isLoading, refetch, isFetching } = useGetSentMessages({
page: currentPage,
limit: 10,
...filters
});
const columns: ColumnType<Message>[] = [
const [selectedMessages, setSelectedMessages] = useState<InboxMessage[]>([]);
const columns: ColumnType<InboxMessage>[] = [
{
key: 'title',
title: 'عنوان پیام',
key: 'subject',
title: 'موضوع پیام',
render: (item) => (
<div className="flex items-center">
{!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 className="flex items-center gap-2">
<span className="truncate">{item.subject}</span>
{item.attachments && (
<AttachSquare size={16} className="text-gray-500 flex-shrink-0" />
)}
</div>
)
},
{
key: 'sender',
title: 'فرستنده',
key: 'to',
title: 'گیرنده',
render: (item) => (
<span className="truncate">{item.sender}</span>
<div className="flex items-center">
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
<span className="truncate">
{item.to.map(recipient => recipient.address).join(', ')}
</span>
</div>
)
},
{
key: 'date',
title: 'تاریخ',
title: 'تاریخ ارسال',
align: 'center',
render: (item) => (
<span className="text-xs md:text-sm">{item.date}</span>
<span className="text-xs md:text-sm">
{formatDate(item.date)}
</span>
)
},
];
const tableActions = (
<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' />
</div>
);
const selectedActions = (
<>
<Star size={18} color='black' onClick={() => handleFavoriteSelected()} className="cursor-pointer" />
<Archive size={18} color='black' onClick={() => handleArchiveSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
const handleFilterChange = (newFilters: FilterValues) => {
console.log('Applied filters:', newFilters);
setFilters(newFilters);
setCurrentPage(1);
};
const handleRowClick = (id: number) => {
navigate(`${Paths.detailEmail}${id}`);
};
const handleSelectionChange = (selectedRows: InboxMessage[]) => {
setSelectedMessages(selectedRows);
};
const handleDeleteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkMoveToTrash(messageIds);
setSelectedMessages([]);
};
const handleArchiveSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkArchive(messageIds);
setSelectedMessages([]);
};
const handleFavoriteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkFavorite(messageIds);
setSelectedMessages([]);
};
const getRowActions = (message: InboxMessage): RowActionItem[] => [
{
label: 'اضافه به علاقه‌مندی‌ها',
icon: <Star size={16} color="black" />,
onClick: () => emailActions.favorite(message.id),
},
{
label: 'بایگانی',
icon: <Archive size={16} color="black" />,
onClick: () => emailActions.archive(message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => emailActions.moveToTrash(message.id),
},
];
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const messages = sentData?.data?.sentMails || [];
const pager = sentData?.data?.pager;
return (
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg mb-4 md:mb-0">{t('sent.title')}</h1>
<h1 className="text-lg mb-4 md:mb-0">ارسال شده</h1>
<Filters
fields={[
{ type: 'date', name: 'from_date', placeholder: t('sent.from_date') },
{ type: 'date', name: 'to_date', placeholder: t('sent.to_date') },
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
{
type: 'select',
name: 'status',
placeholder: t('sent.all'),
name: 'isRead',
placeholder: 'همه',
options: [
{ value: 'all', label: t('sent.all') },
{ value: 'read', label: t('sent.read') },
{ value: 'unread', label: t('sent.unread') }
{ value: '', label: 'همه' },
{ value: 'true', label: 'خوانده شده' },
{ value: 'false', label: 'خوانده نشده' }
]
},
{ type: 'input', name: 'search', placeholder: t('sent.search') }
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
]}
onChange={handleFilterChange}
searchField="search"
/>
<Table<Message>
<Table<InboxMessage>
columns={columns}
data={messageData}
isLoading={isLoading}
data={messages}
isLoading={isLoading || emailActions.isLoading}
showHeader={false}
actions={tableActions}
onRowClick={(id) => console.log(`کلیک روی پیام با شناسه ${id}`)}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
selectedRows={selectedMessages}
onRowClick={handleRowClick}
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" />
<div className="text-sm">هیچ پیامی یافت نشد</div>
</div>
}
selectable={true}
rowActions={getRowActions}
pagination={pager ? {
totalPages: pager.totalPages,
currentPage: pager.page,
onPageChange: handlePageChange
} : undefined}
/>
</div>
+11
View File
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { getSentMessages } from '../service/SentService';
import { MessageListQueryDto } from '../types/SentTypes';
export const useGetSentMessages = (query: MessageListQueryDto) => {
return useQuery({
queryKey: ['sent-messages', query],
queryFn: () => getSentMessages(query),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
+28
View File
@@ -0,0 +1,28 @@
import axios from "../../../config/axios";
import {
MessageListQueryDto,
InboxResponse,
MessageDetail,
MessageDetailResponse,
} from "../types/SentTypes";
export const getSentMessages = async (
query: MessageListQueryDto
): Promise<InboxResponse> => {
const { data } = await axios.get(`/email/messages/sent`, { params: query });
return data;
};
export const getSentMessageDetail = async (
messageId: number
): Promise<MessageDetail> => {
const { data }: { data: MessageDetailResponse } = await axios.get(
`/email/messages/sent/${messageId}`
);
return data.data.message;
};
export const deleteSentMessage = async (messageId: number): Promise<void> => {
const { data } = await axios.delete(`/email/messages/sent/${messageId}`);
return data;
};
+76
View File
@@ -0,0 +1,76 @@
export interface MessageListQueryDto {
page?: number;
limit?: number;
search?: string;
from_date?: string;
to_date?: string;
status?: "all" | "read" | "unread";
}
export interface EmailAddress {
address: string;
name: string;
}
export interface ContentType {
value: string;
params: {
boundary: string;
};
}
export interface InboxMessage extends Record<string, unknown> {
id: number;
mailbox: string;
thread: string;
from: EmailAddress;
to: EmailAddress[];
cc: EmailAddress[];
bcc: EmailAddress[];
messageId: string;
subject: string;
date: string;
idate: string;
intro: string;
attachments: boolean;
attachmentsList: unknown[];
size: number;
seen: boolean;
deleted: boolean;
flagged: boolean;
draft: boolean;
answered: boolean;
forwarded: boolean;
references: unknown[];
contentType: ContentType;
}
export interface Pager {
page: number;
limit: number;
totalItems: number;
totalPages: number;
prevPage: boolean;
nextPage: boolean;
}
export interface InboxResponse {
statusCode: number;
success: boolean;
data: {
pager: Pager;
sentMails: InboxMessage[];
};
}
export interface MessageDetail extends InboxMessage {
body: string;
}
export interface MessageDetailResponse {
statusCode: number;
success: boolean;
data: {
message: MessageDetail;
};
}
+167
View File
@@ -0,0 +1,167 @@
import Filters, { FilterValues } from '../../components/Filters';
import { FC, useState } from 'react'
import Table from '../../components/Table';
import { ColumnType } from '../../components/types/TableTypes';
import { InfoCircle, More, Refresh2, Trash, RecoveryConvert } from 'iconsax-react';
import { RowActionItem } from '../../components/RowActionsDropdown';
import { useNavigate } from 'react-router-dom';
import { useGetSpamMessages } from './hooks/useSpamData';
import { SpamMessage } from './types/SpamTypes';
import { formatDate } from '@/config/func';
import { useEmailActions } from '@/hooks/useEmailActions';
const List: FC = () => {
const navigate = useNavigate();
const [currentPage, setCurrentPage] = useState(1);
const [filters, setFilters] = useState<FilterValues>({});
const emailActions = useEmailActions();
const { data: spamData, isLoading, refetch, isFetching } = useGetSpamMessages({
page: currentPage,
limit: 10,
...filters
});
const [selectedMessages, setSelectedMessages] = useState<SpamMessage[]>([]);
const columns: ColumnType<SpamMessage>[] = [
{
key: 'subject',
title: 'عنوان پیام',
render: (item) => (
<span className="truncate">{item.subject || 'بدون موضوع'}</span>
)
},
{
key: 'from',
title: 'فرستنده',
render: (item) => (
<div className="flex items-center">
{!item.seen && <div className="w-1.5 h-1.5 md:w-2 md:h-2 rounded-full bg-blue-500 ml-2" />}
<span className="truncate">{item.from.name || item.from.address}</span>
</div>
)
},
{
key: 'date',
title: 'تاریخ',
align: 'center',
render: (item) => (
<span>{formatDate(item.date)}</span>
)
},
];
const tableActions = (
<div className='flex items-center gap-2 md:gap-4'>
<Refresh2
size={18}
color='black'
className={`cursor-pointer ${isFetching ? 'animate-spin-reverse' : ''}`}
onClick={() => refetch()}
/>
<More size={18} color='black' className='rotate-90 cursor-pointer' />
</div>
);
const selectedActions = (
<>
<RecoveryConvert size={18} color='black' onClick={() => handleNotSpamSelected()} className="cursor-pointer" />
<Trash size={18} color='black' onClick={() => handleDeleteSelected()} className="cursor-pointer" />
</>
);
const handleFilterChange = (newFilters: FilterValues) => {
setFilters(newFilters);
setCurrentPage(1);
};
const handleSelectionChange = (selectedRows: SpamMessage[]) => {
setSelectedMessages(selectedRows);
};
const handleDeleteSelected = () => {
const messageIds = selectedMessages.map(message => message.id);
emailActions.bulkMoveToTrash(messageIds);
setSelectedMessages([]);
};
const handleNotSpamSelected = () => {
// TODO: پیاده‌سازی علامت‌گذاری به عنوان غیر اسپم - نیاز به API endpoint جداگانه
console.log('علامت‌گذاری به عنوان غیر اسپم:', selectedMessages.map(m => m.id));
setSelectedMessages([]);
};
const getRowActions = (message: SpamMessage): RowActionItem[] => [
{
label: 'غیر اسپم',
icon: <RecoveryConvert size={16} color="black" />,
onClick: () => console.log('علامت‌گذاری به عنوان غیر اسپم', message.id),
},
{
label: 'حذف',
icon: <Trash size={16} color="red" />,
onClick: () => emailActions.moveToTrash(message.id),
},
];
const handlePageChange = (page: number) => {
setCurrentPage(page);
};
const messages = spamData?.data?.results || [];
const pager = spamData?.data?.pager;
return (
<div className='mt-2 md:mt-4 px-2 md:px-0'>
<h1 className="text-lg mb-4 md:mb-0">پیامهای اسپم</h1>
<Filters
fields={[
{ type: 'date', name: 'dateFrom', placeholder: 'از تاریخ' },
{ type: 'date', name: 'dateTo', placeholder: 'تا تاریخ' },
{
type: 'select',
name: 'isRead',
placeholder: 'همه',
options: [
{ value: '', label: 'همه' },
{ value: 'true', label: 'خوانده شده' },
{ value: 'false', label: 'خوانده نشده' }
]
},
{ type: 'input', name: 'search', placeholder: 'جستجو...' }
]}
onChange={handleFilterChange}
searchField="search"
/>
<Table<SpamMessage>
columns={columns}
data={messages}
isLoading={isLoading || emailActions.isLoading}
showHeader={false}
actions={tableActions}
selectedActions={selectedActions}
onSelectionChange={handleSelectionChange}
selectedRows={selectedMessages}
onRowClick={(id) => navigate(`/mail/${id}`)}
noDataMessage={
<div className="flex flex-col items-center">
<InfoCircle size={32} className="text-gray-300 mb-2" />
<div className="text-sm">هیچ پیامی یافت نشد</div>
</div>
}
selectable={true}
rowActions={getRowActions}
pagination={pager ? {
totalPages: pager.totalPages,
currentPage: pager.page,
onPageChange: handlePageChange
} : undefined}
/>
</div>
)
}
export default List
+11
View File
@@ -0,0 +1,11 @@
import { useQuery } from '@tanstack/react-query';
import { getSpamMessages } from '../service/SpamService';
import { MessageListQueryDto } from '../types/SpamTypes';
export const useGetSpamMessages = (query: MessageListQueryDto) => {
return useQuery({
queryKey: ['spam-messages', query],
queryFn: () => getSpamMessages(query),
staleTime: 5 * 60 * 1000, // 5 minutes
});
};
+9
View File
@@ -0,0 +1,9 @@
import axios from "../../../config/axios";
import { MessageListQueryDto, SpamResponse } from "../types/SpamTypes";
export const getSpamMessages = async (
query: MessageListQueryDto
): Promise<SpamResponse> => {
const { data } = await axios.get(`/email/messages/junk`, { params: query });
return data;
};
+60
View File
@@ -0,0 +1,60 @@
export interface EmailAddress {
address: string;
name: string;
}
export interface SpamMessage extends Record<string, unknown> {
id: number;
mailbox: string;
thread: string;
from: EmailAddress;
to: EmailAddress[];
cc: EmailAddress[];
bcc: EmailAddress[];
messageId: string;
subject: string;
date: string;
idate: string;
intro: string;
attachments: boolean;
attachmentsList: unknown[];
size: number;
seen: boolean;
deleted: boolean;
flagged: boolean;
draft: boolean;
answered: boolean;
forwarded: boolean;
references: string[];
contentType: {
value: string;
params: {
boundary: string;
};
};
}
export interface MessageListQueryDto {
page?: number;
limit?: number;
order?: "asc" | "desc";
search?: string;
dateFrom?: string;
dateTo?: string;
isRead?: string;
}
export interface SpamResponse {
success: boolean;
data: {
results: SpamMessage[];
pager: {
page: number;
totalPages: number;
totalItems: number;
limit: number;
prevPage: boolean;
nextPage: boolean;
};
};
}
+7
View File
@@ -6,8 +6,12 @@ import SentList from '@/pages/sent/List'
import DraftList from '@/pages/draft/List'
import ArchiveList from '@/pages/archive/List'
import TrashList from '@/pages/Trash/List'
import FavoriteList from '@/pages/favorite/List'
import SpamList from '@/pages/spam/List'
import SearchList from '@/pages/search/List'
import DetailEmail from '@/pages/received/Detail'
import Login from '@/pages/auth/Login'
const AppRouter: FC = () => {
return (
<Routes>
@@ -16,6 +20,9 @@ const AppRouter: FC = () => {
<Route path={Paths.draft} element={<DraftList />} />
<Route path={Paths.archive} element={<ArchiveList />} />
<Route path={Paths.trash} element={<TrashList />} />
<Route path={Paths.favorite} element={<FavoriteList />} />
<Route path={Paths.spam} element={<SpamList />} />
<Route path={Paths.search} element={<SearchList />} />
<Route path={Paths.detailEmail + ':id'} element={<DetailEmail />} />
<Route path={Paths.login} element={<Login />} />
</Routes>
+83
View File
@@ -0,0 +1,83 @@
import axios from "../config/axios";
import { BulkActionRequest } from "../pages/received/types/Types";
// Bulk action for multiple messages
export const bulkAction = async (request: BulkActionRequest) => {
const { data } = await axios.post(`/email/messages/bulk-action`, request);
return data;
};
// Mark message as seen
export const markMessageAsSeen = async (messageId: number) => {
const { data } = await axios.patch(`/email/messages/${messageId}/seen`);
return data;
};
// Move message to archive
export const moveMessageToArchive = async (messageId: number) => {
const { data } = await axios.patch(`/email/messages/${messageId}/archive`);
return data;
};
// Move message to favorite
export const moveMessageToFavorite = async (messageId: number) => {
const { data } = await axios.patch(`/email/messages/${messageId}/favorite`);
return data;
};
// Move message to trash
export const moveMessageToTrash = async (messageId: number) => {
const { data } = await axios.patch(`/email/messages/${messageId}/trash`);
return data;
};
// Delete message permanently (only for trash mailbox)
export const deleteMessagePermanently = async (messageId: number) => {
const { data } = await axios.delete(`/email/messages/${messageId}`);
return data;
};
// Send email
export const sendEmail = async (emailData: unknown) => {
const { data } = await axios.post(`/email/send`, emailData);
return data;
};
// Update draft
export const updateDraft = async (messageId: number, draftData: unknown) => {
const { data } = await axios.patch(
`/email/messages/drafts/${messageId}`,
draftData
);
return data;
};
// Delete draft
export const deleteDraft = async (messageId: number) => {
const { data } = await axios.delete(`/email/messages/drafts/${messageId}`);
return data;
};
// Send draft
export const sendDraft = async (messageId: number) => {
const { data } = await axios.post(`/email/messages/drafts/${messageId}/send`);
return data;
};
// Unfavorite message
export const unfavoriteMessage = async (messageId: number) => {
const { data } = await axios.patch(`/email/messages/${messageId}/unfavorite`);
return data;
};
// Unarchive message
export const unarchiveMessage = async (messageId: number) => {
const { data } = await axios.patch(`/email/messages/${messageId}/unarchive`);
return data;
};
// Restore message from trash
export const restoreMessage = async (messageId: number) => {
const { data } = await axios.patch(`/email/messages/${messageId}/restore`);
return data;
};
+2 -2
View File
@@ -1,6 +1,6 @@
import { FC } from 'react'
import LogoImage from '../assets/images/logo.svg'
import { Add, ArchiveTick, DirectInbox, DirectSend, Edit, InfoCircle, Logout, Star, Trash } from 'iconsax-react'
import { Add, ArchiveTick, DirectInbox, DirectSend, Edit, InfoCircle, Logout, Star1, Trash } from 'iconsax-react'
import SideBarItem from './SideBarItem'
import { useLocation } from 'react-router-dom'
import { useSharedStore } from './store/sharedStore'
@@ -98,7 +98,7 @@ const SideBar: FC = () => {
<div className='text-xs text-[#8C90A3]'>
<SideBarItem
icon={<Star variant={isActive(Paths.favorite) ? 'Bold' : 'Outline'} color={isActive(Paths.favorite) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
icon={<Star1 variant={isActive(Paths.favorite) ? 'Bold' : 'Outline'} color={isActive(Paths.favorite) ? 'black' : '#8C90A3'} size={iconSizeSideBar} />}
title={t('sidebar.favorite')}
isActive={isActive(Paths.favorite)}
link={Paths.favorite}
+1
View File
@@ -7,6 +7,7 @@ export const Paths = {
trash: "/trash",
favorite: "/favorite",
spam: "/spam",
search: "/search",
setting: "/setting",
detailEmail: "/mail/",
login: "/auth/login",