Compare commits
214
Commits
c9f230a76f
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6696cc1027 | ||
|
|
98cb5ab09b | ||
|
|
c501c2bbbf | ||
|
|
a74ea966ad | ||
|
|
0a7d29419f | ||
|
|
a9599d8a1f | ||
|
|
cbc1311e9d | ||
|
|
587d7ceead | ||
|
|
ac8ef8caa2 | ||
|
|
7d0fb265d4 | ||
|
|
743db57bb3 | ||
|
|
d7d2e36573 | ||
|
|
8b157b8e87 | ||
|
|
0935e8cbc7 | ||
|
|
f04ce348ae | ||
|
|
508e0d182c | ||
|
|
dbd5d43d65 | ||
|
|
7d8a725e34 | ||
|
|
491adb4090 | ||
|
|
1a3f5277d9 | ||
|
|
ffcd5fc535 | ||
|
|
65e0c2ed62 | ||
|
|
36b4a3b481 | ||
|
|
e0455b200a | ||
|
|
348a5398d0 | ||
|
|
af87bfc7b6 | ||
|
|
beefe86ffd | ||
|
|
3a7c609588 | ||
|
|
aeeee04166 | ||
|
|
288d6782a4 | ||
|
|
55915af9df | ||
|
|
bfc457470c | ||
|
|
210145f5ca | ||
|
|
d0793a281e | ||
|
|
6994edd216 | ||
|
|
3a0ae93846 | ||
|
|
13a5db6313 | ||
|
|
9c011b6416 | ||
|
|
294e16e1a6 | ||
|
|
71f7120239 | ||
|
|
0e28986dc7 | ||
|
|
51a3ed7d18 | ||
|
|
c15867a41c | ||
|
|
c4e4d775f5 | ||
|
|
cd07502128 | ||
|
|
1ef2cc227a | ||
|
|
7226285746 | ||
|
|
dd93cd302c | ||
|
|
37b7ccb52e | ||
|
|
b8a76bd6a9 | ||
|
|
07e1237f05 | ||
|
|
29546740c7 | ||
|
|
42cc96a28a | ||
|
|
84b07af246 | ||
|
|
8a72dbaf68 | ||
|
|
af6b997ceb | ||
|
|
e97dd392c0 | ||
|
|
ae0526a58a | ||
|
|
d419c1c2cb | ||
|
|
48e03b4237 | ||
|
|
2fad519b7e | ||
|
|
84b73b912f | ||
|
|
a3cbaaa2e7 | ||
|
|
881f3295c0 | ||
|
|
0cc9230672 | ||
|
|
010a8f3a5a | ||
|
|
4952e0daa1 | ||
|
|
455b39bb1e | ||
|
|
34772bd3b4 | ||
|
|
2296044280 | ||
|
|
2d201bfcde | ||
|
|
17931191f5 | ||
|
|
4e8289e976 | ||
|
|
41c56e026f | ||
|
|
1e8bf782ca | ||
|
|
e4eedf464b | ||
|
|
c274cb134d | ||
|
|
c0fe6cce48 | ||
|
|
9e89a37b4d | ||
|
|
a704e410c5 | ||
|
|
d24884f7e6 | ||
|
|
2cada9ba27 | ||
|
|
a7c203d7a4 | ||
|
|
abc07a5e2f | ||
|
|
904c615665 | ||
|
|
e9af59769d | ||
|
|
e933d64790 | ||
|
|
c07fe860e5 | ||
|
|
1b6dd3c40b | ||
|
|
7c82f940fc | ||
|
|
a69636d7e3 | ||
|
|
83f535c2e9 | ||
|
|
47bf0b3e77 | ||
|
|
e09cd3a77c | ||
|
|
5d521358f7 | ||
|
|
cfb2f42e96 | ||
|
|
cbbfb1c6ad | ||
|
|
86263bd235 | ||
|
|
fc9dc836e7 | ||
|
|
a508ede373 | ||
|
|
fd4da24b1d | ||
|
|
2f0acfc07f | ||
|
|
f8cf3618f7 | ||
|
|
0ad1ce98ad | ||
|
|
04c27a9b7f | ||
|
|
12fb7104a1 | ||
|
|
f5548f39da | ||
|
|
b16d0949a4 | ||
|
|
5dfc3573e5 | ||
|
|
7800499c72 | ||
|
|
045eef24c0 | ||
|
|
3a8a1f9c08 | ||
|
|
f55f4cedfe | ||
|
|
85b77aeb91 | ||
|
|
1b4ad64ef6 | ||
|
|
c7939b8923 | ||
|
|
a155a90fba | ||
|
|
31d9307fe1 | ||
|
|
86f56b4735 | ||
|
|
1f44211088 | ||
|
|
fcc1f3bec9 | ||
|
|
cbf30604af | ||
|
|
839dd8cf94 | ||
|
|
365aeded43 | ||
|
|
ad247fb269 | ||
|
|
1e0267c03d | ||
|
|
47a16c0c2b | ||
|
|
6dcf3de29e | ||
|
|
8974d00078 | ||
|
|
d0263e7252 | ||
|
|
0d2cb18745 | ||
|
|
00084d769a | ||
|
|
c1271d9c34 | ||
|
|
4d957ea271 | ||
|
|
9b4fb796f9 | ||
|
|
c4aefdb8b9 | ||
|
|
35b7f82ad9 | ||
|
|
25ad9b0350 | ||
|
|
f22ea522b8 | ||
|
|
9f317d9878 | ||
|
|
aaa46e7d3a | ||
|
|
11692c8537 | ||
|
|
d0901961be | ||
|
|
d8d1eec3ed | ||
|
|
0182616050 | ||
|
|
8da7056cd6 | ||
|
|
50d2497288 | ||
|
|
02b378612b | ||
|
|
90903a67ac | ||
|
|
a551893322 | ||
|
|
ea9d57cc65 | ||
|
|
e141545035 | ||
|
|
62cd597994 | ||
|
|
1eff2d2c0a | ||
|
|
2af7c12cf6 | ||
|
|
3a6034a460 | ||
|
|
9c9bf201c4 | ||
|
|
cb8389bb4a | ||
|
|
77ad126530 | ||
|
|
e152013e2a | ||
|
|
0c3ccb2806 | ||
|
|
eb2615c303 | ||
|
|
29b6120f80 | ||
|
|
90315a112a | ||
|
|
50f45a133b | ||
|
|
c57685065a | ||
|
|
8048c0ff0e | ||
|
|
10c2ca5bc3 | ||
|
|
f688aa210f | ||
|
|
b377c11c41 | ||
|
|
1fe49c24e8 | ||
|
|
27ca7ddf8e | ||
|
|
b6c5278282 | ||
|
|
6cf7bd766d | ||
|
|
e75fcd7bff | ||
|
|
eb14c5cb21 | ||
|
|
0097f768ef | ||
|
|
d6dee3361e | ||
|
|
9b83ba3176 | ||
|
|
2878f546ee | ||
|
|
e71b65d593 | ||
|
|
1d5925e178 | ||
|
|
45c5137565 | ||
|
|
3544b2af81 | ||
|
|
ea003ea73c | ||
|
|
dd483cb628 | ||
|
|
b89e5b4c6b | ||
|
|
a09ce94da1 | ||
|
|
1256334805 | ||
|
|
5ff450c3bc | ||
|
|
8bb6097d90 | ||
|
|
89506c222b | ||
|
|
0887ccde13 | ||
|
|
d68a35903c | ||
|
|
b05661e120 | ||
|
|
d40e3d563b | ||
|
|
f31713a929 | ||
|
|
f4b0a63e23 | ||
|
|
884fb8dc50 | ||
|
|
4abcc9689c | ||
|
|
154e9cc5c1 | ||
|
|
534e18bbd4 | ||
|
|
0df34bdb1c | ||
|
|
11d65410e6 | ||
|
|
f4a0222e0e | ||
|
|
022943d2af | ||
|
|
3a84394847 | ||
|
|
70717f88fb | ||
|
|
95319e51ff | ||
|
|
6a1a3adce5 | ||
|
|
aff0b7a080 | ||
|
|
9128c0e298 | ||
|
|
6453e8402e | ||
|
|
7ae0fecb61 |
Generated
+265
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.58.0",
|
||||
"version": "0.104.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.58.0",
|
||||
"version": "0.104.0",
|
||||
"dependencies": {
|
||||
"@a2seven/yoo-checkout": "^1.1.4",
|
||||
"@react-input/mask": "^2.0.4",
|
||||
@@ -21,6 +21,7 @@
|
||||
"next": "^16.0.7",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"next-intl": "^4.5.8",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-slick": "^0.31.0",
|
||||
@@ -1129,6 +1130,256 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz",
|
||||
"integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-android-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.100"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-android-arm64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz",
|
||||
"integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-arm64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz",
|
||||
"integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-darwin-x64": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz",
|
||||
"integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz",
|
||||
"integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-arm64-musl": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz",
|
||||
"integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-gnu": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz",
|
||||
"integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-linux-x64-musl": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz",
|
||||
"integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-arm64-msvc": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz",
|
||||
"integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/canvas-win32-x64-msvc": {
|
||||
"version": "0.1.100",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz",
|
||||
"integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@@ -6573,6 +6824,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pdfjs-dist": {
|
||||
"version": "5.7.284",
|
||||
"resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-5.7.284.tgz",
|
||||
"integrity": "sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=22.13.0 || >=24"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas": "^0.1.100"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
|
||||
+2
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "no-copy-frontend",
|
||||
"version": "0.71.0",
|
||||
"version": "0.122.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev -p 2999",
|
||||
@@ -22,6 +22,7 @@
|
||||
"next": "^16.0.7",
|
||||
"next-auth": "5.0.0-beta.30",
|
||||
"next-intl": "^4.5.8",
|
||||
"pdfjs-dist": "^5.7.284",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-slick": "^0.31.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 2.5 KiB |
@@ -1,9 +1,11 @@
|
||||
import Providers from '@/app/providers/getQueryServer'
|
||||
import type { Metadata } from "next";
|
||||
import { NextIntlClientProvider, hasLocale } from 'next-intl';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { routing } from '@/i18n/routing';
|
||||
import { ToastProvider } from '@/app/providers/ToastProvider';
|
||||
import Providers from '@/app/providers/getQueryServer';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
import "../styles/globals.css";
|
||||
import "../styles/global-styles.scss";
|
||||
|
||||
@@ -29,9 +31,9 @@ export default async function RootLayout({
|
||||
}
|
||||
|
||||
return (
|
||||
<html lang={locale}>
|
||||
<html>
|
||||
<body>
|
||||
<NextIntlClientProvider>
|
||||
<NextIntlClientProvider locale={locale}>
|
||||
<Providers>
|
||||
{children}
|
||||
<ToastProvider />
|
||||
|
||||
@@ -10,7 +10,6 @@ import DashboardFilesInfo from '@/app/ui/dashboard/new/dashboard-files-info';
|
||||
import DahboardLimitsSection from '@/app/ui/dashboard/new/dashborad-limits-section';
|
||||
import DahboardGeographySection from '@/app/ui/dashboard/new/dashboard-geography-section';
|
||||
import DashboardLastCheck from '@/app/ui/dashboard/new/dashboard-last-check';
|
||||
import ViolationsCarousel from '@/app/ui/dashboard/new/violations-carousel';
|
||||
import DashboardUserViolations from '@/app/ui/dashboard/new/dashboard-user-violations';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
|
||||
@@ -6,9 +6,8 @@ import FilePageViolationInfo from '@/app/ui/file-page/file-page-violation-info';
|
||||
import ViolationPpageFileStatistic from '@/app/ui/file-page/file-page-file-statistic';
|
||||
import FilePageFileInfo from '@/app/ui/file-page/file-page-file-info';
|
||||
import FilePageActions from '@/app/ui/file-page/file-page-actions';
|
||||
import FilePageViolationsList from '@/app/ui/file-page/file-page-violations-list';
|
||||
import FilePageViolationsList from '@/app/ui/file-page/violation-table/file-page-violations-list';
|
||||
import FilePageNote from '@/app/ui/file-page/file-page-note';
|
||||
import { getFileViolations } from '@/app/actions/violationActions';
|
||||
import { viewFileInfo } from '@/app/actions/fileEntity';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import FilePageNotFound from '@/app/ui/file-page/file-page-file-not-found';
|
||||
@@ -49,20 +48,18 @@ export interface FileDetails {
|
||||
|
||||
interface PageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
searchParams: Promise<{ page?: string }>; // Добавляем searchParams
|
||||
searchParams: Promise<{ page?: string, status?: string, violationId?: string, caseType?: 'complaint' | 'claim', startDate: string, endDate: string }>;
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
searchParams
|
||||
}: PageProps) {
|
||||
/* const t = useTranslations('Global'); */
|
||||
const { id } = await params;
|
||||
const { page } = await searchParams;
|
||||
const { page, status, violationId, caseType, startDate, endDate } = await searchParams;
|
||||
const currentPage = Number(page) || 1;
|
||||
|
||||
try {
|
||||
const fileViolations = await getFileViolations(id, currentPage);
|
||||
const fileInfo: FileDetails = await viewFileInfo(id);
|
||||
|
||||
return (
|
||||
@@ -74,15 +71,19 @@ export default async function Page({
|
||||
>
|
||||
<FilePageFileInfo fileInfo={fileInfo} />
|
||||
{/* <FilePageActions /> */}
|
||||
<ViolationPpageFileStatistic />
|
||||
<ViolationPpageFileStatistic id={id} />
|
||||
</div>
|
||||
<div>
|
||||
<FilePageViolationsList
|
||||
fileViolations={fileViolations}
|
||||
currentPage={currentPage}
|
||||
status={status}
|
||||
fileId={id}
|
||||
violationId={violationId}
|
||||
caseType={caseType}
|
||||
startDate={startDate}
|
||||
endDate={endDate}
|
||||
/>
|
||||
<FilePageNote />
|
||||
{/* <FilePageNote /> */}
|
||||
{/* <FilePageViolationInfo /> */}
|
||||
</div>
|
||||
</div>
|
||||
@@ -93,6 +94,5 @@ export default async function Page({
|
||||
return (
|
||||
<FilePageNotFound />
|
||||
)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import { NotificationsList } from '@/app/ui/notifications/notifications-list';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div>
|
||||
<PageTitle title='notifications' />
|
||||
|
||||
<NotificationsList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import UserUsage from '@/app/ui/payment/user-usage';
|
||||
import TokensSpendStats from '@/app/ui/payment/tokens-spend-stats';
|
||||
import PaymentTabs from '@/app/ui/payment/payment-tabs';
|
||||
import SecurePayments from '@/app/ui/payment/secure-payments';
|
||||
import PaymentUserCards from '@/app/ui/payment/payment-user-cards';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'payment',
|
||||
@@ -22,6 +23,7 @@ export default function Page() {
|
||||
<UserUsage />
|
||||
<TokensSpendStats />
|
||||
<PaymentTabs />
|
||||
<PaymentUserCards />
|
||||
<SecurePayments />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -8,6 +8,7 @@ import DangerZone from '@/app/ui/settings/danger-zone';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import PageTitle from '@/app/ui/page-title';
|
||||
import { CompanyUsersSettingPanel } from '@/app/ui/settings/company-users-setting-panel';
|
||||
import SettingUserVerification from '@/app/ui/settings/setting-user-verification';
|
||||
|
||||
export default function Page() {
|
||||
const t = useTranslations('Global');
|
||||
@@ -24,6 +25,7 @@ export default function Page() {
|
||||
{/* LastActivitySettings возможно нужно будет вернуть */}
|
||||
{/* <LastActivitySettings /> */}
|
||||
<CompanyUsersSettingPanel />
|
||||
<SettingUserVerification />
|
||||
</div>
|
||||
<DangerZone />
|
||||
</>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
'use server'
|
||||
|
||||
import { TrackingStatistic } from '@/app/ui/tracking-page/tracking-statistic';
|
||||
import FilesTable from '@/app/ui/dashboard/files-table';
|
||||
import { TrackingHistoryView } from '@/app/ui/tracking-page/tracking-history-view';
|
||||
|
||||
export default async function Page() {
|
||||
const FILE_TYPE = "document";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="tracking-page"
|
||||
>
|
||||
<TrackingStatistic />
|
||||
<FilesTable fileType={FILE_TYPE} showFileLink={true} />
|
||||
<TrackingHistoryView />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
import PageTitleColorFrame from '@/app/ui/page-title-color-frame';
|
||||
import ViolationsMyCases from '@/app/ui/violations/violations-my-cases';
|
||||
import ViolationsMyComplaint from '@/app/ui/violations/violations-my-complaint';
|
||||
import ViolationsCheckAllSection from '@/app/ui/violations/violations-check-all-section';
|
||||
import ViolationsTable from '@/app/ui/violations/violations-table';
|
||||
import ViolationsStatistic from '@/app/ui/violations/violations-statistic';
|
||||
import AnalyticsCardGeography from '@/app/ui/reports/analytics-card-geography';
|
||||
import AnalyticsCardDistribution from '@/app/ui/reports/analytics-card-distribution';
|
||||
import ViolationsMyClaims from '@/app/ui/violations/violations-my-claims';
|
||||
import ViolationsLastCombinedCases from '@/app/ui/violations/violations-last-combined-cases';
|
||||
import { ViolationsMyComplaintsTable } from '@/app/ui/violations/tables/violations-my-complaints-table';
|
||||
import { ViolationsMyClaimsTable } from '@/app/ui/violations/tables/violations-my-claims-table';
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
@@ -16,11 +20,17 @@ export default function Page() {
|
||||
<ViolationsStatistic />
|
||||
<ViolationsCheckAllSection />
|
||||
<ViolationsTable />
|
||||
<ViolationsMyCases />
|
||||
{/* <ViolationsLastCombinedCases /> */}
|
||||
{/* <ViolationsMyComplaint />
|
||||
<ViolationsMyClaims /> */}
|
||||
<div className="analytics-grid">
|
||||
<AnalyticsCardDistribution />
|
||||
<AnalyticsCardGeography />
|
||||
</div>
|
||||
<div className="analytics-grid">
|
||||
<ViolationsMyClaimsTable />
|
||||
<ViolationsMyComplaintsTable />
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
'use client'
|
||||
|
||||
import { PdfProvider } from '@/app/contexts/PdfContext';
|
||||
import { WatchDocProviders } from '@/app/providers/WatchDocProviders';
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<WatchDocProviders>
|
||||
<PdfProvider>
|
||||
{children}
|
||||
</PdfProvider>
|
||||
</WatchDocProviders>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
'use server';
|
||||
|
||||
import Link from 'next/link';
|
||||
import PdfViewer from '@/app/components/PdfViewer';
|
||||
import { DownloadButton } from '@/app/components/document-viewer/DownloadButton';
|
||||
import { CopyLinkButton } from '@/app/components/document-viewer/CopyLinkButton';
|
||||
import { fetchDocumentInfo } from '@/app/actions/trackingActions';
|
||||
import { LoginUserBage } from '@/app/components/document-viewer/LoginUserBage';
|
||||
|
||||
interface PageProps {
|
||||
searchParams: Promise<{ docid: string; title?: string }>;
|
||||
}
|
||||
|
||||
interface DocumentInfo {
|
||||
checksCount?: number;
|
||||
checksum?: string;
|
||||
createdAt?: string;
|
||||
downloadUrl?: string;
|
||||
existsOnDisk?: boolean;
|
||||
fileExtension?: string;
|
||||
fileFormat?: string | null;
|
||||
fileName?: string;
|
||||
filePath?: string;
|
||||
fileSize?: number;
|
||||
fileUploadDate?: string;
|
||||
formattedSize?: string;
|
||||
id: string;
|
||||
mimeType?: string;
|
||||
monitoring?: string;
|
||||
originalFileName?: string;
|
||||
ownerCompany?: string | null;
|
||||
ownerEmail?: string;
|
||||
ownerName?: string;
|
||||
permissions?: {
|
||||
DOWNLOAD?: boolean;
|
||||
};
|
||||
protectStatus?: string;
|
||||
protectedFilePath?: string;
|
||||
status?: string;
|
||||
storedFileName?: string;
|
||||
supportId?: number;
|
||||
thumbnailFileUrl?: string;
|
||||
updatedAt?: string;
|
||||
uploadSessionId?: string;
|
||||
userId?: number;
|
||||
appealInfos?: any[]; //Не знаю что тут будет
|
||||
}
|
||||
|
||||
export default async function Page({ searchParams }: PageProps) {
|
||||
const { docid, title } = await searchParams;
|
||||
|
||||
let response: DocumentInfo | null = null;
|
||||
let error: string | null = null;
|
||||
|
||||
try {
|
||||
response = await fetchDocumentInfo(docid);
|
||||
} catch (err) {
|
||||
error = 'Не удалось загрузить информацию о документе';
|
||||
}
|
||||
|
||||
const documentTitle = response?.fileName || 'FileName';
|
||||
|
||||
if (error || !response) {
|
||||
return (
|
||||
<div className="watch-doc-wrapper">
|
||||
<div className="watch-doc-page">
|
||||
<header className="watch-doc-header">
|
||||
<Link href="#" className="header-logo">NoCopy</Link>
|
||||
<LoginUserBage />
|
||||
</header>
|
||||
<div className="error-container">
|
||||
<h2>Ошибка загрузки документа</h2>
|
||||
<p>{error || 'Документ не найден'}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="watch-doc-wrapper">
|
||||
<div className="watch-doc-page">
|
||||
<header className="watch-doc-header">
|
||||
<Link href="#" className="header-logo">NoCopy</Link>
|
||||
<LoginUserBage />
|
||||
</header>
|
||||
|
||||
<div className="watch-doc-status">
|
||||
<div className="st-title">Защита подтверждена</div>
|
||||
<div className="st-sub">
|
||||
Документ <strong>{response?.fileName ? response?.fileName : '#'}</strong> зарегистрирован и защищён платформой NoCopy.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-grid">
|
||||
<div
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<div className="watch-doc-card">
|
||||
<div className="card-head">Правообладатель</div>
|
||||
<div className="card-body">
|
||||
<div className="owner-row">
|
||||
<div className="owner-av">
|
||||
{response?.fileName ? response?.fileName[0] : ''}
|
||||
</div>
|
||||
<div>
|
||||
{response?.ownerName && (
|
||||
<div className="owner-name">{response?.ownerName}</div>
|
||||
)}
|
||||
{response?.ownerCompany && (
|
||||
<div className="owner-name">{response?.ownerCompany}</div>
|
||||
)}
|
||||
{response?.ownerName && (
|
||||
<div className="owner-meta">Зарегистрирован в NoCopy</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-actions">
|
||||
{response?.permissions?.DOWNLOAD && (
|
||||
<DownloadButton fileId={docid} fileName={response?.fileName}/>
|
||||
)}
|
||||
<CopyLinkButton />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="watch-doc-card">
|
||||
<div className="card-head">Предпросмотр</div>
|
||||
<div className="card-body">
|
||||
<PdfViewer
|
||||
fileId={docid}
|
||||
fileName={documentTitle}
|
||||
showDownload={response.permissions?.DOWNLOAD}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="watch-doc-footer">
|
||||
<Link href="#">NoCopy</Link>
|
||||
<Link href="/terms-of-use">Условия</Link>
|
||||
<Link href="/privacy-policy">Конфиденциальность</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -44,12 +44,12 @@ export async function getUserFilesInfo() {
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'user_files_info',
|
||||
token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -81,13 +81,13 @@ export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
|
||||
msg_id: 30001,
|
||||
message_body: {
|
||||
action: 'get',
|
||||
user_token: token,
|
||||
tariff_term: tariffTerm
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -108,6 +108,7 @@ export async function fetchTariffs(tariffTerm: 'MONTHLY' | 'YEAR') {
|
||||
}
|
||||
|
||||
export async function fetchTokensBundle() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
@@ -122,7 +123,8 @@ export async function fetchTokensBundle() {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -143,6 +145,7 @@ export async function fetchTokensBundle() {
|
||||
}
|
||||
|
||||
export async function getBuildData() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/check/api/build`, {
|
||||
@@ -150,6 +153,7 @@ export async function getBuildData() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -165,7 +169,7 @@ export async function getBuildData() {
|
||||
|
||||
export async function fetchINN(inn: string) {
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
|
||||
+9
-12
@@ -72,7 +72,7 @@ export async function authorization(
|
||||
|
||||
try {
|
||||
const { email, password } = validatedFields.data;
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
@@ -89,6 +89,7 @@ export async function authorization(
|
||||
});
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
await createSession(parsed.message_body.token, email);
|
||||
} else {
|
||||
@@ -234,7 +235,7 @@ export async function registration(
|
||||
|
||||
try {
|
||||
const { fullName, email, password, phone } = validatedFields.data;
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
@@ -400,13 +401,11 @@ export async function tokenLifeExtension() {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20008,
|
||||
message_body: {
|
||||
token: token
|
||||
}
|
||||
msg_id: 20008
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -431,14 +430,14 @@ export async function confirmEmail(userId: number, verifyToken: string) {
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"Accept": "application/json",
|
||||
'Authorization': `Bearer ${verifyToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20009,
|
||||
message_body: {
|
||||
resend: 0,
|
||||
user_id: userId,
|
||||
verify_token: verifyToken
|
||||
user_id: userId
|
||||
}
|
||||
}),
|
||||
});
|
||||
@@ -474,7 +473,6 @@ export async function confirmEmail(userId: number, verifyToken: string) {
|
||||
export async function resendConfirmEmail(userId: number) {
|
||||
console.log('resendConfirmEmail');
|
||||
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -524,10 +522,9 @@ export async function resetPassword(
|
||||
formData: FormData,
|
||||
) {
|
||||
const email = formData.get('email');
|
||||
console.log('resetPassword');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -611,6 +608,7 @@ export async function confirmPassword(
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${verifyToken}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
@@ -618,7 +616,6 @@ export async function confirmPassword(
|
||||
message_body: {
|
||||
email: email,
|
||||
password: password,
|
||||
verifyToken: verifyToken,
|
||||
action: "confirmVerification"
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -95,7 +95,6 @@ export async function companyUserRegistration(
|
||||
phone: phone,
|
||||
password,
|
||||
accountType: 'b2b',
|
||||
authToken: token,
|
||||
mail_verified: false,
|
||||
ip: ip,
|
||||
user_agent: userAgent,
|
||||
@@ -103,7 +102,8 @@ export async function companyUserRegistration(
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -113,14 +113,6 @@ export async function companyUserRegistration(
|
||||
message_body: { token: string },
|
||||
message_code: number
|
||||
};
|
||||
console.log({
|
||||
fullName: fullName,
|
||||
email,
|
||||
phone: phone,
|
||||
password,
|
||||
accountType: 'b2b',
|
||||
authToken: token
|
||||
});
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
console.log(`registration - ${email}`);
|
||||
|
||||
@@ -83,6 +83,30 @@ export const loginFormSchema = z
|
||||
.trim(),
|
||||
})
|
||||
|
||||
|
||||
export const createComplaintSchema = z
|
||||
.object({
|
||||
textArea: z
|
||||
.string()
|
||||
.min(1, { error: 'text-area-error-fill-complaint-text' })
|
||||
.max(2000, {error: 'text-area-error-complaint-text-too-long'})
|
||||
.trim(),
|
||||
})
|
||||
|
||||
export const createClaimSchema = z
|
||||
.object({
|
||||
textArea: z
|
||||
.string()
|
||||
.min(1, { error: 'fill-text-of-the-case' })
|
||||
.max(2000, {error: 'claim-text-must-not-exceed-2000-characters'})
|
||||
.trim(),
|
||||
caseTitle: z
|
||||
.string()
|
||||
.min(1, { error: 'fill-title-of-the-case' })
|
||||
.max(300, {error: 'claim-title-must-not-exceed-300-characters'})
|
||||
.trim(),
|
||||
})
|
||||
|
||||
export const localDevelopmentUrl = process.env.DEV_URL ? process.env.DEV_URL : 'http://localhost';
|
||||
|
||||
export const API_BASE_URL = process.env.NODE_ENV === 'development'
|
||||
|
||||
@@ -22,7 +22,6 @@ export async function getUserFilesData({
|
||||
query?: string;
|
||||
}) {
|
||||
const token = await getSessionData('token');
|
||||
/* console.log('getUserFilesData', { page, pageSize, sortBy, sortOrder, type, date, query }); */
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
@@ -32,7 +31,6 @@ export async function getUserFilesData({
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'search_files',
|
||||
token: token,
|
||||
page: page,
|
||||
page_size: pageSize,
|
||||
query: query,
|
||||
@@ -44,26 +42,24 @@ export async function getUserFilesData({
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
/* console.log(parsed); */
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw new Error(parsed.message_text || 'API error');
|
||||
throw parsed.message_text ? parsed.message_text : 'error';
|
||||
}
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
throw `${response.status}`;
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('getUserFilesData error:', error);
|
||||
return error;
|
||||
}
|
||||
}
|
||||
@@ -81,12 +77,12 @@ export async function removeUserFile(fileId: string, fullDelete: number) {
|
||||
action: 'delete_file',
|
||||
file_id: fileId,
|
||||
full_delete: fullDelete,
|
||||
token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -118,13 +114,13 @@ export async function viewFileInfo(fileId: string) {
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'file_info',
|
||||
file_id: fileId,
|
||||
token: token
|
||||
file_id: fileId
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -166,4 +162,44 @@ export async function downloadFile(fileId: string, fileName: string) {
|
||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||
fileName: fileName
|
||||
}
|
||||
}
|
||||
|
||||
export async function FileAnAppeal(fileId: string, appealReason: string, additionalInfo: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
action: 'submit_appeal',
|
||||
file_id: fileId,
|
||||
appeal_reason: appealReason,
|
||||
additional_info: additionalInfo
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
return error
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,6 @@
|
||||
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { FormState } from '@/app/actions/auth';
|
||||
import { unknown } from 'zod';
|
||||
|
||||
interface initMessageBody {
|
||||
file_name: string,
|
||||
@@ -22,15 +20,11 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
|
||||
}
|
||||
const message = messageBody;
|
||||
message.action = 'init';
|
||||
message.token = token;
|
||||
|
||||
if (convertTo && convertTo !== 'reject') {
|
||||
message.convertTo = convertTo;
|
||||
}
|
||||
|
||||
// нужно будет добавить свойство convertTo в тело запроса он может прийти со значением reject тогда конвертировать не нужно.
|
||||
// console.log(convertTo);
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -41,7 +35,8 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -71,6 +66,8 @@ export async function fileUpload(messageBody: initMessageBody, convertTo?: strin
|
||||
}
|
||||
|
||||
export async function cancelUpload(udloadId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -84,7 +81,8 @@ export async function cancelUpload(udloadId: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -116,16 +114,30 @@ export type ChunkResponse = {
|
||||
fileId?: string
|
||||
};
|
||||
|
||||
export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
|
||||
export async function chunkUpload(formData: FormData, itPrivate?: boolean): Promise<ChunkResponse> {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/chunk`, {
|
||||
const url = itPrivate
|
||||
? `${API_BASE_URL}/api/v1/private/files/chunk`
|
||||
: `${API_BASE_URL}/api/v1/files/chunk`;
|
||||
|
||||
|
||||
/* if (token && itPrivate) {
|
||||
formData.append('token', token);
|
||||
} */
|
||||
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
/* ...(token && itPrivate && { 'Authorization': `Bearer ${token}` }) возможно нужно будет условно отправлять токен*/
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return {
|
||||
@@ -161,6 +173,8 @@ export async function chunkUpload(formData: FormData): Promise<ChunkResponse> {
|
||||
}
|
||||
|
||||
export async function checkChunkStatus(upload_id: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -174,7 +188,8 @@ export async function checkChunkStatus(upload_id: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -196,6 +211,8 @@ export async function checkChunkStatus(upload_id: string) {
|
||||
}
|
||||
|
||||
export async function getAllowedFilesExtensions(type: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -209,7 +226,8 @@ export async function getAllowedFilesExtensions(type: string) {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -240,7 +258,6 @@ export async function checkOperationPrice(action: string, filesCount: number, fi
|
||||
version: 1,
|
||||
msg_id: 30008,
|
||||
message_body: {
|
||||
auth_token: token,
|
||||
action: action,
|
||||
file_type: fileType,
|
||||
count_files: filesCount
|
||||
@@ -248,13 +265,13 @@ export async function checkOperationPrice(action: string, filesCount: number, fi
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
|
||||
@@ -14,13 +14,13 @@ export async function setMonitoring(fileId: string, monitoringType: string) {
|
||||
msg_id: 30007,
|
||||
message_body: {
|
||||
monitoring_type: monitoringType,
|
||||
file_id: fileId,
|
||||
auth_token: token
|
||||
file_id: fileId
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -44,4 +44,108 @@ export async function setMonitoring(fileId: string, monitoringType: string) {
|
||||
errorMessage: 'error'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export interface MonitoringStats {
|
||||
total_searches: number;
|
||||
searches_by_status: {
|
||||
processing: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
};
|
||||
recent_searches: any[];
|
||||
}
|
||||
|
||||
export async function fetchMonitoringStats(): Promise<MonitoringStats | null> {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30011
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed?.message_body?.searches_by_status) {
|
||||
return parsed.message_body;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLastMonitoringCheck() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30011,
|
||||
message_body: {
|
||||
limit: 1
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body?.searches_by_status) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchMonitoringInfo() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30027
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed.message_body;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,173 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
|
||||
export interface NotificationBody {
|
||||
createdAt: string | null,
|
||||
id: number,
|
||||
message: string,
|
||||
status: string | null,
|
||||
type: string,
|
||||
context: Record<string, string>
|
||||
}
|
||||
|
||||
export async function fetchActiveNotifications() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30015,
|
||||
message_body: {
|
||||
action: 'active'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchAllNotifications(page: number = 0, size: number = 10) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30015,
|
||||
message_body: {
|
||||
action: 'list',
|
||||
page: page,
|
||||
size: size,
|
||||
sortBy: "createdAt",
|
||||
sortDirection: "DESC", //DESC, ASC
|
||||
types: [], //"SEARCH_RESULT", "MONITORING_RESULT"
|
||||
statuses: ["NEW", "READIED"] //"NEW"
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function notificationsMarkAsRead(ids: number[]) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30015,
|
||||
message_body: {
|
||||
action: 'markRead',
|
||||
notificationIds: ids
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function notificationsDelete(ids: number[]) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30015,
|
||||
message_body: {
|
||||
action: 'delete',
|
||||
notificationIds: ids
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
console.log('parsed');
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
'use server'
|
||||
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
|
||||
export async function getPaymentsMethods() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/methods`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
return parsed;
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function deletePaymentsMethods(cardId: string): Promise<boolean | undefined> {
|
||||
if (!cardId) {
|
||||
return
|
||||
}
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/methods/${cardId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
if (parsed.message === 'Payment method deleted') {
|
||||
return true;
|
||||
}
|
||||
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -14,12 +14,12 @@ export async function fetchReferralsLevels() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'levels',
|
||||
token: token,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -57,12 +57,12 @@ export async function fetchReferralUserStats() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'userStats',
|
||||
token: token,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -96,14 +96,14 @@ export async function fetchReferralInvitees() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'invitees',
|
||||
token: token,
|
||||
pageSize: 100,
|
||||
pageNumber: 0
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,25 +137,18 @@ export async function referralRefill() {
|
||||
msg_id: 30003,
|
||||
message_body: {
|
||||
action: 'refill',
|
||||
token: token,
|
||||
amount: 2000,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
console.log('referralRefill');
|
||||
console.log({
|
||||
action: 'refill',
|
||||
token: token,
|
||||
amount: 2000,
|
||||
})
|
||||
let parsed = await response.json();
|
||||
console.log(parsed);
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
@@ -360,12 +353,14 @@ export async function createPayoutRequest(
|
||||
const amount = formData.get('amount') as string || '';
|
||||
const payoutMethodId = formData.get('payoutMethodId') as string || '';
|
||||
|
||||
console.log('createPayoutRequest');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payouts/create-request`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
"amount": amount,
|
||||
"payoutMethodId": payoutMethodId
|
||||
'amount': amount,
|
||||
'payoutMethodId': payoutMethodId
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
@@ -378,6 +373,17 @@ export async function createPayoutRequest(
|
||||
throw JSON.parse(responseText)?.message;
|
||||
}
|
||||
|
||||
if (response.status === 409) {
|
||||
const responseText = await response.text();
|
||||
if (responseText.includes('Not have money for payout')) {
|
||||
throw 'insufficient-funds-to-receive-payment'
|
||||
}
|
||||
|
||||
if (responseText.includes('Not found payout method')) {
|
||||
throw 'not-found-payout-method'
|
||||
}
|
||||
}
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
console.log(parsed);
|
||||
@@ -403,6 +409,12 @@ export async function createPayoutRequest(
|
||||
case 'Min sum for pay 1000':
|
||||
typedError = 'min-sum-for-payout-1000';
|
||||
break;
|
||||
case 'insufficient-funds-to-receive-payment':
|
||||
typedError = 'insufficient-funds-to-receive-payment';
|
||||
break;
|
||||
case 'not-found-payout-method':
|
||||
typedError = 'not-found-payout-method';
|
||||
break
|
||||
default:
|
||||
typedError = 'payment-error';
|
||||
}
|
||||
|
||||
@@ -7,8 +7,11 @@ export async function searchUserFiles(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY&auth_token=${token}`, {
|
||||
method: 'GET'
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/${fileId}/similar?similarityLevels=DUPLICATE,SIMILARITY`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -40,48 +43,6 @@ export async function searchUserFiles(fileId: string) {
|
||||
export async function searchGlobalFiles(fileId: string, currentPage: number) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
//удалить когда поиск будет нормально работать
|
||||
/* return {
|
||||
images: [
|
||||
{
|
||||
url: 'string1',
|
||||
pageTitle: `string1 from page ${currentPage}`,
|
||||
height: 100,
|
||||
width: 100,
|
||||
host: 'string1',
|
||||
pageUrl: 'string1'
|
||||
},
|
||||
{
|
||||
url: 'string1',
|
||||
pageTitle: `string2 from page ${currentPage}`,
|
||||
height: 100,
|
||||
width: 100,
|
||||
host: 'string1',
|
||||
pageUrl: 'string1'
|
||||
},
|
||||
{
|
||||
url: 'string1',
|
||||
pageTitle: `string3 from page ${currentPage}`,
|
||||
height: 100,
|
||||
width: 100,
|
||||
host: 'string1',
|
||||
pageUrl: 'string1'
|
||||
}
|
||||
],
|
||||
page: 0,
|
||||
pageSize: 0,
|
||||
totalPages: 11,
|
||||
totalResults: 100,
|
||||
} */
|
||||
|
||||
/* return {
|
||||
images: [],
|
||||
page: 0,
|
||||
pageSize: 0,
|
||||
totalPages: 0,
|
||||
totalResults: 0
|
||||
} */
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
@@ -91,11 +52,13 @@ export async function searchGlobalFiles(fileId: string, currentPage: number) {
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
page: currentPage,
|
||||
check_similar_first: true,
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -151,6 +114,7 @@ export async function getSearchStats() {
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
return parsed;
|
||||
} else {
|
||||
return null;
|
||||
|
||||
@@ -37,7 +37,7 @@ export async function createSession(token: string, email: string) {
|
||||
});
|
||||
const cookieStore = await cookies();
|
||||
|
||||
cookieStore.set('session', session, {
|
||||
cookieStore.set('session_nc_db', session, {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
expires: expiresAt,
|
||||
@@ -48,7 +48,7 @@ export async function createSession(token: string, email: string) {
|
||||
|
||||
export async function updateSession(newToken: string) {
|
||||
const cookieStore = await cookies();
|
||||
const session = cookieStore.get('session')?.value;
|
||||
const session = cookieStore.get('session_nc_db')?.value;
|
||||
|
||||
if (!session) {
|
||||
return null;
|
||||
@@ -68,7 +68,7 @@ export async function updateSession(newToken: string) {
|
||||
|
||||
const updatedSession = await encrypt(updatedPayload);
|
||||
|
||||
cookieStore.set('session', updatedSession, {
|
||||
cookieStore.set('session_nc_db', updatedSession, {
|
||||
httpOnly: true,
|
||||
secure: false,
|
||||
expires: updatedPayload.expiresAt,
|
||||
@@ -79,7 +79,7 @@ export async function updateSession(newToken: string) {
|
||||
|
||||
export async function getSessionData(type: 'token' | 'email'): Promise<string | null> {
|
||||
const cookieStore = await cookies();
|
||||
const sessionCookie = cookieStore.get('session')?.value;
|
||||
const sessionCookie = cookieStore.get('session_nc_db')?.value;
|
||||
|
||||
if (!sessionCookie) {
|
||||
return null;
|
||||
@@ -99,7 +99,7 @@ export async function deleteSession() {
|
||||
console.log('delete session')
|
||||
try {
|
||||
const cookieStore = await cookies()
|
||||
cookieStore.delete('session');
|
||||
cookieStore.delete('session_nc_db');
|
||||
} catch (error) {
|
||||
|
||||
} finally {
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
'use server';
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { headers } from 'next/headers';
|
||||
|
||||
export async function requestFileWithTracking(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
const headersList = await headers();
|
||||
const forwardedFor = headersList.get('x-forwarded-for');
|
||||
const realIp = headersList.get('x-real-ip');
|
||||
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
|
||||
const userAgent = headersList.get('user-agent') || 'unknown';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
file_id: fileId,
|
||||
ip: ip,
|
||||
user_agent: userAgent
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const contentType = response.headers.get('content-type');
|
||||
|
||||
if (contentType?.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
return data.message_body;
|
||||
} else {
|
||||
const blob = await response.blob();
|
||||
return URL.createObjectURL(blob);
|
||||
}
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function requestFileAsBuffer(fileId: string): Promise<ArrayBuffer | null> {
|
||||
const token = await getSessionData('token');
|
||||
const headersList = await headers();
|
||||
const forwardedFor = headersList.get('x-forwarded-for');
|
||||
const realIp = headersList.get('x-real-ip');
|
||||
const ip = forwardedFor?.split(',')[0] || realIp || 'unknown';
|
||||
const userAgent = headersList.get('user-agent') || 'unknown';
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/file/link`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
file_id: fileId,
|
||||
ip: ip,
|
||||
user_agent: userAgent
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
|
||||
const contentType = response.headers.get('content-type');
|
||||
if (contentType?.includes('application/json')) {
|
||||
const data = await response.json();
|
||||
const fileUrl = data.message_body;
|
||||
const fileResponse = await fetch(fileUrl);
|
||||
if (!fileResponse.ok) {
|
||||
throw new Error(`Failed to fetch file: ${fileResponse.status}`);
|
||||
}
|
||||
|
||||
return await fileResponse.arrayBuffer();
|
||||
}
|
||||
else {
|
||||
return await response.arrayBuffer();
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchDocumentInfo(fileId: string) {
|
||||
const token = await getSessionData('token');
|
||||
console.log('fetchDocumentInfo');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/files/public/file-info/${fileId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
return parsed;
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function filePermisionChange(fileId: string, permissions: boolean) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 20005,
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
action: 'change_permission',
|
||||
permissions: {
|
||||
DOWNLOAD: permissions
|
||||
}
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchHistoryView(
|
||||
page: number,
|
||||
size: number,
|
||||
locale: string,
|
||||
sortDirection?: 'asc' | 'desc',
|
||||
sortBy?: string
|
||||
) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30028,
|
||||
message_body: {
|
||||
action: 'history_view',
|
||||
page: page,
|
||||
size: size,
|
||||
locale: locale,
|
||||
sort_direction: sortDirection,
|
||||
sort_by: sortBy
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchTrackingStats() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30028,
|
||||
message_body: {
|
||||
action: 'view_stats'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
} else {
|
||||
throw new Error(`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function downloadTrackingFile(fileId: string, fileName?: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/files/public-download/${fileId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw 'failed-to-download-file';
|
||||
}
|
||||
|
||||
const arrayBuffer = await response.arrayBuffer()
|
||||
const base64 = Buffer.from(arrayBuffer).toString('base64')
|
||||
|
||||
return {
|
||||
data: base64,
|
||||
contentType: response.headers.get('content-type') || 'application/octet-stream',
|
||||
fileName: fileName ? fileName : 'fileName'
|
||||
}
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
'use server'
|
||||
|
||||
import { getSessionData } from '@/app/actions/session';
|
||||
import { API_BASE_URL } from '@/app/actions/definitions';
|
||||
import { API_BASE_URL, createComplaintSchema, createClaimSchema } from '@/app/actions/definitions';
|
||||
|
||||
export async function getViolationSearchStatus() {
|
||||
const token = await getSessionData('token');
|
||||
@@ -22,10 +22,10 @@ export async function getViolationSearchStatus() {
|
||||
if (text) {
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
console.log('Parsed:', parsed);
|
||||
|
||||
return parsed;
|
||||
} catch (e) {
|
||||
console.log('Not JSON, raw text:', text);
|
||||
|
||||
return {
|
||||
status: text === 'Task not found' ? 'task-not-found' : text
|
||||
};
|
||||
@@ -44,7 +44,6 @@ export async function getViolationSearchStatus() {
|
||||
|
||||
export async function startGlobalMonitoring(monitoringType: 'monitoring' | 'all_files') {
|
||||
const token = await getSessionData('token');
|
||||
console.log('startGlobalMonitoring');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/start`, {
|
||||
@@ -67,7 +66,6 @@ export async function startGlobalMonitoring(monitoringType: 'monitoring' | 'all_
|
||||
totalFiles: number
|
||||
} = await response.json();
|
||||
|
||||
console.log(parsed);
|
||||
if (parsed?.status === 'ACCEPTED') {
|
||||
return parsed
|
||||
} else {
|
||||
@@ -82,23 +80,39 @@ export async function startGlobalMonitoring(monitoringType: 'monitoring' | 'all_
|
||||
}
|
||||
}
|
||||
|
||||
export async function getViolationFilesArray() {
|
||||
export async function getViolationFilesArray(props: {
|
||||
page?: number,
|
||||
size?: number,
|
||||
start_date?: string,
|
||||
end_date?: string,
|
||||
file_name?: string
|
||||
}) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, start_date, end_date, file_name } = props;
|
||||
|
||||
try {
|
||||
const body: Record<string, any> = {};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/violation-summary`, {
|
||||
method: 'GET',
|
||||
if (page !== undefined) body.page = page;
|
||||
if (size !== undefined) body.size = size;
|
||||
if (start_date !== undefined) body.start_date = start_date;
|
||||
if (end_date !== undefined) body.end_date = end_date;
|
||||
if (file_name !== undefined) body.file_name = file_name;
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/global-search/violation-summary-with-files`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`,
|
||||
}
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed.length) {
|
||||
|
||||
if (parsed.content.length) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw parsed;
|
||||
@@ -112,42 +126,7 @@ export async function getViolationFilesArray() {
|
||||
}
|
||||
}
|
||||
|
||||
/* статистика нурешений */
|
||||
export async function fetchViolationStats() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30010,
|
||||
message_body: {
|
||||
token: token
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
if (parsed) {
|
||||
return parsed;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFileViolations(fileId: string, page: number) {
|
||||
export async function getFileMatches(fileId: string, page: number, status?: string, violationId?: string, startDate?: string, endDate?: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
@@ -158,20 +137,25 @@ export async function getFileViolations(fileId: string, page: number) {
|
||||
msg_id: 30009,
|
||||
message_body: {
|
||||
file_id: fileId,
|
||||
page: page,
|
||||
page: (page > 0 ? page - 1 : 0),
|
||||
size: 5,
|
||||
sort_direction: "desc",
|
||||
token: token
|
||||
sort_direction: 'desc',
|
||||
status: status,
|
||||
...(violationId && { action: 'getByViolationId' }),
|
||||
...(violationId && { violation_id: violationId }),
|
||||
...((startDate && endDate) && { start_date: startDate, end_date: endDate })
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_body) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
@@ -198,13 +182,13 @@ export async function fetchViolationAnalyticStatistic(group: 'domain' | 'tld') {
|
||||
message_body: {
|
||||
group_by: group,
|
||||
action: "group",
|
||||
token: token,
|
||||
sort_direction: 'desc'
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -224,7 +208,7 @@ export async function fetchViolationAnalyticStatistic(group: 'domain' | 'tld') {
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchViolationStatistic() {
|
||||
export async function fetchViolationStatistic(id?: string) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
@@ -234,12 +218,13 @@ export async function fetchViolationStatistic() {
|
||||
version: 1,
|
||||
msg_id: 30010,
|
||||
message_body: {
|
||||
token: token,
|
||||
...(id && { file_id: id })
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
@@ -257,4 +242,478 @@ export async function fetchViolationStatistic() {
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchViolationFinalSummaryStatistic() {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30023
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return parsed?.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
interface complainBody {
|
||||
previousText: string,
|
||||
violationID: string | null,
|
||||
success: boolean,
|
||||
errorMessage?: Record<string, string> | null
|
||||
}
|
||||
|
||||
export async function createComplaint(
|
||||
state: complainBody | undefined,
|
||||
formData: FormData
|
||||
): Promise<complainBody> {
|
||||
const email = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
const violationId = formData.get('violationId') as string || '';
|
||||
const textArea = formData.get('note') as string || '';
|
||||
|
||||
const validatedFields = createComplaintSchema.safeParse({
|
||||
textArea
|
||||
});
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30013,
|
||||
message_body: {
|
||||
action: 'create',
|
||||
violation_id: violationId,
|
||||
text: textArea,
|
||||
email: email
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: true
|
||||
};
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (error && typeof error === 'object' && 'message_code' in error) {
|
||||
const err = error as { message_code: number };
|
||||
if (err.message_code === 2) {
|
||||
errors['server'] = 'the-complaint-has-not-been-registered'
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
errors['server'] = 'error-save';
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchComplainInfo(id: number) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30013,
|
||||
message_body: {
|
||||
action: 'get_by_violation',
|
||||
violation_id: id
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
body: parsed?.message_body
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
errorMessage: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export type MatchStatus = 'NEW' | 'SHOWED' | 'LEGAL_IN_WORK' | 'COMPLAINT_IN_WORK' | 'COMPLAINT_AND_LEGAL_IN_WORK' | 'AUTHORIZED_USE' | 'NOT_OWNER_FILE';
|
||||
|
||||
export async function updateMatchStatus(id: number, status: MatchStatus) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30009,
|
||||
message_body: {
|
||||
action: 'updateStatus',
|
||||
violation_id: id,
|
||||
status: status
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_body.status) {
|
||||
return true
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchClaimInfo(id: number) {
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30017,
|
||||
message_body: {
|
||||
action: 'get_all',
|
||||
pageSize: 1,
|
||||
pageNumber: 0,
|
||||
sortBy: "createdAt",
|
||||
sortDir: "desc",
|
||||
violation_id: id
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
body: parsed?.message_body
|
||||
}
|
||||
} else if (parsed?.message_code !== 0 && parsed?.message_desc === 'Law case not found') {
|
||||
return {
|
||||
body: null
|
||||
}
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
errorMessage: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface caseBody {
|
||||
previousText: string,
|
||||
previousCaseTitle: string,
|
||||
previousAmount: string,
|
||||
violationID: string | null,
|
||||
success: boolean,
|
||||
errorMessage?: Record<string, string> | null
|
||||
}
|
||||
|
||||
export async function createClaim(
|
||||
state: caseBody | undefined,
|
||||
formData: FormData
|
||||
): Promise<caseBody> {
|
||||
const email = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
const violationId = formData.get('violationId') as string || '';
|
||||
const textArea = formData.get('note') as string || '';
|
||||
const caseTitle = formData.get('caseTitle') as string || '';
|
||||
const amount = formData.get('amount') as string || '';
|
||||
|
||||
const validatedFields = createClaimSchema.safeParse({
|
||||
textArea,
|
||||
caseTitle
|
||||
});
|
||||
|
||||
if (!validatedFields.success) {
|
||||
const errors: Record<string, string> = {}
|
||||
JSON.parse(validatedFields.error.message).forEach((obj: {
|
||||
message: string,
|
||||
path: string
|
||||
}) => {
|
||||
if (errors[obj.path[0]]) {
|
||||
errors[obj.path[0]] = `${errors[obj.path[0]]} ${obj.message}`;
|
||||
} else {
|
||||
errors[obj.path[0]] = obj.message;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30017,
|
||||
message_body: {
|
||||
action: 'create',
|
||||
violation_id: violationId,
|
||||
description: textArea,
|
||||
email: email,
|
||||
name: caseTitle,
|
||||
priority: 'HIGH',
|
||||
amount: Number(amount)
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed?.message_code === 0) {
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousText: textArea,
|
||||
previousAmount: amount,
|
||||
success: true
|
||||
};
|
||||
} else {
|
||||
throw parsed;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
const errors: Record<string, string> = {}
|
||||
|
||||
if (error && typeof error === 'object' && 'message_code' in error) {
|
||||
const err = error as { message_code: number };
|
||||
if (err.message_code === 2) {
|
||||
errors['server'] = 'the-complaint-has-not-been-registered'
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
errors['server'] = 'error-save';
|
||||
|
||||
return {
|
||||
violationID: violationId,
|
||||
previousText: textArea,
|
||||
previousCaseTitle: caseTitle,
|
||||
previousAmount: amount,
|
||||
success: false,
|
||||
errorMessage: errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ComplaintsCaseQueryParams {
|
||||
page?: number,
|
||||
size?: number,
|
||||
sort_by?: string,
|
||||
sort_direction?: 'desc' | 'asc'
|
||||
}
|
||||
|
||||
export async function fetchLastClaims(params: ComplaintsCaseQueryParams) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, sort_by, sort_direction } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30017,
|
||||
message_body: {
|
||||
action: "get_all",
|
||||
pageNumber: page,
|
||||
size,
|
||||
sort_by,
|
||||
sort_direction
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchLastComplaint(params: ComplaintsCaseQueryParams) {
|
||||
const token = await getSessionData('token');
|
||||
const { page, size, sort_by, sort_direction } = params;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
version: 1,
|
||||
msg_id: 30013,
|
||||
message_body: {
|
||||
action: "get_all",
|
||||
page,
|
||||
size,
|
||||
sort_by,
|
||||
sort_direction
|
||||
}
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let parsed = await response.json();
|
||||
|
||||
if (parsed.message_code === 0) {
|
||||
return parsed.message_body;
|
||||
} else {
|
||||
throw null;
|
||||
}
|
||||
} else {
|
||||
throw (`${response.status}`);
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ const checkout = new YooCheckout({ shopId: '1276731', secretKey: 'test_0Yns_0NHV
|
||||
|
||||
export async function makePaymentOperation(email: string, tariffId: number, operationUuid: string, operationType: 'TARIFF' | 'TOKEN') {
|
||||
const formData = new FormData();
|
||||
const token = await getSessionData('token');
|
||||
|
||||
formData.append('email', email);
|
||||
formData.append('tariffId', tariffId.toString());
|
||||
@@ -17,7 +18,10 @@ export async function makePaymentOperation(email: string, tariffId: number, oper
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/payments/create`, {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
body: formData,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
@@ -79,6 +83,7 @@ export async function yooKasaCreatePayment(amount: number, tariff: number, opera
|
||||
|
||||
export async function fetchPaymentsHistory() {
|
||||
const email = await getSessionData('email');
|
||||
const token = await getSessionData('token');
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/v1/data`, {
|
||||
@@ -93,7 +98,8 @@ export async function fetchPaymentsHistory() {
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json'
|
||||
'Accept': 'application/json',
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
export async function POST(request: Request) {
|
||||
const { code } = await request.json();
|
||||
|
||||
try {
|
||||
const response = await fetch('https://oauth.yandex.ru/token', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code,
|
||||
client_id: 'b356a7ceac0f4c50a3b434220f86bce0',
|
||||
client_secret: '0800f7774f7d4e19ad4b434f10afd4ab',
|
||||
})
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log(data);
|
||||
|
||||
if (data.access_token) {
|
||||
const userInfo = await fetch('https://login.yandex.ru/info?format=json', {
|
||||
headers: { 'Authorization': `OAuth ${data.access_token}` }
|
||||
}).then(r => r.json());
|
||||
|
||||
console.log(userInfo);
|
||||
|
||||
// Тут мне нужно будет отправить сделать запрос в бек с userInfo и там уже редиректить на дешборд
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Token exchange failed:', error);
|
||||
}
|
||||
|
||||
return NextResponse.json({ success: false }, { status: 400 });
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
interface NotificationTextHandlerProps {
|
||||
notificationType: string
|
||||
notificationMessage: string
|
||||
notificationContext: Record<string, string>
|
||||
}
|
||||
|
||||
export function NotificationTextHandler({
|
||||
notificationType,
|
||||
notificationMessage,
|
||||
notificationContext
|
||||
}: NotificationTextHandlerProps) {
|
||||
const t = useTranslations('Notifications');
|
||||
|
||||
const getNotificationText = () => {
|
||||
if (!notificationType) {
|
||||
return t.has(notificationMessage) ? t(notificationMessage) : notificationMessage
|
||||
}
|
||||
|
||||
switch (notificationType) {
|
||||
case 'USER_VERIFIED':
|
||||
return t('user-verified', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
updateDate: notificationContext?.update_date || t('not-specified-date')
|
||||
});
|
||||
|
||||
case 'MONITORING_RESULT':
|
||||
return t('monitoring-result', {
|
||||
message: notificationContext?.message || t('message-missing')
|
||||
});
|
||||
|
||||
case 'COMPLAINT_STATUS_CHANGED':
|
||||
return t('complaint-status-changed', {
|
||||
newStatus: notificationContext?.new_status || t('not-specified'),
|
||||
oldStatus: notificationContext?.old_status || t('not-specified'),
|
||||
violation: notificationContext?.violation || t('not-specified'),
|
||||
file: notificationContext?.file || t('not-specified')
|
||||
});
|
||||
|
||||
case 'FILE_ADDED_TO_SYSTEM':
|
||||
return t('file-added-to-system', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
fileName: notificationContext?.file_name || t('not-specified-file-name')
|
||||
});
|
||||
|
||||
case 'FILE_MODERATION_EVENT':
|
||||
return t('file-moderation-event', {
|
||||
fileName: notificationContext?.file_name || t('not-specified-file-name'),
|
||||
oldStatus: notificationContext?.old_status || t('not-specified'),
|
||||
newStatus: notificationContext?.new_status || t('not-specified'),
|
||||
comment: notificationContext?.comment || t('no-comment')
|
||||
});
|
||||
|
||||
case 'PAYMENT_RESULT':
|
||||
return t('payment-result', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
amount: notificationContext?.amount || '0'
|
||||
});
|
||||
|
||||
case 'REFERRAL_REGISTERED':
|
||||
return t('referral-registered', {
|
||||
referralUser: notificationContext?.referral_user || t('unknown-user')
|
||||
});
|
||||
|
||||
case 'SEARCH_RESULT':
|
||||
return t('search-result', {
|
||||
startDate: notificationContext?.start_date || t('not-specified-date'),
|
||||
endDate: notificationContext?.end_date || t('not-specified-date'),
|
||||
searchType: notificationContext?.search_type || t('not-specified'),
|
||||
count: notificationContext?.count || '0'
|
||||
});
|
||||
|
||||
case 'SEARCH_OPERATION_FAILED':
|
||||
return t('search-operation-failed', {
|
||||
status: notificationContext?.status || t('not-specified'),
|
||||
errorMessage: notificationContext?.error_message || t('error-not-described'),
|
||||
message: notificationContext?.message || t('message-missing'),
|
||||
reason: notificationContext?.reason || t('reason-not-specified'),
|
||||
nextRun: notificationContext?.next_run || t('not-scheduled')
|
||||
});
|
||||
|
||||
case 'TOKEN_NOT_FOUND':
|
||||
return t('token-not-found', {
|
||||
currentTokens: notificationContext?.current_tokens || '0',
|
||||
needTokens: notificationContext?.need_tokens || t('not-specified'),
|
||||
message: notificationContext?.message || t('message-missing'),
|
||||
nextRun: notificationContext?.next_run || t('not-scheduled')
|
||||
});
|
||||
|
||||
default:
|
||||
return notificationMessage || t('notification-without-text');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
{getNotificationText()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { usePdf } from '@/app/contexts/PdfContext';
|
||||
import { requestFileAsBuffer } from '@/app/actions/trackingActions';
|
||||
|
||||
interface PdfViewerProps {
|
||||
fileId: string;
|
||||
fileName?: string;
|
||||
showDownload?: boolean;
|
||||
}
|
||||
|
||||
export default function PdfViewer({ fileId, fileName = 'document.pdf', showDownload }: PdfViewerProps) {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [totalPages, setTotalPages] = useState(0);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [fullscreen, setFullscreen] = useState(false);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const pdfDocumentRef = useRef<any>(null);
|
||||
const { setPdfData } = usePdf();
|
||||
|
||||
useEffect(() => {
|
||||
let isMounted = true;
|
||||
let objectUrl: string | null = null;
|
||||
|
||||
const loadAndDisplayPdf = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const pdfData = await requestFileAsBuffer(fileId);
|
||||
|
||||
if (!pdfData) {
|
||||
throw new Error('Failed to load document');
|
||||
}
|
||||
|
||||
//это нужно для скачивания так-как иначе pdfData теряется для скачивания
|
||||
if (showDownload) {
|
||||
const pdfDataForDownload = pdfData.slice(0);
|
||||
setPdfData(pdfDataForDownload, fileName);
|
||||
}
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
const pdfjs = await import('pdfjs-dist');
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
|
||||
'pdfjs-dist/build/pdf.worker.mjs',
|
||||
import.meta.url
|
||||
).toString();
|
||||
|
||||
const loadingTask = pdfjs.getDocument({
|
||||
data: pdfData,
|
||||
useSystemFonts: true,
|
||||
disableFontFace: false,
|
||||
});
|
||||
|
||||
const pdfDocument = await loadingTask.promise;
|
||||
|
||||
if (!isMounted) return;
|
||||
|
||||
pdfDocumentRef.current = pdfDocument;
|
||||
setTotalPages(pdfDocument.numPages);
|
||||
setCurrentPage(1);
|
||||
|
||||
await renderPage(pdfDocument, 1);
|
||||
|
||||
} catch (err) {
|
||||
console.error('Error loading PDF:', err);
|
||||
if (isMounted) {
|
||||
setError('Не удалось загрузить документ');
|
||||
}
|
||||
} finally {
|
||||
if (isMounted) {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loadAndDisplayPdf();
|
||||
|
||||
return () => {
|
||||
isMounted = false;
|
||||
if (pdfDocumentRef.current) {
|
||||
pdfDocumentRef.current.destroy();
|
||||
}
|
||||
if (objectUrl) {
|
||||
URL.revokeObjectURL(objectUrl);
|
||||
}
|
||||
};
|
||||
}, [fileId, setPdfData]);
|
||||
|
||||
const renderPage = async (pdfDocument: any, pageNum: number) => {
|
||||
if (!canvasRef.current) return;
|
||||
|
||||
try {
|
||||
const page = await pdfDocument.getPage(pageNum);
|
||||
|
||||
const container = canvasRef.current.parentElement;
|
||||
|
||||
if (!container) return;
|
||||
|
||||
const containerWidth = container.clientWidth || 800;
|
||||
|
||||
// Если ширина контейнера 0, откладываем рендер
|
||||
if (containerWidth === 0) {
|
||||
console.log('Container width is 0, waiting...');
|
||||
setTimeout(() => renderPage(pdfDocument, pageNum), 100);
|
||||
return;
|
||||
}
|
||||
|
||||
const originalViewport = page.getViewport({ scale: 1 });
|
||||
const scale = containerWidth / originalViewport.width;
|
||||
const viewport = page.getViewport({ scale: Math.min(scale, 1.5) });
|
||||
|
||||
const canvas = canvasRef.current;
|
||||
const context = canvas.getContext('2d');
|
||||
|
||||
canvas.height = viewport.height;
|
||||
canvas.width = viewport.width;
|
||||
|
||||
const renderContext = {
|
||||
canvasContext: context!,
|
||||
viewport: viewport,
|
||||
};
|
||||
|
||||
await page.render(renderContext).promise;
|
||||
} catch (err) {
|
||||
console.log('Error rendering page:', err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!pdfDocumentRef.current || loading) return;
|
||||
|
||||
const checkContainer = () => {
|
||||
const container = canvasRef.current?.parentElement;
|
||||
if (container && container.clientWidth > 0) {
|
||||
renderPage(pdfDocumentRef.current, currentPage);
|
||||
} else {
|
||||
setTimeout(checkContainer, 100);
|
||||
}
|
||||
};
|
||||
|
||||
checkContainer();
|
||||
}, [pdfDocumentRef.current, loading]);
|
||||
|
||||
const goToPrevPage = async () => {
|
||||
if (pdfDocumentRef.current && currentPage > 1) {
|
||||
const newPage = currentPage - 1;
|
||||
setCurrentPage(newPage);
|
||||
await renderPage(pdfDocumentRef.current, newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const goToNextPage = async () => {
|
||||
if (pdfDocumentRef.current && currentPage < totalPages) {
|
||||
const newPage = currentPage + 1;
|
||||
setCurrentPage(newPage);
|
||||
await renderPage(pdfDocumentRef.current, newPage);
|
||||
}
|
||||
};
|
||||
|
||||
const toggleFullscreen = () => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
if (!document.fullscreenElement) {
|
||||
containerRef.current.requestFullscreen();
|
||||
setFullscreen(true);
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
setFullscreen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleFullscreenChange = () => {
|
||||
setFullscreen(!!document.fullscreenElement);
|
||||
if (pdfDocumentRef.current && !document.fullscreenElement) {
|
||||
setTimeout(() => renderPage(pdfDocumentRef.current, currentPage), 100);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('fullscreenchange', handleFullscreenChange);
|
||||
return () => document.removeEventListener('fullscreenchange', handleFullscreenChange);
|
||||
}, [currentPage]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (pdfDocumentRef.current && !loading) {
|
||||
renderPage(pdfDocumentRef.current, currentPage);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [currentPage, loading]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="prev-wrap">
|
||||
<div className="pdf-loading">Загрузка документа...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="prev-wrap">
|
||||
<div className="pdf-error">{error}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="pdf-container">
|
||||
<div className="prev-wrap">
|
||||
<canvas ref={canvasRef} className="pdf-canvas" />
|
||||
</div>
|
||||
{totalPages > 0 && (
|
||||
<div className="document-controls">
|
||||
<div className="document-controls__nav">
|
||||
<button
|
||||
className="document-controls__prev"
|
||||
onClick={goToPrevPage}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
← Пред.
|
||||
</button>
|
||||
<span className="document-controls__current-page">
|
||||
{currentPage} / {totalPages}
|
||||
</span>
|
||||
<button
|
||||
className="document-controls__next"
|
||||
onClick={goToNextPage}
|
||||
disabled={currentPage === totalPages}
|
||||
>
|
||||
След. →
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
className="document-controls__fullscreen"
|
||||
onClick={toggleFullscreen}
|
||||
>
|
||||
{fullscreen ? 'Выйти' : 'На весь экран'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -602,15 +602,32 @@ export default function UploadSectionFile({
|
||||
</>
|
||||
)
|
||||
}
|
||||
<div>
|
||||
<span className={
|
||||
`upload-progress ${file.status === 'completed' ? 'text-green-600' :
|
||||
file.status === 'uploading' ? 'text-blue-600' :
|
||||
'text-gray-600'
|
||||
}`
|
||||
}>
|
||||
{file.progress} %
|
||||
</span>
|
||||
<div
|
||||
className="flex"
|
||||
>
|
||||
{file.status !== 'error' && (
|
||||
<div className="upload-progress-circle">
|
||||
<svg viewBox="0 0 36 36" className="circular-chart">
|
||||
<path
|
||||
className="circle-bg"
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke="#e5e7eb"
|
||||
strokeWidth="3"
|
||||
/>
|
||||
<path
|
||||
className="circle"
|
||||
d="M18 2.0845 a 15.9155 15.9155 0 0 1 0 31.831 a 15.9155 15.9155 0 0 1 0 -31.831"
|
||||
fill="none"
|
||||
stroke={file.status === 'completed' ? '#10b981' : file.status === 'uploading' ? '#155dfc' : '#6b7280'}
|
||||
strokeWidth="4"
|
||||
strokeDasharray={`${file.progress}, 100`}
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
<span className="circle-percentage">{file.progress}%</span>
|
||||
</div>
|
||||
)}
|
||||
< button
|
||||
className="btn btn-cancel"
|
||||
onClick={() => handleClearFile(file.uploadId || file.name)}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface CopyLinkButtonProps {
|
||||
className?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export function CopyLinkButton({ className = 'btn btn-out', children = 'Скопировать ссылку' }: CopyLinkButtonProps) {
|
||||
|
||||
const copyCurrentPageLink = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(window.location.href);
|
||||
toast.success('Ссылка скопирована');
|
||||
} catch (err) {
|
||||
toast.error('Не удалось скопировать ссылку');
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
className={className}
|
||||
onClick={copyCurrentPageLink}
|
||||
type="button"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import { downloadTrackingFile } from '@/app/actions/trackingActions';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
interface DownloadButtonProps {
|
||||
fileId: string;
|
||||
fileName?: string;
|
||||
}
|
||||
|
||||
export function DownloadButton({ fileId, fileName }: DownloadButtonProps) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const handleDownload = async () => {
|
||||
try {
|
||||
const result = await downloadTrackingFile(fileId, fileName);
|
||||
|
||||
if (!result) {
|
||||
throw 'failed-to-download-file'
|
||||
}
|
||||
|
||||
const byteCharacters = atob(result.data);
|
||||
const byteNumbers = new Array(byteCharacters.length);
|
||||
for (let i = 0; i < byteCharacters.length; i++) {
|
||||
byteNumbers[i] = byteCharacters.charCodeAt(i)
|
||||
}
|
||||
|
||||
const byteArray = new Uint8Array(byteNumbers);
|
||||
const blob = new Blob([byteArray], { type: result.contentType });
|
||||
|
||||
const url = window.URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = result.fileName;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
window.URL.revokeObjectURL(url);
|
||||
document.body.removeChild(a);
|
||||
toast.success(`${t('file-is-downloading')} - `);
|
||||
} catch (error) {
|
||||
toast.error(t('failed-to-download-file'));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<button className="btn btn-out" onClick={handleDownload}>
|
||||
Скачать документ
|
||||
</button>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
'use client';
|
||||
|
||||
import Link from 'next/link';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
|
||||
export function LoginUserBage() {
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
if (!userData?.fullName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Link href="#" className="header-user">
|
||||
{userData?.fullName}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -48,11 +48,11 @@ export function MonitoringDropDown({ file }: {
|
||||
};
|
||||
}, [dropdownRef, setIsOpen, isOpen]);
|
||||
|
||||
/* useEffect(() => {
|
||||
useEffect(() => {
|
||||
if (file.monitoring) {
|
||||
setCurrentMonitoringStatus(file.monitoring ? file.monitoring : 'NONE');
|
||||
}
|
||||
}, [file.monitoring]); */
|
||||
}, [file.monitoring]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && buttonRef.current) {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function SelectedFilesAction({ filesId, callBack }: { filesId: Set<string>, callBack: (value: Set<string>) => void }) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
if (filesId.size === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function downloadHandler() {
|
||||
if (filesId.size) {
|
||||
const filesArray = Array.from(filesId);
|
||||
const filesString = filesArray.join(', ');
|
||||
toast.success(`${t('download')}: ${filesString}`)
|
||||
}
|
||||
}
|
||||
|
||||
function clearHandler() {
|
||||
callBack(new Set());
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="selected-files-wrapper"
|
||||
>
|
||||
<div
|
||||
className="selected-files-count"
|
||||
>
|
||||
{t('selected-files')}: {filesId.size}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="selected-files-action"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
downloadHandler();
|
||||
}}
|
||||
>
|
||||
{t('download-all')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
clearHandler();
|
||||
}}
|
||||
>
|
||||
{t('deselect')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
ColumnFiltersState,
|
||||
PaginationState
|
||||
} from '@tanstack/react-table';
|
||||
import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconInfo } from '@/app/ui/icons/icons';
|
||||
import { IconDoubleArrowRight, IconArrowRight, IconDoubleArrowLeft, IconArrowLeft, IconArrowUp, IconArrowDown, IconFilter, IconFileDownload, IconInfo, IconEye, IconCheck, IconImageFile, IconLink } from '@/app/ui/icons/icons';
|
||||
import { useTranslations, useLocale } from 'next-intl';
|
||||
import DropDownList from '@/app/components/DropDownList';
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query';
|
||||
@@ -29,6 +29,11 @@ import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||
import { useViewport } from '@/app/hooks/useViewport';
|
||||
import { downloadFile } from '@/app/actions/fileEntity';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import Link from 'next/link';
|
||||
import { SelectedFilesAction } from '@/app/components/tanstak-table/SelectedFilesActions';
|
||||
import Image from 'next/image';
|
||||
import { filePermisionChange } from '@/app/actions/trackingActions';
|
||||
import { FileAppealModalWindow } from '@/app/ui/modal-windows/file-appeal-modal-window';
|
||||
|
||||
export type FileItem = {
|
||||
id: string;
|
||||
@@ -37,11 +42,15 @@ export type FileItem = {
|
||||
violations?: number | undefined;
|
||||
size?: number | undefined;
|
||||
uploadDate?: number;
|
||||
status?: string;
|
||||
status: string;
|
||||
monitoring: string;
|
||||
protectStatus: string;
|
||||
_original?: ApiFile;
|
||||
supportId: number;
|
||||
thumbnailFileUrl: string;
|
||||
permissions: {
|
||||
DOWNLOAD: boolean;
|
||||
}
|
||||
};
|
||||
|
||||
type ApiFile = {
|
||||
@@ -56,8 +65,25 @@ type ApiFile = {
|
||||
protectStatus: string;
|
||||
supportId: number;
|
||||
fileName: string;
|
||||
thumbnailFileUrl: string;
|
||||
permissions: {
|
||||
DOWNLOAD: boolean;
|
||||
},
|
||||
appealInfos: AppealInfos[];
|
||||
};
|
||||
|
||||
export type AppealInfos = {
|
||||
additionalInfo: string | null;
|
||||
adminComment: null;
|
||||
appealId: string | null;
|
||||
appealReason: string | null;
|
||||
createdAt: string | null;
|
||||
fileId: string | null;
|
||||
fileName: string | null;
|
||||
resolvedAt: string | null;
|
||||
status: string;
|
||||
}
|
||||
|
||||
type ApiResponse = {
|
||||
files?: ApiFile[];
|
||||
total_count: number;
|
||||
@@ -71,7 +97,7 @@ const cutFileExtension = (fileName: string) => {
|
||||
return extension;
|
||||
}
|
||||
|
||||
export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
export default function TanstakFilesTable({ fileType, showFileLink }: { fileType: string, showFileLink?: boolean }) {
|
||||
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [dateFilter, setDateFilter] = useState<string>('month');
|
||||
@@ -80,9 +106,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
const [query, setQuery] = useState<string>('');
|
||||
/* const [query, setQuery] = useState<string>(''); */
|
||||
const [searchInputValue, setSearchInputValue] = useState<string>('');
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
|
||||
|
||||
const debouncedSetSearchQuery = useDebouncedCallback(
|
||||
(value: string) => {
|
||||
@@ -138,6 +165,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
const {
|
||||
data: apiResponse,
|
||||
isLoading,
|
||||
isFetching,
|
||||
isError,
|
||||
error,
|
||||
refetch
|
||||
@@ -182,7 +210,11 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
monitoring: item.monitoring,
|
||||
protectStatus: item.protectStatus,
|
||||
supportId: item.supportId,
|
||||
_original: item
|
||||
_original: item,
|
||||
thumbnailFileUrl: item.thumbnailFileUrl,
|
||||
permissions: {
|
||||
DOWNLOAD: item.permissions.DOWNLOAD,
|
||||
}
|
||||
}));
|
||||
|
||||
return {
|
||||
@@ -196,22 +228,6 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
placeholderData: (previousData) => previousData // для оптимистичных обновлений
|
||||
});
|
||||
|
||||
const {
|
||||
data: referralLink
|
||||
} = useQuery({
|
||||
queryKey: ['referralUserStats'],
|
||||
queryFn: () => {
|
||||
return fetchReferralUserStats();
|
||||
},
|
||||
select: (data) => {
|
||||
if (data?.referralLink) {
|
||||
return data?.referralLink;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Данные для таблицы
|
||||
@@ -230,9 +246,9 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
const viewport = useViewport();
|
||||
|
||||
const getMaxLengthByWidth = (): number => {
|
||||
if (viewport.device === 'MOBILE') return 26;
|
||||
if (viewport.device === 'TABLET') return 32;
|
||||
if (viewport.device === 'DESKTOP') return 64;
|
||||
if (viewport.device === 'MOBILE') return 22;
|
||||
if (viewport.device === 'TABLET') return 30;
|
||||
if (viewport.device === 'DESKTOP') return 50;
|
||||
return 100;
|
||||
};
|
||||
|
||||
@@ -260,9 +276,112 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
return nameWithoutExtension.substring(0, maxNameLength) + '...' /* + extension */;
|
||||
}
|
||||
|
||||
function selectHandler(fileId: string) {
|
||||
|
||||
setSelectedFiles((prev) => {
|
||||
const newSet = new Set(prev)
|
||||
if (newSet.has(fileId)) {
|
||||
newSet.delete(fileId)
|
||||
} else {
|
||||
newSet.add(fileId)
|
||||
}
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const selectAllHandler = useCallback(() => {
|
||||
if (!apiResponse?.items?.length) return;
|
||||
|
||||
setSelectedFiles((prev) => {
|
||||
const allIds = new Set(apiResponse.items.map(item => item.id));
|
||||
|
||||
const isAllSelected = Array.from(allIds).every(id => prev.has(id));
|
||||
|
||||
if (isAllSelected) {
|
||||
const newSet = new Set(prev);
|
||||
allIds.forEach(id => newSet.delete(id));
|
||||
return newSet;
|
||||
} else {
|
||||
|
||||
return new Set([...prev, ...allIds]);
|
||||
}
|
||||
});
|
||||
}, [apiResponse]);
|
||||
|
||||
const StatusBadge = ({ status, protectStatus }: { status: string, protectStatus: string }) => {
|
||||
|
||||
switch (status) {
|
||||
case 'ACTIVE':
|
||||
return <span className="table-item-status-badge protected">
|
||||
{protectStatus ? t(protectStatus) : t('error')}
|
||||
</span>;
|
||||
case 'MODERATION':
|
||||
return <span className="table-item-status-badge moderation">
|
||||
{t('pending-moderation')}
|
||||
</span>;
|
||||
case 'BLOCKED':
|
||||
return <span className="table-item-status-badge blocked">
|
||||
{t('blocked')}
|
||||
</span>;
|
||||
case 'REMOVED':
|
||||
return <span className="table-item-status-badge blocked">
|
||||
{t('file-has-been-deleted')}
|
||||
</span>;
|
||||
default:
|
||||
return <span className="table-item-status-badge protected">
|
||||
{protectStatus ? t(protectStatus) : t('error')}
|
||||
</span>;
|
||||
}
|
||||
}
|
||||
|
||||
const isAllSelected = useMemo(() => {
|
||||
if (!apiResponse?.items?.length) return false;
|
||||
const allIds = new Set(apiResponse.items.map(item => item.id));
|
||||
return Array.from(allIds).every(id => selectedFiles.has(id));
|
||||
}, [apiResponse, selectedFiles]);
|
||||
|
||||
// Определение колонок
|
||||
const columns = useMemo<ColumnDef<FileItem>[]>(
|
||||
() => [
|
||||
{
|
||||
accessorKey: 'selectd',
|
||||
header: ({ column }) => (
|
||||
<div
|
||||
className="column"
|
||||
onClick={() => {
|
||||
selectAllHandler();
|
||||
}}
|
||||
>
|
||||
<span>
|
||||
<button
|
||||
className={`table-item-checkbox ${isAllSelected ? 'selected' : ''}`}
|
||||
>
|
||||
{isAllSelected && <IconCheck />}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div
|
||||
className="text-center table-item table-item-id"
|
||||
onClick={() => {
|
||||
selectHandler(row.original.id);
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className={`table-item-checkbox ${selectedFiles.has(row.original.id) ? 'selected' : ''}`}
|
||||
>
|
||||
{selectedFiles.has(row.original.id) && (
|
||||
<IconCheck />
|
||||
)}
|
||||
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'id',
|
||||
header: ({ column }) => (
|
||||
@@ -327,16 +446,32 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="flex items-center space-x-2 table-item table-item-file-name">
|
||||
<FileTypeIcon type={row.original.fileType} />
|
||||
{row.original.fileType === 'image' && row.original?.thumbnailFileUrl.length !== 0 ? (
|
||||
<span
|
||||
className="table-item-file-name-image-wrapper"
|
||||
>
|
||||
<Image
|
||||
sizes="40px"
|
||||
src={row.original?.thumbnailFileUrl}
|
||||
alt={row.original?.fileName}
|
||||
unoptimized
|
||||
fill
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = '/images/no-image.png';
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<FileTypeIcon type={row.original.fileType} />
|
||||
)}
|
||||
<div>
|
||||
<div className="font-medium w-full truncate" title={row.original.fileName}>
|
||||
<span className="font-semibold">
|
||||
{cutFileName(row.original.fileName)}
|
||||
</span>
|
||||
<br />
|
||||
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <span className="table-item-protected">
|
||||
{row.original?.protectStatus ? t(row.original?.protectStatus) : t('error')}
|
||||
</span>
|
||||
<span className="table-item-extension">{cutFileExtension(row.original.fileName)}</span> <StatusBadge status={row.original.status} protectStatus={row.original.protectStatus} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -466,6 +601,33 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
accessorKey: 'downloadPermision',
|
||||
header: ({ column }) => (
|
||||
<div className="column">
|
||||
<span>
|
||||
Можно скачать по ссылке
|
||||
</span>
|
||||
</div>
|
||||
),
|
||||
cell: ({ row }) => {
|
||||
return (
|
||||
<div className="text-center font-semibold table-item">
|
||||
<button
|
||||
onClick={() => changeDownloadPermition(row.original.id, row.original.permissions.DOWNLOAD)}
|
||||
disabled={isFileLoading}
|
||||
className={`toggle-switch ${row.original.permissions?.DOWNLOAD ? 'active' : 'inactive'}`}
|
||||
title={'change-permission'}
|
||||
>
|
||||
<span className="toggle-slider"></span>
|
||||
<span className="toggle-label">
|
||||
{row.original.permissions?.DOWNLOAD ? 'Да' : 'Нет'}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'actions',
|
||||
header: () => {
|
||||
@@ -477,37 +639,75 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
},
|
||||
cell: ({ row }) => (
|
||||
<div className="actions">
|
||||
<div className="actions-group">
|
||||
</div>
|
||||
<div className="actions-group">
|
||||
{row.original.protectStatus === 'PROTECTED' && (
|
||||
<button
|
||||
onClick={() => handleDownload(row.original)}
|
||||
disabled={isFileLoading}
|
||||
className="table-action-download"
|
||||
title={t('download')}
|
||||
{(showFileLink && row.original.fileType === 'document') ? (
|
||||
<div className="actions-group">
|
||||
<Link
|
||||
href={`/watch-doc?docid=${row.original.id}`}
|
||||
className="bg-indigo-500 hover:bg-indigo-600"
|
||||
title={t('view')}
|
||||
target='_blank'
|
||||
>
|
||||
<IconFileDownload />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
handleView(row.original)
|
||||
}}
|
||||
className="table-action-view"
|
||||
title={t('make-check')}
|
||||
>
|
||||
<IconInfo />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<IconLink />
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* "BLOCKED" "REMOVED" */}
|
||||
{(row.original.status !== 'BLOCKED' && row.original.status !== 'REMOVED') ? (
|
||||
<>
|
||||
<div className="actions-group">
|
||||
<Link
|
||||
href={`/pages/file/${row.original.id}`}
|
||||
className="bg-violet-500 hover:bg-violet-600"
|
||||
title={t('view')}
|
||||
>
|
||||
<IconEye />
|
||||
</Link>
|
||||
</div>
|
||||
<div className="actions-group">
|
||||
{row.original.protectStatus === 'PROTECTED' && (
|
||||
<button
|
||||
onClick={() => handleDownload(row.original)}
|
||||
disabled={isFileLoading}
|
||||
className="table-action-download"
|
||||
title={t('download')}
|
||||
>
|
||||
<IconFileDownload />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
onClick={() => {
|
||||
handleView(row.original)
|
||||
}}
|
||||
className="table-action-view"
|
||||
title={t('make-check')}
|
||||
>
|
||||
<IconInfo />
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="actions-group">
|
||||
<button
|
||||
onClick={() => {
|
||||
handleAppeal(row.original)
|
||||
}}
|
||||
className="table-action-appeal"
|
||||
title={t('file-an-appeal')}
|
||||
>
|
||||
<IconInfo />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
enableSorting: false,
|
||||
enableColumnFilter: false,
|
||||
},
|
||||
],
|
||||
[viewport.device]
|
||||
[viewport.device, selectedFiles, selectAllHandler]
|
||||
);
|
||||
|
||||
const handleView = async (file: FileItem) => {
|
||||
@@ -521,6 +721,16 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
setOpenWindow(true);
|
||||
};
|
||||
|
||||
const handleAppeal = async (file: FileItem) => {
|
||||
|
||||
setOpenWindowChildren(() => {
|
||||
return (
|
||||
<FileAppealModalWindow fileId={file.id} appeal={file._original?.appealInfos} setWindowClose={setOpenWindow} setWindowChildren={setOpenWindowChildren} />
|
||||
)
|
||||
})
|
||||
setOpenWindow(true);
|
||||
};
|
||||
|
||||
const handleDownload = async (file: FileItem) => {
|
||||
setIsFileLoading(true)
|
||||
|
||||
@@ -555,6 +765,16 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
}
|
||||
}
|
||||
|
||||
const changeDownloadPermition = async (fileId: string, permission: boolean) => {
|
||||
const response = await filePermisionChange(fileId, !permission);
|
||||
if (response) {
|
||||
queryClient.invalidateQueries({ queryKey: ['userFilesData'] });
|
||||
toast.success('Разрешение на скачивание изменено.');
|
||||
} else {
|
||||
toast.warning('Не удалось изменить разрешение на скачивание.');
|
||||
}
|
||||
}
|
||||
|
||||
// Фильтрация по типу файла и дате
|
||||
const filteredData = useMemo(() => {
|
||||
let result = tableData;
|
||||
@@ -613,7 +833,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [query, typeFilter, dateFilter]);
|
||||
}, [/* query, */ typeFilter, dateFilter]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentPageRows = table.getRowModel().rows;
|
||||
@@ -631,6 +851,10 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
state: {
|
||||
sorting,
|
||||
pagination,
|
||||
columnVisibility: {
|
||||
monitoring: showFileLink ? false : true,
|
||||
downloadPermision: showFileLink ? true : false,
|
||||
}
|
||||
},
|
||||
pageCount: totalPages,
|
||||
manualPagination: true,
|
||||
@@ -681,6 +905,8 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
return t('protected-video')
|
||||
case 'audio':
|
||||
return t('protected-audio')
|
||||
case 'document':
|
||||
return t('protected-document')
|
||||
default:
|
||||
return t('table-description')
|
||||
}
|
||||
@@ -805,7 +1031,22 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
</tr>
|
||||
))}
|
||||
</thead>
|
||||
<tbody className="tanstak-table-body">
|
||||
<tbody className={`tanstak-table-body ${isFetching ? 'loading' : ''}`}>
|
||||
{isLoading && (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center relative h-12.5">
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
|
||||
{table.getRowModel().rows.length > 0 ? (
|
||||
table.getRowModel().rows.map(row => (
|
||||
<tr key={row.id}>
|
||||
@@ -820,9 +1061,11 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
))
|
||||
) : (
|
||||
<tr>
|
||||
<td colSpan={columns.length} className="text-center">
|
||||
{t('no-data-for-selected-filters')}
|
||||
</td>
|
||||
{!isLoading && (
|
||||
<td colSpan={columns.length} className="text-center">
|
||||
{t('no-data-for-selected-filters')}
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
@@ -836,10 +1079,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
{t('page')}{' '}
|
||||
<strong>
|
||||
{pagination.pageIndex + 1} {t('out-of')} {totalPages || 1}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="pagination-info-files">
|
||||
| {t('shown')} {tableData.length} {t('out-of')} {totalItems} {pluralizeFiles(totalItems)}
|
||||
</strong> | {t('total-files')}: {totalItems}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -904,6 +1144,7 @@ export default function TanstakFilesTable({ fileType }: { fileType: string }) {
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<SelectedFilesAction filesId={selectedFiles} callBack={setSelectedFiles} />
|
||||
</div >
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useRef, ReactNode } from 'react';
|
||||
|
||||
interface PdfContextType {
|
||||
downloadPdf: () => Promise<void>;
|
||||
setPdfData: (data: ArrayBuffer | null, fileName: string) => void;
|
||||
}
|
||||
|
||||
const PdfContext = createContext<PdfContextType | null>(null);
|
||||
|
||||
export function PdfProvider({ children }: { children: ReactNode }) {
|
||||
const pdfDataRef = useRef<ArrayBuffer | null>(null);
|
||||
const fileNameRef = useRef<string>('');
|
||||
|
||||
const setPdfData = (data: ArrayBuffer | null, fileName: string) => {
|
||||
pdfDataRef.current = data;
|
||||
fileNameRef.current = fileName;
|
||||
};
|
||||
|
||||
const downloadPdf = async () => {
|
||||
if (!pdfDataRef.current) {
|
||||
console.log('No PDF data available');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const blob = new Blob([pdfDataRef.current], { type: 'application/pdf' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const link = document.createElement('a');
|
||||
link.href = url;
|
||||
link.download = fileNameRef.current || 'document.pdf';
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
URL.revokeObjectURL(url);
|
||||
} catch (err) {
|
||||
console.log('Error downloading PDF:', err);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<PdfContext.Provider value={{ downloadPdf, setPdfData }}>
|
||||
{children}
|
||||
</PdfContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export function usePdf() {
|
||||
const context = useContext(PdfContext);
|
||||
if (!context) {
|
||||
throw new Error('usePdf must be used within PdfProvider');
|
||||
}
|
||||
return context;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchActiveNotifications, NotificationBody } from '@/app/actions/notificationActions';
|
||||
|
||||
export interface UseActiveNotifications {
|
||||
notifications: NotificationBody[],
|
||||
unreadCount: number
|
||||
}
|
||||
|
||||
export const useActiveNotifications = () => {
|
||||
return useQuery({
|
||||
queryKey: ['activeNotifications'],
|
||||
queryFn: () => {
|
||||
return fetchActiveNotifications()
|
||||
},
|
||||
select: (data: UseActiveNotifications | null) => {
|
||||
if (!data) {
|
||||
return {
|
||||
notifications: [],
|
||||
unreadCount: 0
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
refetchInterval: 10000
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,31 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchAllNotifications, NotificationBody } from '@/app/actions/notificationActions';
|
||||
|
||||
export interface UseAllNotifications {
|
||||
notifications: NotificationBody[],
|
||||
currentPage: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useAllNotifications = (page: number, size: number = 10) => {
|
||||
return useQuery({
|
||||
queryKey: ['allNotifications', page, size],
|
||||
queryFn: () => {
|
||||
return fetchAllNotifications(page, size)
|
||||
},
|
||||
select: (data: UseAllNotifications | null) => {
|
||||
if (!data) {
|
||||
return {
|
||||
notifications: [],
|
||||
currentPage: 0,
|
||||
totalElements: 0,
|
||||
totalPages: 0,
|
||||
}
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
refetchInterval: 10000
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,41 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getFileMatches } from '@/app/actions/violationActions';
|
||||
|
||||
export interface ViolationFileDetail {
|
||||
id: number;
|
||||
url: string;
|
||||
page_url: string;
|
||||
page_title: string;
|
||||
host: string;
|
||||
status: string;
|
||||
created_date: string;
|
||||
file_id: string;
|
||||
country: string;
|
||||
country_code: string;
|
||||
}
|
||||
|
||||
interface ViolationFile {
|
||||
violations: ViolationFileDetail[];
|
||||
total_elements: number;
|
||||
total_pages: number;
|
||||
current_page: number;
|
||||
page_size: number;
|
||||
has_next: boolean;
|
||||
has_previous: boolean;
|
||||
}
|
||||
|
||||
export const useFileViolations = (id: string, page: number, status?: string, violationId?: string, startDate?: string, endDate?: string,) => {
|
||||
return useQuery({
|
||||
queryKey: ['fileViolations', id, page, status, violationId, startDate, endDate],
|
||||
queryFn: () => {
|
||||
return getFileMatches(id, page, status, violationId, startDate, endDate)
|
||||
},
|
||||
select: (data: ViolationFile | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchHistoryView } from '@/app/actions/trackingActions';
|
||||
|
||||
export interface HistoryViewItem {
|
||||
city: string;
|
||||
country: string;
|
||||
viewer: string;
|
||||
document: string;
|
||||
view_date: string;
|
||||
}
|
||||
|
||||
export interface UseHistoryView {
|
||||
documentsCount: number;
|
||||
history: HistoryViewItem[];
|
||||
pageNumber: number;
|
||||
pageSize: number;
|
||||
totalElements: number;
|
||||
totalPages: number;
|
||||
uniqueCityCount: number;
|
||||
uniqueCountryCount: number;
|
||||
}
|
||||
|
||||
export const useHistoryView = (
|
||||
page: number,
|
||||
size: number,
|
||||
locale: string,
|
||||
sortDirection?: 'asc' | 'desc',
|
||||
sortBy?: string
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ['violationStatistic', page, size, locale, sortDirection, sortBy],
|
||||
queryFn: () => {
|
||||
return fetchHistoryView(page, size, locale, sortDirection, sortBy)
|
||||
},
|
||||
select: (data: UseHistoryView | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false,
|
||||
placeholderData: (previousData) => previousData // для оптимистичных обновлений
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchLastClaims, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
|
||||
|
||||
export interface Case {
|
||||
amount: number;
|
||||
content: null | string;
|
||||
created_at: string;
|
||||
description: string;
|
||||
id: number;
|
||||
lawyer: null | string;
|
||||
name: string;
|
||||
page_number: number;
|
||||
page_size: number;
|
||||
priority: "HIGH" | "MEDIUM" | "LOW";
|
||||
total_elements: number;
|
||||
total_pages: number;
|
||||
type: "ACTIVE" | "INACTIVE" | string;
|
||||
updated_at: string;
|
||||
violation_id: number;
|
||||
file_id: string;
|
||||
}
|
||||
export interface useLastCase {
|
||||
content: Case[],
|
||||
page: number,
|
||||
size: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useLastCases = (params: ComplaintsCaseQueryParams) => {
|
||||
return useQuery({
|
||||
queryKey: ['lastCases', params.page, params.size, params.sort_by, params.sort_direction],
|
||||
queryFn: () => {
|
||||
return fetchLastClaims(params)
|
||||
},
|
||||
select: (data: useLastCase | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchLastComplaint, ComplaintsCaseQueryParams } from '@/app/actions/violationActions';
|
||||
|
||||
export interface Complaint {
|
||||
complaint_text: string;
|
||||
created_at: string;
|
||||
email: string;
|
||||
id: number;
|
||||
not_moderated: boolean;
|
||||
status: "CREATED" | string;
|
||||
updated_at: string;
|
||||
violation_id: number;
|
||||
file_id: string;
|
||||
}
|
||||
export interface useLastComplaint {
|
||||
content: Complaint[],
|
||||
page: number,
|
||||
size: number,
|
||||
totalElements: number,
|
||||
totalPages: number,
|
||||
}
|
||||
|
||||
export const useLastComplaints = (params: ComplaintsCaseQueryParams) => {
|
||||
return useQuery({
|
||||
queryKey: ['lastComplaints', params.page, params.size, params.sort_by, params.sort_direction],
|
||||
queryFn: () => {
|
||||
return fetchLastComplaint(params)
|
||||
},
|
||||
select: (data: useLastComplaint | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
placeholderData: (previousData) => previousData,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,24 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchMonitoringInfo } from '@/app/actions/monitoringActions';
|
||||
|
||||
interface MonitoringCount {
|
||||
day: number;
|
||||
month: number;
|
||||
weekly: number;
|
||||
}
|
||||
|
||||
export const useMonitoringCount = () => {
|
||||
return useQuery({
|
||||
queryKey: ['monitoringCount'],
|
||||
queryFn: () => {
|
||||
return fetchMonitoringInfo()
|
||||
},
|
||||
select: (data: MonitoringCount | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchMonitoringStats } from '@/app/actions/monitoringActions';
|
||||
|
||||
interface MonitoringStats {
|
||||
total_searches: number;
|
||||
searches_by_status: {
|
||||
processing: number;
|
||||
completed: number;
|
||||
failed: number;
|
||||
total: number;
|
||||
};
|
||||
recent_searches: any[];
|
||||
}
|
||||
|
||||
export const useMonitoringStats = () => {
|
||||
return useQuery({
|
||||
queryKey: ['monitoringStats'],
|
||||
queryFn: () => {
|
||||
return fetchMonitoringStats()
|
||||
},
|
||||
select: (data: MonitoringStats | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,26 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getPaymentsMethods } from '@/app/actions/paymentAction';
|
||||
|
||||
export interface PaymentsMethods {
|
||||
cardType: string,
|
||||
expiryMonth: string,
|
||||
expiryYear: string,
|
||||
lastFour: string,
|
||||
paymentMethodId: string
|
||||
}
|
||||
|
||||
export const usePaymentsMethods = () => {
|
||||
return useQuery({
|
||||
queryKey: ['paymentsMethods'],
|
||||
queryFn: () => {
|
||||
return getPaymentsMethods()
|
||||
},
|
||||
select: (data: PaymentsMethods[] | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchTrackingStats } from '@/app/actions/trackingActions';
|
||||
|
||||
export interface useTrackingStats {
|
||||
totalViews: number,
|
||||
uniqueIps: number,
|
||||
uniqueUsers: number,
|
||||
documentsCount: number,
|
||||
uniqueCountryCount: number,
|
||||
uniqueCityCount: number,
|
||||
firstViewDate: string,
|
||||
lastViewDate: string
|
||||
}
|
||||
|
||||
export const useTrackingStats = () => {
|
||||
return useQuery({
|
||||
queryKey: ['trackingStats'],
|
||||
queryFn: () => {
|
||||
return fetchTrackingStats()
|
||||
},
|
||||
select: (data: useTrackingStats | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -39,6 +39,7 @@ export interface UserData {
|
||||
tariffs: Tariff[];
|
||||
tariffInfo: TariffInfo;
|
||||
permission: number;
|
||||
verifiedStatus?: 'unknow' | 'NOT_VERIFIED' | 'VERIFICATION_IN_PROGRESS' | 'VERIFIED' | 'VERIFICATION_FAILED';
|
||||
}
|
||||
|
||||
export const useUserProfile = () => {
|
||||
@@ -72,6 +73,7 @@ export const useUserProfile = () => {
|
||||
maxFileCounts: 0,
|
||||
},
|
||||
permission: 0,
|
||||
verifiedStatus: 'unknow'
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export const useUserPaymentsData = () => {
|
||||
|
||||
return data;
|
||||
},
|
||||
refetchInterval: 30000,
|
||||
refetchInterval: 10000,
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { fetchViolationFinalSummaryStatistic } from '@/app/actions/violationActions';
|
||||
|
||||
export interface UseViolationFinalSummaryStatistic {
|
||||
complaints_without_case: number;
|
||||
total_complaints: number;
|
||||
total_law_cases: number;
|
||||
total_violations: number;
|
||||
violations_with_complaint: number;
|
||||
violations_without_complaint: number;
|
||||
}
|
||||
|
||||
export const useViolationFinalSummaryStatistic = () => {
|
||||
return useQuery({
|
||||
queryKey: ['violationFinalSummaryStatistic'],
|
||||
queryFn: () => {
|
||||
return fetchViolationFinalSummaryStatistic()
|
||||
},
|
||||
select: (data: UseViolationFinalSummaryStatistic | null) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
};
|
||||
@@ -5,14 +5,16 @@ export interface UseViolationStatistic {
|
||||
total_violations: number,
|
||||
new_violations: number,
|
||||
in_progress_violations: number,
|
||||
resolved_violations: number
|
||||
resolved_violations: number,
|
||||
total_complaints: number,
|
||||
total_law_cases: number
|
||||
}
|
||||
|
||||
export const useViolationStatistic = () => {
|
||||
export const useViolationStatistic = (id?: string) => {
|
||||
return useQuery({
|
||||
queryKey: ['violationStatistic'],
|
||||
queryKey: ['violationStatistic', id],
|
||||
queryFn: () => {
|
||||
return fetchViolationStatistic()
|
||||
return fetchViolationStatistic(id)
|
||||
},
|
||||
select: (data: UseViolationStatistic | null) => {
|
||||
if (!data) {
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
'use client';
|
||||
|
||||
import Providers from './getQueryServer';
|
||||
|
||||
export function WatchDocProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Providers>
|
||||
{children}
|
||||
</Providers>
|
||||
);
|
||||
}
|
||||
@@ -1,34 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
isServer,
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
} from '@tanstack/react-query'
|
||||
|
||||
import { getQueryClient } from '@/app/providers/getQueryClient';
|
||||
|
||||
/* export function makeQueryClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: {
|
||||
staleTime: 60 * 1000,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
let browserQueryClient: QueryClient | undefined = undefined
|
||||
|
||||
function getQueryClient() {
|
||||
if (isServer) {
|
||||
return makeQueryClient()
|
||||
} else {
|
||||
if (!browserQueryClient) browserQueryClient = makeQueryClient()
|
||||
return browserQueryClient
|
||||
}
|
||||
} */
|
||||
|
||||
export default function Providers({ children }: { children: React.ReactNode }) {
|
||||
const queryClient = getQueryClient();
|
||||
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
@use './variable.scss' as v;
|
||||
|
||||
.loading-animation {
|
||||
display: block;
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
.global-spinner {
|
||||
display: block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 4px solid #e5e7eb;
|
||||
border-top: 4px solid #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
&.large {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
|
||||
&::before {
|
||||
border: 10px solid #e5e7eb;
|
||||
border-top: 10px solid #667eea;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.loading-placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
/* min-width: 80px;
|
||||
min-height: 80px; */
|
||||
|
||||
&.start {
|
||||
justify-content: start;
|
||||
}
|
||||
|
||||
.loading-animation {
|
||||
display: block;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
left: auto;
|
||||
top: auto;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.global-spinner {
|
||||
display: inline-block;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
@use './reports.scss';
|
||||
@use './privacy-and-terms-pages.scss';
|
||||
@use './VKLogin.scss';
|
||||
@use './animation.scss';
|
||||
@use './violation-details.scss';
|
||||
@use './watch-doc.scss';
|
||||
@use './tracking-statistic.scss';
|
||||
|
||||
.page-title {
|
||||
font-size: 30px;
|
||||
@@ -151,10 +155,50 @@
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.btn,
|
||||
.btn-s,
|
||||
.btn-l {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.3s;
|
||||
text-decoration: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 12px 24px;
|
||||
font-size: 14px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.btn-s {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.btn-l {
|
||||
padding: 16px 32px;
|
||||
font-size: 16px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: linear-gradient(135deg, v.$p-color, v.$s-color);
|
||||
/* background: linear-gradient(135deg, v.$p-color, v.$s-color); */
|
||||
background: #6366f1;
|
||||
color: v.$white;
|
||||
border: 2px solid transparent;
|
||||
|
||||
@@ -179,6 +223,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: v.$bg-hover;
|
||||
color: v.$text-p;
|
||||
border: 2px solid v.$border-color-1;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
}
|
||||
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 2px solid v.$p-color;
|
||||
@@ -190,6 +244,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background: v.$color-warning;
|
||||
|
||||
&:hover {
|
||||
background: v.$color-warning-hover;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
@@ -728,7 +790,7 @@
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: #000000;
|
||||
background: v.$black;
|
||||
color: #FFFFFF;
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -52,4 +52,7 @@ path:focus {
|
||||
outline-style: none;
|
||||
}
|
||||
|
||||
@import "tailwindcss";
|
||||
@import "tailwindcss";
|
||||
@import 'swiper/css';
|
||||
@import 'swiper/css/navigation';
|
||||
@import 'swiper/css/pagination';
|
||||
+113
-29
@@ -4,11 +4,12 @@
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
align-items: center;
|
||||
margin-bottom: 30px;
|
||||
background: v.$white;
|
||||
padding: 20px 30px;
|
||||
border-radius: 20px;
|
||||
box-shadow: 0 4px 20px v.$shadow-1;
|
||||
max-width: 1920px;
|
||||
margin: 0 auto 30px;
|
||||
|
||||
@media (max-width: 490px) {
|
||||
padding: 20px 20px;
|
||||
@@ -49,6 +50,10 @@
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
/* &.active {
|
||||
background: v.$color-warning;
|
||||
} */
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,6 +90,11 @@
|
||||
@media (max-width: 440px) {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: v.$p-color;
|
||||
color: v.$white;
|
||||
}
|
||||
}
|
||||
|
||||
.tokens-count {
|
||||
@@ -110,7 +120,7 @@
|
||||
background: v.$white;
|
||||
border-radius: 12px;
|
||||
box-shadow: 10px 10px 20px 10px v.$shadow-1;
|
||||
min-width: 320px;
|
||||
min-width: 350px;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transform: translateY(-10px);
|
||||
@@ -124,8 +134,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.notification-dropdown {
|
||||
.notification-header {
|
||||
.notification {
|
||||
/* &-dropdown {} */
|
||||
|
||||
&-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid v.$b-color-1;
|
||||
display: flex;
|
||||
@@ -138,65 +150,137 @@
|
||||
font-weight: 600;
|
||||
color: v.$text-p;
|
||||
}
|
||||
|
||||
.notification-count {
|
||||
font-size: 12px;
|
||||
color: v.$p-color;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-list {
|
||||
max-height: 400px;
|
||||
&-count {
|
||||
font-size: 10px;
|
||||
background: v.$p-color;
|
||||
padding: 5px;
|
||||
color: v.$white;
|
||||
font-weight: 500;
|
||||
border-radius: 5px;
|
||||
margin-left: 5px;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
&-list {
|
||||
max-height: 600px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.notification-item {
|
||||
&-item {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
padding: 15px 20px;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid v.$b-color-1;
|
||||
transition: background-color 0.2s;
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
|
||||
&.loading {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.notification-title {
|
||||
&-title {
|
||||
display: block;
|
||||
font-size: 15px;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: v.$text-p;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.notification-icon {
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
&-content {
|
||||
width: 100%;
|
||||
position: relative;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.notification-text {
|
||||
&-item-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
&-text {
|
||||
font-size: 13px;
|
||||
color: v.$text-s;
|
||||
margin-bottom: 4px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.notification-time {
|
||||
font-size: 12px;
|
||||
&-data {
|
||||
font-size: 10px;
|
||||
color: v.$text-m;
|
||||
}
|
||||
|
||||
.notification-footer {
|
||||
padding: 12px 20px;
|
||||
&-icon {
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
height: max-content;
|
||||
color: v.$p-color;
|
||||
align-self: start;
|
||||
padding-top: 5px;
|
||||
|
||||
svg {
|
||||
fill: v.$p-color;
|
||||
|
||||
&:hover {
|
||||
fill: v.$p-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: v.$p-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&-mark-all-as-read {
|
||||
font-size: 10px;
|
||||
color: v.$p-color;
|
||||
text-align: end;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: v.$p-color-hover;
|
||||
}
|
||||
}
|
||||
|
||||
&-footer {
|
||||
padding: 8px 20px;
|
||||
border-top: 1px solid v.$b-color-1;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
|
||||
.notification-link {
|
||||
color: v.$p-color;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
color: v.$p-color-hover;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&-unread-count {
|
||||
position: absolute;
|
||||
right: -5px;
|
||||
bottom: -5px;
|
||||
/* width: 22px;
|
||||
height: 22px; */
|
||||
border-radius: 15px;
|
||||
background: v.$red;
|
||||
color: v.$white;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
padding: 2px 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.user-menu {
|
||||
|
||||
+1144
-686
File diff suppressed because it is too large
Load Diff
@@ -201,11 +201,18 @@
|
||||
}
|
||||
|
||||
.subscription-grid {
|
||||
display: grid;
|
||||
/* grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); */
|
||||
/* display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr 1fr;
|
||||
grid-template-columns: repeat(4, auto);
|
||||
gap: 20px;
|
||||
margin-bottom: 25px;
|
||||
justify-content: center; */
|
||||
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(230px, 278px));
|
||||
gap: 18px;
|
||||
margin-bottom: 25px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.plan-card {
|
||||
@@ -217,7 +224,7 @@
|
||||
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
cursor: pointer;
|
||||
overflow: visible;
|
||||
max-width: 350px;
|
||||
max-width: 278px;
|
||||
|
||||
.plan-header {
|
||||
text-align: center;
|
||||
|
||||
@@ -70,13 +70,18 @@
|
||||
}
|
||||
|
||||
.analytics-grid {
|
||||
display: flex;
|
||||
gap: 30px;
|
||||
margin-bottom: 40px;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
|
||||
@media (max-width: 970px) {
|
||||
.tanstak-table-wrapper .tanstak-table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 1370px) {
|
||||
flex-direction: column;
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.analytics-card {
|
||||
@@ -85,10 +90,8 @@
|
||||
padding: 30px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.1);
|
||||
transition: all 0.3s ease;
|
||||
flex: 1;
|
||||
min-width: 350px;
|
||||
max-width: calc(50% - 15px);
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
|
||||
@media (max-width: 970px) {
|
||||
max-width: calc(100vw - var(--side-bar-width) - 30px);
|
||||
|
||||
@@ -219,6 +219,64 @@
|
||||
padding: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&.verefication-section {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.verefication-block {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h3 {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.drag-drop-zone {
|
||||
border: 2px dashed #6366f1;
|
||||
border-radius: .75rem;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.selected-file-action-panel {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.selected-file-actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: v.$text-s;
|
||||
}
|
||||
|
||||
.btn {
|
||||
padding: 5px 12px;
|
||||
}
|
||||
|
||||
.btn-confirm {
|
||||
background: v.$p-color;
|
||||
color: v.$white;
|
||||
|
||||
background: linear-gradient(135deg, v.$p-color, v.$s-color);
|
||||
color: v.$white;
|
||||
border: 2px solid transparent;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
border: 2px solid v.$p-color;
|
||||
padding: 2px 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.danger-zone {
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
@use './variable.scss' as v;
|
||||
|
||||
:root {
|
||||
--p: #6366f1;
|
||||
--pd: #4f46e5;
|
||||
--pl: #e0e7ff;
|
||||
--g: #10b981;
|
||||
--a: #f59e0b;
|
||||
--r: #f43f5e;
|
||||
--s: #0ea5e9;
|
||||
--bg: #f1f5f9;
|
||||
--sur: #fff;
|
||||
--brd: #e2e8f0;
|
||||
--tx: #1e293b;
|
||||
--mu: #64748b;
|
||||
--rad: 14px;
|
||||
--sh: 0 2px 10px rgba(0, 0, 0, .07);
|
||||
}
|
||||
|
||||
.tracking-page {
|
||||
.general-statistic {
|
||||
background: linear-gradient(135deg, v.$p-color 55%, v.$s-color);
|
||||
border-radius: var(--rad);
|
||||
padding: 32px;
|
||||
color: #fff;
|
||||
margin-bottom: 24px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
&-title {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
&-sub {
|
||||
font-size: 13px;
|
||||
opacity: .75;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&-content {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
&-item {
|
||||
background: rgba(255, 255, 255, .13);
|
||||
backdrop-filter: blur(8px);
|
||||
border-radius: 12px;
|
||||
padding: 16px 18px;
|
||||
border: 1px solid rgba(255, 255, 255, .15);
|
||||
|
||||
&-ico {
|
||||
font-size: 20px;
|
||||
margin-bottom: 6px;
|
||||
svg {
|
||||
fill: v.$white;
|
||||
color: v.$white;
|
||||
}
|
||||
}
|
||||
|
||||
&-lbl {
|
||||
font-size: 11px;
|
||||
opacity: .8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .5px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&-val {
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
line-height: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
$p-color: #6366f1;
|
||||
$p-color-hover: #1e0ff0;
|
||||
$s-color: #8b5cf6;
|
||||
$text-p: #1f2937;
|
||||
$text-s: #4b5563;
|
||||
@@ -10,17 +11,31 @@ $bg-hover: #e9ecef;
|
||||
$bg-light: #f8f9ff;
|
||||
$bg-darkening: #0000007a;
|
||||
$shadow-1: #0000001a;
|
||||
$shadow-2: #8686861a;
|
||||
$white: #fff;
|
||||
$black: #000;
|
||||
$red: #dc2626;
|
||||
$green: #00a63e;
|
||||
$green-2: #10b981;
|
||||
$grey: #5a6e8a;
|
||||
$p-color-disabled: #6365f18e;
|
||||
$s-color-disabled: #8a5cf663;
|
||||
$border-color-1: #e2e8f0;
|
||||
$border-color-1-hover: #b1b7be;
|
||||
$color-warning: #fab005;
|
||||
$color-warning-hover: #fa9805;
|
||||
$color-warning-2: #f08c00;
|
||||
|
||||
$color-image: #f08c00;
|
||||
$color-video: #2f9e44;
|
||||
$color-audio: #1971c2;
|
||||
$color-document: #a561e6;
|
||||
|
||||
$status-new: #fab005;
|
||||
$status-showed: #5a6e8a;
|
||||
$status-in-work: #3b82f6;
|
||||
$authorized-use: #2ecc71;
|
||||
|
||||
:root {
|
||||
--side-bar-width: 280px;
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,319 @@
|
||||
@use './variable.scss' as v;
|
||||
|
||||
:root {
|
||||
--bg: #f5f6fa;
|
||||
--card: #fff;
|
||||
--border: #e2e5ef;
|
||||
--border2: #d0d4e4;
|
||||
--text: #0f1221;
|
||||
--text2: #4b5270;
|
||||
--muted: #8890ad;
|
||||
--accent: #5b47e0;
|
||||
--accent2: #7c5cf6;
|
||||
--green: #059669;
|
||||
--green-bg: #ecfdf5;
|
||||
--green-bd: #a7f3d0;
|
||||
--red: #dc2626;
|
||||
--red-bg: #fef2f2;
|
||||
--amber: #b45309;
|
||||
--shadow: 0 2px 12px rgba(15, 18, 33, .07);
|
||||
}
|
||||
|
||||
.watch-doc {
|
||||
&-wrapper {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
|
||||
|
||||
.btn-acc {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
box-shadow: 0 3px 12px rgba(91, 71, 224, .28);
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent2);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-out {
|
||||
background: var(--card);
|
||||
color: var(--text2);
|
||||
border: 1px solid var(--border2);
|
||||
width: 100%;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.document-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
gap: 10px;
|
||||
|
||||
&__nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
&__next,
|
||||
&__prev {
|
||||
font-size: 0.75rem;
|
||||
color: var(--accent);
|
||||
background: none;
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: 6px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
display: inline;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&__current-page {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted);
|
||||
display: inline;
|
||||
}
|
||||
|
||||
&__fullscreen {
|
||||
font-size: .75rem;
|
||||
color: var(--accent);
|
||||
background: none;
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: 6px;
|
||||
padding: 3px 10px;
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
|
||||
&:hover {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.prev-wrap {
|
||||
min-height: 400px;
|
||||
min-width: 300px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.pdf-canvas {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.pdf-container {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.pdf-container:fullscreen {
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
gap: 20px;
|
||||
|
||||
.prev-wrap {
|
||||
flex: 1;
|
||||
min-height: auto;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.pdf-canvas {
|
||||
max-width: 100%;
|
||||
max-height: calc(100vh - 120px);
|
||||
width: auto;
|
||||
height: auto;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.document-controls {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
backdrop-filter: blur(10px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
border-radius: 12px;
|
||||
padding: 0.75rem 1.5rem;
|
||||
z-index: 1000;
|
||||
min-width: 300px;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.error-container {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-page {
|
||||
max-width: 880px;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px 60px;
|
||||
}
|
||||
|
||||
&-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 18px 0 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 28px;
|
||||
|
||||
.header-logo {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
color: v.$p-color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.header-user {
|
||||
font-size: .83rem;
|
||||
color: var(--text2);
|
||||
background: #fff;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 7px 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&-status {
|
||||
background: var(--green-bg);
|
||||
border: 1.5px solid var(--green-bd);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 14px;
|
||||
margin-bottom: 22px;
|
||||
padding: 20px 22px;
|
||||
border-radius: 14px;
|
||||
|
||||
.st-title {
|
||||
color: var(--green);
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.st-sub {
|
||||
font-size: .83rem;
|
||||
color: var(--text2);
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
&-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
&-footer {
|
||||
text-align: center;
|
||||
padding: 28px 0 0;
|
||||
color: var(--muted);
|
||||
font-size: .76rem;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&-card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 14px;
|
||||
overflow: hidden;
|
||||
box-shadow: var(--shadow);
|
||||
height: 100%;
|
||||
|
||||
.card-head {
|
||||
padding: 14px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
|
||||
font-size: .78rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .6px;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 14px 20px;
|
||||
|
||||
.owner-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.owner-av {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, var(--accent), var(--accent2));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.owner-name {
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.owner-meta {
|
||||
color: var(--muted);
|
||||
}
|
||||
}
|
||||
|
||||
.download-description {
|
||||
font-size: .82rem;
|
||||
color: var(--text2);
|
||||
margin-bottom: 14px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
&-actions {
|
||||
display: flex;
|
||||
gap: 9px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1,14 @@
|
||||
import TanstakFilesTable from '@/app/components/tanstak-table/TanstakTable';
|
||||
|
||||
type FilesTable = {
|
||||
fileType: string
|
||||
fileType: string;
|
||||
showFileLink?: boolean;
|
||||
}
|
||||
|
||||
export default function FilesTable({ fileType }: FilesTable) {
|
||||
export default function FilesTable({ fileType, showFileLink }: FilesTable) {
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<TanstakFilesTable fileType={fileType} />
|
||||
<TanstakFilesTable fileType={fileType} showFileLink={showFileLink}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -25,10 +25,6 @@ export default function DashboardFilesInfo() {
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.protected ? filesInfo?.images.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.images.violation ? filesInfo?.images.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -47,10 +43,6 @@ export default function DashboardFilesInfo() {
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.protected ? filesInfo?.videos.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.videos.violation ? filesInfo?.videos.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -69,10 +61,6 @@ export default function DashboardFilesInfo() {
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.protected ? filesInfo?.audios.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.audios.violation ? filesInfo?.audios.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -85,16 +73,12 @@ export default function DashboardFilesInfo() {
|
||||
<div className="type-card-stats">
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('size')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.check? convertBytes(filesInfo?.documents.check) : 0}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.size? convertBytes(filesInfo?.documents.size) : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('PROTECTED')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.protected ? filesInfo?.documents.protected : 0}</span>
|
||||
</div>
|
||||
<div className="type-card-stat-row">
|
||||
<span className="type-card-stat-label">{t('violations')}</span>
|
||||
<span className="type-card-stat-value">{filesInfo?.documents.violation ? filesInfo?.documents.violation : 0}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,63 @@
|
||||
'use client'
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import Link from 'next/link';
|
||||
import { fetchLastMonitoringCheck } from '@/app/actions/monitoringActions';
|
||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function DashboardLastCheck() {
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['lastMonitoringCheck'],
|
||||
queryFn: () => {
|
||||
return fetchLastMonitoringCheck()
|
||||
},
|
||||
select: (data) => {
|
||||
if (!data) {
|
||||
return null
|
||||
}
|
||||
return data;
|
||||
},
|
||||
retry: false
|
||||
});
|
||||
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<div className="dashboard-last-check">
|
||||
<div className="last-check-info">
|
||||
<div className="last-check-title">Последняя проверка контента</div>
|
||||
<div className="last-check-date">
|
||||
17.10.2025 в 10:23</div>
|
||||
<div className="last-check-title">
|
||||
{t('last-content-check')}
|
||||
</div>
|
||||
<div
|
||||
className="relative"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="last-check-date">
|
||||
{data?.recent_searches[0]?.created_at ? `${formatDate(data.recent_searches[0].created_at)} ${t('in')} ${formatDateTime(data.recent_searches[0].created_at)}` : '---'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="last-check-actions">
|
||||
<Link
|
||||
className="btn-report"
|
||||
href={'reports'}
|
||||
href={'/pages/reports'}
|
||||
>
|
||||
📄 Отчёт
|
||||
{t('reports')}
|
||||
</Link>
|
||||
<Link
|
||||
{/* <Link
|
||||
className="btn-new-search"
|
||||
href={'violations'}
|
||||
>
|
||||
🔍 Мониторинг нарушений
|
||||
</Link>
|
||||
Мониторинг нарушений
|
||||
</Link> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getUserFilesData } from '@/app/actions/fileEntity';
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { FileTypeIcon } from '@/app/components/FileTypeIcon';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import Link from 'next/link';
|
||||
|
||||
type FileItem = {
|
||||
@@ -71,15 +69,6 @@ export default function DashboardUserFiles() {
|
||||
});
|
||||
|
||||
const t = useTranslations('Global');
|
||||
const [openDropDownList, setOpenDropDownList] = useState(false);
|
||||
const dropDownList = useRef(null);
|
||||
useClickOutside(
|
||||
dropDownList,
|
||||
() => {
|
||||
setOpenDropDownList(false)
|
||||
},
|
||||
openDropDownList
|
||||
);
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp);
|
||||
@@ -97,34 +86,25 @@ export default function DashboardUserFiles() {
|
||||
{t('your-files')}
|
||||
</h3>
|
||||
<div
|
||||
className="section-add-file btn view-all-link"
|
||||
onClick={() => {
|
||||
setOpenDropDownList(!openDropDownList);
|
||||
}}
|
||||
ref={dropDownList}
|
||||
className="btn view-all-link"
|
||||
>
|
||||
{t('add')}
|
||||
<div
|
||||
className={`section-drop-down-list ${openDropDownList ? 'opened' : ''}`}
|
||||
>
|
||||
<div
|
||||
className="flex flex-col text-center"
|
||||
>
|
||||
<Link href='/pages/marking-images'>
|
||||
{t('photo-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-video'>
|
||||
{t('video-marking')}
|
||||
</Link>
|
||||
<Link href='/pages/marking-audio'>
|
||||
{t('audio-marking')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<Link href='/pages/my-content'>
|
||||
{t('all')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-files-list">
|
||||
{isLoading && (
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{tableData?.length ? (
|
||||
tableData?.map((file) => {
|
||||
@@ -151,9 +131,11 @@ export default function DashboardUserFiles() {
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div>
|
||||
{t('there-are-no-files-yet')}
|
||||
</div>
|
||||
!isLoading && (
|
||||
<div>
|
||||
{t('there-are-no-files-yet')}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,12 +5,12 @@ import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
import { useEffect } from 'react';
|
||||
import { useViolationStatistic } from '@/app/hooks/react-query/useViolationStatistic';
|
||||
import {IconDocument, IconSearch, IconWarning, IconGraph, IconDiscet, IconShield} from '@/app/ui/icons/icons';
|
||||
import { IconDocument, IconSearch, IconWarning, IconGraph, IconDiscet, IconShield } from '@/app/ui/icons/icons';
|
||||
|
||||
export default function DashboardUserStats() {
|
||||
const t = useTranslations("Global");
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
const { data: violationStatistic } = useViolationStatistic();
|
||||
const { data: violationStatistic, isLoading: violationStatisticLoading } = useViolationStatistic();
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -22,7 +22,19 @@ export default function DashboardUserStats() {
|
||||
<IconShield />
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.protected ? filesInfo?.total.protected : 0}</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
filesInfo?.total.protected ? filesInfo?.total.protected : 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('files-under-protection')}
|
||||
</div>
|
||||
@@ -34,21 +46,58 @@ export default function DashboardUserStats() {
|
||||
<IconSearch />
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.check ? filesInfo?.total.check : 0}</div>
|
||||
{/* <div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
filesInfo?.total.check ? filesInfo?.total.check : 0
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{violationStatisticLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
violationStatistic?.total_violations ?? 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('matches-found')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card violations">
|
||||
<div className="stat-card violations hidden">
|
||||
<div className="stat-header">
|
||||
<div className="stat-icon">
|
||||
<IconWarning />
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">{violationStatistic?.total_violations ?? 0}</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{violationStatisticLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
violationStatistic?.total_violations ?? 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('violations')}
|
||||
{t('matches')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -59,7 +108,17 @@ export default function DashboardUserStats() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">
|
||||
{filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0}
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
filesInfo?.total.size ? convertBytes(filesInfo?.total.size) : 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('disk-space-used')}
|
||||
@@ -72,7 +131,19 @@ export default function DashboardUserStats() {
|
||||
<IconDocument />
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-value">{violationStatistic?.in_progress_violations ?? 0}</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{violationStatisticLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
violationStatistic?.in_progress_violations ?? 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('violations-in-progress')}
|
||||
</div>
|
||||
|
||||
@@ -11,7 +11,7 @@ import Link from 'next/link';
|
||||
import { getViolationFilesArray } from '@/app/actions/violationActions';
|
||||
import { IconEye } from '@/app/ui/icons/icons';
|
||||
|
||||
interface ViolationFile {
|
||||
interface FileViolation {
|
||||
createdAt: string;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
@@ -23,20 +23,26 @@ interface ViolationFile {
|
||||
violationCount: number;
|
||||
}
|
||||
|
||||
interface ViolationData {
|
||||
content: FileViolation[];
|
||||
}
|
||||
|
||||
export default function DashboardUserViolations() {
|
||||
const {
|
||||
data: violationData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<ViolationFile[]>({
|
||||
queryKey: ['violationData'],
|
||||
queryFn: () => getViolationFilesArray(),
|
||||
} = useQuery<ViolationData>({
|
||||
queryKey: ['violationData', { page: 0, size: 5 }],
|
||||
queryFn: () => getViolationFilesArray({
|
||||
page: 0,
|
||||
size: 5,
|
||||
}),
|
||||
|
||||
select: (data) => {
|
||||
return data?.slice(0, 5) || [];
|
||||
},
|
||||
/* refetchInterval: 30000 */
|
||||
return data;
|
||||
}
|
||||
});
|
||||
|
||||
const t = useTranslations('Global');
|
||||
@@ -56,18 +62,28 @@ export default function DashboardUserViolations() {
|
||||
{t('violation')}
|
||||
</h3>
|
||||
<div
|
||||
className="section-add-file btn view-all-link"
|
||||
className="btn view-all-link"
|
||||
>
|
||||
<Link href='/pages/violations'>
|
||||
{t('add')}
|
||||
{t('all')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-files-list">
|
||||
{isLoading && (
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{violationData?.length ? (
|
||||
violationData?.map((file) => {
|
||||
{violationData?.content?.length ? (
|
||||
violationData?.content.map((file) => {
|
||||
return (
|
||||
<div
|
||||
key={file.fileId}
|
||||
@@ -94,9 +110,11 @@ export default function DashboardUserViolations() {
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div>
|
||||
{t('there-are-no-files-yet')}
|
||||
</div>
|
||||
!isLoading && (
|
||||
<div>
|
||||
{t('there-are-no-files-yet')}
|
||||
</div>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,173 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Swiper, SwiperSlide } from 'swiper/react';
|
||||
import { Pagination, Autoplay } from 'swiper/modules';
|
||||
import Image from 'next/image';
|
||||
import 'swiper/css';
|
||||
import 'swiper/css/pagination';
|
||||
import testImage from '@/app/src/image-preview.png'
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { getViolationFilesArray } from '@/app/actions/violationActions';
|
||||
|
||||
interface ViolationFile {
|
||||
createdAt: string;
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
fileSize: number;
|
||||
latestViolationDate: string;
|
||||
mimeType: string;
|
||||
status: string;
|
||||
supportId: number;
|
||||
violationCount: number;
|
||||
}
|
||||
|
||||
export default function ViolationsCarousel() {
|
||||
const {
|
||||
data: violationData,
|
||||
isLoading,
|
||||
isError,
|
||||
error,
|
||||
} = useQuery<ViolationFile[]>({
|
||||
queryKey: ['violationData'],
|
||||
queryFn: () => getViolationFilesArray(),
|
||||
|
||||
select: (data) => {
|
||||
return data;
|
||||
},
|
||||
/* refetchInterval: 30000 */
|
||||
});
|
||||
const paginationRef = useRef(null);
|
||||
const t = useTranslations('Global');
|
||||
|
||||
useEffect(() => {
|
||||
console.log(violationData);
|
||||
}, [violationData])
|
||||
|
||||
return (
|
||||
<div className="content-section">
|
||||
<div className="section-header">
|
||||
<h3 className="section-title">{t('violation')}</h3>
|
||||
<Link
|
||||
href="violations"
|
||||
className="view-all-link"
|
||||
>
|
||||
Смотреть все
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
{t('are-no-violations')}
|
||||
</div>
|
||||
<div className="violations-carousel">
|
||||
<Swiper
|
||||
spaceBetween={50}
|
||||
slidesPerView={1}
|
||||
modules={[Pagination, Autoplay]}
|
||||
pagination={{
|
||||
clickable: true,
|
||||
el: '.custon-swiper-pagination'
|
||||
}}
|
||||
loop={true}
|
||||
autoplay={{
|
||||
delay: 3000,
|
||||
disableOnInteraction: false,
|
||||
}}
|
||||
>
|
||||
{/* {violationData?.map(item => {
|
||||
return (
|
||||
<SwiperSlide>
|
||||
<div
|
||||
className="violation-slide"
|
||||
>
|
||||
|
||||
</div>
|
||||
</SwiperSlide>
|
||||
)
|
||||
})} */}
|
||||
{/* <SwiperSlide>
|
||||
<div className="violation-slide">
|
||||
<div className="violation-image-container">
|
||||
<Image src={testImage} alt="Preview" className="violation-image" />
|
||||
<div className="violation-badge">НАРУШЕНИЕ</div>
|
||||
<div className="violation-status-badge">ОБРАБОТАНО</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-content">
|
||||
<div className="violation-title-row">
|
||||
<div className="violation-filename">
|
||||
🖼️banner111.png</div>
|
||||
<div className="violation-type-badge photo">
|
||||
Фото
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-grid">
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">
|
||||
🌍 Регион
|
||||
</div>
|
||||
<div className="violation-info-value">
|
||||
<span className="region-flag">🇺🇸</span>
|
||||
<span>США</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📅 Обнаружено</div>
|
||||
<div className="violation-info-value">
|
||||
17.10.2025</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">🌐 Домен</div>
|
||||
<div className="violation-info-value">
|
||||
m.apkpure.com </div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📊 Категория</div>
|
||||
<div className="violation-info-value">
|
||||
Единичное
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">📁 Тип</div>
|
||||
<div className="violation-info-value">
|
||||
Изображение</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-info-item">
|
||||
<div className="violation-info-label">⚡ Статус</div>
|
||||
<div className="violation-info-value">
|
||||
✅ Обработано</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="violation-actions-row">
|
||||
<button className="btn-visit-site">
|
||||
🔗 Перейти на сайт
|
||||
</button>
|
||||
<button className="violation-action">
|
||||
🚨 Открыть дело
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SwiperSlide> */}
|
||||
</Swiper>
|
||||
<div
|
||||
className="flex justify-center mt-6"
|
||||
>
|
||||
<div
|
||||
ref={paginationRef}
|
||||
className="flex justify-center gap-2 custon-swiper-pagination"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -125,7 +125,7 @@ export default function FrequentlyAskedQuestions() {
|
||||
<FaqItem
|
||||
question="Какие форматы поддерживаются?"
|
||||
>
|
||||
Изображения: JPG, JPEG, PNG, GIF, BMP. Видео: MP4, AVI, MOV, WMV. Аудио: WAV. Документы: PDF.
|
||||
Изображения: JPG, JPEG, PNG, BMP. Видео: MP4, AVI, MOV, WMV. Аудио: WAV. Документы: PDF.
|
||||
</FaqItem >
|
||||
</section>
|
||||
|
||||
@@ -165,12 +165,12 @@ export default function FrequentlyAskedQuestions() {
|
||||
<FaqItem
|
||||
question="Безопасна ли оплата через ваш сайт?"
|
||||
>
|
||||
Да, все платежи обрабатываются через Яндекс.Кассу с использованием защищенного SSL-соединения. Мы не храним данные ваших банковских карт. Платформа соответствует стандартам безопасности PCI DSS.
|
||||
Да, все платежи обрабатываются через ЮKassa с использованием защищенного SSL-соединения. Мы не храним данные ваших банковских карт. Платформа соответствует стандартам безопасности PCI DSS.
|
||||
</FaqItem >
|
||||
<FaqItem
|
||||
question="Как получить чек об оплате?"
|
||||
>
|
||||
Электронный чек автоматически отправляется на email, указанный при оплате. Также вы можете найти все чеки в разделе "История платежей" в вашем личном кабинете.
|
||||
Электронный чек автоматически отправляется на email, указанный при оплате.
|
||||
</FaqItem >
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
export default function FilePpageFileStatistic() {
|
||||
'use client'
|
||||
|
||||
import { useViolationStatistic } from '@/app/hooks/react-query/useViolationStatistic';
|
||||
|
||||
export default function FilePageFileStatistic({ id }: { id: string }) {
|
||||
const { data: violationStatistic } = useViolationStatistic(id);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="block-wrapper"
|
||||
@@ -13,36 +19,36 @@ export default function FilePpageFileStatistic() {
|
||||
<div className="stat-mini-card">
|
||||
<div
|
||||
className="stat-mini-value"
|
||||
style={{'color': '#dc2626'}}
|
||||
style={{ 'color': '#dc2626' }}
|
||||
>
|
||||
0
|
||||
{violationStatistic?.new_violations ? violationStatistic?.new_violations : 0}
|
||||
</div>
|
||||
<div className="stat-mini-label">Новых</div>
|
||||
</div>
|
||||
<div className="stat-mini-card">
|
||||
<div
|
||||
className="stat-mini-value"
|
||||
style={{'color': '#d97706'}}
|
||||
style={{ 'color': '#d97706' }}
|
||||
>
|
||||
0
|
||||
{violationStatistic?.in_progress_violations ? violationStatistic?.in_progress_violations : 0}
|
||||
</div>
|
||||
<div className="stat-mini-label">В работе</div>
|
||||
</div>
|
||||
<div className="stat-mini-card">
|
||||
<div
|
||||
className="stat-mini-value"
|
||||
style={{'color': '#059669'}}
|
||||
style={{ 'color': '#059669' }}
|
||||
>
|
||||
0
|
||||
{violationStatistic?.resolved_violations ? violationStatistic?.resolved_violations : 0}
|
||||
</div>
|
||||
<div className="stat-mini-label">Решено</div>
|
||||
</div>
|
||||
<div className="stat-mini-card">
|
||||
<div
|
||||
className="stat-mini-value"
|
||||
style={{'color': '#6366f1'}}
|
||||
style={{ 'color': '#6366f1' }}
|
||||
>
|
||||
0
|
||||
{violationStatistic?.total_violations ? violationStatistic?.total_violations : 0}
|
||||
</div>
|
||||
<div className="stat-mini-label">Всего</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,7 @@ export default function FilePageNote() {
|
||||
<form method="POST">
|
||||
<input type="hidden" name="action" value="add_note" />
|
||||
<textarea name="note" className="note-textarea" placeholder="Добавить заметку к этому нарушению..." ></textarea>
|
||||
<button type="submit" className="btn-small btn-primary-small">
|
||||
<button type="submit" className="btn-s btn-primary-small">
|
||||
Сохранить заметку
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
// app/ui/violations/file-page/violation-page-violations-list.tsx
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, usePathname } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { formatDate } from '@/app/lib/formatDate';
|
||||
import ViolationPageNote from '@/app/ui/file-page/file-page-note';
|
||||
|
||||
interface ViolationFileDetail {
|
||||
id: number;
|
||||
url: string;
|
||||
page_url: string;
|
||||
page_title: string;
|
||||
host: string;
|
||||
status: string;
|
||||
created_date: string;
|
||||
file_id: string;
|
||||
}
|
||||
|
||||
interface ViolationFile {
|
||||
violations: ViolationFileDetail[],
|
||||
total_elements: number,
|
||||
total_pages: number,
|
||||
current_page: number,
|
||||
page_size: number,
|
||||
has_next: boolean,
|
||||
has_previous: boolean
|
||||
}
|
||||
|
||||
export default function FilePageViolationsList({
|
||||
fileViolations,
|
||||
currentPage,
|
||||
fileId
|
||||
}: {
|
||||
fileViolations: ViolationFile,
|
||||
currentPage: number,
|
||||
fileId: string
|
||||
}) {
|
||||
const t = useTranslations('Global');
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const [selectedViolation, setSelectedViolation] = useState<ViolationFileDetail | null>(null);
|
||||
|
||||
if (!fileViolations?.violations) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
router.push(`${pathname}?page=${page}`, { scroll: false });
|
||||
};
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
const maxVisible = 5;
|
||||
const total = fileViolations.total_pages;
|
||||
|
||||
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
|
||||
let end = Math.min(total, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
function selectHandler(violation: ViolationFileDetail) {
|
||||
setSelectedViolation(violation);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{t('all-file-violations')}
|
||||
</h3>
|
||||
<span>
|
||||
{t('total-violations')}: {fileViolations.total_elements} | {t('page')} {fileViolations.current_page} {t('out-of')} {fileViolations.total_pages}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="sources-list-wrapper"
|
||||
>
|
||||
<div className="sources-list">
|
||||
{fileViolations.violations.map(item => {
|
||||
return (
|
||||
<div
|
||||
className={`source-card ${selectedViolation?.id === item.id ? 'selected' : ''}`}
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
selectHandler(item);
|
||||
}}
|
||||
>
|
||||
{/* <div className="source-header">
|
||||
<div className="source-image">
|
||||
<img src={item.url} alt="" />
|
||||
</div>
|
||||
|
||||
</div> */}
|
||||
<div
|
||||
className="source-header-left"
|
||||
>
|
||||
<div className="source-image">
|
||||
<img src={item.url} alt="" />
|
||||
</div>
|
||||
<div>
|
||||
<span
|
||||
className="source-meta"
|
||||
>
|
||||
{t('source')}:
|
||||
</span>
|
||||
<span className="source-url">
|
||||
{item.host}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="source-header-right"
|
||||
>
|
||||
<span className="source-status new">
|
||||
{item.status}
|
||||
</span>
|
||||
|
||||
<div className="source-actions">
|
||||
<Link
|
||||
href={item.page_url}
|
||||
className="btn-small btn-primary-small"
|
||||
target="_blank"
|
||||
scroll={false}
|
||||
>
|
||||
{t('open')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="violation-info"
|
||||
>
|
||||
<div
|
||||
className="violation-info-grid"
|
||||
>
|
||||
<div
|
||||
className="violation-info-content"
|
||||
>
|
||||
<h3
|
||||
className="violation-info-title"
|
||||
>
|
||||
{selectedViolation?.page_title}
|
||||
</h3>
|
||||
<div
|
||||
className="violation-info-image"
|
||||
>
|
||||
<img src={selectedViolation?.url} alt={selectedViolation?.page_title} />
|
||||
</div>
|
||||
<div
|
||||
className="violation-info-source"
|
||||
>
|
||||
|
||||
<span>
|
||||
{t('source')}:
|
||||
</span>
|
||||
<Link
|
||||
href={selectedViolation?.page_url ? selectedViolation?.page_url : '#'}
|
||||
className="source"
|
||||
target="_blank"
|
||||
scroll={false}
|
||||
>
|
||||
{selectedViolation?.page_url}
|
||||
</Link>
|
||||
</div>
|
||||
<div
|
||||
className="violation-info-source"
|
||||
>
|
||||
<span>
|
||||
{t('date')} {selectedViolation?.created_date ? formatDate(selectedViolation?.created_date) : '#'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{selectedViolation?.status}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="mt-auto"
|
||||
>
|
||||
Тут думаю нужно будет отображать инфу о работе с нарушением и её статус
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="violation-info-actions"
|
||||
>
|
||||
<div className="action-buttons">
|
||||
<button type="button" className="btn-action btn-warning">
|
||||
Взять в работу
|
||||
</button>
|
||||
|
||||
<button type="button" className="btn-action btn-success">
|
||||
Отметить решенным
|
||||
</button>
|
||||
|
||||
<button type="button" className="btn-action btn-secondary">
|
||||
Ложное срабатывание
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* <ViolationPageNote /> */}
|
||||
</div>
|
||||
</div>
|
||||
{fileViolations.total_pages > 1 && (
|
||||
<div className="pagination">
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
className="pagination-item pagination-item-prev"
|
||||
disabled={!fileViolations.has_previous}
|
||||
>
|
||||
{t('previous')}
|
||||
</button>
|
||||
|
||||
{getPageNumbers().map(pageNum => (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
className={`pagination-item ${currentPage === pageNum ? 'pagination-item-active' : ''}`}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
className="pagination-item pagination-item-next"
|
||||
disabled={!fileViolations.has_next}
|
||||
>
|
||||
{t('next')}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,324 @@
|
||||
'use client'
|
||||
|
||||
import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations';
|
||||
import { useEffect, useActionState, SetStateAction } from 'react';
|
||||
import { fetchClaimInfo, createClaim, MatchStatus } from '@/app/actions/violationActions';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { toast } from 'sonner';
|
||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||
|
||||
interface CaseInfo {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
amount: number;
|
||||
priority: string;
|
||||
type: string;
|
||||
lawyer: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
page_number: number;
|
||||
page_size: number;
|
||||
total_elements: number;
|
||||
total_pages: number;
|
||||
violation_id: number;
|
||||
content: string | null;
|
||||
}
|
||||
|
||||
const ENABLED_STATUSES = ['LEGAL_IN_WORK', 'COMPLAINT_AND_LEGAL_IN_WORK'];
|
||||
type UpdateStatusHandler = (id: number, status: MatchStatus) => Promise<boolean>;
|
||||
|
||||
export default function CaseViolation({ selectedViolation, updateStatusHandler, setLocalViolation }:
|
||||
{
|
||||
selectedViolation: ViolationFileDetail,
|
||||
updateStatusHandler: UpdateStatusHandler,
|
||||
setLocalViolation: React.Dispatch<SetStateAction<ViolationFileDetail>>
|
||||
}
|
||||
) {
|
||||
const t = useTranslations('Global');
|
||||
const [state, formAction, isPending] = useActionState(createClaim, undefined);
|
||||
|
||||
const { data: caseInfo, isLoading: isLoadingCase } = useQuery({
|
||||
queryKey: ['caseInfo', selectedViolation.id],
|
||||
queryFn: () => fetchClaimInfo(selectedViolation.id),
|
||||
select: (data) => {
|
||||
if (data) {
|
||||
return data
|
||||
}
|
||||
},
|
||||
enabled: ENABLED_STATUSES.includes(selectedViolation.status)
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success) {
|
||||
toast.success(t('the-claims-has-been-registered'));
|
||||
|
||||
const newStatus = selectedViolation.status === 'COMPLAINT_IN_WORK'
|
||||
? 'COMPLAINT_AND_LEGAL_IN_WORK'
|
||||
: 'LEGAL_IN_WORK';
|
||||
|
||||
setLocalViolation(prev => ({
|
||||
...prev,
|
||||
status: newStatus
|
||||
}))
|
||||
|
||||
updateStatusHandler(selectedViolation.id, newStatus);
|
||||
} else if (state && !state.success) {
|
||||
console.log(state.errorMessage);
|
||||
if (state.errorMessage?.server) {
|
||||
toast.warning(t(state.errorMessage.server));
|
||||
} else {
|
||||
toast.warning(t('the-claims-has-not-been-registered'));
|
||||
}
|
||||
}
|
||||
}, [state, updateStatusHandler, selectedViolation.id]);
|
||||
|
||||
if (!ENABLED_STATUSES.includes(selectedViolation.status)) {
|
||||
return (
|
||||
<div className="violation-info-case">
|
||||
<div className="note-form">
|
||||
<form action={formAction}>
|
||||
<input type="hidden" name="violationId" value={selectedViolation.id} />
|
||||
<div
|
||||
className="note-form-flex"
|
||||
>
|
||||
<section
|
||||
className="note-form-section"
|
||||
>
|
||||
<div
|
||||
className="note-form-group"
|
||||
>
|
||||
<label
|
||||
className="note-case-label"
|
||||
htmlFor="caseTitle"
|
||||
>
|
||||
{t('case-name')}
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
name="caseTitle"
|
||||
className="note-case-input"
|
||||
placeholder={t('fill-title-of-the-case') + '...'}
|
||||
defaultValue={state?.previousCaseTitle ? state?.previousCaseTitle : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state?.errorMessage?.caseTitle && (
|
||||
<p
|
||||
className="text-sm text-red-500"
|
||||
>
|
||||
{
|
||||
state?.errorMessage?.caseTitle.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
<section
|
||||
className="note-form-section"
|
||||
>
|
||||
<div
|
||||
className="note-form-group"
|
||||
>
|
||||
<label
|
||||
className="note-case-label"
|
||||
htmlFor="amount"
|
||||
>
|
||||
{t('amount-of-damage')}
|
||||
</label>
|
||||
<input
|
||||
type="number"
|
||||
name="amount"
|
||||
className="note-case-input"
|
||||
placeholder={t('enter-the-amount-of-damage') + '...'}
|
||||
defaultValue={state?.previousAmount ? state?.previousAmount : ''}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{state?.errorMessage?.amount && (
|
||||
<p
|
||||
className="text-sm text-red-500"
|
||||
>
|
||||
{
|
||||
state?.errorMessage?.amount.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<section
|
||||
className="note-form-section"
|
||||
>
|
||||
<div
|
||||
className="note-form-group"
|
||||
>
|
||||
<label
|
||||
className="note-case-label"
|
||||
htmlFor="note"
|
||||
>
|
||||
{t('text-of-the-case')}
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
name="note"
|
||||
className="note-textarea"
|
||||
placeholder={t('fill-text-of-the-case') + '...'}
|
||||
defaultValue={state?.previousText ? state?.previousText : ''}
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
{state?.errorMessage?.textArea && (
|
||||
<p
|
||||
className="text-sm text-red-500"
|
||||
>
|
||||
{
|
||||
state?.errorMessage?.textArea.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
<div
|
||||
className="button-actions"
|
||||
>
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-s btn-primary-small"
|
||||
disabled={isPending}
|
||||
>
|
||||
{t('submit-violation')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
if (isLoadingCase) {
|
||||
return (
|
||||
<div className="violation-info-case">
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner large"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="violation-info-case">
|
||||
{caseInfo?.body?.content ? (
|
||||
<>
|
||||
{caseInfo.body.content.map((item: CaseInfo) => {
|
||||
return (
|
||||
<div key={item.id}>
|
||||
<div className="complaint-details">
|
||||
<div className="complaint-details-item">
|
||||
<h5>{item.name}</h5>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">
|
||||
{t('Status')}:
|
||||
</div>
|
||||
<div
|
||||
className="complaint-details-content"
|
||||
>
|
||||
{item.type}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">
|
||||
{t('date-of-creation')}:
|
||||
</div>
|
||||
<div
|
||||
className="complaint-details-content"
|
||||
>
|
||||
{item.created_at ? `${formatDate(item.created_at)}: ${formatDateTime(item.created_at)}` : '---'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">
|
||||
{t('date-of-update')}:
|
||||
</div>
|
||||
<div
|
||||
className="complaint-details-content"
|
||||
>
|
||||
{item.updated_at ? `${formatDate(item.updated_at)}: ${formatDateTime(item.updated_at)}` : '---'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">
|
||||
{t('amount-of-damage')}:
|
||||
</div>
|
||||
<div
|
||||
className="complaint-details-content"
|
||||
>
|
||||
{item.amount}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">
|
||||
{t('lawyer')}:
|
||||
</div>
|
||||
<div
|
||||
className="complaint-details-content"
|
||||
>
|
||||
{item.lawyer}
|
||||
</div>
|
||||
</div>
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">
|
||||
{t('description')}:
|
||||
</div>
|
||||
<div
|
||||
className="complaint-details-content"
|
||||
>
|
||||
{item.description}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
{t('no-information-about-the-complaint')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,405 @@
|
||||
'use client'
|
||||
|
||||
import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations';
|
||||
import { useActionState, useEffect, useState, SetStateAction } from 'react';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { createComplaint, fetchComplainInfo, MatchStatus } from '@/app/actions/violationActions';
|
||||
import { toast } from 'sonner';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||
import ModalWindow from '@/app/components/ModalWindow';
|
||||
|
||||
interface complaintInfo {
|
||||
id: number,
|
||||
status: string,
|
||||
complaint_text: string,
|
||||
violation_id: number,
|
||||
email: string,
|
||||
created_at: string,
|
||||
updated_at: string,
|
||||
not_moderated: boolean
|
||||
}
|
||||
|
||||
const ENABLED_STATUSES = ['COMPLAINT_IN_WORK', 'COMPLAINT_AND_LEGAL_IN_WORK'];
|
||||
type UpdateStatusHandler = (id: number, status: MatchStatus) => Promise<boolean>;
|
||||
|
||||
export default function CaseComplaint({ selectedViolation, updateStatusHandler, setLocalViolation }:
|
||||
{
|
||||
selectedViolation: ViolationFileDetail,
|
||||
updateStatusHandler: UpdateStatusHandler,
|
||||
setLocalViolation: React.Dispatch<SetStateAction<ViolationFileDetail>>
|
||||
}
|
||||
) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const [noteText, setNoteText] = useState('');
|
||||
const [state, formAction, isPending] = useActionState(createComplaint, undefined);
|
||||
const [isTemplateModalOpen, setIsTemplateModalOpen] = useState(false);
|
||||
const [templateName, setTemplateName] = useState('');
|
||||
|
||||
const { data: complainInfo, isLoading: isLoadingComplain } = useQuery({
|
||||
queryKey: ['complainInfo', selectedViolation.id],
|
||||
queryFn: () => fetchComplainInfo(selectedViolation.id),
|
||||
select: (data) => {
|
||||
if (data) {
|
||||
return data
|
||||
}
|
||||
},
|
||||
enabled: ENABLED_STATUSES.includes(selectedViolation.status),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success) {
|
||||
toast.success(t('the-complaint-has-been-registered'));
|
||||
|
||||
const newStatus = selectedViolation.status === 'LEGAL_IN_WORK'
|
||||
? 'COMPLAINT_AND_LEGAL_IN_WORK'
|
||||
: 'COMPLAINT_IN_WORK';
|
||||
|
||||
setLocalViolation(prev => ({
|
||||
...prev,
|
||||
status: newStatus
|
||||
}))
|
||||
|
||||
updateStatusHandler(selectedViolation.id, newStatus);
|
||||
} else if (state && !state.success) {
|
||||
console.log(state.errorMessage);
|
||||
if (state.errorMessage?.server) {
|
||||
toast.warning(t(state.errorMessage.server));
|
||||
} else {
|
||||
toast.warning(t('the-complaint-has-not-been-registered'));
|
||||
}
|
||||
}
|
||||
}, [state, updateStatusHandler, selectedViolation.id]);
|
||||
|
||||
function templateHandler(e: React.MouseEvent<HTMLLIElement>) {
|
||||
const sampleItemText = e.currentTarget.querySelector('.sample-item-text');
|
||||
|
||||
if (sampleItemText) {
|
||||
const text = sampleItemText.textContent || '';
|
||||
setNoteText(text);
|
||||
}
|
||||
}
|
||||
|
||||
function openTemplateSaveModal() {
|
||||
if (!noteText.trim()) {
|
||||
toast.warning(t('please-enter-complaint-text-first'));
|
||||
return;
|
||||
}
|
||||
setTemplateName('');
|
||||
setIsTemplateModalOpen(true);
|
||||
}
|
||||
|
||||
function saveTemplateHandler() {
|
||||
if (!templateName.trim()) {
|
||||
toast.warning(t('please-enter-template-name'));
|
||||
return;
|
||||
}
|
||||
|
||||
// пока нету эндпоинта оставлю заглушку
|
||||
console.log('СОХРАНЕНИЕ ШАБЛОНА');
|
||||
console.log('Название шаблона:', templateName);
|
||||
console.log('Текст жалобы:', noteText);
|
||||
console.log('========================');
|
||||
|
||||
toast.success(t('template-saved-successfully') || 'Шаблон успешно сохранен');
|
||||
setIsTemplateModalOpen(false);
|
||||
setTemplateName('');
|
||||
}
|
||||
|
||||
if (!ENABLED_STATUSES.includes(selectedViolation.status)) {
|
||||
return (
|
||||
<>
|
||||
<div className="violation-info-case">
|
||||
<div className="note-form">
|
||||
<form action={formAction}>
|
||||
<input type="hidden" name="violationId" value={selectedViolation.id} />
|
||||
<section
|
||||
className="note-form-section"
|
||||
>
|
||||
<div
|
||||
className="note-form-group"
|
||||
>
|
||||
<label
|
||||
className="note-case-label"
|
||||
htmlFor="note"
|
||||
>
|
||||
{t('text-of-the-complaint')}
|
||||
</label>
|
||||
|
||||
<textarea
|
||||
name="note"
|
||||
className="note-textarea"
|
||||
placeholder={`${t('text-area-error-fill-complaint-text')}...`}
|
||||
value={noteText}
|
||||
onChange={(e) => setNoteText(e.target.value)}
|
||||
>
|
||||
</textarea>
|
||||
</div>
|
||||
|
||||
{state?.errorMessage?.textArea && (
|
||||
<p
|
||||
className="text-sm text-red-500"
|
||||
>
|
||||
{
|
||||
state?.errorMessage?.textArea.split('&').map((e, index) => {
|
||||
return (
|
||||
<span key={index}>
|
||||
{t(e)}
|
||||
<br />
|
||||
</span>
|
||||
)
|
||||
})
|
||||
}
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
|
||||
<div
|
||||
className="button-actions"
|
||||
>
|
||||
{/* пока скроем интерфейс с шаблонами */}
|
||||
{/* <button
|
||||
type="button"
|
||||
className="btn-s btn-primary-small"
|
||||
onClick={openTemplateSaveModal}
|
||||
>
|
||||
{t('save-template')}
|
||||
</button> */}
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="btn-s btn-primary-small"
|
||||
disabled={isPending}
|
||||
>
|
||||
{t('submit-violation')}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{/* пока скроем интерфейс с шаблонами */}
|
||||
{/* <div
|
||||
className="samples-wrapper"
|
||||
>
|
||||
<h5>Шаблоны</h5>
|
||||
|
||||
<ul
|
||||
className="samples-list"
|
||||
>
|
||||
<li
|
||||
className="sample-item"
|
||||
onClick={(e) => {
|
||||
templateHandler(e)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="sample-item-title"
|
||||
>
|
||||
title 1
|
||||
</div>
|
||||
<div
|
||||
className="sample-item-text"
|
||||
>
|
||||
1text text text text text text text text text text text text
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="sample-item"
|
||||
onClick={(e) => {
|
||||
templateHandler(e)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="sample-item-title"
|
||||
>
|
||||
title 2
|
||||
</div>
|
||||
<div
|
||||
className="sample-item-text"
|
||||
>
|
||||
2text text text text text text text text text text text text
|
||||
</div>
|
||||
</li>
|
||||
<li
|
||||
className="sample-item"
|
||||
onClick={(e) => {
|
||||
templateHandler(e)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="sample-item-title"
|
||||
>
|
||||
title 3
|
||||
</div>
|
||||
<div
|
||||
className="sample-item-text"
|
||||
>
|
||||
3text text text text text text text text text text text text
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li
|
||||
className="sample-item"
|
||||
onClick={(e) => {
|
||||
templateHandler(e)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="sample-item-title"
|
||||
>
|
||||
title 3
|
||||
</div>
|
||||
<div
|
||||
className="sample-item-text"
|
||||
>
|
||||
3text text text text text text text text text text text text
|
||||
</div>
|
||||
</li>
|
||||
|
||||
<li
|
||||
className="sample-item"
|
||||
onClick={(e) => {
|
||||
templateHandler(e)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="sample-item-title"
|
||||
>
|
||||
title 3
|
||||
</div>
|
||||
<div
|
||||
className="sample-item-text"
|
||||
>
|
||||
3text text text text text text text text text text text text
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<ModalWindow
|
||||
state={isTemplateModalOpen}
|
||||
callBack={setIsTemplateModalOpen}
|
||||
>
|
||||
<div className="template-modal-content">
|
||||
<div className="template-modal-header">
|
||||
<h3>{t('save-template')}</h3>
|
||||
<button
|
||||
className="template-modal-close"
|
||||
onClick={() => setIsTemplateModalOpen(false)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div className="template-modal-body">
|
||||
<div className="template-form-group">
|
||||
<label htmlFor="templateName">
|
||||
{t('template-name')}:
|
||||
</label>
|
||||
<input
|
||||
id="templateName"
|
||||
type="text"
|
||||
className="template-name-input"
|
||||
placeholder={t('enter-template-name')}
|
||||
value={templateName}
|
||||
onChange={(e) => setTemplateName(e.target.value)}
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="template-preview">
|
||||
<div className="template-preview-label">
|
||||
{t('complaint-text-preview')}
|
||||
</div>
|
||||
<div className="template-preview-text">
|
||||
{noteText || t('no-text')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="template-modal-footer">
|
||||
<button
|
||||
className="btn-s btn-primary-small"
|
||||
onClick={() => setIsTemplateModalOpen(false)}
|
||||
>
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="btn-s btn-resolve"
|
||||
onClick={saveTemplateHandler}
|
||||
>
|
||||
{t('save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ModalWindow>
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
if (isLoadingComplain) {
|
||||
return (
|
||||
<div className="violation-info-case">
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner large"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="violation-info-case">
|
||||
{complainInfo?.body ? (
|
||||
<div>
|
||||
<div className="complaint-details">
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-header">
|
||||
<h5>COM-{complainInfo.body?.id}</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">{t('Status')}:</div>
|
||||
<div className="complaint-details-content">
|
||||
{complainInfo.body?.status}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">{t('date-of-creation')}:</div>
|
||||
<div className="complaint-details-content">
|
||||
{complainInfo.body?.created_at
|
||||
? `${formatDate(complainInfo.body.created_at)}: ${formatDateTime(complainInfo.body.created_at)}`
|
||||
: '---'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">{t('date-of-update')}:</div>
|
||||
<div className="complaint-details-content">
|
||||
{complainInfo.body?.updated_at
|
||||
? `${formatDate(complainInfo.body.updated_at)}: ${formatDateTime(complainInfo.body.updated_at)}`
|
||||
: '---'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="complaint-details-item">
|
||||
<div className="complaint-details-title">{t('text-of-the-complaint')}:</div>
|
||||
<div className="complaint-details-content">
|
||||
{complainInfo.body?.complaint_text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
{t('no-information-about-the-complaint')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
|
||||
export default function FilePageViolationEpmtyScreen() {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
return (
|
||||
<div
|
||||
className="violation-info"
|
||||
>
|
||||
<h4
|
||||
className="violation-info-title not-selected-match"
|
||||
>
|
||||
{t('to-view-a-file-match-click-on-the-card-from-the-list')}
|
||||
</h4>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
'use client'
|
||||
|
||||
import CaseComplaint from '@/app/ui/file-page/violation-table/case-complaint';
|
||||
import CaseLegal from '@/app/ui/file-page/violation-table/case-claims';
|
||||
import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations';
|
||||
import { MatchStatus } from '@/app/actions/violationActions';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
type UpdateStatusHandler = (id: number, status: MatchStatus) => Promise<boolean>;
|
||||
|
||||
const STATUSES_IN_WORK = ['COMPLAINT_IN_WORK', 'LEGAL_IN_WORK', 'COMPLAINT_AND_LEGAL_IN_WORK'];
|
||||
|
||||
type TabType = 'complaint' | 'claim'
|
||||
|
||||
export default function FilePageViolationInfoTabs({ selectedViolation, updateStatusHandler, caseType }: { selectedViolation: ViolationFileDetail, updateStatusHandler: UpdateStatusHandler, caseType?: 'claim' | 'complaint' }) {
|
||||
const t = useTranslations('Global');
|
||||
const [activeTab, setActiveTab] = useState<TabType>('complaint');
|
||||
const [showTabs, setShowTabs] = useState(false);
|
||||
const isInWork = STATUSES_IN_WORK.includes(selectedViolation.status);
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
const [localViolation, setLocalViolation] = useState<ViolationFileDetail>(selectedViolation);
|
||||
|
||||
useEffect(() => {
|
||||
setLocalViolation(selectedViolation);
|
||||
|
||||
if (caseType) {
|
||||
setActiveTab(caseType)
|
||||
}
|
||||
}, [selectedViolation]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowTabs(false);
|
||||
}, [localViolation.id]);
|
||||
|
||||
if (userData?.verifiedStatus !== "VERIFIED") {
|
||||
return (
|
||||
<div
|
||||
className="violation-info-not-verified"
|
||||
>
|
||||
<div>
|
||||
Подача жалобы/претензии невозможна,
|
||||
<br />
|
||||
так как вы не подтвердили свою личность
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="btn-s"
|
||||
>
|
||||
Подтвердить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isInWork && !showTabs) {
|
||||
return (
|
||||
<div className="violation-info-choice">
|
||||
<h4>
|
||||
{t('you-can')}
|
||||
</h4>
|
||||
<div className="violation-info-choice-buttons">
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setShowTabs(true);
|
||||
setActiveTab('complaint');
|
||||
}}
|
||||
>
|
||||
{t('file-a-complaint')}
|
||||
</button>
|
||||
|
||||
<span>
|
||||
{t('or')}
|
||||
</span>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setShowTabs(true);
|
||||
setActiveTab('claim');
|
||||
}}
|
||||
>
|
||||
{t('file-a-claim')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="violation-info-tabs-wrapper">
|
||||
<div className="violation-info-tabs">
|
||||
<button
|
||||
className={`btn-s btn-primary ${activeTab === 'complaint' ? 'active' : ''} ${localViolation.status === 'COMPLAINT_IN_WORK' || localViolation.status === 'COMPLAINT_AND_LEGAL_IN_WORK' ? '' : 'action'}`}
|
||||
onClick={() => setActiveTab('complaint')}
|
||||
>
|
||||
{localViolation.status === 'COMPLAINT_IN_WORK' || localViolation.status === 'COMPLAINT_AND_LEGAL_IN_WORK' ? t('complaint') : t('file-a-complaint')}
|
||||
</button>
|
||||
<button
|
||||
className={`btn-s btn-primary ${activeTab === 'claim' ? 'active' : ''} ${localViolation.status === 'LEGAL_IN_WORK' || localViolation.status === 'COMPLAINT_AND_LEGAL_IN_WORK' ? '' : 'action'}`}
|
||||
onClick={() => setActiveTab('claim')}
|
||||
>
|
||||
{localViolation.status === 'LEGAL_IN_WORK' || localViolation.status === 'COMPLAINT_AND_LEGAL_IN_WORK' ? t('case') : t('file-a-claim')}
|
||||
</button>
|
||||
</div>
|
||||
<div className="violation-info-content">
|
||||
{activeTab === 'complaint' && (
|
||||
<CaseComplaint
|
||||
key={"complaint_" + localViolation.id}
|
||||
selectedViolation={localViolation}
|
||||
updateStatusHandler={updateStatusHandler}
|
||||
setLocalViolation={setLocalViolation}
|
||||
/>
|
||||
)}
|
||||
{activeTab === 'claim' && (
|
||||
<CaseLegal
|
||||
key={"legal_" + localViolation.id}
|
||||
selectedViolation={localViolation}
|
||||
updateStatusHandler={updateStatusHandler}
|
||||
setLocalViolation={setLocalViolation}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link';
|
||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations';
|
||||
import CaseComplaint from '@/app/ui/file-page/violation-table/case-complaint';
|
||||
import CaseViolation from '@/app/ui/file-page/violation-table/case-claims';
|
||||
import { updateMatchStatus } from '@/app/actions/violationActions';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { MatchStatus } from '@/app/actions/violationActions';
|
||||
import { toast } from 'sonner';
|
||||
import FilePageViolationEpmtyScreen from '@/app/ui/file-page/violation-table/file-page-violation-epmty-screen';
|
||||
import Image from 'next/image';
|
||||
import FilePageViolationInfoTabs from '@/app/ui/file-page/violation-table/file-page-violation-info-tabs';
|
||||
import { createPortal } from 'react-dom';
|
||||
import { IconContentCopy, IconFollowToLink, IconFullScreen } from '@/app/ui/icons/icons';
|
||||
import { useViewport } from '@/app/hooks/useViewport';
|
||||
|
||||
const IN_WORK_STATUS = ['COMPLAINT_AND_LEGAL_IN_WORK', 'LEGAL_IN_WORK', 'COMPLAINT_IN_WORK'];
|
||||
|
||||
export default function FilePageViolationInfo(
|
||||
{
|
||||
selectedViolation,
|
||||
caseType,
|
||||
callBackSelectedViolation
|
||||
}:
|
||||
{
|
||||
selectedViolation: ViolationFileDetail | null,
|
||||
caseType?: 'claim' | 'complaint',
|
||||
callBackSelectedViolation: React.Dispatch<React.SetStateAction<ViolationFileDetail | null>>,
|
||||
}
|
||||
) {
|
||||
const t = useTranslations('Global');
|
||||
const tStatus = useTranslations('Match-status');
|
||||
const queryClient = useQueryClient();
|
||||
const [localViolation, setLocalViolation] = useState<ViolationFileDetail | null>(null);
|
||||
const [isUpdating, setIsUpdating] = useState(false);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [isHovered, setIsHovered] = useState(false);
|
||||
const [copied, setCopied] = useState(false);
|
||||
const viewport = useViewport();
|
||||
|
||||
const openInNewTab = () => {
|
||||
if (selectedViolation?.url) {
|
||||
window.open(selectedViolation.url, '_blank');
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setLocalViolation(selectedViolation);
|
||||
}, [selectedViolation]);
|
||||
|
||||
|
||||
const updateStatusHandler = useCallback(async (id: number, status: MatchStatus) => {
|
||||
setIsUpdating(true);
|
||||
|
||||
setLocalViolation(prev => prev ? { ...prev, status } : null);
|
||||
|
||||
try {
|
||||
const response = await updateMatchStatus(id, status);
|
||||
if (response) {
|
||||
if (status === 'AUTHORIZED_USE') {
|
||||
toast.success('Статус обновлен');
|
||||
}
|
||||
queryClient.invalidateQueries({ queryKey: ['fileViolations'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['complainInfo', id] });
|
||||
queryClient.invalidateQueries({ queryKey: ['violationData'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['lastCases'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['lastComplaints'] });
|
||||
|
||||
if (status === 'NOT_OWNER_FILE') {
|
||||
console.log('NOT_OWNER_FILE');
|
||||
setLocalViolation(null);
|
||||
callBackSelectedViolation(null);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
setLocalViolation(selectedViolation);
|
||||
toast.error('Ошибка при обновлении статуса');
|
||||
return false;
|
||||
}
|
||||
} catch (error) {
|
||||
setLocalViolation(selectedViolation);
|
||||
toast.error('Ошибка при обновлении статуса');
|
||||
return false;
|
||||
} finally {
|
||||
setIsUpdating(false);
|
||||
}
|
||||
}, [queryClient, selectedViolation]);
|
||||
|
||||
const cleanUrlForDisplay = (url: string) => {
|
||||
if (!url) return '#';
|
||||
return url.split('?')[0];
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const updateStatus = async () => {
|
||||
if (selectedViolation && (selectedViolation.status === 'CREATED' || selectedViolation.status === 'NEW')) {
|
||||
await updateStatusHandler(selectedViolation.id, 'SHOWED');
|
||||
}
|
||||
};
|
||||
|
||||
updateStatus();
|
||||
}, [selectedViolation, updateStatusHandler]);
|
||||
|
||||
if (!selectedViolation) {
|
||||
return (
|
||||
<FilePageViolationEpmtyScreen />
|
||||
)
|
||||
}
|
||||
|
||||
const ImageModal = ({ src, alt, onClose }: { src: string; alt: string; onClose: () => void }) => {
|
||||
return (
|
||||
<div className="image-modal-overlay" onClick={onClose}>
|
||||
<div className="image-modal-content" onClick={(e) => e.stopPropagation()}>
|
||||
<button className="image-modal-close" onClick={onClose}>×</button>
|
||||
<img src={src} alt={alt} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const handleCopyLink = () => {
|
||||
if (!selectedViolation?.page_url) return;
|
||||
|
||||
const linkToCopy = cleanUrlForDisplay(selectedViolation.page_url);
|
||||
|
||||
if (!navigator.clipboard) {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = linkToCopy;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
toast.success(`${t('copied')}: ${linkToCopy}`)
|
||||
return;
|
||||
}
|
||||
|
||||
navigator.clipboard.writeText(linkToCopy)
|
||||
.then(() => {
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
toast.success(`${t('copied')}: ${linkToCopy}`)
|
||||
})
|
||||
.catch();
|
||||
};
|
||||
|
||||
const getMaxLengthByWidth = (): number => {
|
||||
if (viewport.device === 'MOBILE') return 22;
|
||||
if (viewport.device === 'TABLET') return 50;
|
||||
if (viewport.device === 'DESKTOP') return 100;
|
||||
return 100;
|
||||
};
|
||||
|
||||
const cutFileName = (fileName: string) => {
|
||||
const MAX_FILE_NAME_LENGTH = getMaxLengthByWidth();
|
||||
const nameWithoutExtension = fileName.substring(0, MAX_FILE_NAME_LENGTH);
|
||||
|
||||
if (fileName.length <= MAX_FILE_NAME_LENGTH) {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
const maxNameLength = MAX_FILE_NAME_LENGTH;
|
||||
|
||||
if (maxNameLength <= 0) {
|
||||
return '...';
|
||||
}
|
||||
|
||||
return nameWithoutExtension.substring(0, maxNameLength) + '...';
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="violation-info"
|
||||
>
|
||||
{isModalOpen && createPortal(
|
||||
<ImageModal
|
||||
src={selectedViolation?.url || '/images/no-image.png'}
|
||||
alt={selectedViolation?.page_title || 'Image'}
|
||||
onClose={() => setIsModalOpen(false)}
|
||||
/>,
|
||||
document.body
|
||||
)}
|
||||
<div
|
||||
className="violation-info-content"
|
||||
>
|
||||
<div
|
||||
className="violation-info-header"
|
||||
>
|
||||
<div
|
||||
className="violation-info-header-grid"
|
||||
>
|
||||
<div className="violation-info-image-wrapper">
|
||||
<div
|
||||
className="violation-info-image"
|
||||
>
|
||||
<Image
|
||||
src={selectedViolation?.url}
|
||||
alt={selectedViolation?.page_title}
|
||||
unoptimized
|
||||
fill
|
||||
sizes="100px"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = '/images/no-image.png';
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="violation-info-image-buttons">
|
||||
<button
|
||||
className="image-btn image-btn-view"
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
title="Увеличить"
|
||||
>
|
||||
<IconFullScreen />
|
||||
</button>
|
||||
<button
|
||||
className="image-btn image-btn-open"
|
||||
onClick={openInNewTab}
|
||||
title="Открыть в новой вкладке"
|
||||
>
|
||||
<IconFollowToLink />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="violation-info-right-side"
|
||||
>
|
||||
<h3
|
||||
className="violation-info-title"
|
||||
>
|
||||
{selectedViolation?.page_title ? cutFileName(selectedViolation?.page_title) : '#'}
|
||||
</h3>
|
||||
<div
|
||||
className="mt-auto"
|
||||
>
|
||||
{selectedViolation?.country && (
|
||||
<div
|
||||
className="violation-info-source"
|
||||
>
|
||||
<span>
|
||||
{t('country')}:
|
||||
</span>
|
||||
<br />
|
||||
{selectedViolation?.country}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="violation-info-source"
|
||||
>
|
||||
<span>
|
||||
{t('full-link')}:
|
||||
</span>
|
||||
<div
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
<span
|
||||
className="source"
|
||||
>
|
||||
{selectedViolation?.page_url ? cutFileName(cleanUrlForDisplay(selectedViolation?.page_url)) : selectedViolation?.page_url}
|
||||
</span>
|
||||
|
||||
<span
|
||||
className={`violation-info-icon ${!isHovered ? 'hiden' : ''}`}
|
||||
onClick={handleCopyLink}
|
||||
>
|
||||
<IconContentCopy />
|
||||
</span>
|
||||
|
||||
<Link
|
||||
className={`violation-info-icon ${!isHovered ? 'hiden' : ''}`}
|
||||
href={selectedViolation?.page_url ? cleanUrlForDisplay(selectedViolation?.page_url) : '#'}
|
||||
target="_blank"
|
||||
scroll={false}
|
||||
>
|
||||
<IconFollowToLink />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="violation-info-status"
|
||||
>
|
||||
{/* COMPLAINT_IN_WORK COMPLAINT_AND_LEGAL_IN_WORK*/}
|
||||
{localViolation?.status === "COMPLAINT_AND_LEGAL_IN_WORK" ? (
|
||||
<>
|
||||
<span className="source-status legal_in_work">
|
||||
{tStatus('LEGAL_IN_WORK')}
|
||||
</span>
|
||||
<span className="source-status complaint_in_work">
|
||||
{tStatus('COMPLAINT_IN_WORK')}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
localViolation?.status && (
|
||||
<span className={`source-status ${localViolation?.status.toLowerCase()}`}>
|
||||
{tStatus(localViolation?.status)}
|
||||
</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="violation-info-header-bottom"
|
||||
>
|
||||
<div
|
||||
className="violation-info-date"
|
||||
>
|
||||
<span>
|
||||
{t('date-of-discovery')}:
|
||||
</span>
|
||||
<div
|
||||
className="violation-info-date-value"
|
||||
>
|
||||
<div>
|
||||
{selectedViolation?.created_date ? `${formatDate(selectedViolation?.created_date)}` : '#'}
|
||||
</div>
|
||||
<div>
|
||||
{selectedViolation?.created_date ? `${formatDateTime(selectedViolation?.created_date)}` : '#'}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="violation-info-action"
|
||||
>
|
||||
{!IN_WORK_STATUS.includes(selectedViolation.status) && (
|
||||
<button
|
||||
className="btn-s btn-no-my-file"
|
||||
onClick={() => {
|
||||
if (selectedViolation?.id) {
|
||||
updateStatusHandler(selectedViolation.id, 'NOT_OWNER_FILE');
|
||||
}
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{tStatus('NOT_OWNER_FILE')}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{!IN_WORK_STATUS.includes(selectedViolation.status) && (
|
||||
<button
|
||||
className="btn-s btn-resolve"
|
||||
onClick={() => {
|
||||
if (selectedViolation?.id) {
|
||||
updateStatusHandler(selectedViolation.id, 'AUTHORIZED_USE');
|
||||
}
|
||||
}}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
{t('allow-use')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FilePageViolationInfoTabs
|
||||
selectedViolation={selectedViolation}
|
||||
updateStatusHandler={updateStatusHandler}
|
||||
caseType={caseType}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,289 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, usePathname, useSearchParams } from 'next/navigation';
|
||||
import { useEffect, useState } from 'react';
|
||||
import FilePageViolationInfo from '@/app/ui/file-page/violation-table/file-page-violation-info';
|
||||
import { useFileViolations } from '@/app/hooks/react-query/useFileViolations';
|
||||
import { ViolationFileDetail } from '@/app/hooks/react-query/useFileViolations';
|
||||
import DropDownList from '@/app/components/DropDownList';
|
||||
import { IconArrowLeft, IconArrowRight, IconDoubleArrowLeft, IconDoubleArrowRight, IconFollowToLink } from '@/app/ui/icons/icons';
|
||||
import FilePageViolationsLoadingList from '@/app/ui/file-page/violation-table/file-page-violations-loading-list';
|
||||
import Image from 'next/image';
|
||||
|
||||
export default function FilePageViolationsList({
|
||||
currentPage,
|
||||
fileId,
|
||||
status,
|
||||
violationId,
|
||||
caseType,
|
||||
startDate,
|
||||
endDate
|
||||
}: {
|
||||
currentPage: number,
|
||||
fileId: string,
|
||||
status: string | undefined,
|
||||
violationId?: string | undefined,
|
||||
caseType?: 'claim' | 'complaint',
|
||||
startDate?: string,
|
||||
endDate?: string
|
||||
}) {
|
||||
const t = useTranslations('Global');
|
||||
const tStatus = useTranslations('Match-status');
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useSearchParams();
|
||||
const [selectedViolation, setSelectedViolation] = useState<ViolationFileDetail | null>(null);
|
||||
|
||||
const [cachedPagination, setCachedPagination] = useState<{
|
||||
total_pages: number;
|
||||
total_elements: number;
|
||||
current_page: number;
|
||||
} | null>(null);
|
||||
|
||||
const { data: fileViolations, error, isPending } = useFileViolations(fileId, currentPage, status, violationId, startDate, endDate);
|
||||
|
||||
function selectHandler(violation: ViolationFileDetail) {
|
||||
setSelectedViolation(violation);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (fileViolations?.violations) {
|
||||
setCachedPagination({
|
||||
total_pages: fileViolations.total_pages,
|
||||
total_elements: fileViolations.total_elements,
|
||||
current_page: fileViolations.current_page
|
||||
});
|
||||
}
|
||||
|
||||
if (violationId && fileViolations?.violations.length) {
|
||||
setSelectedViolation(fileViolations?.violations[0]);
|
||||
}
|
||||
}, [fileViolations]);
|
||||
|
||||
const paginationData = fileViolations?.violations ? fileViolations : cachedPagination;
|
||||
|
||||
if (!fileViolations?.violations) {
|
||||
return <FilePageViolationsLoadingList
|
||||
currentPage={currentPage}
|
||||
fileId={fileId}
|
||||
status={status}
|
||||
cachedTotalPages={paginationData?.total_pages}
|
||||
cachedTotalElements={paginationData?.total_elements}
|
||||
cachedCurrentPage={paginationData?.current_page}
|
||||
/>;
|
||||
}
|
||||
|
||||
const handlePageChange = (page: number) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
params.set('page', page.toString());
|
||||
params.delete('violationId');
|
||||
params.delete('caseType');
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
const handleStatusChange = (status: string) => {
|
||||
const params = new URLSearchParams(searchParams);
|
||||
if (status) {
|
||||
params.set('status', status);
|
||||
} else {
|
||||
params.delete('status');
|
||||
}
|
||||
setSelectedViolation(null);
|
||||
params.set('page', '1');
|
||||
params.delete('violationId');
|
||||
params.delete('caseType');
|
||||
router.push(`${pathname}?${params.toString()}`, { scroll: false });
|
||||
};
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
const maxVisible = 5;
|
||||
const total = fileViolations.total_pages;
|
||||
/* const totalPages = Math.ceil(fileViolations.total_elements / fileViolations.page_size); */
|
||||
|
||||
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
|
||||
let end = Math.min(total, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{t('all-file-violations')}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="status-filter-select"
|
||||
>
|
||||
<DropDownList
|
||||
value={status ? tStatus(status) : t('all-statuses')}
|
||||
callBack={handleStatusChange}
|
||||
>
|
||||
<li value=''>{t('all-statuses')}</li>
|
||||
<li value="NEW">{tStatus('NEW')}</li>
|
||||
<li value="SHOWED">{tStatus('SHOWED')}</li>
|
||||
<li value="LEGAL_IN_WORK">{tStatus('LEGAL_IN_WORK')}</li>
|
||||
<li value="COMPLAINT_IN_WORK">{tStatus('COMPLAINT_IN_WORK')}</li>
|
||||
<li value="COMPLAINT_AND_LEGAL_IN_WORK">{tStatus('COMPLAINT_AND_LEGAL_IN_WORK')}</li>
|
||||
<li value="AUTHORIZED_USE">{tStatus('AUTHORIZED_USE')}</li>
|
||||
</DropDownList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="sources-list-wrapper"
|
||||
>
|
||||
<div className="sources-list">
|
||||
{fileViolations?.violations.map(item => {
|
||||
return (
|
||||
<div
|
||||
className={`source-card ${selectedViolation?.id === item.id ? 'selected' : ''}`}
|
||||
key={item.id}
|
||||
onClick={() => {
|
||||
selectHandler(item);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="source-header-left"
|
||||
>
|
||||
<div className="source-image">
|
||||
<Image
|
||||
src={item.url}
|
||||
alt={item.page_title}
|
||||
unoptimized
|
||||
fill
|
||||
sizes="100px"
|
||||
onError={(e) => {
|
||||
const target = e.target as HTMLImageElement;
|
||||
target.src = '/images/no-image.png';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
className="source-url-block"
|
||||
>
|
||||
<span
|
||||
className="source-meta"
|
||||
>
|
||||
{t('source')}:
|
||||
</span>
|
||||
<br />
|
||||
<span className="source-url">
|
||||
{item.host}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="source-header-right"
|
||||
>
|
||||
{item.status === "COMPLAINT_AND_LEGAL_IN_WORK" ? (
|
||||
<>
|
||||
<span className="source-status legal_in_work">
|
||||
{tStatus('LEGAL_IN_WORK')}
|
||||
</span>
|
||||
<span className="source-status complaint_in_work">
|
||||
{tStatus('COMPLAINT_IN_WORK')}
|
||||
</span>
|
||||
</>
|
||||
) : (
|
||||
<span className={`source-status ${item.status.toLowerCase()}`}>
|
||||
{tStatus(item.status)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="source-actions">
|
||||
<Link
|
||||
href={item.page_url}
|
||||
target="_blank"
|
||||
scroll={false}
|
||||
>
|
||||
<IconFollowToLink />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{/* general info for match */}
|
||||
<FilePageViolationInfo selectedViolation={selectedViolation} caseType={caseType} callBackSelectedViolation={setSelectedViolation} />
|
||||
|
||||
</div>
|
||||
{/* pagination */}
|
||||
<div
|
||||
className="sources-list-pagination"
|
||||
>
|
||||
<span>
|
||||
{t('page')} <strong>{fileViolations?.current_page + 1} {t('out-of')} {fileViolations?.total_pages}</strong> | {t('total-violations')}: {fileViolations?.total_elements}
|
||||
</span>
|
||||
{fileViolations?.total_pages > 1 && (
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => handlePageChange(1)}
|
||||
disabled={currentPage === 1}
|
||||
>
|
||||
<IconDoubleArrowLeft />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage - 1)}
|
||||
className="arrow"
|
||||
disabled={!fileViolations?.has_previous || isPending}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="pagination-controls-pages"
|
||||
>
|
||||
{getPageNumbers().map(pageNum => (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => handlePageChange(pageNum)}
|
||||
className={currentPage === pageNum ? 'current' : 'other'}
|
||||
disabled={isPending}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => handlePageChange(currentPage + 1)}
|
||||
className="arrow"
|
||||
disabled={!fileViolations?.has_next}
|
||||
>
|
||||
<IconArrowRight />
|
||||
</button>
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => handlePageChange(fileViolations?.total_pages)}
|
||||
disabled={currentPage === fileViolations?.total_pages || isPending}
|
||||
>
|
||||
<IconDoubleArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link';
|
||||
import FilePageViolationInfo from '@/app/ui/file-page/violation-table/file-page-violation-info';
|
||||
import DropDownList from '@/app/components/DropDownList';
|
||||
import { IconArrowLeft, IconArrowRight, IconDoubleArrowLeft, IconDoubleArrowRight } from '@/app/ui/icons/icons';
|
||||
import { useTranslations } from 'next-intl';
|
||||
|
||||
export default function FilePageViolationsLoadingList({
|
||||
currentPage,
|
||||
fileId,
|
||||
status,
|
||||
cachedTotalPages,
|
||||
cachedTotalElements,
|
||||
cachedCurrentPage,
|
||||
}: {
|
||||
currentPage: number,
|
||||
fileId: string,
|
||||
status: string | undefined,
|
||||
cachedTotalPages: number | undefined,
|
||||
cachedTotalElements: number | undefined,
|
||||
cachedCurrentPage: number | undefined
|
||||
}) {
|
||||
const t = useTranslations('Global');
|
||||
const tStatus = useTranslations('Match-status');
|
||||
|
||||
const getPageNumbers = () => {
|
||||
const pages = [];
|
||||
const maxVisible = 5;
|
||||
const total = cachedTotalPages;
|
||||
|
||||
let start = Math.max(1, currentPage - Math.floor(maxVisible / 2));
|
||||
let end = Math.min(total ? total : 0, start + maxVisible - 1);
|
||||
|
||||
if (end - start + 1 < maxVisible) {
|
||||
start = Math.max(1, end - maxVisible + 1);
|
||||
}
|
||||
|
||||
for (let i = start; i <= end; i++) {
|
||||
pages.push(i);
|
||||
}
|
||||
|
||||
return pages;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="block-wrapper">
|
||||
<div className="card-header">
|
||||
<h3 className="card-title">
|
||||
{t('all-file-violations')}
|
||||
</h3>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="status-filter-select"
|
||||
>
|
||||
<DropDownList
|
||||
value={status ? tStatus(status) : t('all-statuses')}
|
||||
callBack={() => { }}
|
||||
>
|
||||
<li value=''>{t('all-statuses')}</li>
|
||||
<li value="NEW">{tStatus('NEW')}</li>
|
||||
<li value="SHOWED">{tStatus('SHOWED')}</li>
|
||||
<li value="LEGAL_IN_WORK">{tStatus('LEGAL_IN_WORK')}</li>
|
||||
<li value="COMPLAINT_IN_WORK">{tStatus('COMPLAINT_IN_WORK')}</li>
|
||||
<li value="COMPLAINT_AND_LEGAL_IN_WORK">{tStatus('COMPLAINT_AND_LEGAL_IN_WORK')}</li>
|
||||
<li value="AUTHORIZED_USE">{tStatus('AUTHORIZED_USE')}</li>
|
||||
</DropDownList>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="sources-list-wrapper relative"
|
||||
>
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<div
|
||||
className="global-spinner large"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="sources-list-pagination"
|
||||
>
|
||||
|
||||
<span>
|
||||
{t('total-violations')}: {cachedTotalElements} | {t('page')} {cachedCurrentPage} {t('out-of')} {cachedTotalPages}
|
||||
</span>
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => { }}
|
||||
disabled={true}
|
||||
>
|
||||
<IconDoubleArrowLeft />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { }}
|
||||
className="arrow"
|
||||
disabled={true}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</button>
|
||||
|
||||
<div
|
||||
className="pagination-controls-pages"
|
||||
>
|
||||
{getPageNumbers().map(pageNum => (
|
||||
<button
|
||||
key={pageNum}
|
||||
onClick={() => { }}
|
||||
className={currentPage === pageNum ? 'current' : 'other'}
|
||||
disabled={true}
|
||||
>
|
||||
{pageNum}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => { }}
|
||||
className="arrow"
|
||||
disabled={true}
|
||||
>
|
||||
<IconArrowRight />
|
||||
</button>
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => { }}
|
||||
disabled={true}
|
||||
>
|
||||
<IconDoubleArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -256,9 +256,12 @@ export default function RegisterForm() {
|
||||
<ConfirmMailModalWindow userId={state?.userId} email={formState?.previousState?.email} onClose={() => { setShowMailConfirmWindow(false) }} />
|
||||
</div>
|
||||
)}
|
||||
<form action={(e) => {
|
||||
formAction(e);
|
||||
}}>
|
||||
<form
|
||||
action={(e) => {
|
||||
formAction(e);
|
||||
}}
|
||||
autoComplete="off"
|
||||
>
|
||||
<div className={`${styles['form-switcher']}`}>
|
||||
<button
|
||||
className={`${styles['form-switcher-button']} ${accountType === 'b2c' ? styles['active'] : ''}`}
|
||||
@@ -432,6 +435,7 @@ export default function RegisterForm() {
|
||||
e.target.value = e.target.value.toLocaleLowerCase();
|
||||
onChangeHandler(e)
|
||||
}}
|
||||
autoComplete="off"
|
||||
/>
|
||||
{formState?.error?.email && (
|
||||
<p className="text-sm text-red-500">
|
||||
@@ -486,6 +490,7 @@ export default function RegisterForm() {
|
||||
placeholder={t('password-placeholder')}
|
||||
defaultValue={formState?.previousState?.password ? formState?.previousState?.password : ''}
|
||||
onChange={onChangeHandler}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<button
|
||||
onClick={() => {
|
||||
|
||||
@@ -1,17 +1,27 @@
|
||||
"use client"
|
||||
|
||||
import { useState, useRef } from 'react';
|
||||
import { useState, useRef, useEffect } from 'react';
|
||||
import { useClickOutside } from '@/app/hooks/useClickOutside';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import Link from 'next/link';
|
||||
import { IconNotification } from '@/app/ui/icons/icons';
|
||||
import { IconInfoMessage, IconPolicy, IconFiles, IconAccountBalanceWallet, IconCheckbook, IconGavel } from '@/app/ui/icons/icons';
|
||||
import { useActiveNotifications } from '@/app/hooks/react-query/useActiveNotificationsList';
|
||||
import { notificationsMarkAsRead } from '@/app/actions/notificationActions';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { formatDate } from '@/app/lib/formatDate';
|
||||
import { NotificationTextHandler } from '@/app/components/NotificationTextHandler';
|
||||
|
||||
export default function NotificationsButton() {
|
||||
const t = useTranslations('Global');
|
||||
const norificationTranslate = useTranslations('Notifications');
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [data, setData] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const menuRef = useRef<HTMLDivElement | null>(null);
|
||||
const t = useTranslations('Global');
|
||||
const [loadingIds, setLoadingIds] = useState<Set<number>>(new Set());
|
||||
const [isMarkingAll, setIsMarkingAll] = useState(false);
|
||||
|
||||
const { data: notificationList } = useActiveNotifications();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
useClickOutside(
|
||||
menuRef,
|
||||
@@ -29,10 +39,108 @@ export default function NotificationsButton() {
|
||||
}
|
||||
};
|
||||
|
||||
async function markAsReadHandler(ids: number[]) {
|
||||
setLoadingIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
ids.forEach(id => newSet.add(id));
|
||||
return newSet;
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await notificationsMarkAsRead(ids);
|
||||
|
||||
if (response.success) {
|
||||
await queryClient.invalidateQueries({ queryKey: ['activeNotifications'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['allNotifications'] });
|
||||
}
|
||||
} catch (error) {
|
||||
setError(error instanceof Error ? error.message : 'Failed to mark as read');
|
||||
} finally {
|
||||
setLoadingIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
ids.forEach(id => newSet.delete(id));
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function markAllAsReadHandler() {
|
||||
const array = notificationList?.notifications?.map(e => e.id) ?? [];
|
||||
if (array.length === 0) return;
|
||||
|
||||
setIsMarkingAll(true);
|
||||
|
||||
try {
|
||||
await markAsReadHandler(array);
|
||||
} finally {
|
||||
setIsMarkingAll(false);
|
||||
}
|
||||
}
|
||||
|
||||
const getNotificationIcon = (type: string) => {
|
||||
switch (type) {
|
||||
case 'SEARCH_RESULT':
|
||||
case 'MONITORING_RESULT':
|
||||
case 'SEARCH_OPERATION_FAILED':
|
||||
return <IconPolicy />;
|
||||
|
||||
case 'FILE_MODERATION_EVENT':
|
||||
case 'FILE_ADDED_TO_SYSTEM':
|
||||
return <IconFiles />;
|
||||
|
||||
case 'PAYOUT_RESULT':
|
||||
case 'PAYMENT_RESULT':
|
||||
case 'REFERRAL_DEPOSIT':
|
||||
return <IconAccountBalanceWallet />;
|
||||
|
||||
case 'REFERRAL_REGISTERED':
|
||||
case 'REFERRAL_ACTIVATED':
|
||||
return <IconCheckbook />;
|
||||
|
||||
case 'COMPLAINT_STATUS_CHANGED':
|
||||
case 'CASE_STATUS_CHANGED':
|
||||
return <IconGavel />;
|
||||
|
||||
case 'OPERATION_IMPOSSIBLE':
|
||||
case 'START_FAILED_NOW':
|
||||
case 'START_FAILED_NEXT':
|
||||
case 'TOKEN_NOT_FOUND':
|
||||
case 'USER_NOT_VERIFIED':
|
||||
return <IconInfoMessage />;
|
||||
|
||||
case 'USER_VERIFIED':
|
||||
return <IconPolicy />;
|
||||
|
||||
case 'INCOMING_MESSAGE':
|
||||
return <IconInfoMessage />;
|
||||
|
||||
case 'TARIFF_EXPIRING':
|
||||
return <IconInfoMessage />;
|
||||
|
||||
default:
|
||||
return <IconInfoMessage />;
|
||||
}
|
||||
};
|
||||
|
||||
const getFontSizeByCount = (count: number | undefined) => {
|
||||
if (!count) return '12px';
|
||||
|
||||
const countStr = String(count);
|
||||
|
||||
switch (countStr.length) {
|
||||
case 3:
|
||||
return '10px';
|
||||
case 4:
|
||||
return '8px';
|
||||
default:
|
||||
return '12px';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="notification-wrapper" ref={menuRef}>
|
||||
<button
|
||||
className="icon-btn"
|
||||
className={`icon-btn ${notificationList?.unreadCount ? 'active' : ''}`}
|
||||
id="notifications-btn"
|
||||
title="Уведомления"
|
||||
onClick={handleButtonClick}
|
||||
@@ -40,6 +148,14 @@ export default function NotificationsButton() {
|
||||
<svg viewBox="0 0 24 24" fill="currentColor" style={{ width: "20px", height: "20px" }}>
|
||||
<path d="M12 22c1.1 0 2-.9 2-2h-4c0 1.1.89 2 2 2zm6-6v-5c0-3.07-1.64-5.64-4.5-6.32V4c0-.83-.67-1.5-1.5-1.5s-1.5.67-1.5 1.5v.68C7.63 5.36 6 7.92 6 11v5l-2 2v1h16v-1l-2-2z"></path>
|
||||
</svg>
|
||||
{(notificationList?.unreadCount !== 0 && notificationList?.unreadCount !== undefined) && (
|
||||
<span
|
||||
className="notification-unread-count"
|
||||
/* style={{ fontSize: getFontSizeByCount(notificationList?.unreadCount) }} */
|
||||
>
|
||||
{notificationList?.unreadCount > 9 ? '9+' : notificationList?.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<div className={`notification-dropdown ${isOpen ? 'show' : ''}`}>
|
||||
{error ? (
|
||||
@@ -49,40 +165,122 @@ export default function NotificationsButton() {
|
||||
<div className='notification-header'>
|
||||
<h4>
|
||||
{t('notifications')}
|
||||
{notificationList?.notifications?.length !== 0 ? (
|
||||
<span
|
||||
className="notification-count"
|
||||
>
|
||||
{notificationList?.unreadCount ? notificationList?.unreadCount : ''}
|
||||
</span>
|
||||
) : null}
|
||||
</h4>
|
||||
<span className="notification-count">
|
||||
{t('no-new')}
|
||||
</span>
|
||||
</div>
|
||||
{data ? (
|
||||
<div className="notification-list">
|
||||
<div className="notification-item">
|
||||
<div className="notification-icon">
|
||||
<IconNotification />
|
||||
</div>
|
||||
<div className="notification-content">
|
||||
<div className="notification-title">
|
||||
{t('tokens-added')}
|
||||
{notificationList?.notifications?.length ? (
|
||||
<button
|
||||
className="notification-mark-all-as-read"
|
||||
onClick={() => {
|
||||
markAllAsReadHandler()
|
||||
}}
|
||||
disabled={isMarkingAll || loadingIds.size > 0}
|
||||
>
|
||||
{isMarkingAll || loadingIds.size > 0 && (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
<div className="notification-text">{t('added')} 100 {t('tokens')}.</div>
|
||||
<div className="notification-time">4 {t('days-ago')}</div>
|
||||
</div>
|
||||
</div>
|
||||
{JSON.stringify(data)}
|
||||
</div>
|
||||
) : (
|
||||
<p>
|
||||
{t('no-data')}
|
||||
</p>
|
||||
)}
|
||||
<div className="notification-footer">
|
||||
<Link href="#" className="notification-link">
|
||||
{t('view-all')}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{t('mark-all-as-read')}
|
||||
</button>
|
||||
) : null}
|
||||
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
{notificationList?.notifications?.length !== 0 ? (
|
||||
<>
|
||||
<div
|
||||
className="notification-list"
|
||||
>
|
||||
{notificationList?.notifications?.map((item) => {
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className={`notification-item ${loadingIds.has(item.id) ? 'loading' : ''}`}
|
||||
>
|
||||
<button
|
||||
className="notification-icon"
|
||||
onClick={() => {
|
||||
markAsReadHandler([item.id]);
|
||||
}}
|
||||
disabled={loadingIds.has(item.id)}
|
||||
>
|
||||
{getNotificationIcon(item.type)}
|
||||
</button>
|
||||
|
||||
<div className="notification-content">
|
||||
<div
|
||||
className="notification-item-header"
|
||||
>
|
||||
<div className="notification-title">
|
||||
{norificationTranslate.has(item.message) ? norificationTranslate(item.message) : item.message}
|
||||
</div>
|
||||
|
||||
<div className="notification-data">
|
||||
{item.createdAt ? formatDate(item.createdAt) : '#'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="notification-text">
|
||||
<NotificationTextHandler notificationType={item.type} notificationMessage={item.message} notificationContext={item.context} />
|
||||
</div>
|
||||
|
||||
|
||||
{loadingIds.has(item.id) && (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="notification-footer">
|
||||
<Link
|
||||
href={'/pages/notifications'}
|
||||
className="notification-link"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('all-notifications')}
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div>
|
||||
<p
|
||||
className="text-center mb-4 mt-4"
|
||||
>
|
||||
{t('no-new-notifications')}
|
||||
</p>
|
||||
|
||||
<div className="notification-footer">
|
||||
<Link
|
||||
href={'/pages/notifications'}
|
||||
className="notification-link"
|
||||
onClick={() => {
|
||||
setIsOpen(false);
|
||||
}}
|
||||
>
|
||||
{t('all-notifications')}
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { logout } from '@/app/actions/auth';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import {formatDate} from '@/app/lib/formatDate';
|
||||
|
||||
export default function UserMenuButton() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@@ -24,6 +25,7 @@ export default function UserMenuButton() {
|
||||
);
|
||||
|
||||
const handleButtonClick = () => {
|
||||
console.log('click');
|
||||
if (!isOpen) {
|
||||
setIsOpen(true);
|
||||
} else {
|
||||
@@ -44,7 +46,12 @@ export default function UserMenuButton() {
|
||||
>
|
||||
{userData?.fullName ? userData.fullName[0] : ''}
|
||||
</div>
|
||||
<div className={`user-dropdown ${isOpen ? 'show' : ''}`} id="user-dropdown">
|
||||
<div
|
||||
className={`user-dropdown ${isOpen ? 'show' : ''}`} id="user-dropdown"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
<div className="user-info-header">
|
||||
<div className="user-avatar-large">
|
||||
{userData?.fullName ? userData.fullName[0] : ''}
|
||||
@@ -56,25 +63,39 @@ export default function UserMenuButton() {
|
||||
<div className="user-email">
|
||||
{userData?.email ?? ''}
|
||||
</div>
|
||||
<div className="user-subscription">
|
||||
<span className="text-[14px]">{t('rate')}: </span>
|
||||
<span className="subscription-badge">
|
||||
{userData?.subscriptionType ? userData.subscriptionType : ''}
|
||||
</span>
|
||||
<br />
|
||||
<span className="user-subscription-expire">{t('expires')}: 00.00.00</span>
|
||||
</div>
|
||||
{userData?.tariffInfo?.tariffName && (
|
||||
<div className="user-subscription">
|
||||
<span className="text-[14px]">{t('rate')}: </span>
|
||||
<span className="subscription-badge">
|
||||
{userData?.tariffInfo?.tariffName ? userData?.tariffInfo?.tariffName : '---'}
|
||||
</span>
|
||||
<br />
|
||||
<span className="user-subscription-expire">{t('expires')}: {formatDate(userData?.tariffInfo.endTariff)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="user-menu-items">
|
||||
<Link href="/pages/settings" className="user-menu-item">
|
||||
<Link
|
||||
href="/pages/settings"
|
||||
className="user-menu-item"
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M19.14,12.94c0.04-0.3,0.06-0.61,0.06-0.94c0-0.32-0.02-0.64-0.07-0.94l2.03-1.58c0.18-0.14,0.23-0.41,0.12-0.61 l-1.92-3.32c-0.12-0.22-0.37-0.29-0.59-0.22l-2.39,0.96c-0.5-0.38-1.03-0.7-1.62-0.94L14.4,2.81c-0.04-0.24-0.24-0.41-0.48-0.41 h-3.84c-0.24,0-0.43,0.17-0.47,0.41L9.25,5.35C8.66,5.59,8.12,5.92,7.63,6.29L5.24,5.33c-0.22-0.08-0.47,0-0.59,0.22L2.74,8.87 C2.62,9.08,2.66,9.34,2.86,9.48l2.03,1.58C4.84,11.36,4.82,11.69,4.82,12s0.02,0.64,0.07,0.94l-2.03,1.58 c-0.18,0.14-0.23,0.41-0.12,0.61l1.92,3.32c0.12,0.22,0.37,0.29,0.59,0.22l2.39-0.96c0.5,0.38,1.03,0.7,1.62,0.94l0.36,2.54 c0.05,0.24,0.24,0.41,0.48,0.41h3.84c0.24,0,0.44-0.17,0.47-0.41l0.36-2.54c0.59-0.24,1.13-0.56,1.62-0.94l2.39,0.96 c0.22,0.08,0.47,0,0.59-0.22l1.92-3.32c0.12-0.22,0.07-0.47-0.12-0.61L19.14,12.94z M12,15.6c-1.98,0-3.6-1.62-3.6-3.6 s1.62-3.6,3.6-3.6s3.6,1.62,3.6,3.6S13.98,15.6,12,15.6z"></path>
|
||||
</svg>
|
||||
{t('settings')}
|
||||
</Link>
|
||||
<Link href="/pages/faq" className="user-menu-item">
|
||||
<Link
|
||||
href="/pages/faq"
|
||||
className="user-menu-item"
|
||||
onClick={() => {
|
||||
setIsOpen(false)
|
||||
}}
|
||||
>
|
||||
<svg viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-2h2v2zm2.07-7.75l-.9.92C13.45 12.9 13 13.5 13 15h-2v-.5c0-1.1.45-2.1 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41 0-1.1-.9-2-2-2s-2 .9-2 2H8c0-2.21 1.79-4 4-4s4 1.79 4 4c0 .88-.36 1.68-.93 2.25z"></path>
|
||||
</svg>
|
||||
@@ -88,6 +109,7 @@ export default function UserMenuButton() {
|
||||
className="user-menu-logout"
|
||||
onClick={async (e) => {
|
||||
e.preventDefault();
|
||||
setIsOpen(false);
|
||||
queryClient.clear();
|
||||
await logout();
|
||||
}}
|
||||
|
||||
@@ -200,4 +200,94 @@ export function IconGraph() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><path fill="currentColor" d="m16 11.78l4.24-7.33l1.73 1l-5.23 9.05l-6.51-3.75L5.46 19H22v2H2V3h2v14.54L9.5 8z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconCheck() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" className="icon"><path fill="currentColor" d="m9.55 15.15l8.475-8.475q.3-.3.7-.3t.7.3t.3.713t-.3.712l-9.175 9.2q-.3.3-.7.3t-.7-.3L4.55 13q-.3-.3-.288-.712t.313-.713t.713-.3t.712.3z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconPolicy() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M480-80q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 85-29 163.5T688-214L560-342q-18 11-38.5 16.5T480-320q-66 0-113-47t-47-113q0-66 47-113t113-47q66 0 113 47t47 113q0 22-5.5 42.5T618-398l60 60q20-41 31-86t11-92v-189l-240-90-240 90v189q0 121 68 220t172 132q26-8 49.5-20.5T576-214l56 56q-33 27-71.5 47T480-80Zm56.5-343.5Q560-447 560-480t-23.5-56.5Q513-560 480-560t-56.5 23.5Q400-513 400-480t23.5 56.5Q447-400 480-400t56.5-23.5ZM488-477Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconFiles() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M160-160q-33 0-56.5-23.5T80-240v-400q0-33 23.5-56.5T160-720h240l80-80h320q33 0 56.5 23.5T880-720v480q0 33-23.5 56.5T800-160H160Zm73-280h207v-207L233-440Zm-73-40 160-160H160v160Zm0 120v120h640v-480H520v280q0 33-23.5 56.5T440-360H160Zm280-160Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconAccountBalanceWallet() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M200-200v-560 560Zm0 80q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h560q33 0 56.5 23.5T840-760v100h-80v-100H200v560h560v-100h80v100q0 33-23.5 56.5T760-120H200Zm320-160q-33 0-56.5-23.5T440-360v-240q0-33 23.5-56.5T520-680h280q33 0 56.5 23.5T880-600v240q0 33-23.5 56.5T800-280H520Zm280-80v-240H520v240h280Zm-117.5-77.5Q700-455 700-480t-17.5-42.5Q665-540 640-540t-42.5 17.5Q580-505 580-480t17.5 42.5Q615-420 640-420t42.5-17.5Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconCheckbook() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M240-360h280l80-80H240v80Zm0-160h240v-80H240v80Zm-80-160v400h280l-80 80H80v-560h800v120h-80v-40H160Zm756 212q5 5 5 11t-5 11l-36 36-70-70 36-36q5-5 11-5t11 5l48 48ZM520-120v-70l266-266 70 70-266 266h-70ZM160-680v400-400Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconGavel() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M160-120v-80h480v80H160Zm226-194L160-540l84-86 228 226-86 86Zm254-254L414-796l86-84 226 226-86 86Zm184 408L302-682l56-56 522 522-56 56Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconInfoMessage() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M440-280h80v-240h-80v240Zm68.5-331.5Q520-623 520-640t-11.5-28.5Q497-680 480-680t-28.5 11.5Q440-657 440-640t11.5 28.5Q463-600 480-600t28.5-11.5ZM480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-134-93-227t-227-93q-134 0-227 93t-93 227q0 134 93 227t227 93Zm0-320Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconContentCopy() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M360-240q-33 0-56.5-23.5T280-320v-480q0-33 23.5-56.5T360-880h360q33 0 56.5 23.5T800-800v480q0 33-23.5 56.5T720-240H360Zm0-80h360v-480H360v480ZM200-80q-33 0-56.5-23.5T120-160v-560h80v560h440v80H200Zm160-240v-480 480Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconFollowToLink() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M200-120q-33 0-56.5-23.5T120-200v-560q0-33 23.5-56.5T200-840h280v80H200v560h560v-280h80v280q0 33-23.5 56.5T760-120H200Zm188-212-56-56 372-372H560v-80h280v280h-80v-144L388-332Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconFullScreen() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M120-120v-200h80v120h120v80H120Zm520 0v-80h120v-120h80v200H640ZM120-640v-200h200v80H200v120h-80Zm640 0v-120H640v-80h200v200h-80Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconLink() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon"><path d="M440-280H280q-83 0-141.5-58.5T80-480q0-83 58.5-141.5T280-680h160v80H280q-50 0-85 35t-35 85q0 50 35 85t85 35h160v80ZM320-440v-80h320v80H320Zm200 160v-80h160q50 0 85-35t35-85q0-50-35-85t-85-35H520v-80h160q83 0 141.5 58.5T880-480q0 83-58.5 141.5T680-280H520Z" /></svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconPerson() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon">
|
||||
<path d="M367-527q-47-47-47-113t47-113q47-47 113-47t113 47q47 47 47 113t-47 113q-47 47-113 47t-113-47ZM160-160v-112q0-34 17.5-62.5T224-378q62-31 126-46.5T480-440q66 0 130 15.5T736-378q29 15 46.5 43.5T800-272v112H160Zm80-80h480v-32q0-11-5.5-20T700-306q-54-27-109-40.5T480-360q-56 0-111 13.5T260-306q-9 5-14.5 14t-5.5 20v32Zm296.5-343.5Q560-607 560-640t-23.5-56.5Q513-720 480-720t-56.5 23.5Q400-673 400-640t23.5 56.5Q447-560 480-560t56.5-23.5ZM480-640Zm0 400Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconGlobe() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon">
|
||||
<path d="M480-80q-83 0-156-31.5T197-197q-54-54-85.5-127T80-480q0-83 31.5-156T197-763q54-54 127-85.5T480-880q83 0 156 31.5T763-763q54 54 85.5 127T880-480q0 83-31.5 156T763-197q-54 54-127 85.5T480-80Zm0-80q134 0 227-93t93-227q0-7-.5-14.5T799-507q-5 29-27 48t-52 19h-80q-33 0-56.5-23.5T560-520v-40H400v-80q0-33 23.5-56.5T480-720h40q0-23 12.5-40.5T563-789q-20-5-40.5-8t-42.5-3q-134 0-227 93t-93 227h200q66 0 113 47t47 113v40H400v110q20 5 39.5 7.5T480-160Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IconCity() {
|
||||
return (
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" className="icon">
|
||||
<path d="M120-120v-560h240v-80l120-120 120 120v240h240v400H120Zm80-80h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm240 320h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm0-160h80v-80h-80v80Zm240 480h80v-80h-80v80Zm0-160h80v-80h-80v80Z" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -35,7 +35,6 @@ export default function BankCardInput({ bankCardState, validateHandler, disabled
|
||||
return (
|
||||
<InputMask
|
||||
type="phone"
|
||||
id="bankCard"
|
||||
name="bankCard"
|
||||
className={`${styles['form-input']} ${isValid ? styles['valid-card'] : ''}`}
|
||||
mask="____ ____ ____ ____"
|
||||
|
||||
@@ -3,12 +3,17 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function ProtectionStatistic({ fileType }: { fileType: string }) {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
|
||||
useEffect(() => {
|
||||
console.log(filesInfo);
|
||||
}, [filesInfo])
|
||||
|
||||
const getFileTypeData = (fileType: string) => {
|
||||
switch (fileType) {
|
||||
case 'image':
|
||||
@@ -35,7 +40,9 @@ export default function ProtectionStatistic({ fileType }: { fileType: string })
|
||||
<div className="protection-statistic-stat-label">{t(`${fileType}-protected`)}</div>
|
||||
</div>
|
||||
<div className="protection-statistic-stat-card">
|
||||
<div className="protection-statistic-stat-number">0</div>
|
||||
<div className="protection-statistic-stat-number">
|
||||
{currentData?.check ?? 0}
|
||||
</div>
|
||||
<div className="protection-statistic-stat-label">{t(`${fileType}-checked`)}</div>
|
||||
</div>
|
||||
<div className="protection-statistic-stat-card">
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
'use client';
|
||||
|
||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { FileDetails } from '@/app/[locale]/pages/file/[id]/page';
|
||||
import { FileAnAppeal } from '@/app/actions/fileEntity';
|
||||
import { useState } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { AppealInfos } from '@/app/components/tanstak-table/TanstakTable';
|
||||
|
||||
export function FileAppealModalWindow({ fileId, appeal, setWindowClose, setWindowChildren }: {
|
||||
fileId: string,
|
||||
appeal?: AppealInfos[],
|
||||
setWindowClose: any,
|
||||
setWindowChildren: any
|
||||
}) {
|
||||
const t = useTranslations('Global');
|
||||
const [appealReason, setAppealReason] = useState('');
|
||||
const [additionalInfo, setAdditionalInfo] = useState('');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const existingAppeal = appeal && appeal.length > 0 ? appeal[0] : null;
|
||||
const hasActiveAppeal = existingAppeal && existingAppeal.status === 'pending';
|
||||
|
||||
const handleSubmitAppeal = async () => {
|
||||
if (!appealReason.trim()) {
|
||||
toast.error(t('please-enter-appeal-reason'));
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
const result = await FileAnAppeal(fileId, appealReason, additionalInfo);
|
||||
|
||||
if (result?.message_desc === 'Appeal submitted successfully') {
|
||||
toast.success(t('appeal-submitted-successfully'));
|
||||
setWindowClose(false);
|
||||
} else if (result?.message_desc === 'Active appeal already exists for this file') {
|
||||
toast.error(t('active-appeal-already-exists-for-this-file'));
|
||||
}
|
||||
else {
|
||||
toast.error(result?.message_desc || t('appeal-submission-failed'));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Appeal submission error:', error);
|
||||
toast.error(t('appeal-submission-failed'));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="modal-window-view-file">
|
||||
<div className="modal-window-view-file-content">
|
||||
{existingAppeal && (
|
||||
<div className="file-info-card appeal-info">
|
||||
<div className="info-header">
|
||||
<h4>{t('appeal-information')}</h4>
|
||||
</div>
|
||||
|
||||
<div className="info-item">
|
||||
<div className="info-label">{t('appeal-status')}</div>
|
||||
<div className="info-value">
|
||||
<span className="appeal-status">
|
||||
{existingAppeal.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="info-item">
|
||||
<div className="info-label">{t('appeal-reason')}</div>
|
||||
<div className="info-value">
|
||||
<div className="appeal-reason-text">
|
||||
{existingAppeal.appealReason || t('not-provided')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{existingAppeal.additionalInfo && (
|
||||
<div className="info-item">
|
||||
<div className="info-label">{t('additional-info')}</div>
|
||||
<div className="info-value">
|
||||
<div className="appeal-additional-text">
|
||||
{existingAppeal.additionalInfo}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{existingAppeal.createdAt && (
|
||||
<div className="info-item">
|
||||
<div className="info-label">{t('appeal-created-at')}</div>
|
||||
<div className="info-value">
|
||||
{formatDate(existingAppeal.createdAt)} {formatDateTime(existingAppeal.createdAt)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{existingAppeal.adminComment && (
|
||||
<div className="info-item">
|
||||
<div className="info-label">{t('admin-comment')}</div>
|
||||
<div className="info-value">
|
||||
<div className="admin-comment-text">
|
||||
{existingAppeal.adminComment}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{existingAppeal.resolvedAt && (
|
||||
<div className="info-item">
|
||||
<div className="info-label">{t('resolved-at')}</div>
|
||||
<div className="info-value">
|
||||
{formatDateTime(existingAppeal.resolvedAt)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{hasActiveAppeal && (
|
||||
<div className="appeal-warning">
|
||||
{t('active-appeal-pending-message')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasActiveAppeal && (
|
||||
<div className="file-info-card appeal-form">
|
||||
<div className="info-header">
|
||||
<h4>{t('file-an-appeal')}</h4>
|
||||
</div>
|
||||
|
||||
<div className="info-item">
|
||||
<div className="info-label">
|
||||
{t('appeal-reason')} <span className="required">*</span>
|
||||
</div>
|
||||
<div className="info-value">
|
||||
<textarea
|
||||
className="appeal-textarea"
|
||||
value={appealReason}
|
||||
onChange={(e) => setAppealReason(e.target.value)}
|
||||
placeholder={t('enter-appeal-reason')}
|
||||
rows={4}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="info-item">
|
||||
<div className="info-label">{t('additional-info')}</div>
|
||||
<div className="info-value">
|
||||
<textarea
|
||||
className="appeal-textarea"
|
||||
value={additionalInfo}
|
||||
onChange={(e) => setAdditionalInfo(e.target.value)}
|
||||
placeholder={t('enter-additional-info')}
|
||||
rows={3}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="appeal-actions">
|
||||
<button
|
||||
className="btn btn-cancel"
|
||||
onClick={() => setWindowClose(false)}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{t('cancel')}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={handleSubmitAppeal}
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? t('submitting') : t('submit-appeal')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { IconDocument, IconShield, IconSearch, IconDiscet } from '@/app/ui/icons/icons';
|
||||
import {useUserFilesInfo} from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
import { useUserFilesInfo } from '@/app/hooks/react-query/useUserFilesInfo';
|
||||
import { useMonitoringStats } from '@/app/hooks/react-query/useMonitoringStats';
|
||||
|
||||
export default function MyContentStatsOverview() {
|
||||
const t = useTranslations('Global');
|
||||
const { data: filesInfo, isLoading, isError, error } = useUserFilesInfo();
|
||||
const { data: monitoringStats, isLoading: monitoringStatsLoading } = useMonitoringStats();
|
||||
|
||||
return (
|
||||
<div className="my-content-stats-overview">
|
||||
@@ -16,10 +18,23 @@ export default function MyContentStatsOverview() {
|
||||
<div className="stat-icon">
|
||||
<IconDocument />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.quantity}</div>
|
||||
<div className="stat-label">Всего файлов</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
filesInfo?.total.quantity
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('total-files')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card protected" data-icon="">
|
||||
@@ -27,10 +42,23 @@ export default function MyContentStatsOverview() {
|
||||
<div className="stat-icon">
|
||||
<IconShield />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">{filesInfo?.total.protected}</div>
|
||||
<div className="stat-label">Защищено файлов</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
filesInfo?.total.protected
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('protected-files')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card checks" data-icon="">
|
||||
@@ -38,10 +66,23 @@ export default function MyContentStatsOverview() {
|
||||
<div className="stat-icon">
|
||||
<IconSearch />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">0</div>
|
||||
<div className="stat-label">Проверок защиты</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{monitoringStatsLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
monitoringStats?.total_searches ? monitoringStats?.total_searches : 0
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('checks')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="stat-card storage" data-icon="">
|
||||
@@ -49,10 +90,23 @@ export default function MyContentStatsOverview() {
|
||||
<div className="stat-icon">
|
||||
<IconDiscet />
|
||||
</div>
|
||||
{/* <div className="stat-trend">+0%</div> */}
|
||||
</div>
|
||||
<div className="stat-value">{convertBytes(filesInfo?.total.size ?? 0)}</div>
|
||||
<div className="stat-label">Хранилище</div>
|
||||
<div className="stat-value">
|
||||
<div
|
||||
className="loading-placeholder start"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="loading-animation">
|
||||
<div className="global-spinner" />
|
||||
</div>
|
||||
) : (
|
||||
convertBytes(filesInfo?.total.size ?? 0)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="stat-label">
|
||||
{t('storage')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function DropDownList() {
|
||||
|
||||
const links = [
|
||||
{
|
||||
name: 'images',
|
||||
name: 'images-few',
|
||||
href: '/pages/marking-images'
|
||||
},
|
||||
{
|
||||
@@ -29,7 +29,7 @@ export default function DropDownList() {
|
||||
href: '/pages/marking-audio'
|
||||
},
|
||||
{
|
||||
name: 'documents-few',
|
||||
name: 'documents',
|
||||
href: '/pages/marking-document'
|
||||
}
|
||||
];
|
||||
|
||||
@@ -14,6 +14,11 @@
|
||||
"href": "/pages/my-content",
|
||||
"img": "M20 6h-2.18c.11-.31.18-.65.18-1a2.996 2.996 0 0 0-5.5-1.65l-.5.67-.5-.68C10.96 2.54 10 2 9 2 7.34 2 6 3.34 6 5c0 .35.07.69.18 1H4c-1.11 0-1.99.89-1.99 2L2 19c0 1.11.89 2 2 2h16c1.11 0 2-.89 2-2V8c0-1.11-.89-2-2-2zm-5-2c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1zM9 4c.55 0 1 .45 1 1s-.45 1-1 1-1-.45-1-1 .45-1 1-1z"
|
||||
},
|
||||
{
|
||||
"name": "tracking",
|
||||
"href": "/pages/tracking",
|
||||
"img": "M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"
|
||||
},
|
||||
{
|
||||
"name": "referral-program",
|
||||
"href": "/pages/refferals",
|
||||
|
||||
@@ -0,0 +1,403 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl';
|
||||
import { IconCheck, IconArrowLeft, IconArrowRight, IconDoubleArrowLeft, IconDoubleArrowRight } from '@/app/ui/icons/icons';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useAllNotifications } from '@/app/hooks/react-query/useAllNotificationsList';
|
||||
import { notificationsMarkAsRead, notificationsDelete, NotificationBody } from '@/app/actions/notificationActions';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { formatDate, formatDateTime } from '@/app/lib/formatDate';
|
||||
|
||||
import {
|
||||
useReactTable,
|
||||
getCoreRowModel,
|
||||
getPaginationRowModel,
|
||||
flexRender,
|
||||
createColumnHelper,
|
||||
RowSelectionState,
|
||||
} from '@tanstack/react-table';
|
||||
import { NotificationTextHandler } from '@/app/components/NotificationTextHandler';
|
||||
|
||||
export function NotificationsList() {
|
||||
const t = useTranslations('Global');
|
||||
const norificationTranslate = useTranslations('Notifications');
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
|
||||
const [loadingIds, setLoadingIds] = useState<Set<number>>(new Set());
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [pagination, setPagination] = useState({
|
||||
pageIndex: 0,
|
||||
pageSize: 10,
|
||||
});
|
||||
|
||||
const { data: allNotifications, isLoading } = useAllNotifications(
|
||||
pagination.pageIndex,
|
||||
pagination.pageSize
|
||||
);
|
||||
|
||||
const totalPages = allNotifications?.totalPages || 0;
|
||||
const totalRecords = allNotifications?.totalElements || 0;
|
||||
|
||||
const columnHelper = createColumnHelper<NotificationBody>();
|
||||
|
||||
const columns = [
|
||||
columnHelper.display({
|
||||
id: 'select',
|
||||
header: ({ table }) => {
|
||||
const currentPageRows = table.getCoreRowModel().rows;
|
||||
const unreadRowsOnPage = currentPageRows.filter(row => row.original.status !== 'READIED');
|
||||
const allNotificationSelectedOnPage = currentPageRows.every(row => rowSelection[row.original.id]);
|
||||
|
||||
const unreadIdsSet = new Set(unreadRowsOnPage.map(row => row.original.id));
|
||||
const selectedUnreadIds = Object.keys(rowSelection)
|
||||
.map(Number)
|
||||
.filter(id => unreadIdsSet.has(id));
|
||||
|
||||
return (
|
||||
<div className="select-all-header">
|
||||
{selectedUnreadIds.length > 0 && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
const selectedIds = Object.keys(rowSelection).map(Number);
|
||||
notificationActionHandler(selectedUnreadIds, 'mark-as-read');
|
||||
}}
|
||||
>
|
||||
{t('mark-as-read')} ({selectedUnreadIds.length})
|
||||
</button>
|
||||
)}
|
||||
{Object.keys(rowSelection).length > 0 && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
const selectedIds = Object.keys(rowSelection).map(Number);
|
||||
notificationActionHandler(selectedIds, 'remove');
|
||||
}}
|
||||
>
|
||||
{t('remove')} ({Object.keys(rowSelection).length})
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{allNotifications?.notifications.length !== 0 && (
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
if (allNotificationSelectedOnPage) {
|
||||
|
||||
const newSelection = { ...rowSelection };
|
||||
currentPageRows.forEach(row => {
|
||||
delete newSelection[row.original.id];
|
||||
});
|
||||
setRowSelection(newSelection);
|
||||
} else {
|
||||
|
||||
const newSelection = { ...rowSelection };
|
||||
currentPageRows.forEach(row => {
|
||||
newSelection[row.original.id] = true;
|
||||
});
|
||||
setRowSelection(newSelection);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{allNotificationSelectedOnPage ? t('deselect') : t('select-all')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const isReadied = row.original.status === 'READIED';
|
||||
const isSelected = !!rowSelection[row.original.id];
|
||||
|
||||
return (
|
||||
<div className="notification-check">
|
||||
<button
|
||||
onClick={() => {
|
||||
setRowSelection((prev) => {
|
||||
const newSelection = { ...prev };
|
||||
|
||||
if (newSelection[row.original.id]) {
|
||||
delete newSelection[row.original.id];
|
||||
} else {
|
||||
newSelection[row.original.id] = true;
|
||||
}
|
||||
|
||||
return newSelection;
|
||||
});
|
||||
}}
|
||||
>
|
||||
{isSelected && <IconCheck />}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
columnHelper.accessor('message', {
|
||||
id: 'message',
|
||||
header: () => null,
|
||||
cell: ({ row }) => {
|
||||
const item = row.original;
|
||||
return (
|
||||
<div className="notification-body">
|
||||
<div className="notification-header">
|
||||
<div
|
||||
className="notification-title"
|
||||
>
|
||||
{norificationTranslate.has(item.message)
|
||||
? norificationTranslate(item.message)
|
||||
: item.message} ({item.id})
|
||||
</div>
|
||||
</div>
|
||||
<div className="notification-text">
|
||||
<NotificationTextHandler notificationType={item.type} notificationMessage={item.message} notificationContext={item.context} />
|
||||
</div>
|
||||
<div className="notification-footer">
|
||||
<div className="notification-date">
|
||||
{item.createdAt
|
||||
? `${formatDate(item.createdAt)}: ${formatDateTime(item.createdAt)}`
|
||||
: '---'}
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-remove"
|
||||
onClick={() => {
|
||||
notificationActionHandler([item.id], 'remove');
|
||||
}}
|
||||
disabled={loadingIds.has(item.id)}
|
||||
>
|
||||
{t('remove')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const table = useReactTable({
|
||||
data: allNotifications?.notifications || [],
|
||||
columns,
|
||||
state: {
|
||||
rowSelection,
|
||||
pagination,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onPaginationChange: setPagination,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getPaginationRowModel: getPaginationRowModel(),
|
||||
manualPagination: true,
|
||||
pageCount: totalPages,
|
||||
});
|
||||
|
||||
async function notificationActionHandler(ids: number[], action: 'remove' | 'mark-as-read') {
|
||||
setLoadingIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
ids.forEach(id => newSet.add(id));
|
||||
return newSet;
|
||||
});
|
||||
|
||||
try {
|
||||
let response;
|
||||
|
||||
if (action === 'mark-as-read') {
|
||||
response = await notificationsMarkAsRead(ids);
|
||||
} else if (action === 'remove') {
|
||||
response = await notificationsDelete(ids);
|
||||
}
|
||||
|
||||
if (response?.success) {
|
||||
setRowSelection({});
|
||||
await queryClient.invalidateQueries({ queryKey: ['activeNotifications'] });
|
||||
await queryClient.invalidateQueries({ queryKey: ['allNotifications'] });
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = action === 'mark-as-read' ? 'Failed to mark as read' : 'Failed to remove';
|
||||
setError(errorMessage);
|
||||
} finally {
|
||||
setLoadingIds(prev => {
|
||||
const newSet = new Set(prev);
|
||||
ids.forEach(id => newSet.delete(id));
|
||||
return newSet;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const getPageNumbers = (): (number | string)[] => {
|
||||
const delta: number = 2;
|
||||
const currentPageNum = pagination.pageIndex;
|
||||
const range: number[] = [];
|
||||
const rangeWithDots: (number | string)[] = [];
|
||||
let l: number | undefined;
|
||||
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
if (i === 0 || i === totalPages - 1 || (i >= currentPageNum - delta && i <= currentPageNum + delta)) {
|
||||
range.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
range.forEach((i: number) => {
|
||||
if (l !== undefined) {
|
||||
if (i - l === 2) {
|
||||
rangeWithDots.push(l + 1);
|
||||
} else if (i - l !== 1) {
|
||||
rangeWithDots.push('...');
|
||||
}
|
||||
}
|
||||
rangeWithDots.push(i);
|
||||
l = i;
|
||||
});
|
||||
|
||||
return rangeWithDots;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="notifications-list-wrapper">
|
||||
<div className="notifications-list-header">
|
||||
<h4>{t('all-notifications')}</h4>
|
||||
<div className="notifications-table-header">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<div key={headerGroup.id} className="table-row n-header">
|
||||
{headerGroup.headers.map((header) => (
|
||||
<div key={header.id} className="table-cell">
|
||||
{flexRender(
|
||||
header.column.columnDef.header,
|
||||
header.getContext()
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{/* <div
|
||||
className="notifications-list-action"
|
||||
>
|
||||
{(selectedFiles.size !== 0) && (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
clearHandler();
|
||||
}}
|
||||
>
|
||||
{t('deselect')}
|
||||
</button>
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
const array = Array.from(selectedFiles);
|
||||
notificationActionHandler(array, 'mark-as-read');
|
||||
}}
|
||||
>
|
||||
{t('mark-as-read')}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
selectAllHandler();
|
||||
}}
|
||||
>
|
||||
{t('select-all')}
|
||||
</button>
|
||||
</div> */}
|
||||
</div>
|
||||
|
||||
<div className="notifications-list-body">
|
||||
{allNotifications?.notifications.length !== 0 ? (
|
||||
<div className="notifications-table">
|
||||
<div className="notifications-table-body">
|
||||
{table.getRowModel().rows.map((row) => (
|
||||
<div
|
||||
key={row.id}
|
||||
className={`table-row ${loadingIds.has(row.original.id) ? 'loading' : ''} ${row.original.status}`}
|
||||
>
|
||||
{row.getVisibleCells().map((cell) => (
|
||||
<div key={cell.id} className="table-cell">
|
||||
{flexRender(
|
||||
cell.column.columnDef.cell,
|
||||
cell.getContext()
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="no-notifications">{t('no-notifications')}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="notifications-list-footer">
|
||||
{totalPages > 0 && (
|
||||
<div className="pagination-controls">
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => {
|
||||
setRowSelection({});
|
||||
table.firstPage();
|
||||
}}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<IconDoubleArrowLeft />
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setRowSelection({});
|
||||
table.previousPage()
|
||||
}}
|
||||
className="arrow"
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
>
|
||||
<IconArrowLeft />
|
||||
</button>
|
||||
|
||||
<div className="pagination-controls-pages">
|
||||
{getPageNumbers().map((pageNum, index) => (
|
||||
<button
|
||||
key={index}
|
||||
onClick={() => {
|
||||
setRowSelection({});
|
||||
typeof pageNum === 'number' && table.setPageIndex(pageNum);
|
||||
}}
|
||||
className={pagination.pageIndex === pageNum ? 'current' : 'other'}
|
||||
disabled={pageNum === '...'}
|
||||
>
|
||||
{pageNum === '...' ? '...' : (pageNum as number) + 1}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
setRowSelection({});
|
||||
table.nextPage();
|
||||
}}
|
||||
className="arrow"
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<IconArrowRight />
|
||||
</button>
|
||||
<button
|
||||
className="arrow"
|
||||
onClick={() => {
|
||||
setRowSelection({});
|
||||
table.lastPage();
|
||||
}}
|
||||
disabled={!table.getCanNextPage()}
|
||||
>
|
||||
<IconDoubleArrowRight />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <div className="error-message">{error}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -14,7 +14,9 @@ export default function PaymentTabTransactionHistory() {
|
||||
const t = useTranslations('Global');
|
||||
|
||||
// Состояния
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
const [sorting, setSorting] = useState<SortingState>([
|
||||
{ id: 'date', desc: true }
|
||||
]);
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [dateFilter, setDateFilter] = useState<string>('all');
|
||||
const [pagination, setPagination] = useState({
|
||||
@@ -123,7 +125,7 @@ export default function PaymentTabTransactionHistory() {
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<div className="text-center table-item">
|
||||
{row.original.operationType === 'TOKEN'? t('purchasing-tokens') : t('payment-of-the-tariff')}
|
||||
{row.original.operationType === 'TOKEN' ? t('purchasing-tokens') : t('payment-of-the-tariff')}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -427,9 +429,6 @@ export default function PaymentTabTransactionHistory() {
|
||||
{table.getState().pagination.pageIndex + 1} {t('out-of')} {table.getPageCount() ? table.getPageCount() : 1}
|
||||
</strong>
|
||||
</span>
|
||||
<span className="pagination-info-files">
|
||||
| {t('shown')} {table.getRowModel().rows.length} {t('out-of')} {filteredData.length}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="pagination-controls">
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
'use client'
|
||||
|
||||
import { useTranslations } from 'next-intl'
|
||||
import BankCardInput from '@/app/ui/inputs/bank-card-inpit';
|
||||
import { usePaymentsMethods, PaymentsMethods } from '@/app/hooks/react-query/usePaymentsMethods';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { deletePaymentsMethods } from '@/app/actions/paymentAction';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import ModalWindow from '@/app/components/ModalWindow';
|
||||
|
||||
export default function PaymentUserCards() {
|
||||
const t = useTranslations('Global');
|
||||
const { data: paymentsMethods } = usePaymentsMethods();
|
||||
const queryClient = useQueryClient();
|
||||
const [openWindow, setOpenWindow] = useState<boolean>(false);
|
||||
const [selectedCard, setSelectedCard] = useState<PaymentsMethods | null>(null);
|
||||
const [modalLoading, setModalLoading] = useState(false);
|
||||
|
||||
function openConfirmWindowHandler(item: PaymentsMethods) {
|
||||
setOpenWindow(true);
|
||||
setSelectedCard(item);
|
||||
}
|
||||
|
||||
async function removeCardHandler(id: string) {
|
||||
setModalLoading(true);
|
||||
|
||||
try {
|
||||
const response = await deletePaymentsMethods(id);
|
||||
|
||||
if (response) {
|
||||
await queryClient.invalidateQueries({ queryKey: ['paymentsMethods'] });
|
||||
toast.success(t('bank-card-has-been-successfully-deleted'));
|
||||
} else {
|
||||
throw 'error'
|
||||
}
|
||||
} catch (error) {
|
||||
toast.warning(t('failed-to-delete-bank-card'))
|
||||
} finally {
|
||||
setModalLoading(false);
|
||||
setOpenWindow(false);
|
||||
setSelectedCard(null);
|
||||
}
|
||||
}
|
||||
|
||||
const ConfirmModalWindow = () => {
|
||||
return (
|
||||
<div
|
||||
className="card-item-confirm-window"
|
||||
>
|
||||
<h4>
|
||||
{t('delete-a-bank-card')}?
|
||||
</h4>
|
||||
<div
|
||||
className="card-item-confirm-action"
|
||||
>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
setSelectedCard(null);
|
||||
setOpenWindow(false);
|
||||
}}
|
||||
disabled={modalLoading}
|
||||
>
|
||||
{t('no')}
|
||||
{modalLoading && (
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<span
|
||||
className="global-spinner"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => {
|
||||
if (selectedCard?.paymentMethodId) {
|
||||
removeCardHandler(selectedCard?.paymentMethodId)
|
||||
}
|
||||
}}
|
||||
disabled={modalLoading}
|
||||
>
|
||||
{t('yes')}
|
||||
{modalLoading && (
|
||||
<div
|
||||
className="loading-animation"
|
||||
>
|
||||
<span
|
||||
className="global-spinner"
|
||||
>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div >
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className="block-wrapper payment-user-cards"
|
||||
>
|
||||
<ModalWindow state={openWindow} callBack={setOpenWindow}>
|
||||
<ConfirmModalWindow />
|
||||
</ModalWindow>
|
||||
<h3>
|
||||
{t('linked-bank-cards')}
|
||||
</h3>
|
||||
|
||||
<div
|
||||
className="cards-wrapper"
|
||||
>
|
||||
{paymentsMethods?.length !== 0 ? (
|
||||
paymentsMethods?.map((item: PaymentsMethods) => {
|
||||
return (
|
||||
<div
|
||||
className="card-item"
|
||||
key={item?.paymentMethodId}
|
||||
>
|
||||
<div
|
||||
className="card-item-info"
|
||||
>
|
||||
<BankCardInput
|
||||
bankCardState={item?.lastFour}
|
||||
validateHandler={() => { }}
|
||||
disabled={true}
|
||||
isValid={undefined}
|
||||
/>
|
||||
|
||||
<span
|
||||
className="card-item-type"
|
||||
>
|
||||
{item?.cardType}
|
||||
</span>
|
||||
|
||||
{/* <span
|
||||
className="card-item-expired"
|
||||
>
|
||||
<span
|
||||
className="ml-1"
|
||||
>
|
||||
expired at:
|
||||
</span>
|
||||
<span>
|
||||
{item?.expiryMonth}/
|
||||
</span>
|
||||
<span>
|
||||
{item?.expiryYear}
|
||||
</span>
|
||||
</span> */}
|
||||
</div>
|
||||
|
||||
<button
|
||||
className="btn btn-warning"
|
||||
onClick={() => {
|
||||
openConfirmWindowHandler(item)
|
||||
}}
|
||||
>
|
||||
{t('remove')}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
) : (
|
||||
<div
|
||||
className="payment-user-cards-empty"
|
||||
>
|
||||
<div>
|
||||
<p
|
||||
className="empty-card-title"
|
||||
>
|
||||
{t('the-card-is-not-linked')}
|
||||
</p>
|
||||
<p
|
||||
className="empty-card-text"
|
||||
>
|
||||
{t('the-card-is-linked-automatically-the-next-time-you-pay-for-a-subscription')}
|
||||
<br />
|
||||
{t('after-this-the-subscription-will-renew-automatically')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="payment-user-cards-notification"
|
||||
>
|
||||
{t('when-you-pay-for-a-subscription-your-card-is-saved-for-automatic-renewal')}
|
||||
{t('the-debit-occurs-1-day-before-the-end-of-the-subscription')}
|
||||
{t('you-can-unlink-your-card-and-cancel-auto-renewal-at-any-time')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -3,10 +3,16 @@
|
||||
import { convertBytes } from '@/app/lib/convertBytes';
|
||||
import { getProcents } from '@/app/lib/getProcents';
|
||||
import { useUserProfile } from '@/app/hooks/react-query/useUserDataInfo';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export default function UserUsage() {
|
||||
const { data: userData, isLoading, isError, error } = useUserProfile();
|
||||
|
||||
useEffect(() => {
|
||||
console.log('log from user-usage');
|
||||
console.log(userData);
|
||||
}, [userData]);
|
||||
|
||||
return (
|
||||
<div className="usage-stats">
|
||||
<h3>
|
||||
@@ -49,7 +55,7 @@ export default function UserUsage() {
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">
|
||||
{userData?.tariffInfo?.tariffName ? userData?.tariffInfo?.tariffName : 'ERROR'}
|
||||
{userData?.tariffInfo?.tariffName ? userData?.tariffInfo?.tariffName : '---'}
|
||||
</div>
|
||||
<div className="stat-label">Текущий план</div>
|
||||
</div>
|
||||
|
||||
@@ -55,7 +55,7 @@ export default function IncomeAndPayments() {
|
||||
<div className="earning-card warning">
|
||||
<h4>Выплачено</h4>
|
||||
<p>
|
||||
{activeInvites?.totalIncome - activeInvites?.availableIncome + activeInvites?.holdBalance}
|
||||
{activeInvites?.spentBalance}
|
||||
</p>
|
||||
<small>Успешно выплачено на ваши счета</small>
|
||||
</div>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user