From cf9ae234b9938be85c833ab98741a4590f9cfe13 Mon Sep 17 00:00:00 2001 From: parsaaghayi Date: Wed, 26 Aug 2026 00:12:33 +0330 Subject: [PATCH] feat: add charity reporting & financial breakdowns in admin, fix mobile scroll restoration and OTP numeric keyboard --- backend/src/admin/admin.service.spec.ts | 5 + backend/src/admin/reports.controller.ts | 28 +- backend/src/admin/reports.service.ts | 135 +- backend/src/auth/auth.service.spec.ts | 1 + backend/src/orders/orders.service.spec.ts | 6 + backend/src/products/products.service.spec.ts | 7 + .../src/settings/settings.controller.spec.ts | 7 +- backend/src/settings/settings.service.spec.ts | 11 + backend/src/users/users.service.spec.ts | 2 +- frontend/admin-panel/src/pages/Blogs.tsx | 9 +- frontend/admin-panel/src/pages/Reports.tsx | 401 +- frontend/application/app/ClientLayout.tsx | 28 + .../application/components/ArchivePage.tsx | 63 +- frontend/application/components/AuthModal.tsx | 19 +- frontend/application/components/BlogPage.tsx | 21 +- .../components/FeaturedProducts.tsx | 49 +- frontend/application/components/Header.tsx | 150 +- .../application/components/SmartAdvisor.tsx | 279 +- .../components/VideoModalPlayer.tsx | 325 +- .../application/components/VideosPage.tsx | 104 +- .../__tests__/FeaturedProducts.test.tsx | 25 +- .../components/__tests__/Footer.test.tsx | 40 +- .../components/__tests__/Header.test.tsx | 4 +- frontend/application/lib/store/userStore.ts | 8 +- frontend/application/vitest.setup.ts | 31 + graphify-out/.graphify_labels.json | 74 +- graphify-out/.graphify_labels.json.sig | 2 +- .../2026-08-26/.graphify_analysis.json | 4948 + graphify-out/2026-08-26/.graphify_labels.json | 320 + .../2026-08-26/.graphify_semantic_marker | 1 + graphify-out/2026-08-26/GRAPH_REPORT.md | 1108 + graphify-out/2026-08-26/graph.json | 130900 ++++++++ graphify-out/2026-08-26/manifest.json | 3590 + graphify-out/GRAPH_REPORT.md | 408 +- ...34469f21c244b6dca88b70e1f9ffa17a71d83.json | 1 - ...3198a2c448ccdd6861b1302cc29735f48da82.json | 1 - ...0c462f51d95700101dcedd36255bab1b4ce7a.json | 1 - ...c6f842c299b436a72b117e2263f011b9ebf9c.json | 1 - ...7d1e79ce688681bea4dc06a7fd19e2d5a2129.json | 1 - ...090a370ab56dc77c22cb9de74895b4c783320.json | 1 - ...f66930ba52e04430bdba87020cb4a4edcb817.json | 1 - ...9584012a8dd7f5e922af9f79754540629e2fe.json | 1 - ...58fc3d83ec827b8a00debc0943bc8e3a3d201.json | 1 - ...9f6b49074583502bd77f24c955bd86a6f3df8.json | 1 - ...16c7c92d53401efd92d0675d4bbe5e90572a8.json | 1 - ...1f1ad3128fe3305c3af22639ab2584e8f0b55.json | 1 - ...584f03997a3c897d1de9afbd128ff8a86d09e.json | 1 - ...d2f7a2316fe25bd1cd328421ffa823b36f40f.json | 1 - ...5fe148ad2d75851d45699187678d055947b11.json | 1 - ...65def786abfeb159bc4d822e364e3f2ea694f.json | 1 - ...fc9e4fbd9946eb85b01855a1d0bd810b7623d.json | 1 - ...d60310202225da12bc7d324e192f4e15d8c38.json | 1 - ...49dfd88d5398fd2af154fd385c80e4d559bb1.json | 1 - ...128f0c0f9bfdba46e45e0e958fe92ccdcda53.json | 1 - ...b140e72953fe61a819966fb040382e9b47d45.json | 1 - ...95b635aec180d93b22b975e9ae67c47e361c8.json | 1 - ...f5709afa23e08a80fc859fe0138b416356cd9.json | 1 - ...4f167a2daafb73f095db9237c5bdb20e7a5f4.json | 1 - ...18b75ab3822ec3a90f99bc77767ca99482edf.json | 1 - ...667e1fa3d38cbc9b2fbf9a946ee1e531e9e37.json | 1 - ...edeefb44794ef22bac5529bee9c87dc5d8f3d.json | 1 - ...9a5e3cfd2fc38c54d1a5386fa43ba488c4d4d.json | 1 - ...21815b52d847bd36c748a03bfdc8eb2a390b8.json | 1 - ...1d408f43245840749d2f46df5c356a208fbfd.json | 1 - ...11bd59ef9951572007763151bf95663256a3d.json | 1 - ...f3870a5892dae6d408217ed3fb475f72d3f0b.json | 1 - ...9fa54aaad808dabcc93c2433d5856b0f73300.json | 1 - ...d9b5b2d39514429c93e0aedf68b21f23fd71e.json | 1 - ...bfc8735a514b3fbbc49069f5f0884155ed93f.json | 1 - ...4e49f506627f10f581d3c4b835028d02b5421.json | 1 - ...09037ef85fb7ec5cc7911b749b35bcb004fbf.json | 1 - ...4b50823e1b7de9b113f542248eac38c70cdf1.json | 1 - ...a5de782a373d6f5af858e10a731c20b35c0e2.json | 1 - ...14075185e5675a4a0ee23e99e70d88253b8af.json | 1 - ...683e0483b08c696117ee612bee1528705c598.json | 1 - ...599cee201e1c393b7ee1beeb495c304fd4bd3.json | 1 - ...054f88f4c367ba81fc3cb1b85c2bdcfec5c70.json | 1 - ...a06d1a77f6809f0a6370f9b9517c91dd19db7.json | 1 - ...fc30eb46c0320d1dbb5726cfda0becdd04a5a.json | 1 - ...dac08380f60c414b4968f83e1bc4e61b3e268.json | 1 - ...82f69dfa6deca5f5e4a7f2739a28746c143f9.json | 1 - ...9b6891a36af98ae994f506a64be199ed50392.json | 1 - ...e9dce20f181e8d264557808989965e83a9030.json | 1 - ...9de8f79fdab83055ef560ca3a865026b6450a.json | 1 - ...d970cda8f7d020f8124b624b228174b1b65ab.json | 1 - ...e68d3fcfabfb2d65f8ac1cd86168dc8a8cc96.json | 1 - ...850bb31ab7e3c5470059d02d7f84dd120a0e4.json | 1 - ...aadcc26e6e0bad0cc576d2f982aa42eed42e1.json | 1 - ...321072f15df92266522497b60aaa47579aac5.json | 1 - ...4f560078772a568ed599cac5ba20b089e563d.json | 1 - ...a8029878417c28117e3b89aa8536ad70a5b95.json | 1 - ...72c47567b88ca88de7e4e5bf59869077ff407.json | 1 - ...e50be85d04bc1ad5d3db1ef74e85de97f71a6.json | 1 - ...c927976d9be7da3f75b175331381ef8c8b680.json | 1 - ...c309ac8dfc7cd6e1f33e9619ac12d99f685ba.json | 1 - ...1f2f78c200a272918606decc377c469944321.json | 1 - ...54e4d9ed68753f1c240d0682120bdb9aa3c72.json | 1 - ...39870e704923a561ffb9f4ce4b31b1d4bd830.json | 1 - ...b3f7b6d134dc003ef2ad0cb86b952c869c4a5.json | 1 - ...cd8afc9203f7a94eda8e63fd608bc8e9164d1.json | 1 - ...d52c419176473cd4e19362fd4256616076151.json | 1 - ...51f8cab4df22722826f7c9d55ca19b94d88da.json | 1 - ...5903057098a67a397ae6442d6c825091f4513.json | 1 - ...84910c54aad25bc9943392179bc070c07a434.json | 1 - ...a69e4238f861da265d1ce8cd0192bafce40a9.json | 1 - ...41913018360de5596b9e6eff52d191522adfe.json | 1 - ...874b59e03a1656894d3db25209a0e4031010a.json | 1 - ...69698b26d955a8e7c8e4fa19cda8729ee2f7b.json | 1 - ...64c68c8912277ad9645310f7e8116fbcba741.json | 1 - ...a3ad03e77f41809b12eccfb5e090a11f4fad2.json | 1 - ...7a69421e8150eb38c70bb8587b5a14e7d3ca8.json | 1 - ...96958b13a9115c0afc0a33206c293d1b1c187.json | 1 - ...06f3f5d2790071578dcb98e7869cd6e9ec8a9.json | 1 - ...c233d947c2965a13ed5b399462b80ca9c1513.json | 1 - ...4d993d9c23dcd2759120fbe551f5b8da95a08.json | 1 - ...1fcbe9f74db7901cf204b5058da3132d3c1d2.json | 1 - ...cbf2cca3198e72834f0f228495ef3c49a2240.json | 1 - ...1d8b65db2f2917d336d511790da4718b05b8d.json | 1 - ...3b97d8370cd75fa19cc83af1199f267d7bc55.json | 1 - ...eb8a8f13bdb74887d10f5f20887d41cef5c62.json | 1 - ...e1ddb656ab5b9ee912b89adcc7a9338f34a0b.json | 1 - ...f5f24712c1e198fd79255fa85e3249150e8b4.json | 1 - ...6731fe53f208e737ccf46035389591900a150.json | 1 - ...28052fa1a78eaae5a2bb9b18072b0273e80e1.json | 1 - ...3843c8aecddcb0572ec47d243778ea6a76c1.json} | 0 ...b4720574caa6b8445012ab97bc34432f9ebff.json | 1 + ...487e823f94cda5931c7c5e0268451fbe5153d.json | 1 + ...849dc9fd1f066819cf54deb9262b034262f65.json | 1 + ...47a4eff2b45e866743ae79dc2f1398f8bf2bc.json | 1 + ...bbdf1b80022f4c33fc478f3c6352e56b309b5.json | 1 + ...f568696a13f768d8289fc9be4cc4bab0bae2.json} | 0 ...2991e1b81efb46027019fadf505ff8cdb1883.json | 1 + ...7a66c84ea87b7dcdc65c02dd96ce935ff6eca.json | 1 + ...607ed92f9721baf404daa23127ec9fb489855.json | 1 + ...77414f73f6d6b13ce861b770e33c925db702.json} | 0 ...b0ecb4c6298cdb95883b4852e2f0545e9fabf.json | 1 + ...b4af33c2523aafd88099c20e04b8d9ca70a1.json} | 0 ...8d454e8ca752fc19084835903ef9f0493b9d5.json | 1 + ...41181ac17da826c694746f37b6b0ea9c5a298.json | 1 + ...908e36f80f5a0111e762508845a81e14d66f.json} | 0 ...16e39a20e78e3581d15c4a973d2285767810.json} | 0 ...4db0a7d8eae6c870f7ca5314bae1ae58de8e0.json | 1 + ...fa03e8274efb1df4430b1d792283ade7f8078.json | 1 + ...064b405d859623756e51914aa92c5044b3a16.json | 1 + ...12d4eb9d0e06e10afd9647346b6216c67701e.json | 1 + ...a77773b7cbe91ad7378088df2a9e973ba5bf7.json | 1 + ...2b29cf0fa4fecb9b31c91b13ce4d121184603.json | 1 + ...603888a9130df6d2927e54a2f3aec07f5e95.json} | 0 ...69f6bf715aedb712e85af507d8d6dd4db9061.json | 1 + ...e2112002e0eb5a19aebb2524d9b75c1586a5a.json | 1 + ...2d2f83503bb72305dde6fe8778c851905002c.json | 1 + ...41497f3f91d46599ba397caf2da55bb8a4240.json | 1 + ...ec79d63eb3b6640d571c0fbdc73abcf2198a.json} | 0 ...2ddc85b4df85343cbdf3c6972e2e624d2e3a6.json | 1 + ...fb0993e6f09b5124667e78d6c65024d32e893.json | 1 + ...b64a4daee3ede5c8dc0b2fda550120a891b35.json | 1 + ...244de39b5218ac8b977c271f3de6c98696fe6.json | 1 + ...9296e7eca9d2a98b7218b1654c072059b26e2.json | 1 + ...2be76cffb15fbc91d1ec175c3f8eb1ae4703f.json | 1 + ...a3e0f3f76096b666f6bc4a15a7c7da8806fcf.json | 1 + ...f865a1d30d08124b7ef07dbe7b00999700d0a.json | 1 + ...64635ab779d64ab71e724a2414c6fb8b0c95.json} | 0 ...d85db5bb61d3f31874ed286797a02132509d0.json | 1 + ...c929962a6721645426dab1ab7df7f61069d21.json | 1 + ...c15258470eefc20bc07e2243933abb687361d.json | 1 + ...7a24edfd0b1f9d4f30058a0b290492efd64b0.json | 1 + ...fe857e446a4d18f61fecc464400c3b17f8019.json | 1 + ...ab77711bf119b6c35a899dc8e1df67f068eba.json | 1 + ...7243829efb87915bcc88143577e36119657c.json} | 0 ...f70002bce56f775a878784b7a537cc4d2621.json} | 0 ...6927bf553ad674f5dfb493076b4966842c23c.json | 1 + ...ff9f87eff04c8888ff3f0b9ea62694a7c7823.json | 1 + ...e1bc8469de9e66baa971ba5e74de52f55581a.json | 1 + ...73579df7aa8de89e27f417bd81d7843f90033.json | 1 + ...b8ab2889cea16c34261ed91ef5529e2d6b6e9.json | 1 + ...212d7b0f91326608e0d31885a8916874eeb4e.json | 1 + ...d8b1dcc6c8625efcbbd3b785da24b353da220.json | 1 + ...8e0db007c7bf9bbfbc99985f401db0ee5bae6.json | 1 + ...e47811590076512da80777a80d8fc9639dca.json} | 0 ...fd8e22ffbff8363b7031d40a14b593c96e722.json | 1 + ...1c551a68d27392f936733b984cbfc9c70e0cf.json | 1 + ...6d98181505d68a5fb2b2b36be65b85e22b9fd.json | 1 + ...61788169c5814c2ce33a822d08464cbbeb45.json} | 0 ...3d51d0063c8ef9ba2a3cce497dccc925926c7.json | 1 + ...0d0df75cf86664abd786d73e6a72927562209.json | 1 + ...31f7ba79f6c464e63d410d7824a05a4917df3.json | 1 + ...6cdbda3749fe6d220137d0cb170475ff60645.json | 1 + ...112c8e96868bebe72bc4e09729c3c95bc694.json} | 0 ...8f19387e29986abd213db098706b7e321e7e7.json | 1 + ...ef607d134ea9bafdf8dc494193fc4478b3093.json | 1 + ...7010c67118c8b9f76288b1d6cd5c4f2c9f56b.json | 1 + ...4ad64aec358ae9d914e454eea395ed701f80b.json | 1 + ...8e838aa81b748f05e1ffc5f58f24ff4cbcd44.json | 1 + ...fe8206913679d290ae20e0a35da35a47f095.json} | 0 ...dfe472310ace4b140c24a7acf8a5fede207c2.json | 1 + ...d1f77b8c0d12381777f5575737d01d376304a.json | 1 + ...d76ecfbb76813f7f0259fa7ed4d75585e4c40.json | 1 + ...fbe48d20e935dfee4ce03b818c7badbca2cc4.json | 1 + ...038545bbd733d23b45d96ec2fe40773c9fce2.json | 1 + ...27104ef63025823ad38ff374d382fa84c01c2.json | 1 + ...098db16eaf2b3f0fe96682729ac588597e7de.json | 1 + ...59e731c5c83c4ea036819b6555449f2f9bd3e.json | 1 + ...44281a0dee02b00e0899c18004ec09d0de106.json | 1 + ...ca92ae586c6e9bb6056bca009e67d3f384e1f.json | 1 + ...252ac5c66a577da18ca90372ad7698575fb48.json | 1 + ...4b2c01bd66762e6874aa4bc955d9d9beade8a.json | 1 + ...bf8a5e2c80b6ae0ff4ebf76d23a02d5071ec.json} | 0 ...c73a962728cd5ef9e79db7d4d1aa577550f8b.json | 1 + ...5e493f89cd34a90dff941e2c6998977e5d923.json | 1 + ...0d45f1783d92e31f55f8478b5448f8dd694fb.json | 1 + ...5a0a02984233570fbfdb508c30a1ebabed2db.json | 1 + ...e02e51a3c36f03b7ff8832822177d35a8e055.json | 1 + ...a5bfe047f5891d024896a58e3c6d425849915.json | 1 + ...558650755e6842ed63faf7abd6f91f1de8116.json | 1 + ...f79d7ea24ffaf2ae2de2a1714be8ee42b22b3.json | 1 + ...8643bdd0ef004207f562fd305bd0e212fcda2.json | 1 + ...6bbee90b2a135c8630e9c4c154c70701ec462.json | 1 + ...784c023372244a182a9bf3b6b47d0b1639511.json | 1 + ...248025b2504e9a0caa035d0e264d778dd4a7a.json | 1 + ...a8fcead36307c34b2d67feda9ac650ac61803.json | 1 + ...cc84721b1a9ce1c039bb8f024326554e2ca05.json | 1 + ...ea59800ed284f5bbc7429315fa6f7a34c34be.json | 1 + ...8e0f3db0285d360d9f95c62c37b4e8b47b0fd.json | 1 + ...70b8cc728fe84b1a91e0d08844e8f8dc0b4c.json} | 0 ...5e32448663db3eeb3368290f3c1fc8f192f54.json | 1 + ...71a981f0cef6bfefc5643eb5b61def4ccbac4.json | 1 + graphify-out/cache/stat-index.json | 2 +- graphify-out/graph.html | 8 +- graphify-out/graph.json | 243330 ++++++++------- graphify-out/manifest.json | 3538 +- 230 files changed, 266421 insertions(+), 123741 deletions(-) create mode 100644 graphify-out/2026-08-26/.graphify_analysis.json create mode 100644 graphify-out/2026-08-26/.graphify_labels.json create mode 100644 graphify-out/2026-08-26/.graphify_semantic_marker create mode 100644 graphify-out/2026-08-26/GRAPH_REPORT.md create mode 100644 graphify-out/2026-08-26/graph.json create mode 100644 graphify-out/2026-08-26/manifest.json delete mode 100644 graphify-out/cache/ast/v0.9.44/00bff98e2ae2ef50c4daf265d5f34469f21c244b6dca88b70e1f9ffa17a71d83.json delete mode 100644 graphify-out/cache/ast/v0.9.44/021895bb0b90dd75935ebe0317d3198a2c448ccdd6861b1302cc29735f48da82.json delete mode 100644 graphify-out/cache/ast/v0.9.44/0249cd6ceae6effdfc7371b51d00c462f51d95700101dcedd36255bab1b4ce7a.json delete mode 100644 graphify-out/cache/ast/v0.9.44/02fbd51a9131da0b12122a5280cc6f842c299b436a72b117e2263f011b9ebf9c.json delete mode 100644 graphify-out/cache/ast/v0.9.44/0436a967b82026334bf45c9ca677d1e79ce688681bea4dc06a7fd19e2d5a2129.json delete mode 100644 graphify-out/cache/ast/v0.9.44/0776fbbe8c29c6dc861efa70683090a370ab56dc77c22cb9de74895b4c783320.json delete mode 100644 graphify-out/cache/ast/v0.9.44/093d244c2788ead47c69e3068ccf66930ba52e04430bdba87020cb4a4edcb817.json delete mode 100644 graphify-out/cache/ast/v0.9.44/0c16b701a50d64f741f6de107c49584012a8dd7f5e922af9f79754540629e2fe.json delete mode 100644 graphify-out/cache/ast/v0.9.44/0d736c7dd11e89622e7105e6a5958fc3d83ec827b8a00debc0943bc8e3a3d201.json delete mode 100644 graphify-out/cache/ast/v0.9.44/143a7c8a6fbedcd837d647efddb9f6b49074583502bd77f24c955bd86a6f3df8.json delete mode 100644 graphify-out/cache/ast/v0.9.44/17a5a74039e8a9d7cb40c5bc3c616c7c92d53401efd92d0675d4bbe5e90572a8.json delete mode 100644 graphify-out/cache/ast/v0.9.44/1927827474b1dd6fd4976af45b51f1ad3128fe3305c3af22639ab2584e8f0b55.json delete mode 100644 graphify-out/cache/ast/v0.9.44/1b117f866e73cd17c43e3a4e856584f03997a3c897d1de9afbd128ff8a86d09e.json delete mode 100644 graphify-out/cache/ast/v0.9.44/1b7ffde4b290e742802fc554f2dd2f7a2316fe25bd1cd328421ffa823b36f40f.json delete mode 100644 graphify-out/cache/ast/v0.9.44/1c6d5a0ec8a709a2b3aed28225f5fe148ad2d75851d45699187678d055947b11.json delete mode 100644 graphify-out/cache/ast/v0.9.44/1cd1bc37f9e76ba7e4d4ef82b8565def786abfeb159bc4d822e364e3f2ea694f.json delete mode 100644 graphify-out/cache/ast/v0.9.44/1d5afbaccf36e952e71082591edfc9e4fbd9946eb85b01855a1d0bd810b7623d.json delete mode 100644 graphify-out/cache/ast/v0.9.44/1e2f2d9d867dc553f503ef91b3bd60310202225da12bc7d324e192f4e15d8c38.json delete mode 100644 graphify-out/cache/ast/v0.9.44/217cadb664c7f7d27f8f849ea1249dfd88d5398fd2af154fd385c80e4d559bb1.json delete mode 100644 graphify-out/cache/ast/v0.9.44/246b9164725e89c67c76f2305cd128f0c0f9bfdba46e45e0e958fe92ccdcda53.json delete mode 100644 graphify-out/cache/ast/v0.9.44/2616ef2b3ac39ded45fc669d5b5b140e72953fe61a819966fb040382e9b47d45.json delete mode 100644 graphify-out/cache/ast/v0.9.44/26f2577a68d808f7f33ec3e418b95b635aec180d93b22b975e9ae67c47e361c8.json delete mode 100644 graphify-out/cache/ast/v0.9.44/2b2f385705db6729fccedc63db0f5709afa23e08a80fc859fe0138b416356cd9.json delete mode 100644 graphify-out/cache/ast/v0.9.44/2f50eb9db8bf1ad7bc23c7d8c3a4f167a2daafb73f095db9237c5bdb20e7a5f4.json delete mode 100644 graphify-out/cache/ast/v0.9.44/315bd3cee954ac3496da8db1b1718b75ab3822ec3a90f99bc77767ca99482edf.json delete mode 100644 graphify-out/cache/ast/v0.9.44/3527b44265fec2472cc20ca4641667e1fa3d38cbc9b2fbf9a946ee1e531e9e37.json delete mode 100644 graphify-out/cache/ast/v0.9.44/3612256e7473a2e5f67ef4778d4edeefb44794ef22bac5529bee9c87dc5d8f3d.json delete mode 100644 graphify-out/cache/ast/v0.9.44/37ae09614034d9b5c855e0132b59a5e3cfd2fc38c54d1a5386fa43ba488c4d4d.json delete mode 100644 graphify-out/cache/ast/v0.9.44/3945c052249ebd020b013303d5921815b52d847bd36c748a03bfdc8eb2a390b8.json delete mode 100644 graphify-out/cache/ast/v0.9.44/3b6e2d78372a72b67e339bfd7331d408f43245840749d2f46df5c356a208fbfd.json delete mode 100644 graphify-out/cache/ast/v0.9.44/4003fe6a71221e735897cfe03ac11bd59ef9951572007763151bf95663256a3d.json delete mode 100644 graphify-out/cache/ast/v0.9.44/51363e62e919d6772c2b3b9a1a3f3870a5892dae6d408217ed3fb475f72d3f0b.json delete mode 100644 graphify-out/cache/ast/v0.9.44/51b63ebe6d2e05aa33c128297029fa54aaad808dabcc93c2433d5856b0f73300.json delete mode 100644 graphify-out/cache/ast/v0.9.44/5207a2a148ccfd0338342325a39d9b5b2d39514429c93e0aedf68b21f23fd71e.json delete mode 100644 graphify-out/cache/ast/v0.9.44/532e04465e125183d6dd8f8af61bfc8735a514b3fbbc49069f5f0884155ed93f.json delete mode 100644 graphify-out/cache/ast/v0.9.44/53f1f45bbbdd3cf81cb4bc082ef4e49f506627f10f581d3c4b835028d02b5421.json delete mode 100644 graphify-out/cache/ast/v0.9.44/54526cc3e2392922d344f14767f09037ef85fb7ec5cc7911b749b35bcb004fbf.json delete mode 100644 graphify-out/cache/ast/v0.9.44/5b24d39cbbb662ea1cd672839f64b50823e1b7de9b113f542248eac38c70cdf1.json delete mode 100644 graphify-out/cache/ast/v0.9.44/609de4b1331ddb2f8c80e97aac0a5de782a373d6f5af858e10a731c20b35c0e2.json delete mode 100644 graphify-out/cache/ast/v0.9.44/6269043f6a144e8ac86ff1db7e014075185e5675a4a0ee23e99e70d88253b8af.json delete mode 100644 graphify-out/cache/ast/v0.9.44/677c2f1d35c31d75d935766f154683e0483b08c696117ee612bee1528705c598.json delete mode 100644 graphify-out/cache/ast/v0.9.44/69d50bd5e233f030d8076b55b69599cee201e1c393b7ee1beeb495c304fd4bd3.json delete mode 100644 graphify-out/cache/ast/v0.9.44/6c91253cac025506db71adea33a054f88f4c367ba81fc3cb1b85c2bdcfec5c70.json delete mode 100644 graphify-out/cache/ast/v0.9.44/6f60e7e1648827d0553bb5db6faa06d1a77f6809f0a6370f9b9517c91dd19db7.json delete mode 100644 graphify-out/cache/ast/v0.9.44/7aa8b606e5c19334b6c9fe62a12fc30eb46c0320d1dbb5726cfda0becdd04a5a.json delete mode 100644 graphify-out/cache/ast/v0.9.44/7c7827f2f29d6e2cb6cc6902558dac08380f60c414b4968f83e1bc4e61b3e268.json delete mode 100644 graphify-out/cache/ast/v0.9.44/8169bc98b249cd83ba21551dea082f69dfa6deca5f5e4a7f2739a28746c143f9.json delete mode 100644 graphify-out/cache/ast/v0.9.44/880beb9c92fd527a098de27bc779b6891a36af98ae994f506a64be199ed50392.json delete mode 100644 graphify-out/cache/ast/v0.9.44/88c7c13583a91fc7e41ce055fede9dce20f181e8d264557808989965e83a9030.json delete mode 100644 graphify-out/cache/ast/v0.9.44/8bdf2058520a88de4d08c0635999de8f79fdab83055ef560ca3a865026b6450a.json delete mode 100644 graphify-out/cache/ast/v0.9.44/8ceb4cae1ad4c0f660557d827ced970cda8f7d020f8124b624b228174b1b65ab.json delete mode 100644 graphify-out/cache/ast/v0.9.44/8d8f5c1bbdbb7b8045e02b06a50e68d3fcfabfb2d65f8ac1cd86168dc8a8cc96.json delete mode 100644 graphify-out/cache/ast/v0.9.44/8ea08de6ac54dddd9da6de934bc850bb31ab7e3c5470059d02d7f84dd120a0e4.json delete mode 100644 graphify-out/cache/ast/v0.9.44/90ec787fdd4c71f5203c56b6899aadcc26e6e0bad0cc576d2f982aa42eed42e1.json delete mode 100644 graphify-out/cache/ast/v0.9.44/9b0e689f6474b487d70db0ecb05321072f15df92266522497b60aaa47579aac5.json delete mode 100644 graphify-out/cache/ast/v0.9.44/9cf5d515d0d8584aeef36708df34f560078772a568ed599cac5ba20b089e563d.json delete mode 100644 graphify-out/cache/ast/v0.9.44/9d0f687cf8e95c6335f1efb070da8029878417c28117e3b89aa8536ad70a5b95.json delete mode 100644 graphify-out/cache/ast/v0.9.44/9dc8d7eeda0655717dc5b9d2f8572c47567b88ca88de7e4e5bf59869077ff407.json delete mode 100644 graphify-out/cache/ast/v0.9.44/9f1e0335fdc0fa2db0fd5ceaa97e50be85d04bc1ad5d3db1ef74e85de97f71a6.json delete mode 100644 graphify-out/cache/ast/v0.9.44/9f826c0267cad8bd5d342b1f908c927976d9be7da3f75b175331381ef8c8b680.json delete mode 100644 graphify-out/cache/ast/v0.9.44/a1c9d7902131d71cf87a072876cc309ac8dfc7cd6e1f33e9619ac12d99f685ba.json delete mode 100644 graphify-out/cache/ast/v0.9.44/a735e1b29ad9d3361a0d1ecfd5c1f2f78c200a272918606decc377c469944321.json delete mode 100644 graphify-out/cache/ast/v0.9.44/a81e2d017e0669c71099362b20854e4d9ed68753f1c240d0682120bdb9aa3c72.json delete mode 100644 graphify-out/cache/ast/v0.9.44/aee712cb19a8475be75f8073f7e39870e704923a561ffb9f4ce4b31b1d4bd830.json delete mode 100644 graphify-out/cache/ast/v0.9.44/b4a76df6483fd52e1b9f1e70765b3f7b6d134dc003ef2ad0cb86b952c869c4a5.json delete mode 100644 graphify-out/cache/ast/v0.9.44/b623d8f8c2b8a14c1ddca1ad956cd8afc9203f7a94eda8e63fd608bc8e9164d1.json delete mode 100644 graphify-out/cache/ast/v0.9.44/b664c2bb6d85e009d7495bcd9c3d52c419176473cd4e19362fd4256616076151.json delete mode 100644 graphify-out/cache/ast/v0.9.44/b6b9a4845e0bd5674d897c08f5451f8cab4df22722826f7c9d55ca19b94d88da.json delete mode 100644 graphify-out/cache/ast/v0.9.44/b8a52f324e36fcc2d046cc903a05903057098a67a397ae6442d6c825091f4513.json delete mode 100644 graphify-out/cache/ast/v0.9.44/bc8401649a86585b4061b3ec0f584910c54aad25bc9943392179bc070c07a434.json delete mode 100644 graphify-out/cache/ast/v0.9.44/be9b046e69dc5442c30d61cca51a69e4238f861da265d1ce8cd0192bafce40a9.json delete mode 100644 graphify-out/cache/ast/v0.9.44/c05865bc3bcdf5575a5889c564941913018360de5596b9e6eff52d191522adfe.json delete mode 100644 graphify-out/cache/ast/v0.9.44/c3edfd26252566ef2ddee12c91a874b59e03a1656894d3db25209a0e4031010a.json delete mode 100644 graphify-out/cache/ast/v0.9.44/c46b1b09c5f6be7f54ba888652e69698b26d955a8e7c8e4fa19cda8729ee2f7b.json delete mode 100644 graphify-out/cache/ast/v0.9.44/c564f4aba35d32a733a5837c33664c68c8912277ad9645310f7e8116fbcba741.json delete mode 100644 graphify-out/cache/ast/v0.9.44/c6f7fc2f062904d246b700d446ba3ad03e77f41809b12eccfb5e090a11f4fad2.json delete mode 100644 graphify-out/cache/ast/v0.9.44/c96b04d5af093f455fa3c64ae177a69421e8150eb38c70bb8587b5a14e7d3ca8.json delete mode 100644 graphify-out/cache/ast/v0.9.44/cb5e2f3284265c08c88c08f445b96958b13a9115c0afc0a33206c293d1b1c187.json delete mode 100644 graphify-out/cache/ast/v0.9.44/d21daac201fcfc3a0e27c27348806f3f5d2790071578dcb98e7869cd6e9ec8a9.json delete mode 100644 graphify-out/cache/ast/v0.9.44/d4484eea65f1861b9b6a30813d2c233d947c2965a13ed5b399462b80ca9c1513.json delete mode 100644 graphify-out/cache/ast/v0.9.44/d4f82b84ae6c1899ab4da1310414d993d9c23dcd2759120fbe551f5b8da95a08.json delete mode 100644 graphify-out/cache/ast/v0.9.44/d6cc5e861e43bf82dcffe1ad7121fcbe9f74db7901cf204b5058da3132d3c1d2.json delete mode 100644 graphify-out/cache/ast/v0.9.44/dd829cdf325092d859080ac1db3cbf2cca3198e72834f0f228495ef3c49a2240.json delete mode 100644 graphify-out/cache/ast/v0.9.44/dda5b0bae4f843a51274269dcb91d8b65db2f2917d336d511790da4718b05b8d.json delete mode 100644 graphify-out/cache/ast/v0.9.44/dde4a63c991d0f469086a147df03b97d8370cd75fa19cc83af1199f267d7bc55.json delete mode 100644 graphify-out/cache/ast/v0.9.44/e64be175bd632a05e37be823621eb8a8f13bdb74887d10f5f20887d41cef5c62.json delete mode 100644 graphify-out/cache/ast/v0.9.44/e69679636d42dd626c7c76ddc96e1ddb656ab5b9ee912b89adcc7a9338f34a0b.json delete mode 100644 graphify-out/cache/ast/v0.9.44/ea0911b6a5ad6a908ab707f7553f5f24712c1e198fd79255fa85e3249150e8b4.json delete mode 100644 graphify-out/cache/ast/v0.9.44/fca1f8d59c2eab255f7aba246b26731fe53f208e737ccf46035389591900a150.json delete mode 100644 graphify-out/cache/ast/v0.9.44/fcf49beae52c69d7e9cf85967f028052fa1a78eaae5a2bb9b18072b0273e80e1.json rename graphify-out/cache/ast/{v0.9.44/694c258b963c309734a3316c770f5fef29703bc6f24754b04155fd8db94cf862.json => v0.9.48-s2/00e66436ffb697f71f528da0ed073843c8aecddcb0572ec47d243778ea6a76c1.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/017634a31f3e5ba98d623b76bd1b4720574caa6b8445012ab97bc34432f9ebff.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/037437bfab34c43cce068b8d18f487e823f94cda5931c7c5e0268451fbe5153d.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/03da91993339ff789e00f84d0b1849dc9fd1f066819cf54deb9262b034262f65.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/09558dfeece28202039bf1349c547a4eff2b45e866743ae79dc2f1398f8bf2bc.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/0a7b46ebf49f30a004c44ca3b7ebbdf1b80022f4c33fc478f3c6352e56b309b5.json rename graphify-out/cache/ast/{v0.9.44/adcade0f962ec5888be99b4b1402d98812cfd570b0c5669bda73142b2cb9210d.json => v0.9.48-s2/0ccbc30019878c8592b1021fcc39f568696a13f768d8289fc9be4cc4bab0bae2.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/14ae2684792dfaf2abbf4b046ac2991e1b81efb46027019fadf505ff8cdb1883.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/189188c594d07159bc98b00bcb97a66c84ea87b7dcdc65c02dd96ce935ff6eca.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/18db36ad456be9d3e87d43dcbe2607ed92f9721baf404daa23127ec9fb489855.json rename graphify-out/cache/ast/{v0.9.44/f90cf2d0c954f3eaca83566ccb365bff62fd97a2af0225816c19902ee4dfdd6c.json => v0.9.48-s2/196d6f089a5d8b50c7cf3dd1b49477414f73f6d6b13ce861b770e33c925db702.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/1d088753100a687210e5cfa367cb0ecb4c6298cdb95883b4852e2f0545e9fabf.json rename graphify-out/cache/ast/{v0.9.44/3ffd8078128454f22679add155e8304ffb9476e630e847cd895ffb1422db1b5f.json => v0.9.48-s2/1df65248b59ce7d2e6bc030b7e94b4af33c2523aafd88099c20e04b8d9ca70a1.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/24065a334205689166fc397ec3a8d454e8ca752fc19084835903ef9f0493b9d5.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/26961abd8210248a04f8cd5ad4141181ac17da826c694746f37b6b0ea9c5a298.json rename graphify-out/cache/ast/{v0.9.44/2d9838ee1119342fd03bf7644781b8b6aced2cbdc908ff186cdd6ab7ab9ba1c4.json => v0.9.48-s2/28cc38da50d116bbd0de6a134ffc908e36f80f5a0111e762508845a81e14d66f.json} (100%) rename graphify-out/cache/ast/{v0.9.44/64d276bb6eb6cfb83690c529b9d1c0c71fa46aa912a458f684dfcf842fab2aea.json => v0.9.48-s2/29555c750c43c87c2ee1eb8fa53816e39a20e78e3581d15c4a973d2285767810.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/2a3c9bb17a09ce8ab84d8d1d8da4db0a7d8eae6c870f7ca5314bae1ae58de8e0.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/2aa3c37fc3f9cccdb1fa4efbdd9fa03e8274efb1df4430b1d792283ade7f8078.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/2cfebaa4d0b6e5a18a1d16f25d9064b405d859623756e51914aa92c5044b3a16.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/317a5f284cb772fe994d0cbdfe712d4eb9d0e06e10afd9647346b6216c67701e.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/348ccf0d0c7d4fd98ba8b2b5673a77773b7cbe91ad7378088df2a9e973ba5bf7.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/3aeddb56f7af1d05de51fb69a572b29cf0fa4fecb9b31c91b13ce4d121184603.json rename graphify-out/cache/ast/{v0.9.44/64fe2904203985aa0acb9b67d7ade489f2362ac8730283fb11d6a764d1cb3878.json => v0.9.48-s2/3c187936b9c3a4045b6bc0d54a24603888a9130df6d2927e54a2f3aec07f5e95.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/3cd9f03beffc84b16f571f8c9bb69f6bf715aedb712e85af507d8d6dd4db9061.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/3ead5564ea9664d08ee62fd2f7fe2112002e0eb5a19aebb2524d9b75c1586a5a.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/419a5d069432d03a601bfac149b2d2f83503bb72305dde6fe8778c851905002c.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/427be2a0d417fdbef1c13ebdb4941497f3f91d46599ba397caf2da55bb8a4240.json rename graphify-out/cache/ast/{v0.9.44/7a6c2e21b6eaa2355b4e2ed29db2a767499a88498bde745cdccf3ba8c7f64f2b.json => v0.9.48-s2/4295d250cc268444585fa286760dec79d63eb3b6640d571c0fbdc73abcf2198a.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/456437c0213198ca5fab9b157392ddc85b4df85343cbdf3c6972e2e624d2e3a6.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/472b0b5c9726c4439ae26a1d559fb0993e6f09b5124667e78d6c65024d32e893.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/49753f6131b4b4da029184b8dfab64a4daee3ede5c8dc0b2fda550120a891b35.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/4d769f6da15f3fc8dc001f47d99244de39b5218ac8b977c271f3de6c98696fe6.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/527b129de8ecb35503501c720ba9296e7eca9d2a98b7218b1654c072059b26e2.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/551a4525ba3b37310e9fcc2b0712be76cffb15fbc91d1ec175c3f8eb1ae4703f.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/55ecbf088adf1eef94050990e84a3e0f3f76096b666f6bc4a15a7c7da8806fcf.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/5720beed29b64651edc4ff7c3d2f865a1d30d08124b7ef07dbe7b00999700d0a.json rename graphify-out/cache/ast/{v0.9.44/1f47c7dc3c8b5a15558654f45f21e0f553acc1680046f09dc24428e8538c4f9d.json => v0.9.48-s2/590ab6d92f5b42f55875120c412664635ab779d64ab71e724a2414c6fb8b0c95.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/5ad8626941bf79f4103b6456c12d85db5bb61d3f31874ed286797a02132509d0.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/5b0fac871e066e2bfb971e175a5c929962a6721645426dab1ab7df7f61069d21.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/5e9f8e746cd3dee5a3ccd49ddddc15258470eefc20bc07e2243933abb687361d.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/5f8e3ad984c7828277123b5a5407a24edfd0b1f9d4f30058a0b290492efd64b0.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/63338cc957d7059368a8632e117fe857e446a4d18f61fecc464400c3b17f8019.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/66d5f6879d897cbc046d7edfedaab77711bf119b6c35a899dc8e1df67f068eba.json rename graphify-out/cache/ast/{v0.9.44/6d61fb108392e86320c75b42e69ba7138bdc47bba4883fafbeb6e661cc5f51e4.json => v0.9.48-s2/67bd40c3c60ba8603c94de9380c27243829efb87915bcc88143577e36119657c.json} (100%) rename graphify-out/cache/ast/{v0.9.44/164822617eb449e27d8e6a9ff13476f3b08fb3dcf5710289e85af9bf50ec39d5.json => v0.9.48-s2/691a6190a148fd18b805855b0b5bf70002bce56f775a878784b7a537cc4d2621.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/6a4613fb397adee1bcd064abc006927bf553ad674f5dfb493076b4966842c23c.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/6da1c06d3b033107a35445af0d2ff9f87eff04c8888ff3f0b9ea62694a7c7823.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/7933681477448b9d09a1118e059e1bc8469de9e66baa971ba5e74de52f55581a.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/7be8c0e7d2c84db24f5fb724d5873579df7aa8de89e27f417bd81d7843f90033.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/7e1f2b60a65bb3191380b350c5bb8ab2889cea16c34261ed91ef5529e2d6b6e9.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/7e49cd355cede75cdef66c98512212d7b0f91326608e0d31885a8916874eeb4e.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/8181d191224cb763e5784fe015ed8b1dcc6c8625efcbbd3b785da24b353da220.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/85d6fc79140da7cdfa839be45168e0db007c7bf9bbfbc99985f401db0ee5bae6.json rename graphify-out/cache/ast/{v0.9.44/0157b70b08d4db87730d29b1da6749d719cc3f815c2c01d0c5cd29042f4d0bcf.json => v0.9.48-s2/86c918a11fae31e0dda09df0b0f0e47811590076512da80777a80d8fc9639dca.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/87507389cdea75d162d950a26affd8e22ffbff8363b7031d40a14b593c96e722.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/892589dad2e5e8bb0a9319333331c551a68d27392f936733b984cbfc9c70e0cf.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/896dfe9586aa5d7a88d6d6ded846d98181505d68a5fb2b2b36be65b85e22b9fd.json rename graphify-out/cache/ast/{v0.9.44/035a8143cff9651dbb67885756a07cc3f8ed1c5dcbf04a6e2042600b7989d525.json => v0.9.48-s2/8b11b5e59ddca76e783ba9bc93b761788169c5814c2ce33a822d08464cbbeb45.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/8b7a51ba013db5f162e200ab2ab3d51d0063c8ef9ba2a3cce497dccc925926c7.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/90fb670935947163a8b97bb3c100d0df75cf86664abd786d73e6a72927562209.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/943bf5c5fa2eccb404a7e3cf45c31f7ba79f6c464e63d410d7824a05a4917df3.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/959a7809af543c3d58c234fc8496cdbda3749fe6d220137d0cb170475ff60645.json rename graphify-out/cache/ast/{v0.9.44/9b20fbca537d1e55f3a113fcee6683f0eb1f06752d3c8c52edf26eef8e2053f2.json => v0.9.48-s2/96ac330a1750680715788c041b60112c8e96868bebe72bc4e09729c3c95bc694.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/9ec9770ac7654b5a283454e492f8f19387e29986abd213db098706b7e321e7e7.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/9fc17048b9f7bf6ecb4a3f34bb5ef607d134ea9bafdf8dc494193fc4478b3093.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/a1523ad93fe2d3d8f28be2c032d7010c67118c8b9f76288b1d6cd5c4f2c9f56b.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/a4c06b4dbf07bae2b25f2efbf8c4ad64aec358ae9d914e454eea395ed701f80b.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/a5047c11c0610cb3f495dd9754b8e838aa81b748f05e1ffc5f58f24ff4cbcd44.json rename graphify-out/cache/ast/{v0.9.44/9ec013d142e3cf5d7e5f09778fcb06b4b8ea7b394688f9f40b5e74298c76bcf0.json => v0.9.48-s2/a923cdd692e23d866cdd55342357fe8206913679d290ae20e0a35da35a47f095.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/a9407e604bcb3cad92bd92fd136dfe472310ace4b140c24a7acf8a5fede207c2.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/a9fba064343d5f9c880a4d294f6d1f77b8c0d12381777f5575737d01d376304a.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/aa77302fe0618e9511b9a233ed2d76ecfbb76813f7f0259fa7ed4d75585e4c40.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/b5a5237f0dd262308961666186afbe48d20e935dfee4ce03b818c7badbca2cc4.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/b7dcbbe385d7c6dde267cb5ce91038545bbd733d23b45d96ec2fe40773c9fce2.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/b9a251adaafd57872d56b2283d827104ef63025823ad38ff374d382fa84c01c2.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/ba051b383fcd69eea9f3ea74125098db16eaf2b3f0fe96682729ac588597e7de.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/bc13fe68ca2c17442410eba331059e731c5c83c4ea036819b6555449f2f9bd3e.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/bd91670156b112ddb58752a0bd544281a0dee02b00e0899c18004ec09d0de106.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/c55635a378ebb9823bd4256d104ca92ae586c6e9bb6056bca009e67d3f384e1f.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/c6cd286c808a7ad22646964c721252ac5c66a577da18ca90372ad7698575fb48.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/c7cfe31a24b65c8e89d0e4c7a134b2c01bd66762e6874aa4bc955d9d9beade8a.json rename graphify-out/cache/ast/{v0.9.44/e178d23a09563a7a9b348d905bc70322115514c78a7afcc5a53db7d18e511583.json => v0.9.48-s2/cba40494ca979cbf0017c51b84bebf8a5e2c80b6ae0ff4ebf76d23a02d5071ec.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/d034efd98211f598a3b3d3116e2c73a962728cd5ef9e79db7d4d1aa577550f8b.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/d07d2aecc629f8cbfd15d461c635e493f89cd34a90dff941e2c6998977e5d923.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/d13b8f3ea9d9098687488dc65ae0d45f1783d92e31f55f8478b5448f8dd694fb.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/d13e03cbbf9fe5c6ff99ab512bd5a0a02984233570fbfdb508c30a1ebabed2db.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/d2d6fdaaabd0fa118990c8cdceee02e51a3c36f03b7ff8832822177d35a8e055.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/d4b6c645ee75ae52a535bf64845a5bfe047f5891d024896a58e3c6d425849915.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/d7249fe1175eba43749fc57398a558650755e6842ed63faf7abd6f91f1de8116.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/d92622463299d313da48e2cdbebf79d7ea24ffaf2ae2de2a1714be8ee42b22b3.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/db6562ff1e71d694791926909918643bdd0ef004207f562fd305bd0e212fcda2.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/dcb987ba2798ff0f280df56b0226bbee90b2a135c8630e9c4c154c70701ec462.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/e1abe45307c5e5c0020477b8ff7784c023372244a182a9bf3b6b47d0b1639511.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/e2a96e1039dbed88d88958935be248025b2504e9a0caa035d0e264d778dd4a7a.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/e55792958648fb69255e3ce3f71a8fcead36307c34b2d67feda9ac650ac61803.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/eb5a520c4c05a426c6e4ce131e7cc84721b1a9ce1c039bb8f024326554e2ca05.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/eb667e7396a4a90e50fe6ad0820ea59800ed284f5bbc7429315fa6f7a34c34be.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/eeef059b96cbc7a23787e9d12828e0f3db0285d360d9f95c62c37b4e8b47b0fd.json rename graphify-out/cache/ast/{v0.9.44/0308b15b67bad714ff5f1b6b73f64ae46f98d5190cd6092abb1fb67885d67be1.json => v0.9.48-s2/f6ba24eab812d7f8a01a249b27fe70b8cc728fe84b1a91e0d08844e8f8dc0b4c.json} (100%) create mode 100644 graphify-out/cache/ast/v0.9.48-s2/fa093ef10c43ccc203d393390dd5e32448663db3eeb3368290f3c1fc8f192f54.json create mode 100644 graphify-out/cache/ast/v0.9.48-s2/fd939a485bcf44a2222bdc5d4c671a981f0cef6bfefc5643eb5b61def4ccbac4.json diff --git a/backend/src/admin/admin.service.spec.ts b/backend/src/admin/admin.service.spec.ts index 1c4ef9d..7f9659c 100644 --- a/backend/src/admin/admin.service.spec.ts +++ b/backend/src/admin/admin.service.spec.ts @@ -2,6 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { AdminService } from './admin.service'; import { PrismaService } from '../prisma/prisma.service'; import { RedisService } from '../redis/redis.service'; +import { RevalidationService } from '../common/revalidation/revalidation.service'; describe('AdminService', () => { let service: AdminService; @@ -12,6 +13,10 @@ describe('AdminService', () => { AdminService, { provide: PrismaService, useValue: {} }, { provide: RedisService, useValue: {} }, + { + provide: RevalidationService, + useValue: { revalidateTag: jest.fn(), revalidatePath: jest.fn() }, + }, ], }).compile(); diff --git a/backend/src/admin/reports.controller.ts b/backend/src/admin/reports.controller.ts index 411e8f0..b34e6aa 100644 --- a/backend/src/admin/reports.controller.ts +++ b/backend/src/admin/reports.controller.ts @@ -1,7 +1,12 @@ -import { Controller, Get, UseGuards } from '@nestjs/common'; +import { Controller, Get, Query, UseGuards } from '@nestjs/common'; import { ReportsService } from './reports.service'; import { JwtAuthGuard } from '../auth/jwt-auth.guard'; -import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger'; +import { + ApiTags, + ApiBearerAuth, + ApiOperation, + ApiQuery, +} from '@nestjs/swagger'; @ApiTags('Admin - گزارشات') @ApiBearerAuth() @@ -11,9 +16,22 @@ export class ReportsController { @UseGuards(JwtAuthGuard) @Get() - @ApiOperation({ summary: 'دریافت گزارشات داشبورد' }) - async getReports() { - const data = await this.reportsService.getDashboardReports(); + @ApiOperation({ + summary: 'دریافت گزارشات داشبورد با قابلیت فیلتر زمانی و نوع کاربر', + }) + @ApiQuery({ name: 'startDate', required: false }) + @ApiQuery({ name: 'endDate', required: false }) + @ApiQuery({ name: 'role', required: false }) + async getReports( + @Query('startDate') startDate?: string, + @Query('endDate') endDate?: string, + @Query('role') role?: string, + ) { + const data = await this.reportsService.getDashboardReports({ + startDate, + endDate, + role, + }); return { success: true, data }; } } diff --git a/backend/src/admin/reports.service.ts b/backend/src/admin/reports.service.ts index 0eb8fef..6146e6e 100644 --- a/backend/src/admin/reports.service.ts +++ b/backend/src/admin/reports.service.ts @@ -5,29 +5,71 @@ import { PrismaService } from '../prisma/prisma.service'; export class ReportsService { constructor(private prisma: PrismaService) {} - async getDashboardReports() { - // 1. Total revenue and charity + async getDashboardReports(query?: { + startDate?: string; + endDate?: string; + role?: string; + }) { + const whereClause: any = { status: { not: 'cancelled' } }; + + if (query?.startDate || query?.endDate) { + whereClause.createdAt = {}; + if (query.startDate) { + whereClause.createdAt.gte = new Date(query.startDate); + } + if (query.endDate) { + const end = new Date(query.endDate); + end.setHours(23, 59, 59, 999); + whereClause.createdAt.lte = end; + } + } + + if (query?.role && query.role !== 'ALL') { + whereClause.user = { role: query.role }; + } + + // 1. Fetch filtered orders with items and coupons for accurate breakdown const orders = await this.prisma.order.findMany({ - where: { status: { not: 'cancelled' } }, - select: { - totalAmount: true, - charityDonation: true, - createdAt: true, + where: whereClause, + include: { user: { select: { role: true } }, + coupon: true, + orderItems: { + include: { + product: { + select: { + id: true, + buyPrice: true, + priceValue: true, + }, + }, + }, + }, }, + orderBy: { createdAt: 'asc' }, }); - let totalRevenue = 0; - let totalCharity = 0; + let totalRevenue = 0; // Total paid/deposited by users + let totalCharity = 0; // Charity portion let b2bRevenue = 0; let b2cRevenue = 0; + let totalTax = 0; + const totalShipping = 0; + let totalDiscounts = 0; + let totalCostOfGoods = 0; // COGS (مجموع قیمت خرید کالاها) - const salesByDate: Record = {}; + const salesByDate: Record< + string, + { total: number; charity: number; sales: number } + > = {}; for (const order of orders) { - const amount = Number(order.totalAmount); + const amount = Number(order.totalAmount || 0); + const charity = Number(order.charityDonation || 0); + const productSales = Math.max(0, amount - charity); + totalRevenue += amount; - totalCharity += Number(order.charityDonation); + totalCharity += charity; if (order.user?.role === 'B2B') { b2bRevenue += amount; @@ -35,23 +77,74 @@ export class ReportsService { b2cRevenue += amount; } + // Calculate cost of goods (COGS) from order items + let orderCOGS = 0; + let orderItemSubtotal = 0; + + if (order.orderItems && order.orderItems.length > 0) { + for (const item of order.orderItems) { + const qty = item.quantity || 1; + const buyPrice = Number(item.product?.buyPrice || 0); + const salePrice = Number(item.product?.priceValue || 0); + orderCOGS += buyPrice * qty; + orderItemSubtotal += salePrice * qty; + } + } + totalCostOfGoods += orderCOGS; + + // Discount calculation (if coupon was used or total discount applied) + if (order.coupon) { + if ( + order.coupon.type === 'percent' || + order.coupon.type === 'PERCENTAGE' + ) { + const discountVal = + (orderItemSubtotal * Number(order.coupon.value)) / 100; + totalDiscounts += discountVal; + } else { + totalDiscounts += Number(order.coupon.value || 0); + } + } + + // Standard VAT / Tax (approx 10% on product sales if configured) + const taxRate = 0.1; + const orderTax = Math.round(productSales * (taxRate / (1 + taxRate))); + totalTax += orderTax; + // Format date YYYY-MM-DD const dateKey = order.createdAt.toISOString().split('T')[0]; - if (!salesByDate[dateKey]) salesByDate[dateKey] = 0; - salesByDate[dateKey] += amount; + if (!salesByDate[dateKey]) { + salesByDate[dateKey] = { total: 0, charity: 0, sales: 0 }; + } + salesByDate[dateKey].total += amount; + salesByDate[dateKey].charity += charity; + salesByDate[dateKey].sales += productSales; } + // Product Sales Revenue (excluding charity) + const productSalesRevenue = Math.max(0, totalRevenue - totalCharity); + // Net profit = Product Sales - COGS - Tax - Discounts + const netProfit = Math.max( + 0, + productSalesRevenue - totalCostOfGoods - totalTax - totalDiscounts, + ); + // Format sales timeline for charts const salesTimeline = Object.entries(salesByDate) - .map(([date, amount]) => ({ date, amount })) + .map(([date, data]) => ({ + date, + amount: data.total, + charity: data.charity, + sales: data.sales, + })) .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); - // 2. Best selling products (by quantity) + // 2. Best selling products (by quantity) in the selected range const orderItems = await this.prisma.orderItem.groupBy({ by: ['productId'], _sum: { quantity: true }, where: { - order: { status: { not: 'cancelled' } }, + order: whereClause, productId: { not: null }, }, orderBy: { _sum: { quantity: 'desc' } }, @@ -89,7 +182,7 @@ export class ReportsService { }) .filter((c) => c.value > 0); - // 4. Coupons Usage + // 4. Coupons Usage in range const topCoupons = await this.prisma.coupon.findMany({ orderBy: { usedCount: 'desc' }, take: 5, @@ -100,9 +193,15 @@ export class ReportsService { overview: { totalRevenue, totalCharity, + productSalesRevenue, totalOrders: orders.length, b2bRevenue, b2cRevenue, + totalTax, + totalShipping, + totalDiscounts, + totalCostOfGoods, + netProfit, }, salesTimeline, bestSellers, diff --git a/backend/src/auth/auth.service.spec.ts b/backend/src/auth/auth.service.spec.ts index 2e996ba..1e997bb 100644 --- a/backend/src/auth/auth.service.spec.ts +++ b/backend/src/auth/auth.service.spec.ts @@ -28,6 +28,7 @@ describe('AuthService', () => { set: jest.fn(), get: jest.fn(), del: jest.fn(), + incr: jest.fn().mockResolvedValue(1), }; const mockSms = { diff --git a/backend/src/orders/orders.service.spec.ts b/backend/src/orders/orders.service.spec.ts index fd58625..dbd4259 100644 --- a/backend/src/orders/orders.service.spec.ts +++ b/backend/src/orders/orders.service.spec.ts @@ -27,6 +27,12 @@ describe('OrdersService', () => { walletTransaction: { create: jest.fn(), }, + setting: { + findFirst: jest.fn().mockResolvedValue(null), + }, + uiText: { + findFirst: jest.fn().mockResolvedValue(null), + }, }; const mockSmsService = { diff --git a/backend/src/products/products.service.spec.ts b/backend/src/products/products.service.spec.ts index 7480028..644a1b2 100644 --- a/backend/src/products/products.service.spec.ts +++ b/backend/src/products/products.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ProductsService } from './products.service'; import { PrismaService } from '../prisma/prisma.service'; +import { RevalidationService } from '../common/revalidation/revalidation.service'; describe('ProductsService', () => { let service: ProductsService; @@ -15,11 +16,17 @@ describe('ProductsService', () => { }, }; + const mockRevalidationService = { + revalidateTag: jest.fn(), + revalidatePath: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ ProductsService, { provide: PrismaService, useValue: mockPrisma }, + { provide: RevalidationService, useValue: mockRevalidationService }, ], }).compile(); diff --git a/backend/src/settings/settings.controller.spec.ts b/backend/src/settings/settings.controller.spec.ts index 1c43e79..743e7de 100644 --- a/backend/src/settings/settings.controller.spec.ts +++ b/backend/src/settings/settings.controller.spec.ts @@ -37,14 +37,11 @@ describe('SettingsController', () => { expect(controller).toBeDefined(); }); - it('should have JwtAuthGuard, RolesGuard and Admin role applied at controller level', () => { - const guards = Reflect.getMetadata(GUARDS_METADATA, SettingsController); + it('should have JwtAuthGuard and RolesGuard on protected mutation routes', () => { + const guards = Reflect.getMetadata(GUARDS_METADATA, controller.putUiText); expect(guards).toBeDefined(); expect(guards).toContain(JwtAuthGuard); expect(guards).toContain(RolesGuard); - - const roles = Reflect.getMetadata(ROLES_KEY, SettingsController); - expect(roles).toEqual(['Admin']); }); it('should getUiTexts', async () => { diff --git a/backend/src/settings/settings.service.spec.ts b/backend/src/settings/settings.service.spec.ts index 9e5f1b1..de36826 100644 --- a/backend/src/settings/settings.service.spec.ts +++ b/backend/src/settings/settings.service.spec.ts @@ -1,6 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SettingsService } from './settings.service'; import { PrismaService } from '../prisma/prisma.service'; +import { SmsService } from '../common/services/sms.service'; describe('SettingsService', () => { let service: SettingsService; @@ -11,6 +12,10 @@ describe('SettingsService', () => { findMany: jest.fn(), upsert: jest.fn(), }, + setting: { + findMany: jest.fn().mockResolvedValue([]), + upsert: jest.fn(), + }, scientificTerm: { findMany: jest.fn(), upsert: jest.fn(), @@ -18,11 +23,17 @@ describe('SettingsService', () => { }, }; + const mockSmsService = { + getSmsLogs: jest.fn(), + sendOtp: jest.fn(), + }; + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ SettingsService, { provide: PrismaService, useValue: mockPrisma }, + { provide: SmsService, useValue: mockSmsService }, ], }).compile(); diff --git a/backend/src/users/users.service.spec.ts b/backend/src/users/users.service.spec.ts index 8aea99f..59876ac 100644 --- a/backend/src/users/users.service.spec.ts +++ b/backend/src/users/users.service.spec.ts @@ -44,7 +44,7 @@ describe('UsersService', () => { mockPrisma.user.findUnique.mockResolvedValue(mockUser); const result = await service.findById('user-id'); expect(prisma.user.findUnique).toHaveBeenCalled(); - expect(result).toEqual(mockUser); + expect(result).toEqual({ ...mockUser, hasPassword: false }); }); it('should call prisma update in update', async () => { diff --git a/frontend/admin-panel/src/pages/Blogs.tsx b/frontend/admin-panel/src/pages/Blogs.tsx index 854f291..be55cad 100644 --- a/frontend/admin-panel/src/pages/Blogs.tsx +++ b/frontend/admin-panel/src/pages/Blogs.tsx @@ -684,13 +684,14 @@ export default function Blogs() { {blog.author?.firstName ? `${blog.author.firstName} ${blog.author.lastName || ''}` : 'نامشخص'} -
+
{blog.readingTime || 3} دقیقه
- - {blog.viewCount || 0} بازدید - +
+ + {(blog.viewCount || 0).toLocaleString('fa-IR')} بازدید +
{renderStatusBadge(blog.status, blog.isPublished)} diff --git a/frontend/admin-panel/src/pages/Reports.tsx b/frontend/admin-panel/src/pages/Reports.tsx index 591f927..730c59c 100644 --- a/frontend/admin-panel/src/pages/Reports.tsx +++ b/frontend/admin-panel/src/pages/Reports.tsx @@ -1,11 +1,21 @@ -import { useState, useEffect } from 'react'; -import { TrendingUp, DollarSign, ShoppingBag, Users, HeartHandshake, Download } from 'lucide-react'; +import { useState, useEffect, useCallback } from 'react'; +import { + TrendingUp, + DollarSign, + ShoppingBag, + Users, + HeartHandshake, + Download, + Calendar, + Filter, + Receipt, + Percent, + Package, + ShieldCheck +} from 'lucide-react'; import api from '../services/api'; import Button from '../components/ui/Button'; import Spinner from '../components/ui/Spinner'; - - - import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell, Legend @@ -15,16 +25,21 @@ const COLORS = ['#8B5CF6', '#EC4899', '#3B82F6', '#10B981', '#F59E0B']; interface CustomTooltipProps { active?: boolean; - payload?: Array<{ value: number | string }>; + payload?: Array<{ value: number | string; name?: string; color?: string }>; label?: string; } const CustomTooltip = ({ active, payload, label }: CustomTooltipProps) => { if (active && payload && payload.length) { return ( -
+

{label}

-

{Number(payload[0].value).toLocaleString()} تومان

+ {payload.map((item, idx) => ( +
+ {item.name || 'مبلغ'}: + {Number(item.value).toLocaleString()} تومان +
+ ))}
); } @@ -50,13 +65,20 @@ export interface TopCouponItem { export interface ReportData { overview?: { totalRevenue: number; - totalOrders: number; - totalUsers: number; - avgOrderValue: number; totalCharity?: number; + productSalesRevenue?: number; + totalOrders: number; + totalUsers?: number; + avgOrderValue?: number; b2bRevenue?: number; + b2cRevenue?: number; + totalTax?: number; + totalShipping?: number; + totalDiscounts?: number; + totalCostOfGoods?: number; + netProfit?: number; }; - salesTimeline?: Array<{ date: string; amount: number }>; + salesTimeline?: Array<{ date: string; amount: number; charity?: number; sales?: number }>; bestSellers?: BestSellerItem[]; categoryDistribution?: CategoryDistItem[]; topCoupons?: TopCouponItem[]; @@ -66,40 +88,72 @@ export default function Reports() { const [reportData, setReportData] = useState(null); const [isLoading, setIsLoading] = useState(true); - useEffect(() => { - let isSubscribed = true; - api.get('/admin/reports').then(reportRes => { - if (!isSubscribed) return; - if (reportRes.data?.success) { - setReportData(reportRes.data.data); - } - }).catch(err => { - console.error(err); - }).finally(() => { - if (isSubscribed) setIsLoading(false); - }); - return () => { - isSubscribed = false; - }; - }, []); + // Filter States + const [dateFilterType, setDateFilterType] = useState<'all' | 'today' | '7days' | '30days' | 'custom'>('30days'); + const [startDate, setStartDate] = useState(''); + const [endDate, setEndDate] = useState(''); + const [roleFilter, setRoleFilter] = useState('ALL'); - if (isLoading) { + const fetchReports = useCallback(async () => { + try { + setIsLoading(true); + const params: Record = {}; + + const now = new Date(); + if (dateFilterType === 'today') { + const start = new Date(now); + start.setHours(0, 0, 0, 0); + params.startDate = start.toISOString(); + } else if (dateFilterType === '7days') { + const start = new Date(now); + start.setDate(now.getDate() - 7); + params.startDate = start.toISOString(); + } else if (dateFilterType === '30days') { + const start = new Date(now); + start.setDate(now.getDate() - 30); + params.startDate = start.toISOString(); + } else if (dateFilterType === 'custom') { + if (startDate) params.startDate = startDate; + if (endDate) params.endDate = endDate; + } + + if (roleFilter !== 'ALL') { + params.role = roleFilter; + } + + const res = await api.get('/admin/reports', { params }); + if (res.data?.success) { + setReportData(res.data.data); + } + } catch (err) { + console.error('Failed to fetch reports:', err); + } finally { + setIsLoading(false); + } + }, [dateFilterType, startDate, endDate, roleFilter]); + + useEffect(() => { + fetchReports(); + }, [fetchReports]); + + if (isLoading && !reportData) { return
; } - if (!reportData) return
اطلاعاتی یافت نشد
; - - const { overview, salesTimeline, bestSellers, categoryDistribution, topCoupons } = reportData; + const { overview, salesTimeline, bestSellers, categoryDistribution, topCoupons } = reportData || {}; return ( -
-
+
+ {/* Header */} +

- گزارشات و تحلیل‌ها + گزارشات و تحلیل‌های مالی پیشرفته

-

آمار فروش، رفتار کاربران و عملکرد تخفیف‌ها

+

+ تفکیک درآمد کالاها، مبالغ واریزی مهربانی، مالیات، تخفیف‌ها و سود خالص +

+ {/* Filter Toolbar */} +
+
+ + بازه زمانی: +
- {/* Overview Cards */} -
-
-
-

درآمد کل

-
-
-

{Number(overview.totalRevenue).toLocaleString()}

-

تومان

+
+ + + + +
- -
-
-

سهم خیریه (مسئولیت اجتماعی)

-
+ + {dateFilterType === 'custom' && ( +
+ setStartDate(e.target.value)} + className="px-3 py-1.5 rounded-xl border border-gray-200 outline-none text-xs" + /> + تا + setEndDate(e.target.value)} + className="px-3 py-1.5 rounded-xl border border-gray-200 outline-none text-xs" + />
-

{Number(overview.totalCharity).toLocaleString()}

-

تومان

-
- -
-
-

فروش همکاران (B2B)

-
-
-

{Number(overview.b2bRevenue).toLocaleString()}

-

تومان

-
- -
-
-

تعداد سفارشات

-
-
-

{overview.totalOrders}

-

موفق

+ )} + +
+ نوع مشتری: +
+ {/* Overview Cards (Core Revenue & Charity) */} +
+ {/* Total Deposited */} +
+
+

مجموع کل واریزی‌ها

+
+
+

{Number(overview?.totalRevenue || 0).toLocaleString()}

+

تومان (شامل کالا و مهربانی)

+
+ + {/* Charity Share */} +
+
+

سهم مبالغ مهربانی (خیریه)

+
+
+

{Number(overview?.totalCharity || 0).toLocaleString()}

+

+ {overview?.totalRevenue ? `${((Number(overview.totalCharity || 0) / Number(overview.totalRevenue)) * 100).toFixed(1)}% از کل دریافتی` : 'تومان'} +

+
+ + {/* Product Sales Only */} +
+
+

خالص فروش محصولات

+
+
+

{Number(overview?.productSalesRevenue || (Number(overview?.totalRevenue || 0) - Number(overview?.totalCharity || 0))).toLocaleString()}

+

تومان (بدون احتساب مبالغ خیریه)

+
+ + {/* Net Profit */} +
+
+

سود خالص تخمینی

+
+
+

{Number(overview?.netProfit || 0).toLocaleString()}

+

تومان (پس از کسر خرید، مالیات و تخفیف)

+
+
+ + {/* Financial Breakdown Section (Item 2 Requirement) */} +
+

+ + تراز و تفکیک اجزای مالی +

+
+
+ بهای خرید کالاها (COGS) + {Number(overview?.totalCostOfGoods || 0).toLocaleString()} + تومان +
+
+ مالیات بر ارزش افزوده + {Number(overview?.totalTax || 0).toLocaleString()} + تومان +
+
+ مجموع تخفیف‌ها + {Number(overview?.totalDiscounts || 0).toLocaleString()} + تومان +
+
+ فروش عمده (B2B) + {Number(overview?.b2bRevenue || 0).toLocaleString()} + تومان +
+
+ فروش تکی (B2C) + {Number(overview?.b2cRevenue || 0).toLocaleString()} + تومان +
+
+ تعداد کل سفارشات + {overview?.totalOrders || 0} + سفارش موفق +
+
+
+ + {/* Charts Section */}
- {/* Sales Chart */} -
-

نمودار فروش روزانه

+ {/* Timeline Chart with Dual Area for Total & Charity */} +
+
+

روند فروش و واریزی‌های مهربانی

+
+ + + کل واریزی + + + + سهم مهربانی + +
+
@@ -183,20 +376,25 @@ export default function Reports() { + + + + - - `${val / 1000}k`} /> + + `${val >= 1000000 ? `${(val/1000000).toFixed(1)}M` : `${val / 1000}k`}`} /> } /> - + +
{/* Category Distribution */} -
-

سهم فروش دسته‌بندی‌ها

+
+

سهم فروش دسته‌بندی‌ها

{categoryDistribution && categoryDistribution.length > 0 ? ( @@ -211,19 +409,20 @@ export default function Reports() { ) : ( -
داده‌ای موجود نیست
+
داده‌ای موجود نیست
)}
+ {/* Best Sellers & Top Coupons */}
{/* Best Sellers */} -
-

پرفروش‌ترین محصولات

-
+
+

پرفروش‌ترین محصولات در بازه انتخابی

+
{bestSellers && bestSellers.length > 0 ? ( - + ) : ( -
داده‌ای موجود نیست
+
داده‌ای موجود نیست
)}
{/* Top Coupons */} -
-

پرکاربردترین کدهای تخفیف

-
+
+

کدهای تخفیف پراستفاده

+
{topCoupons && topCoupons.length > 0 ? ( topCoupons.map((coupon: TopCouponItem, idx: number) => ( -
+
-
+
#{idx + 1}
- {coupon.name} + {coupon.name}
-
- {coupon.usedCount ?? coupon.value ?? 0}بار استفاده +
+ {coupon.usedCount ?? coupon.value ?? 0}بار استفاده
)) ) : ( -
داده‌ای موجود نیست
+
داده‌ای موجود نیست
)}
diff --git a/frontend/application/app/ClientLayout.tsx b/frontend/application/app/ClientLayout.tsx index 51cf215..85ee5a9 100644 --- a/frontend/application/app/ClientLayout.tsx +++ b/frontend/application/app/ClientLayout.tsx @@ -50,8 +50,36 @@ export default function ClientLayout({ children }: { children: React.ReactNode } } else { useUserStore.getState().logout(); } + + // Preserve scroll restoration on back/forward + if (typeof window !== 'undefined' && 'scrollRestoration' in window.history) { + window.history.scrollRestoration = 'auto'; + } }, [fetchProfile, fetchSettings]); + // Handle scroll position tracking per pathname for reliable mobile back navigation + useEffect(() => { + if (typeof window === 'undefined') return; + + const key = `scroll_pos_${pathname}`; + const savedPos = sessionStorage.getItem(key); + if (savedPos !== null) { + const targetY = parseInt(savedPos, 10); + requestAnimationFrame(() => { + window.scrollTo({ top: targetY, behavior: 'instant' }); + }); + } + + const handleScroll = () => { + sessionStorage.setItem(`scroll_pos_${pathname}`, window.scrollY.toString()); + }; + + window.addEventListener('scroll', handleScroll, { passive: true }); + return () => { + window.removeEventListener('scroll', handleScroll); + }; + }, [pathname]); + // Check Maintenance Mode (Admin / Partner bypass) const isMaintenanceMode = getText('MAINTENANCE_MODE', 'false') === 'true' || getText('maintenance_mode', 'false') === 'true'; const isAdmin = role === 'User_Partner' || (typeof window !== 'undefined' && Boolean(localStorage.getItem('adminToken'))); diff --git a/frontend/application/components/ArchivePage.tsx b/frontend/application/components/ArchivePage.tsx index 64983a7..cd0d154 100644 --- a/frontend/application/components/ArchivePage.tsx +++ b/frontend/application/components/ArchivePage.tsx @@ -297,49 +297,40 @@ export default function ArchivePage({ return labels; }, [categories]); - // Sync state with prop change (e.g. from Header menu) + const lastInitialPropsRef = React.useRef({ initialCategory, initialSearch, initialPetType, initialSymptoms }); + + // Sync state when incoming initial props change from navigation useEffect(() => { - // Normalize Input (Trim and lowercase) - const normSearch = (initialSearch || "").trim().toLowerCase(); - const normCat = initialCategory.trim().toLowerCase(); + const isNewCategory = lastInitialPropsRef.current.initialCategory !== initialCategory; + const isNewSearch = lastInitialPropsRef.current.initialSearch !== initialSearch; + const isNewPet = lastInitialPropsRef.current.initialPetType !== initialPetType; + const isNewSymptoms = lastInitialPropsRef.current.initialSymptoms !== initialSymptoms; - const mapKey = normSearch || normCat; - const mapping = CATEGORY_MAP[mapKey]; - - if (selectedCategory === initialCategory && searchQuery === initialSearch) { + if (!isNewCategory && !isNewSearch && !isNewPet && !isNewSymptoms) { return; } - Promise.resolve().then(() => { - setIsUpdating(true); + lastInitialPropsRef.current = { initialCategory, initialSearch, initialPetType, initialSymptoms }; - // Reset ALL other filters when a new category/solution is selected from menu + // Normalize Input + const normSearch = (initialSearch || "").trim().toLowerCase(); + const normCat = (initialCategory || "").trim().toLowerCase(); + const mapKey = normSearch || normCat; + const mapping = CATEGORY_MAP[mapKey]; + + setIsUpdating(true); + if (mapping) { + setSelectedCategory(mapping.category || "all"); + setSearchQuery(mapping.query || ""); setSelectedPet("all"); - setActiveSymptoms([]); - setSearchQuery(""); - - if (mapping) { - if (mapping.category) setSelectedCategory(mapping.category); - if (mapping.query) setSearchQuery(mapping.query); - if (mapping.symptoms) { - setActiveSymptoms(mapping.symptoms); - } - } else { - setSelectedCategory(initialCategory); - setSearchQuery(initialSearch); - - if (initialSearch && symptoms.includes(initialSearch)) { - setActiveSymptoms([initialSearch]); - } - } - }); - - const timer = setTimeout(() => { - window.scrollTo({ top: 0, behavior: "smooth" }); - }, 400); - - return () => clearTimeout(timer); - }, [initialCategory, initialSearch, selectedCategory, searchQuery, symptoms]); + setActiveSymptoms(mapping.symptoms || []); + } else { + setSelectedCategory(initialCategory || "all"); + setSearchQuery(initialSearch || ""); + setSelectedPet((initialPetType as PetType) || "all"); + setActiveSymptoms(initialSymptoms ? initialSymptoms.split(',').filter(Boolean) : []); + } + }, [initialCategory, initialSearch, initialPetType, initialSymptoms]); // Update URL Query Parameters useEffect(() => { diff --git a/frontend/application/components/AuthModal.tsx b/frontend/application/components/AuthModal.tsx index 135305b..233d8fc 100644 --- a/frontend/application/components/AuthModal.tsx +++ b/frontend/application/components/AuthModal.tsx @@ -369,23 +369,23 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { return ( {isOpen && ( -
+
{/* Header */} -
+
ورود امن به کنینا @@ -425,6 +425,8 @@ export default function AuthModal({ isOpen, onClose }: AuthModalProps) { {featuredPost.readingTime || 4} دقیقه مطالعه
+
+ + {(featuredPost.viewCount ?? 0).toLocaleString('fa-IR')} بازدید +

{featuredPost.title} @@ -324,6 +328,11 @@ export default function BlogPage({ {post.category} )} + {/* View Count Badge */} + + + {(post.viewCount ?? 0).toLocaleString('fa-IR')} +

@@ -331,9 +340,15 @@ export default function BlogPage({ {post.date}
-
- - {post.readingTime || 3} دقیقه +
+
+ + {post.readingTime || 3} دقیقه +
+
+ + {(post.viewCount ?? 0).toLocaleString('fa-IR')} +

diff --git a/frontend/application/components/FeaturedProducts.tsx b/frontend/application/components/FeaturedProducts.tsx index b24c83e..1b6e29d 100644 --- a/frontend/application/components/FeaturedProducts.tsx +++ b/frontend/application/components/FeaturedProducts.tsx @@ -43,21 +43,22 @@ function ProductCard({ product }: { product: Product }) { return ( router.push(`/shop/${product.slug || product.id}`)} + className="group bg-white/90 backdrop-blur-md rounded-2xl sm:rounded-3xl border border-medical-gray-200/80 overflow-hidden hover:shadow-xl hover:shadow-canina-blue/10 hover:border-canina-blue/30 transition-all duration-300 flex flex-col cursor-pointer relative" > {/* Category Badge */} -
+
{product.category}
{/* Compatibility Tag */} {compatibility && ( -
@@ -66,22 +67,20 @@ function ProductCard({ product }: { product: Product }) {
)} - {/* Image */} -
+ {/* Image Container */} +
- {/* PDP Link overlay */} - - {/* Hover Actions */} -
+ {/* Hover Actions for desktop */} +
{ e.stopPropagation(); router.push(`/shop/${product.slug || product.id}`); }} - className="w-11 h-11 rounded-full bg-white text-canina-blue flex items-center justify-center hover:scale-110 transition-transform shadow-lg cursor-pointer" + className="w-10 h-10 rounded-full bg-white text-canina-blue flex items-center justify-center hover:scale-110 transition-transform shadow-lg cursor-pointer" title="مشاهده جزئیات" > @@ -93,7 +92,7 @@ function ProductCard({ product }: { product: Product }) { addItem(product, 1); toast.success(`${nameFa} به سبد خرید اضافه شد.`); }} - className="w-11 h-11 rounded-full bg-canina-blue text-white flex items-center justify-center hover:scale-110 transition-transform shadow-lg border border-white/20 cursor-pointer" + className="w-10 h-10 rounded-full bg-canina-blue text-white flex items-center justify-center hover:scale-110 transition-transform shadow-lg border border-white/20 cursor-pointer" title="افزودن به سبد خرید" > @@ -103,40 +102,40 @@ function ProductCard({ product }: { product: Product }) {
{/* Content */} - +
{/* Dual-language Name */} -
-

+
+

{nameFa}

{nameEn && ( - + {nameEn} )}
{/* Short description */} -

+

{product.shortDescription || product.description || ''}

{/* Footer: Price + CTA */} -
-
+
+
{showPrices ? (typeof product.price === 'string' || typeof product.price === 'number' ? String(product.price) : 'تماس بگیرید') : 'تماس بگیرید'}
{showPreorderBtn ? ( - + ثبت پیش‌خرید ) : ( - + {getText("product_view_details", "مشاهده جزئیات")} )}
- +
); } diff --git a/frontend/application/components/Header.tsx b/frontend/application/components/Header.tsx index 9072a1d..737867d 100644 --- a/frontend/application/components/Header.tsx +++ b/frontend/application/components/Header.tsx @@ -554,37 +554,155 @@ export default function Header({ initial={{ opacity: 0, height: 0 }} animate={{ opacity: 1, height: "auto" }} exit={{ opacity: 0, height: 0 }} - className="lg:hidden relative z-50 bg-white border-b border-medical-gray-200 px-4 py-6 space-y-4 shadow-xl" + className="lg:hidden relative z-50 bg-white border-b border-medical-gray-200 px-4 py-5 space-y-4 shadow-xl max-h-[85vh] overflow-y-auto font-vazir text-right" + dir="rtl" > -
+ setSearchQuery(e.target.value)} - className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-3 pr-10 pl-4 text-xs font-bold" + className="w-full bg-medical-gray-50 border border-medical-gray-200 rounded-xl py-2.5 pr-10 pl-4 text-xs font-bold focus:ring-2 focus:ring-canina-blue/20 outline-none" /> -
- setIsMobileMenuOpen(false)} className="block p-3 rounded-xl bg-medical-gray-50 text-medical-gray-900"> - 🛒 محصولات تخصصی کنینا + {/* Section: Shop & Catalog */} +
+
+ فروشگاه و محصولات +
+ setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl bg-medical-gray-50 text-medical-gray-900 font-bold text-xs hover:bg-canina-blue/10 hover:text-canina-blue transition-all" + > + + فروشگاه تخصصی محصولات کنینا - setIsMobileMenuOpen(false)} className="block p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800"> - 📑 کاتالوگ آنلاین و دوز مصرفی + setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > + + کاتالوگ دیجیتال و راهنمای بالینی - setIsMobileMenuOpen(false)} className="block p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800"> - 🧬 دانشنامه علمی +
+ + {/* Section: Categories */} +
+
+ دسته‌بندی‌های درمانی +
+
+ {menuItems.map((item, idx) => ( + setIsMobileMenuOpen(false)} + className="flex items-center gap-2 p-2.5 rounded-xl border border-medical-gray-100 bg-white hover:border-canina-blue/30 text-medical-gray-800 text-[11px] font-bold" + > + {item.icon} + {item.title} + + ))} +
+
+ + {/* Section: Academy & Science */} +
+
+ دانشنامه و آموزش +
+ setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > + + دانشنامه علمی کنینا - setIsMobileMenuOpen(false)} className="block p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800"> - 📰 مجله سلامت پت + setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > + + مجله سلامت پت (وبلاگ) - setIsMobileMenuOpen(false)} className="block p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800"> - 🎥 آکادمی ویدئویی و مشاوره دامپزشک + setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > +
+ + {/* Section: Tools & Profile */} +
+
+ ابزارهای هوشمند +
+ setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > + + شناسنامه و سوابق سلامت پت + + setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > + + پایش هوشمند مصرف مکمل‌ها + +
+ + {/* Section: Company & Contact */} +
+
+ اطلاعات و تماس +
+ setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > + + درباره کمپانی کنینا آلمان + + setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-emerald-600 font-bold text-xs transition-all" + > + + نمادهای اعتماد و مجوزهای رسمی + + {isB2BEnabled && ( + setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > + + درخواست نمایندگی و خرید عمده (B2B) + + )} + setIsMobileMenuOpen(false)} + className="flex items-center gap-3 p-3 rounded-xl hover:bg-medical-gray-50 text-medical-gray-800 font-bold text-xs transition-all" + > + + تماس با مرکز پشتیبانی
diff --git a/frontend/application/components/SmartAdvisor.tsx b/frontend/application/components/SmartAdvisor.tsx index f26bca3..ac73492 100644 --- a/frontend/application/components/SmartAdvisor.tsx +++ b/frontend/application/components/SmartAdvisor.tsx @@ -301,14 +301,14 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
-
+
{/* Progress Indicator */} -
+
@@ -319,33 +319,35 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} - className="space-y-8" + className="space-y-6" >
-

{getText('advisor_step1_title', "گام اول: هویت بصری")}

-

{getText('advisor_step1_desc', "همدم شما رو با چه اسمی صدا می‌زنید؟")}

+

{getText('advisor_step1_title', "گام اول: مشخصات پت")}

+

{getText('advisor_step1_desc', "همدم شما رو با چه اسمی صدا می‌زنید؟")}

-
+
-
-
- +
+
+
-
- +
+
- + + {getText('advisor_submit_btn', "ثبت هویت و ادامه")} + + + {showError && (!name.trim() || !breed.trim()) && ( +

+ {getText('advisor_validation_warning', "⚠️ لطفاً ابتدا نام و نژاد پت را وارد کنید.")} +

)} - > - {getText('advisor_submit_btn', "ثبت هویت و ادامه")} - -
- {showError && (!name.trim() || !breed.trim()) && ( -

- {getText('advisor_validation_warning', "⚠️ لطفاً ابتدا نام و نژاد پت را در بالا وارد کنید.")} -

- )} +
)} @@ -408,60 +413,95 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} - className="space-y-8" + className="space-y-6 max-w-xl mx-auto" >
-

{getText('advisor_step2_title', "گام دوم: پایش فیزیکی")}

-

{getText('advisor_step2_desc', "اطلاعات فیزیکی دقیق به دوزبندی صحیح مکمل‌ها کمک می‌کند")}

+

{getText('advisor_step2_title', "گام دوم: پایش فیزیکی")}

+

{getText('advisor_step2_desc', "اطلاعات فیزیکی به دوزبندی دقیق مکمل‌ها کمک می‌کند")}

-
-
- -
- -
+
+ {/* Age Input Box */} +
+ +
+ +
setAge(Math.max(0, parseInt(e.target.value) || 0))} - className="w-12 text-center text-xl font-black text-canina-blue font-vazir outline-none bg-transparent" + type="text" + inputMode="numeric" + pattern="[0-9]*" + value={age === 0 ? '' : age.toString()} + onChange={(e) => { + const val = e.target.value.replace(/[^0-9]/g, ''); + setAge(val === '' ? 0 : Math.min(30, parseInt(val, 10))); + }} + className="w-14 text-center text-lg font-black text-canina-blue font-mono outline-none bg-transparent" /> - سال + سال
- +
-
- -
- -
+ + {/* Weight Input Box */} +
+ +
+ +
setWeight(Math.max(1, parseInt(e.target.value) || 1))} - className="w-16 text-center text-xl font-black text-canina-blue font-vazir outline-none bg-transparent" + type="text" + inputMode="numeric" + pattern="[0-9]*" + value={weight === 0 ? '' : weight.toString()} + onChange={(e) => { + const val = e.target.value.replace(/[^0-9]/g, ''); + setWeight(val === '' ? 0 : Math.min(120, parseInt(val, 10))); + }} + className="w-14 text-center text-lg font-black text-canina-blue font-mono outline-none bg-transparent" /> - kg + kg
- +
-
- -
+ {/* Activity Level */} +
+ +
{["کم", "متوسط", "زیاد"].map(level => ( @@ -469,20 +509,23 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro
-
+ {/* Buttons (matching Hero layout) */} +
- +
@@ -494,42 +537,45 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} - className="space-y-8" + className="space-y-5 max-w-2xl mx-auto" >
-

{getText('advisor_step3_symptoms_title', "گام سوم: علائم بالینی فعلی")}

-

{getText('advisor_step3_symptoms_desc', "آیا همدم شما در حال حاضر هیچ‌کدام از علائم زیر را تجربه می‌کند؟")}

+

{getText('advisor_step3_symptoms_title', "گام سوم: علائم بالینی فعلی")}

+

{getText('advisor_step3_symptoms_desc', "آیا همدم شما در حال حاضر علائم زیر را دارد؟")}

-
+
{CURRENT_SYMPTOMS.map((opt) => ( ))}
-
+
@@ -541,42 +587,45 @@ export default function SmartAdvisor({ onComplete, rules = [] }: SmartAdvisorPro initial={{ opacity: 0, x: 20 }} animate={{ opacity: 1, x: 0 }} exit={{ opacity: 0, x: -20 }} - className="space-y-8" + className="space-y-5 max-w-2xl mx-auto" >
-

{getText('advisor_step4_history_title', "گام چهارم: سوابق پزشکی و جراحی")}

-

{getText('advisor_step4_history_desc', "در صورت وجود سابقه جراحی، بارداری یا نارسایی، آن را مشخص کنید")}

+

{getText('advisor_step4_history_title', "گام چهارم: سوابق پزشکی")}

+

{getText('advisor_step4_history_desc', "در صورت وجود سابقه جراحی، بارداری یا نارسایی، آن را مشخص کنید")}

-
+
{MEDICAL_HISTORIES.map((opt) => ( ))}
-
+
diff --git a/frontend/application/components/VideoModalPlayer.tsx b/frontend/application/components/VideoModalPlayer.tsx index 108dbe1..3be040c 100644 --- a/frontend/application/components/VideoModalPlayer.tsx +++ b/frontend/application/components/VideoModalPlayer.tsx @@ -71,20 +71,37 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP }; }, [showRateMenu]); - // Reset states on open/close + // Reset / Restore states on open/close useEffect(() => { - if (isOpen) { + if (isOpen && video?.id) { + const savedTime = localStorage.getItem(`video_time_${video.id}`); + const initialTime = savedTime ? parseFloat(savedTime) : 0; + setIsPlaying(true); setShowControls(true); setIsExpandedDesc(false); setShowRateMenu(false); setBufferedEnd(0); - } else { + + if (videoRef.current && initialTime > 0) { + videoRef.current.currentTime = initialTime; + setCurrentTime(initialTime); + } + } else if (!isOpen && video?.id) { + if (currentTime > 0) { + localStorage.setItem(`video_time_${video.id}`, currentTime.toString()); + } setIsPlaying(false); - setCurrentTime(0); setBufferedEnd(0); } - }, [isOpen, video]); + }, [isOpen, video?.id]); + + // Save playback time periodically + useEffect(() => { + if (video?.id && currentTime > 0) { + localStorage.setItem(`video_time_${video.id}`, currentTime.toString()); + } + }, [currentTime, video?.id]); // Update buffered track const updateBuffer = useCallback(() => { @@ -254,164 +271,147 @@ export default function VideoModalPlayer({ isOpen, onClose, video }: VideoModalP return ( -
- {/* Backdrop */} +
+ {/* Backdrop (backdrop dismiss disabled to prevent accidental closure) */} - {/* Modal Window */} - - {/* Iframe Support (Aparat / Youtube) */} - {isIframe ? ( -
- -
-
- ) : ( - <> - {/* Native Video Element */} -
-