Compare commits
519 Commits
reorganize
...
7f3a22abb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7f3a22abb8 | ||
|
|
fbc51c4866 | ||
|
|
10b2ad9023 | ||
|
|
7f9bb6aae6 | ||
|
|
1ab689e056 | ||
|
|
f1ee199d92 | ||
|
|
1032891d26 | ||
|
|
9ccd807a55 | ||
|
|
35b1c7ed27 | ||
|
|
28f39a31f6 | ||
|
|
5ed4936898 | ||
|
|
55e4938a57 | ||
|
|
cfee91355b | ||
|
|
0d0f4e5f9c | ||
|
|
0221c905f0 | ||
|
|
0cadc1a642 | ||
|
|
8b01685b48 | ||
|
|
4510eb769a | ||
|
|
414e86bfb5 | ||
|
|
d8c078ad5a | ||
|
|
07367d3183 | ||
|
|
357d346787 | ||
|
|
23d9f2f66f | ||
|
|
00e5ce6b20 | ||
|
|
a339a1c64e | ||
|
|
c9a80da7c3 | ||
|
|
bad3002006 | ||
|
|
178b5f0dc7 | ||
|
|
6cc5d43a10 | ||
|
|
b04e3a67f5 | ||
|
|
0b08802996 | ||
|
|
1af337f005 | ||
|
|
3feb806caa | ||
|
|
ec8ed8f6a8 | ||
|
|
8937aea57c | ||
|
|
c0bce7feac | ||
|
|
9f784406d7 | ||
|
|
461cd8421c | ||
|
|
6231128288 | ||
|
|
fd8e7e1b28 | ||
|
|
000bb78112 | ||
|
|
d3d6632375 | ||
|
|
1e84d67e24 | ||
|
|
c461d9bd5f | ||
|
|
63039b8707 | ||
|
|
908f10e022 | ||
|
|
49aab63124 | ||
|
|
320f1f44b7 | ||
|
|
8a91a862ca | ||
|
|
4ef5ea2f58 | ||
|
|
4c4417dc1d | ||
|
|
ebca66dd4c | ||
|
|
c7d8ef1295 | ||
|
|
f336420415 | ||
|
|
9fa3321322 | ||
|
|
bac415d003 | ||
|
|
0646d587eb | ||
|
|
c3b5820ac9 | ||
|
|
570e3f3c36 | ||
|
|
e6d64abd56 | ||
|
|
5d47101714 | ||
|
|
7a2f2a452f | ||
|
|
6d075fc5b9 | ||
|
|
a95ca37a4b | ||
|
|
ce63931bc2 | ||
|
|
6f9401fa8f | ||
|
|
55b379bd6d | ||
|
|
3e3185cb6e | ||
|
|
48bcffa22c | ||
|
|
65c6d6f5d1 | ||
|
|
42f11dd8c0 | ||
|
|
96c1527d1b | ||
|
|
9e9c11dff2 | ||
|
|
fa6e5cd68b | ||
|
|
f4b92c7909 | ||
|
|
f2499df6a9 | ||
|
|
3dafd872e4 | ||
|
|
96be20c75d | ||
|
|
86091a4742 | ||
|
|
20442eb93c | ||
|
|
22282b1d44 | ||
|
|
6029acc2d4 | ||
|
|
4464fed88a | ||
|
|
3cb81a1500 | ||
|
|
3300494309 | ||
|
|
eba9b7f4f0 | ||
|
|
a32c3f241d | ||
|
|
38e58bf402 | ||
|
|
6cd61fc873 | ||
|
|
261ce6d55b | ||
|
|
d03ef2db50 | ||
|
|
99f7bace2d | ||
|
|
05c85b115d | ||
|
|
3f5ab30a74 | ||
|
|
59855b0fab | ||
|
|
53343fa711 | ||
|
|
fd7ffc8668 | ||
|
|
fc35830846 | ||
|
|
ca343c493f | ||
|
|
3e54e88db7 | ||
|
|
6c6fa00ccf | ||
|
|
a670ca994f | ||
|
|
1163bfd88a | ||
|
|
0982397d6b | ||
|
|
e153a67ec0 | ||
|
|
b909a195f7 | ||
|
|
72846b44b1 | ||
|
|
38253f7e83 | ||
|
|
3a0b1a3746 | ||
|
|
6d663eb238 | ||
|
|
5b3f969dfd | ||
|
|
7b639cabf8 | ||
|
|
46c7358dde | ||
|
|
5374401257 | ||
|
|
5707755576 | ||
|
|
d7f69b272a | ||
|
|
e0bcf9dfb8 | ||
|
|
e4c1c6e2a0 | ||
|
|
d4eb25dd4b | ||
|
|
907ac2cfe0 | ||
|
|
2cbb62a9cc | ||
|
|
0a1acbd610 | ||
|
|
f5f431620e | ||
|
|
565fd9b3a8 | ||
|
|
a03260eccf | ||
|
|
fb1afb72d4 | ||
|
|
3253b297eb | ||
|
|
2e931bbdc4 | ||
|
|
4bf0666fd1 | ||
|
|
61a57142b0 | ||
|
|
28459a8274 | ||
|
|
ce96ef184d | ||
|
|
2e854954ac | ||
|
|
b672cd90d5 | ||
|
|
18beb7b7a1 | ||
|
|
d5603c229e | ||
|
|
99560202c3 | ||
|
|
ea1a5d9b8a | ||
|
|
9ea8c98faf | ||
|
|
2c244ad366 | ||
|
|
63c9ceeaa6 | ||
|
|
712a7b4daf | ||
|
|
4ebc15fff8 | ||
|
|
2e31e80e28 | ||
|
|
f261800159 | ||
|
|
89ae50e9a4 | ||
|
|
e53f90738c | ||
|
|
6510df6566 | ||
|
|
1626718cc3 | ||
|
|
5c54541b4c | ||
|
|
be59db2e2d | ||
|
|
7cb1c8c3c6 | ||
|
|
aaa3604cd4 | ||
|
|
68d679759d | ||
|
|
a362dd4668 | ||
|
|
fbe56015b5 | ||
|
|
f563d47e8a | ||
|
|
e1112bbd50 | ||
|
|
3837ddfb2d | ||
|
|
6b0ad6455b | ||
|
|
043192accf | ||
|
|
330f24c8d7 | ||
|
|
9569b6dc46 | ||
|
|
f3c8a8cc96 | ||
|
|
fd5a436220 | ||
|
|
08976de55a | ||
|
|
63b3cd8744 | ||
|
|
687cc5f5dc | ||
|
|
a0d3f086c1 | ||
|
|
8956cd00d7 | ||
|
|
d70e5d6bdc | ||
|
|
a579117c9c | ||
|
|
2734734752 | ||
|
|
f5225ac326 | ||
|
|
56e311109a | ||
|
|
6b1dc15c80 | ||
|
|
893852cbb1 | ||
|
|
bb461e007e | ||
|
|
ec1f95db09 | ||
|
|
f8dc0cfed4 | ||
|
|
695312f1bb | ||
|
|
6dd57f12f5 | ||
|
|
1c3926332f | ||
|
|
e2d97d1ee9 | ||
|
|
a39b3429ba | ||
|
|
374774901e | ||
|
|
5b063d5462 | ||
|
|
1a5d43523a | ||
|
|
8d995105f4 | ||
|
|
d853ecb1da | ||
|
|
71d5f4d320 | ||
|
|
726df0cee0 | ||
|
|
3b955b116a | ||
|
|
440d2ec211 | ||
|
|
9eacd00136 | ||
|
|
afaf79d327 | ||
|
|
31c64e9647 | ||
|
|
0f81643744 | ||
|
|
ba999b7dbb | ||
|
|
b16e3002e4 | ||
|
|
c452d28eca | ||
|
|
16dec127c9 | ||
|
|
e2ec8dc632 | ||
|
|
574f038738 | ||
|
|
6af746c561 | ||
|
|
3c9b425203 | ||
|
|
097927f124 | ||
|
|
ac615837ab | ||
|
|
aeb48504c5 | ||
|
|
5d52d81c7d | ||
|
|
b31c75214d | ||
|
|
04b36bab9b | ||
|
|
a83cfdfe4d | ||
|
|
f2038a93d2 | ||
|
|
821a1c6ba8 | ||
|
|
419cb9a32c | ||
|
|
eb7b5c996d | ||
|
|
6be3c5892d | ||
|
|
ff4fba379f | ||
|
|
cb3a6ac98a | ||
|
|
aa308d8258 | ||
|
|
1916153e5b | ||
|
|
a8a5de5392 | ||
|
|
c069cafe45 | ||
|
|
76e9689e11 | ||
|
|
2e683cc872 | ||
|
|
eee6695d7f | ||
|
|
2417a7795b | ||
|
|
3839e2e1f6 | ||
|
|
ff53265fc0 | ||
|
|
1c71f8e83e | ||
|
|
b05278c061 | ||
|
|
9e44215dd5 | ||
|
|
2a8c1166b1 | ||
|
|
4543a6b6b6 | ||
|
|
d8456ecb9f | ||
|
|
b183410888 | ||
|
|
feda0f685f | ||
|
|
82b4a7849a | ||
|
|
fffbb64e4b | ||
|
|
35ce9ee78a | ||
|
|
910690c4d1 | ||
|
|
bfee935798 | ||
|
|
cb3dff819e | ||
|
|
4861990551 | ||
|
|
274f2a4101 | ||
|
|
8d317f043c | ||
|
|
96b12a1fbe | ||
|
|
1db0d4dfea | ||
|
|
a7bab6be52 | ||
|
|
7b8382131d | ||
|
|
3bfe820443 | ||
|
|
54725c624e | ||
|
|
3474581122 | ||
|
|
ee1cbdf38b | ||
|
|
897c1f3196 | ||
|
|
b61bf0e5bc | ||
|
|
8021b362cf | ||
|
|
da0f0cd4a4 | ||
|
|
058e2d571f | ||
|
|
0c9a855232 | ||
|
|
1c51a819ed | ||
|
|
475e5ad528 | ||
|
|
ccbf8c6e5c | ||
|
|
3bf1f31a3c | ||
|
|
e18f542357 | ||
|
|
b8d89ca8e7 | ||
|
|
2a133fb40b | ||
|
|
173ceb8081 | ||
|
|
576f2600a5 | ||
|
|
1db63ac99d | ||
|
|
88cc131fdc | ||
|
|
a67ea17ad2 | ||
|
|
17adc9e9fd | ||
|
|
7d65913245 | ||
|
|
2d8d6b6dc4 | ||
|
|
30afb5d778 | ||
|
|
60c63a0d2a | ||
|
|
6a8c4a549a | ||
|
|
dbd905b02f | ||
|
|
f82344e0dd | ||
|
|
53d6a8e169 | ||
|
|
dbb22d1392 | ||
|
|
9ab807342e | ||
|
|
5ffb353011 | ||
|
|
c663c9099c | ||
|
|
b75e753d98 | ||
|
|
9133113ab2 | ||
|
|
97bd4b2b95 | ||
|
|
f11b02134c | ||
|
|
608e1cc02b | ||
|
|
bc03cc3d27 | ||
|
|
bc46a755b1 | ||
|
|
a51aa3cf7e | ||
|
|
efdb03f23e | ||
|
|
439d3d3c95 | ||
|
|
27ff3169b9 | ||
|
|
5cbc9e4873 | ||
|
|
b309afbe8e | ||
|
|
2fc9051f86 | ||
|
|
4e63ff97bb | ||
|
|
cc885c009a | ||
|
|
3cf732797c | ||
|
|
96a338ef6f | ||
|
|
2b52965f2f | ||
|
|
677dfb73e8 | ||
|
|
170243a480 | ||
|
|
ac83c4f57f | ||
|
|
76831b8485 | ||
|
|
94e59ab878 | ||
|
|
325dc17911 | ||
|
|
6aec2ebcb2 | ||
|
|
3877b70fdf | ||
|
|
c6482f0037 | ||
|
|
a7df1980ed | ||
|
|
68442919d3 | ||
|
|
a8b2b4bf0b | ||
|
|
26c07fb3e6 | ||
|
|
1e7add2fc8 | ||
|
|
85038df24a | ||
|
|
223f908887 | ||
|
|
265f2fcbac | ||
|
|
646a68e9fd | ||
|
|
dc8c2eac91 | ||
|
|
fdf11a7766 | ||
|
|
16a134ca42 | ||
|
|
4d65386052 | ||
|
|
b166920ad7 | ||
|
|
74e5099b04 | ||
|
|
e4a3ee25db | ||
|
|
637ae28d47 | ||
|
|
ee269a9e33 | ||
|
|
49d8226411 | ||
|
|
7d6c09a346 | ||
|
|
e2c8747fcc | ||
|
|
7161a81068 | ||
|
|
7ecb19cb1a | ||
|
|
494451bb96 | ||
|
|
aed0a47388 | ||
|
|
86de2cc45b | ||
|
|
6409a91cb0 | ||
|
|
8d652c8259 | ||
|
|
a16c856537 | ||
|
|
55216817b2 | ||
|
|
92e1bdaff8 | ||
|
|
1a8f916942 | ||
|
|
3ef0bd711d | ||
|
|
10251f2fc6 | ||
|
|
c901ec1e49 | ||
|
|
6550250d13 | ||
|
|
79a12c0ec2 | ||
|
|
b0c5c5e051 | ||
|
|
0089790d41 | ||
|
|
145a13857d | ||
|
|
487a3fb913 | ||
|
|
c3d1153f0e | ||
|
|
91d9444875 | ||
|
|
d4a5daeb4c | ||
|
|
0efcfb5225 | ||
|
|
d2f0f0de54 | ||
|
|
b9f4103c8e | ||
|
|
01d2b26021 | ||
|
|
3f5aa2af2a | ||
|
|
3974eefbfe | ||
|
|
b676cec4e9 | ||
|
|
d7d73c2a10 | ||
|
|
ae3512ad37 | ||
|
|
05d75421f5 | ||
|
|
9cf508d319 | ||
|
|
58b5a4e996 | ||
|
|
eaa830af3f | ||
|
|
982f4a39be | ||
|
|
6ea9932aa7 | ||
|
|
6dfe1291ae | ||
|
|
8d47920fa3 | ||
|
|
8132c1a535 | ||
|
|
d6a1d5e3f5 | ||
|
|
ac48799d9a | ||
|
|
95dbea7768 | ||
|
|
ae258382e1 | ||
|
|
3cbb28a116 | ||
|
|
86f1449e10 | ||
|
|
e927a53029 | ||
|
|
af743a3c9f | ||
|
|
69d2f31a9e | ||
|
|
92c2f26780 | ||
|
|
3f00884f33 | ||
|
|
7dca7fee7d | ||
|
|
3b2c1048c9 | ||
|
|
0f1ebbab7e | ||
|
|
e0b73923f9 | ||
|
|
b957112fc7 | ||
|
|
a59ffbcaa4 | ||
|
|
c4063e76de | ||
|
|
3af9ab8144 | ||
|
|
fd9e423076 | ||
|
|
960901d2b2 | ||
|
|
0977f302a4 | ||
|
|
3cf0ef87f8 | ||
|
|
d1c1297fcd | ||
|
|
1190969d67 | ||
|
|
a8b415b4bd | ||
|
|
394ac5ec9d | ||
|
|
4fb918f5e4 | ||
|
|
3b802b7c7b | ||
|
|
1b2a5af2be | ||
|
|
6410321895 | ||
|
|
51445a7341 | ||
|
|
56df8632cb | ||
|
|
824bed199c | ||
|
|
b5728f1238 | ||
|
|
04814aeeda | ||
|
|
9386fbc2f8 | ||
|
|
a06b654103 | ||
|
|
9aaff4d80a | ||
|
|
7a06843bf5 | ||
|
|
1decc08f77 | ||
|
|
c0cfbcbcbb | ||
|
|
688c225911 | ||
|
|
3e79304e5c | ||
|
|
e7d8ec8c63 | ||
|
|
1e3cd99c69 | ||
|
|
3ab67cbe2d | ||
|
|
b3c056980d | ||
|
|
fb3bb6c77c | ||
|
|
bdc330c885 | ||
|
|
31da7f85cf | ||
|
|
69e63fc5f3 | ||
|
|
fe6fc2cb74 | ||
|
|
80cc90d347 | ||
|
|
9b5c2dd95c | ||
|
|
58e0869916 | ||
|
|
14bdd3bcd0 | ||
|
|
a10216a392 | ||
|
|
e53c8230e6 | ||
|
|
c6bc05560e | ||
|
|
63b0e18396 | ||
|
|
1bec150822 | ||
|
|
4d8dc6b59c | ||
|
|
b0a744034b | ||
|
|
49f69f6af0 | ||
|
|
5017b62059 | ||
|
|
ea80f90d0f | ||
|
|
dd74432dd7 | ||
|
|
4aef4881e1 | ||
|
|
7bc3eb10c1 | ||
|
|
55957df00c | ||
|
|
cb2666177a | ||
|
|
6e5fb3b86a | ||
|
|
a15f2bca6a | ||
|
|
1897cbe7a6 | ||
|
|
ab1732d74b | ||
|
|
7df15a4243 | ||
|
|
abb74390e8 | ||
|
|
06a7568386 | ||
|
|
77737f0ba9 | ||
|
|
6de461473e | ||
|
|
db781fd871 | ||
|
|
ce301e9c70 | ||
|
|
64288b5ce1 | ||
|
|
a8bb725f78 | ||
|
|
df2208ab53 | ||
|
|
72deb8d5e3 | ||
|
|
5566e011b7 | ||
|
|
ee23fd2d3c | ||
|
|
2a41062769 | ||
|
|
6624de7a32 | ||
|
|
44553f5bd4 | ||
|
|
5ed255dddb | ||
|
|
650bf137f2 | ||
|
|
3a8bc2f893 | ||
|
|
d29de100c6 | ||
|
|
97214c3a90 | ||
|
|
56f4c56b9e | ||
|
|
0b3b2ee463 | ||
|
|
c3e4e695eb | ||
|
|
c112aded47 | ||
|
|
75f029b872 | ||
|
|
f823df7e15 | ||
|
|
af78c053ba | ||
|
|
4ef4223367 | ||
|
|
7b18376d28 | ||
|
|
c64b9cfee8 | ||
|
|
712281d2e8 | ||
|
|
0626dcbe46 | ||
|
|
d288a5fb3c | ||
|
|
3445f55758 | ||
|
|
350581cbe9 | ||
|
|
377da22761 | ||
|
|
6689acbe57 | ||
|
|
421346d957 | ||
|
|
86d11364f0 | ||
|
|
dcb75b8f4e | ||
|
|
0cb32a22d9 | ||
|
|
caf14eeae1 | ||
|
|
e4206d8abc | ||
|
|
a4765ffe98 | ||
|
|
10b4974719 | ||
|
|
7a00a8f1e3 | ||
|
|
d6097e2b5d | ||
|
|
369af40f20 | ||
|
|
75b45abe4f | ||
|
|
2baa72a022 | ||
|
|
18df968b7a | ||
|
|
e3efb270dd | ||
|
|
2bd98b29eb | ||
|
|
0692cc6360 | ||
|
|
82cbf07120 | ||
|
|
61f441f6b2 | ||
|
|
e07356a700 | ||
|
|
5068a3a114 | ||
|
|
9154660a01 | ||
|
|
4238d59fc6 | ||
|
|
751ad48489 | ||
|
|
88ac37ebc4 | ||
|
|
333ea45c38 | ||
|
|
39290ef776 | ||
|
|
b22390f3eb | ||
|
|
3f285ca15f |
30
.github/workflows/architecture-governance.yml
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
name: Architecture Governance
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
architecture:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install Dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Enforce Boundaries
|
||||||
|
run: npm run arch:check
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
36
.gitignore
vendored
@@ -5,6 +5,11 @@
|
|||||||
/tmp
|
/tmp
|
||||||
/out-tsc
|
/out-tsc
|
||||||
/bazel-out
|
/bazel-out
|
||||||
|
/files
|
||||||
|
changes.txt
|
||||||
|
/agent
|
||||||
|
/agents
|
||||||
|
.agents
|
||||||
|
|
||||||
# Node
|
# Node
|
||||||
/node_modules
|
/node_modules
|
||||||
@@ -36,7 +41,36 @@ yarn-error.log
|
|||||||
/libpeerconnection.log
|
/libpeerconnection.log
|
||||||
testem.log
|
testem.log
|
||||||
/typings
|
/typings
|
||||||
|
/public/images/
|
||||||
# System files
|
# System files
|
||||||
.DS_Store
|
.DS_Store
|
||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
|
# Claude Code worktrees/session state, graphify knowledge-graph output
|
||||||
|
.claude/
|
||||||
|
graphify-out/
|
||||||
|
|
||||||
|
<!-- barry-cache:start -->
|
||||||
|
.context-state/
|
||||||
|
.context-cache/
|
||||||
|
.barry-cache/
|
||||||
|
<!-- barry-cache:end -->
|
||||||
|
AGENTS.md
|
||||||
|
CLAUDE.md
|
||||||
|
GEMINI.md
|
||||||
|
llms.txt
|
||||||
|
.cursor/rules/barry-cache.mdc
|
||||||
|
.github/copilot-instructions.md
|
||||||
|
docs/context/INDEX.md
|
||||||
|
docs/context/LOG.md
|
||||||
|
docs/context/MAINTENANCE.md
|
||||||
|
docs/context/README.md
|
||||||
|
docs/context/adrs/README.md
|
||||||
|
docs/context/concepts/project-context-model.md
|
||||||
|
docs/context/schema/adr.schema.json
|
||||||
|
docs/context/schema/fact.schema.json
|
||||||
|
docs/context/schema/failure.schema.json
|
||||||
|
docs/context/schema/route.schema.json
|
||||||
|
docs/context/schema/strategy.schema.json
|
||||||
|
docs/context/schema/work-state.schema.json
|
||||||
|
docs/context/schema/workspace.schema.json
|
||||||
|
|||||||
124
.impeccable/design.json
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 2,
|
||||||
|
"generatedAt": "2026-07-17T00:00:00Z",
|
||||||
|
"title": "Design System: Marketplaces Platform",
|
||||||
|
"extensions": {
|
||||||
|
"colorMeta": {
|
||||||
|
"primary": { "role": "primary", "displayName": "Muted Pine", "canonical": "#497671", "tonalRamp": ["#182927", "#243d3a", "#2f4f4b", "#3d635f", "#497671", "#6b918d", "#93b3af", "#c3d6d3"] },
|
||||||
|
"secondary": { "role": "secondary", "displayName": "Sage Grey", "canonical": "#a1b4b5", "tonalRamp": ["#2c3838", "#3f5150", "#556c6b", "#6c8583", "#8da3a4", "#a1b4b5", "#c0cfcf", "#e2eaea"] },
|
||||||
|
"accent": { "role": "tertiary", "displayName": "Pale Mint", "canonical": "#a7ceca", "tonalRamp": ["#243936", "#33514d", "#456a65", "#5a857f", "#7fa9a3", "#a7ceca", "#c6e0dd", "#e6f2f0"] },
|
||||||
|
"text-primary": { "role": "neutral", "displayName": "Deep Pine Ink", "canonical": "#1e3c38", "tonalRamp": ["#0f1e1c", "#1e3c38", "#2c5651", "#3d716b", "#5a8d87", "#84aca7", "#b1cbc8", "#dfeae9"] },
|
||||||
|
"bg-secondary": { "role": "neutral", "displayName": "Soft Grey", "canonical": "#f5f5f5", "tonalRamp": ["#2b2b2b", "#4a4a4a", "#6e6e6e", "#949494", "#b8b8b8", "#d7d7d7", "#eaeaea", "#f5f5f5"] },
|
||||||
|
"border": { "role": "neutral", "displayName": "Divider Grey", "canonical": "#d3dad9", "tonalRamp": ["#333938", "#4a5251", "#636d6c", "#7f8a89", "#9da8a7", "#bcc5c4", "#d3dad9", "#eef1f1"] }
|
||||||
|
},
|
||||||
|
"typographyMeta": {
|
||||||
|
"display": { "displayName": "Display", "purpose": "Page-level and storefront hero titles; ceiling ~2.75rem." },
|
||||||
|
"headline": { "displayName": "Headline", "purpose": "Section headings and admin page titles." },
|
||||||
|
"title": { "displayName": "Title", "purpose": "Card titles, editor section labels." },
|
||||||
|
"body": { "displayName": "Body", "purpose": "Default reading text; cap prose at 65-75ch." },
|
||||||
|
"label": { "displayName": "Label", "purpose": "Badges and tags only; tracked uppercase." }
|
||||||
|
},
|
||||||
|
"shadows": [
|
||||||
|
{ "name": "shadow-sm", "value": "0 2px 8px rgba(0,0,0,0.1)", "purpose": "Resting cards, inputs, low panels. Default ambient layer." },
|
||||||
|
{ "name": "shadow-md", "value": "0 4px 12px rgba(0,0,0,0.15)", "purpose": "Hover state for cards and buttons; raised toolbars." },
|
||||||
|
{ "name": "shadow-lg", "value": "0 12px 32px rgba(73,118,113,0.2)", "purpose": "Structural float: modals, dropdowns, save bar. Brand-tinted." }
|
||||||
|
],
|
||||||
|
"motion": [
|
||||||
|
{ "name": "transition-fast", "value": "120ms ease", "purpose": "Button and small-control state changes." },
|
||||||
|
{ "name": "transition-normal", "value": "180ms ease", "purpose": "Card hover lift, transforms." },
|
||||||
|
{ "name": "transition-slow", "value": "300ms ease", "purpose": "Default for links/inputs/textareas." }
|
||||||
|
],
|
||||||
|
"breakpoints": [
|
||||||
|
{ "name": "sm", "value": "640px" },
|
||||||
|
{ "name": "md", "value": "900px" },
|
||||||
|
{ "name": "lg", "value": "1200px" },
|
||||||
|
{ "name": "container", "value": "1280px" }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"components": [
|
||||||
|
{
|
||||||
|
"name": "Primary Button",
|
||||||
|
"kind": "button",
|
||||||
|
"refersTo": "button-primary",
|
||||||
|
"description": "The default confident action. Muted Pine fill, lifts on hover.",
|
||||||
|
"html": "<button class=\"ds-btn-primary\">Save changes</button>",
|
||||||
|
"css": ".ds-btn-primary { display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem; background: #497671; color: #fff; border: 1px solid #497671; border-radius: 12px; padding: 0.625rem 1rem; font-weight: 600; line-height: 1.2; cursor: pointer; transition: background-color 180ms ease, transform 180ms ease, box-shadow 180ms ease; } .ds-btn-primary:hover { background: #3d635f; border-color: #3d635f; transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0,0,0,0.1); } .ds-btn-primary:active { transform: translateY(0); } .ds-btn-primary:focus-visible { outline: 2px solid #497671; outline-offset: 2px; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ghost Button",
|
||||||
|
"kind": "button",
|
||||||
|
"refersTo": "button-ghost",
|
||||||
|
"description": "Low-emphasis action. Transparent with a divider border until hover.",
|
||||||
|
"html": "<button class=\"ds-btn-ghost\">Cancel</button>",
|
||||||
|
"css": ".ds-btn-ghost { display: inline-flex; align-items: center; justify-content: center; background: transparent; color: #1e3c38; border: 1px solid #d3dad9; border-radius: 12px; padding: 0.625rem 1rem; font-weight: 600; cursor: pointer; transition: background-color 180ms ease, border-color 180ms ease; } .ds-btn-ghost:hover { background: rgba(73,118,113,0.08); border-color: #497671; } .ds-btn-ghost:focus-visible { outline: 2px solid #497671; outline-offset: 2px; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Card",
|
||||||
|
"kind": "card",
|
||||||
|
"refersTo": "card",
|
||||||
|
"description": "Resting surface with a soft ambient shadow that lifts on hover.",
|
||||||
|
"html": "<div class=\"ds-card\"><h3 class=\"ds-card-title\">Product title</h3><p class=\"ds-card-body\">Supporting copy sits in Muted Pine Grey at a comfortable line height.</p></div>",
|
||||||
|
"css": ".ds-card { background: #ffffff; border: 1px solid #d3dad9; border-radius: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.1); padding: 16px; transition: transform 180ms ease, box-shadow 180ms ease; } .ds-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } .ds-card-title { margin: 0 0 6px; font-size: 1.125rem; font-weight: 600; color: #1e3c38; line-height: 1.3; } .ds-card-body { margin: 0; font-size: 1rem; font-weight: 400; color: #667a77; line-height: 1.6; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Text Input",
|
||||||
|
"kind": "input",
|
||||||
|
"refersTo": "input",
|
||||||
|
"description": "Editor/admin field with a divider stroke and brand focus outline.",
|
||||||
|
"html": "<label class=\"ds-field\"><span class=\"ds-field-label\">Store name</span><span class=\"ds-field-desc\">Shown in the storefront header.</span><input class=\"ds-input\" type=\"text\" placeholder=\"My marketplace\" /></label>",
|
||||||
|
"css": ".ds-field { display: grid; gap: 6px; color: #1e3c38; font-weight: 600; } .ds-field-label { font-size: 1rem; } .ds-field-desc { font-weight: 400; font-size: 12px; line-height: 1.4; color: #667a77; } .ds-input { width: 100%; padding: 10px 12px; border: 1px solid #d3dad9; border-radius: 10px; background: #fff; color: #1e3c38; font: inherit; } .ds-input:focus-visible { outline: 2px solid #497671; outline-offset: 2px; } .ds-input::placeholder { color: #828e8d; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Badge",
|
||||||
|
"kind": "chip",
|
||||||
|
"refersTo": "badge",
|
||||||
|
"description": "Uppercase status marker overlaid on product media.",
|
||||||
|
"html": "<span class=\"ds-badge ds-badge-sale\">Sale</span>",
|
||||||
|
"css": ".ds-badge { display: inline-block; padding: 2px 8px; border-radius: 8px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.4px; color: #fff; line-height: 1.4; } .ds-badge-sale { background: #f44336; }"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tag",
|
||||||
|
"kind": "chip",
|
||||||
|
"refersTo": "badge",
|
||||||
|
"description": "Low-emphasis metadata pill in brand tint.",
|
||||||
|
"html": "<span class=\"ds-tag\">Digital</span>",
|
||||||
|
"css": ".ds-tag { display: inline-block; padding: 2px 8px; border-radius: 12px; font-size: 0.72rem; color: #497671; background: rgba(73,118,113,0.08); border: 1px solid rgba(73,118,113,0.15); }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"narrative": {
|
||||||
|
"northStar": "The Operator's Workbench",
|
||||||
|
"overview": "This is a tool before it is a brand. The platform chrome is a dependable workbench an operator returns to session after session to build and run a marketplace: state is always legible, controls map to what they change, and nothing competes with the work. The palette is a calm Muted Pine teal-green, warm enough to feel like commerce, quiet enough to disappear behind a tenant's own theme. The system is configuration-first: every storefront is themed per tenant from a runtime bootstrap, so the platform's identity stays neutral and the tenant's leads. Components are tactile and confident; depth is real but restrained, with structural elevation reserved for things that genuinely float.",
|
||||||
|
"keyCharacteristics": [
|
||||||
|
"Quiet, neutral chrome so per-tenant themes lead the storefront.",
|
||||||
|
"Muted Pine teal-green primary; retail-warm but low-drama.",
|
||||||
|
"Tactile, confident components with decisive states.",
|
||||||
|
"Legible state above decoration in every tool surface.",
|
||||||
|
"WCAG 2.2 AA; contrast holds across tenant themes, not just the default."
|
||||||
|
],
|
||||||
|
"rules": [
|
||||||
|
{ "name": "The Quiet Chrome Rule", "body": "The platform's own surfaces stay neutral so tenant themes carry storefront identity. Never introduce a platform-branded color that would fight a tenant's palette.", "section": "colors" },
|
||||||
|
{ "name": "The Variable-Only Rule", "body": "Components and widgets consume CSS custom properties only. A hardcoded hex in a component is a bug (ADR-008) that breaks per-tenant theming.", "section": "colors" },
|
||||||
|
{ "name": "The One Family Rule", "body": "DM Sans in multiple weights carries the entire system. Do not pair a second sans; do not add a display serif. Contrast is weight and size.", "section": "typography" },
|
||||||
|
{ "name": "The Uppercase-Is-Earned Rule", "body": "Tracked uppercase lives on badges/tags exclusively. It is forbidden as a section eyebrow.", "section": "typography" },
|
||||||
|
{ "name": "The Lift-on-Intent Rule", "body": "Resting surfaces carry at most shadow-sm. shadow-md is a response to hover/focus; shadow-lg means the element floats above the page.", "section": "elevation" }
|
||||||
|
],
|
||||||
|
"dos": [
|
||||||
|
"Do consume theme CSS custom properties, never hardcode hex in a component (ADR-008).",
|
||||||
|
"Do keep platform chrome neutral so tenant themes lead the storefront.",
|
||||||
|
"Do carry hierarchy with DM Sans weight and size; one family only.",
|
||||||
|
"Do keep resting surfaces on shadow-sm; reserve shadow-lg for genuinely floating elements.",
|
||||||
|
"Do make state unambiguous in every tool surface.",
|
||||||
|
"Do give every hover/transform a prefers-reduced-motion fallback.",
|
||||||
|
"Do hold 4.5:1 body-text contrast across every tenant theme, not just Dexar."
|
||||||
|
],
|
||||||
|
"donts": [
|
||||||
|
"Don't ship dated enterprise admin: cluttered gray dashboards, tiny dense tables, 2010-era Bootstrap backoffice.",
|
||||||
|
"Don't ship generic AI-SaaS template: cream/violet gradient landings, hero-metric card rows, tracked-uppercase eyebrows, identical card grids.",
|
||||||
|
"Don't ship consumer-toy UI: bubbly rounded-everything, mascots, candy colors, gamified surfaces.",
|
||||||
|
"Don't use tracked uppercase anywhere except badges/tags.",
|
||||||
|
"Don't exceed ~2.75rem on display headings.",
|
||||||
|
"Don't add a second type family or a display serif.",
|
||||||
|
"Don't let platform-branded color fight a tenant's palette."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
6
.impeccable/live/config.json
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"files": ["src/index.html"],
|
||||||
|
"insertBefore": "</body>",
|
||||||
|
"commentSyntax": "html",
|
||||||
|
"cspChecked": true
|
||||||
|
}
|
||||||
550
BACKEND-API-REFERENCE.md
Normal file
@@ -0,0 +1,550 @@
|
|||||||
|
# Backend API Reference
|
||||||
|
|
||||||
|
One document, everyone reads it: product, backend, frontend, QA. It answers three questions for every domain — **what does the frontend already call**, **what shape does it send/expect**, and **is it real or mocked today**. Generated from the actual Angular frontend source (this repo has no backend code — it is a pure client consuming an external API), cross-checked against the frontend's own tolerant adapters, not aspirational.
|
||||||
|
|
||||||
|
Maturity tags used throughout:
|
||||||
|
|
||||||
|
| Tag | Meaning |
|
||||||
|
|---|---|
|
||||||
|
| **LIVE** | Real `HttpClient` call exists in code today, hits a real endpoint. |
|
||||||
|
| **MOCK-SWAPPABLE** | Interface + DI token exist; a real implementation can be dropped in without touching UI. May or may not have a real impl yet. |
|
||||||
|
| **MOCK-ONLY (no seam)** | A mock/local implementation exists but the facade injects the concrete mock class directly — no DI token. A backend needs a token introduced first before it can be wired in. |
|
||||||
|
| **LOCAL-ONLY** | Never talks to a backend by design — localStorage / in-memory / derived from bootstrap. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Core principles
|
||||||
|
|
||||||
|
1. **No response envelope.** There is no `{ success, data, error }` wrapper anywhere. Every call is typed to the bare payload — `HttpClient.get<Item>(...)`, `get<Category[]>(...)`, `get<BootstrapConfig>(...)`. Success = the raw resource (object, array, or `{ items, total }` for lists). Do not wrap new endpoints in an envelope unless it's a deliberate, coordinated breaking change.
|
||||||
|
2. **No API versioning.** No `/v1/` segment, no `Accept-Version` header, anywhere. The only version field in the whole contract is `BootstrapConfig.schemaVersion`, and it's checked for presence only, not semantically enforced.
|
||||||
|
3. **No WebSocket / SSE.** Every "live" feeling feature (QR login polling, payment status) is plain `setInterval`/RxJS polling against a normal request/response endpoint.
|
||||||
|
4. **Tenant resolution is 100% by hostname, not by header or path.** `TenantResolverService` reads the first DNS label (skipping `www`) and uses it to pick a base URL. No `X-Tenant` header, no `/tenant/{id}/...` prefix, ever. Auth requests carry no tenant identifier either — origin is the only signal.
|
||||||
|
5. **Two independent API bases exist**, plus a third for auth:
|
||||||
|
- Marketplace/tenant API — `ApiConfigService.getBaseUrl()` — default `https://api.dexarmarket.ru:445` (or per-tenant subdomain), `/api` on localhost.
|
||||||
|
- Payment/QR API — `environment.qrApiUrl` = `https://qr.vitanova.network/api`.
|
||||||
|
- Session auth API — `environment.authApiUrl` (currently same host as the marketplace API).
|
||||||
|
6. **Two independent mock mechanisms coexist — don't conflate them.** (a) `mock-data.interceptor.ts` globally short-circuits a hardcoded URL list (`/ping`, `/users/sessions*`, `/category`, `/items/*`, `/searchitems`, `/cart`, `/qr*`, `/websession/*`) when `environment.useMockData=true` — off in both shipped environments today. (b) Per-domain DI-token factories (`CONFIG_PROVIDER`, `CATEGORY_REPOSITORY`, `PRODUCT_DATA_PROVIDER`, `BACKOFFICE_DATA_PROVIDER`, `ADMIN_CATEGORIES_GATEWAY`) pick a mock vs. real class per `RuntimeProviderStrategyService`. **`PRODUCT_DATA_PROVIDER` and `CATEGORY_REPOSITORY` always resolve to the real API implementation regardless of mode** — their mock branch is dead code (`product-data-provider.token.ts:12-18`, `category-repository.token.ts:12-19`). `ADMIN_DASHBOARD_METRICS_GATEWAY` always resolves to the local/mock class the other direction — no real implementation is bound yet even though the token exists.
|
||||||
|
7. **GET retries:** `ApiService`/`ApiCategoryRepository` wrap reads in a shared `retry({ count: 2, delay: exponential from 500ms })` — expect up to 3 attempts per read before a caller sees a failure.
|
||||||
|
8. **Dead scaffolding, not missing files:** `src/app/core/error-handling/`, `src/app/core/guards/`, `src/app/core/interceptors/` each contain only a `.gitkeep` — reserved directory structure for a centralized error-handling layer that was never built. Every error today is handled ad hoc at the call site.
|
||||||
|
9. **Backend engineers should not "clean up" the tolerant adapters.** `ApiService.normalizeItem()`/`normalizeCategory()` and `TelegramSessionApiService.normalizeWebSession()` accept multiple historical field-name casings/aliases on purpose (see §7 Products). A payload landing anywhere inside that tolerance envelope works; a stricter renamed shape breaks the client.
|
||||||
|
10. **Nullable fields:** the frontend treats `null`, `undefined`, and an omitted key as the same "absent" signal everywhere except a handful of fields explicitly typed `T | null` (e.g. `AuthSession.userId`) where `null` specifically means "known to be absent." Omit or send `null` interchangeably elsewhere.
|
||||||
|
11. **Do not invent endpoints, fields, or business rules beyond what a real frontend call already implies.** Every open question below is flagged `Requires backend decision` with a recommended default — apply the default and move on unless it's flagged as a business/security decision.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Authentication
|
||||||
|
|
||||||
|
Two **independent, coexisting** mechanisms. Neither is a stand-in for the other; they authenticate different populations today.
|
||||||
|
|
||||||
|
### 2a. Telegram QR / session login — customer AND admin (LIVE)
|
||||||
|
|
||||||
|
Single mechanism for both; only client-side storage differs (separate cookie/signals per surface). Source: `src/app/services/telegram-session-api.service.ts`.
|
||||||
|
|
||||||
|
| Endpoint | Method | Auth | Body / Headers | Response |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `/users/sessions` | POST | none | body `{ webSessionID }` (client-generated GUID) + header `WebSessionID: <same guid>` | `{ webSessionID, url }` — `url` is a `https://t.me/{bot}?start={id}` deep link |
|
||||||
|
| `/users/sessions/{id}` | GET | none | — | Session object, field-tolerant, normalized to `AuthSession` |
|
||||||
|
| `/users/sessions/{id}` | DELETE | none | header `WebSessionID: <id>` | ignored — client clears local state regardless of response |
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST https://api.dexarmarket.ru:445/users/sessions
|
||||||
|
WebSessionID: 3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{ "webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11" }
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{ "webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11", "url": "https://t.me/myAMLKYCBOT?start=3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11" }
|
||||||
|
```
|
||||||
|
|
||||||
|
Poll response (field-tolerant — send real field names, the client accepts many aliases):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11",
|
||||||
|
"status": "active",
|
||||||
|
"user": { "id": 8823771, "username": "buyer_ivan", "firstName": "Ivan", "lastName": "P" },
|
||||||
|
"expiresAt": "2026-07-26T05:00:00Z"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Send a real `expiresAt`/`expires` — if absent, the client fabricates `now + 3600s`.
|
||||||
|
|
||||||
|
Client-side model (`src/app/models/auth.model.ts`):
|
||||||
|
```ts
|
||||||
|
interface AuthSession { sessionId: string; userId: number | null; username: string | null; displayName: string; active: boolean; expires: string; }
|
||||||
|
interface WebSessionStart { webSessionID: string; url: string; }
|
||||||
|
```
|
||||||
|
|
||||||
|
Expiry handling: `expires` drives a client timer that re-polls `GET /users/sessions/{id}` shortly before expiry; if the backend reports inactive, local state clears. There is no reactive 401 handling for this mechanism — expiry is only discovered on the next explicit poll.
|
||||||
|
|
||||||
|
### 2b. Ed25519 challenge/response admin auth (wired client-side, backend not implemented — calls 404 today)
|
||||||
|
|
||||||
|
Source: `src/app/core/auth/services/auth-api.service.ts`. Base `{authApiUrl}/api/admin/auth`.
|
||||||
|
|
||||||
|
| Endpoint | Method | Request | Response |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/challenge` | GET | — | `AuthChallenge { nonce, issuedAt, expiresAt }` |
|
||||||
|
| `/verify` | POST | `VerifySignatureRequest { publicKey, signature, nonce }` | `AuthTokenPair { token, refreshToken }` |
|
||||||
|
| `/refresh` | POST | `RefreshTokenRequest { refreshToken }` | `AuthTokenPair` |
|
||||||
|
| `/logout` | POST | `{ refreshToken }` | void |
|
||||||
|
|
||||||
|
JWT claims (`JwtClaims`, decode-only client-side — the frontend never verifies the signature, that's the backend's job on every request):
|
||||||
|
```ts
|
||||||
|
interface JwtClaims { sub: string; role: AdminRole; iat: number; exp: number; publicKey: string; }
|
||||||
|
type AdminRole = 'Owner' | 'Administrator' | 'Editor' | 'Support' | 'ReadOnly';
|
||||||
|
```
|
||||||
|
|
||||||
|
Storage: `localStorage['ed25519AdminToken']` (access), `localStorage['ed25519AdminRefreshToken']` (refresh, opaque, never decoded client-side).
|
||||||
|
|
||||||
|
**Header:** intended as standard `Authorization: Bearer <token>`, but the interceptor that would auto-attach it (`authInterceptor`) is **not registered** in `app.config.ts` today — no request currently attaches the bearer token automatically. `adminAuthHeadersInterceptor` sets it *if* a token happens to be in storage, but nothing populates one in the live flow yet.
|
||||||
|
|
||||||
|
**Refresh:** client proactively refreshes ~60s before `exp` via a scheduled timer, and (once `authInterceptor` is registered) would reactively refresh once on any 401 before giving up. Every `/refresh` response is expected to return a **new** `refreshToken` (rotation) — the backend should invalidate the one just used.
|
||||||
|
|
||||||
|
**Role → permission table** (`ROLE_PERMISSIONS`, coarse, enforced client-side only for UX — backend must independently authorize every mutation):
|
||||||
|
|
||||||
|
| Role | Permissions |
|
||||||
|
|---|---|
|
||||||
|
| `Owner` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage`, `settings.manage` |
|
||||||
|
| `Administrator` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write`, `users.manage` |
|
||||||
|
| `Editor` | `backoffice.read`, `backoffice.write`, `builder.read`, `builder.write` |
|
||||||
|
| `Support` | `backoffice.read` |
|
||||||
|
| `ReadOnly` | `backoffice.read`, `builder.read` |
|
||||||
|
|
||||||
|
**Known naming collision:** `AdminRole` is defined twice — the string union above (`core/auth/models/permission.model.ts`, the real JWT/auth contract) and an unrelated interface in `features/admin/users/models/admin-user.model.ts` (display-only labels in the Users admin page, not connected to auth). Treat the string union as the authoritative role for auth purposes; the interface needs a rename (e.g. `AdminUserRoleRecord`) — this is flagged, not yet fixed.
|
||||||
|
|
||||||
|
**Route guards:** `adminAuthGuard` (live, checks only "is there an active Telegram session," no role check) gates `/edit`, `/edit/:section`, `/backoffice`. `ed25519AuthGuard` and `permissionGuard(permission)` exist and are fully built but attached to **no route today** — dormant until Mechanism B cuts over. Every guard is a client-side UX gate only; the backend must independently verify authorization on every admin mutation regardless of what a guard decided.
|
||||||
|
|
||||||
|
**Open decision (business, not technical — ask a human):** whether Mechanism A is retired outright in favor of Mechanism B at cutover, or both run in parallel gated by role/tenant config.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Bootstrap — the runtime config document
|
||||||
|
|
||||||
|
The single payload that drives the entire multi-tenant storefront/builder/backoffice. Fetched once at app startup, held in memory; nearly every feature reads from it instead of a dedicated endpoint.
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| Method / Route | `GET /bootstrap` (relative, rewritten onto the tenant base) |
|
||||||
|
| Auth | **None** — must be publicly cacheable per tenant, fetched before any login |
|
||||||
|
| Query / body | none |
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface BootstrapConfig {
|
||||||
|
schemaVersion: string; generatedAt: string;
|
||||||
|
tenant: TenantConfig; branding: BrandingConfig; theme: ThemeConfig; company: CompanyConfig;
|
||||||
|
featureFlags: FeatureFlagsConfig; features?: MarketplaceFeaturesConfig;
|
||||||
|
apiEndpoints: ApiEndpointsConfig; localization: LocalizationConfig; seo: SeoConfig;
|
||||||
|
permissions: PermissionsConfig; header?: HeaderConfig; catalog?: CatalogConfig;
|
||||||
|
layout?: PlatformLayoutConfig; navigation: NavigationConfig; footer?: FooterConfig;
|
||||||
|
productPage?: ProductPageConfig; userExperience?: UserExperienceConfig;
|
||||||
|
pages: PageConfig[]; staticPages?: StaticPagesConfig; widgetRegistry?: WidgetRegistryConfig;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Required top-level keys (must always be emitted): `schemaVersion, generatedAt, tenant, branding, theme, company, featureFlags, apiEndpoints, localization, seo, permissions, navigation, pages`. Everything marked `?` may be omitted — the client applies defaults.
|
||||||
|
|
||||||
|
`apiEndpoints.{website,builder,backoffice}` is where a tenant is meant to declare its per-surface endpoint paths at runtime (`Record<string, { path, method, timeoutMs? }>`) — **these are empty `{}` in the mock today; no builder/backoffice CRUD path exists as a hardcoded literal anywhere in the client.** Any concrete admin CRUD path in this document is a proposal, not a verified literal, until populated here.
|
||||||
|
|
||||||
|
Abridged real example (from `src/assets/mock/bootstrap/bootstrap.json`):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"schemaVersion": "1.0.0",
|
||||||
|
"generatedAt": "2026-07-03T00:00:00Z",
|
||||||
|
"tenant": {
|
||||||
|
"id": "tenant-default-001", "slug": "default", "code": "DEFAULT", "host": "default.local",
|
||||||
|
"name": "Marketplace", "websiteBaseUrl": "https://marketplace.local",
|
||||||
|
"builderBaseUrl": "https://builder.marketplace.local", "backofficeBaseUrl": "https://backoffice.marketplace.local",
|
||||||
|
"defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"],
|
||||||
|
"defaultCurrency": "RUB", "supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
|
||||||
|
"timezone": "Europe/Moscow", "documentationUrl": "https://docs.marketplace.local"
|
||||||
|
},
|
||||||
|
"branding": { "brandName": "Marketplace", "logoUrl": "/icons/icon-192x192.png", "faviconUrl": "/favicon.ico", "supportEmail": "support@marketplace.local" },
|
||||||
|
"theme": {
|
||||||
|
"themeId": "default-light", "mode": "light",
|
||||||
|
"palette": { "primary": "#497671", "secondary": "#a1b4b5", "success": "#10b981", "warning": "#f59e0b", "danger": "#ef4444", "textPrimary": "#1e3c38", "backgroundPrimary": "#ffffff", "border": "#d3dad9" },
|
||||||
|
"typography": { "primaryFontFamily": "DM Sans, sans-serif", "baseFontSize": 16 },
|
||||||
|
"spacing": { "unit": 4, "scale": [0, 4, 8, 12, 16, 24, 32, 48] }
|
||||||
|
},
|
||||||
|
"featureFlags": { "wishlist": true, "compare": true, "reviews": true, "blog": false, "chat": false, "coupons": true },
|
||||||
|
"apiEndpoints": { "bootstrap": { "path": "/bootstrap", "method": "GET", "timeoutMs": 10000 }, "website": {}, "builder": {}, "backoffice": {} },
|
||||||
|
"localization": { "defaultLocale": "ru", "supportedLocales": ["ru", "en", "hy"], "currencyByLocale": { "ru": "RUB", "en": "USD", "hy": "AMD" } },
|
||||||
|
"catalog": { "layout": "grid", "defaultSort": "relevance", "availableSorts": ["relevance", "latest", "price_asc", "price_desc", "rating", "popular", "discount"] },
|
||||||
|
"navigation": { "header": [{ "id": "nav-home", "labelKey": "nav.home", "route": "/", "order": 1 }], "footer": [{ "id": "footer-about", "labelKey": "nav.about", "route": "/about-us", "order": 1 }] },
|
||||||
|
"widgetRegistry": { "manifestUrl": "/assets/mock/bootstrap/widget-manifest.json" },
|
||||||
|
"pages": [{
|
||||||
|
"id": "page-home", "key": "home", "title": "Home", "route": { "path": "/", "exact": true }, "visible": true,
|
||||||
|
"sections": [{ "id": "section-hero", "type": "hero", "order": 1, "layout": { "strategy": "hero", "columns": 1 }, "widgets": [{ "id": "widget-hero-main", "type": "hero", "version": "1.0.0", "order": 1, "props": { "title": { "ru": "Добро пожаловать", "en": "Welcome" } } }] }]
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Which provider fires:** mock (`GET /assets/mock/bootstrap/bootstrap.json`) when `useMockData=true`, or when `useMockBootstrapOnLocal=true` and host is localhost; otherwise real `GET /bootstrap`.
|
||||||
|
|
||||||
|
**No write path exists.** Publishing a marketplace (builder "Publish") only promotes an in-memory/localStorage draft signal today — nothing reaches a backend. See §8 Builder.
|
||||||
|
|
||||||
|
**Requires backend decision:** `X-Language`/`Accept-Language` pre-selection on this call (today the client always gets and holds the full multi-locale document); whether `schemaVersion` is ever semantically enforced (today presence-only); ETag/conditional-request caching (none exists); the entire draft→publish write path.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Pagination, sorting, filtering, search — conventions
|
||||||
|
|
||||||
|
**Two pagination styles coexist — support both, they are not interchangeable:**
|
||||||
|
|
||||||
|
- **Offset/count** (marketplace storefront reads) — query params `count` (page size, default 50) and `skip` (offset, default 0). `searchItems` returns `{ items, total }`; other list reads (`getCategoryItems`, `getRandomItems`) return a bare array with no total.
|
||||||
|
- **Page/pageSize** (admin lists, storefront engagement lists, media) — request `{ page, pageSize, ...filters }`, response `{ items, total, page, pageSize }`. Client derives `totalPages = ceil(total / pageSize)` itself.
|
||||||
|
|
||||||
|
No cursor/keyset pagination exists anywhere. No server-side page-size cap is enforced by the client (it just sends 50 as a default) — **requires backend decision** on max page size.
|
||||||
|
|
||||||
|
**Sorting:** enumerated in bootstrap `catalog.availableSorts`: `relevance | latest | price_asc | price_desc | rating | popular | discount` (7 values). The live `sort` query param on `GET /searchitems` only accepts a 5-value subset: `relevance | price_asc | price_desc | popular | rating` — `latest`/`discount` have no confirmed search-endpoint mapping. **Requires backend decision** to reconcile these two vocabularies, and to define wire encoding for admin-list sorting (no convention exists yet — admin CRUD is mock-only).
|
||||||
|
|
||||||
|
**Filtering:** storefront search accepts `categoryIDs` (comma-joined ints), `minPrice`, `maxPrice`, `tag`. Admin list filter objects (in-memory today, not confirmed wire contracts) all follow `{ search: string, <field>: 'all' | <enum>, page, pageSize }` — `'all'` is the "no filter on this facet" sentinel. **Requires backend decision:** whether `'all'` is sent literally or the param omitted.
|
||||||
|
|
||||||
|
**Search:** `GET /searchitems?search=<q>&count=&skip=[&categoryIDs&minPrice&maxPrice&tag&sort]` → `{ items, total }`. No dedicated autocomplete/suggestion/trending backend endpoint exists — those are derived client-side from already-loaded catalog data today.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Error model
|
||||||
|
|
||||||
|
**The frontend does not currently parse any backend error envelope for any real endpoint** — no interceptor inspects error responses; every caller reacts at the raw `HttpErrorResponse.status`/`.message` level. The one partial exception (Ed25519 admin auth) derives its error code from **HTTP status only**, ignoring any body field, which is itself a known bug (see below). Everything in this section is therefore a **recommended envelope to adopt going forward**, not something already wired end-to-end — apply it to new endpoints and treat the frontend gaps below as follow-up work, not something this doc can silently paper over.
|
||||||
|
|
||||||
|
### The envelope
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"error": {
|
||||||
|
"code": "VALIDATION_FAILED",
|
||||||
|
"message": "One or more fields are invalid.",
|
||||||
|
"status": 422,
|
||||||
|
"requestId": "b3f1c2a0-4e21-4d3a-9e77-1e8f6a2d9c11",
|
||||||
|
"details": [{ "field": "sku", "code": "REQUIRED", "message": "SKU is required." }]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Required | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `error.code` | yes | Stable, `UPPER_SNAKE_CASE`, never localized — this is what code should branch on, never `message`. |
|
||||||
|
| `error.message` | yes | Human-readable English fallback only. |
|
||||||
|
| `error.status` | yes | Mirrors the HTTP status. |
|
||||||
|
| `error.requestId` | recommended | Correlation id for support/ops, echoed in logs. |
|
||||||
|
| `error.details` | only on 422 | `{ field, code, message }[]` — matches the client's existing local-validation issue shape, so a future adapter can merge backend 422s into the same inline-error UI without inventing a second mechanism. |
|
||||||
|
|
||||||
|
### Status-by-status
|
||||||
|
|
||||||
|
| Status | `code` | Frontend reaction today |
|
||||||
|
|---|---|---|
|
||||||
|
| 401 | `UNAUTHENTICATED` | Ed25519 flow → generic "Unauthorized, sign in" screen. Customer Telegram auth: no 401 branch anywhere — session validity is only ever discovered by polling. Admin CRUD facades: none have ever seen a real 401 (all mock). |
|
||||||
|
| 403 | `FORBIDDEN` | Ed25519 flow → "Forbidden, back to dashboard." No tenant-vs-role distinction exists — both render identical copy. |
|
||||||
|
| 404 | `NOT_FOUND` | No code path distinguishes 404 from any other failure — a deleted product and a 500 render the identical generic empty-state today. |
|
||||||
|
| 409 | `CONFLICT` | Nothing reacts to 409 anywhere. Only related mechanism: `AdminCategoriesGateway.isSlugTaken()`, a proactive pre-check, not a 409 handler. |
|
||||||
|
| 422 | `VALIDATION_FAILED` + `details[]` | No admin form parses a backend validation body today (all mock). Client's own `ProjectEditorFacade.fieldError(fieldKey)` inline-error pattern is the convention to align a future adapter to. |
|
||||||
|
| 429 | `RATE_LIMITED` (+`retryAfterSeconds`) | **Zero handling anywhere** — no interceptor, facade, or component references 429 at all. |
|
||||||
|
| 500 | `INTERNAL_ERROR` | Falls into whatever generic catch-all a given caller has (retry-button empty state, or — for `LocationService.getRegions()` — silently falls back to 6 hardcoded regions with no visible error at all). |
|
||||||
|
| 503 (infra down) | `SERVICE_UNAVAILABLE` | Same "backend unavailable, retry" screen as 500, on the Ed25519 flow only. |
|
||||||
|
| 503 (maintenance) | `MAINTENANCE_MODE` (+`maintenanceUntil`) | **No maintenance-mode concept exists in the frontend at all today.** Same HTTP status as infra-down 503 — `error.code` is the only way to distinguish them. |
|
||||||
|
| 403 (tenant disabled) | `TENANT_DISABLED` | **No handling exists.** No code path today distinguishes "tenant exists but is disabled" from any other 403. |
|
||||||
|
| 401 (token expired) | `TOKEN_EXPIRED` | **Known bug, not just a gap:** the client has a dedicated "Session expired" screen wired and ready, but `toAuthErrorShape()` only reaches it via a no-refresh-token-present client-side branch — a *real* backend 401 on `/refresh` always renders the generic "Unauthorized" screen instead, because the mapping function ignores any body code and derives purely from HTTP status. Fix requires the backend to send `error.code: "TOKEN_EXPIRED"` **and** a small frontend change to prefer it. |
|
||||||
|
| 401 (bad signature) | `INVALID_SIGNATURE` | Same bug class as above — dedicated screen exists, unreachable from a real HTTP response for the identical reason. |
|
||||||
|
|
||||||
|
**Every admin backoffice list page** (Users/Orders/Monitoring/Moderation/Transactions/Products/Categories/Analytics/Customers/Dashboard) shares one generic pattern: a boolean `error` signal → "Something went wrong" + retry button. None of them branch on status or `code` today — every status above collapses into the same generic UI until facades are individually updated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Marketplace / storefront API (LIVE)
|
||||||
|
|
||||||
|
Base: `ApiConfigService.getBaseUrl()`. Headers on every call (`apiHeadersInterceptor`): `X-Region`, `X-Language` (`ru→RU, en→EN, hy→AM`), `Currency` (default `RUB`), `WebSessionID`. Source: `src/app/services/api.service.ts`.
|
||||||
|
|
||||||
|
| Endpoint | Method | Params / Body | Response |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `/ping` | GET | — | `{ message }` |
|
||||||
|
| `/category` | GET | — | `Category[]` (normalized) |
|
||||||
|
| `/category/{id}` | GET | `count`, `skip` | `Item[]` |
|
||||||
|
| `/items/{id}` | GET | — | `Item` |
|
||||||
|
| `/items/randomitems` | GET | `count`, `category?` | `Item[]` (featured/random) |
|
||||||
|
| `/searchitems` | GET | `search`, `count`, `skip`, `categoryIDs?`, `minPrice?`, `maxPrice?`, `tag?`, `sort?` | `{ items: Item[], total: number }` |
|
||||||
|
| `/websession/{sessionId}` | POST | item array | cart echo |
|
||||||
|
| `/items/{id}/callback` | POST | `{ rating, comment, sessionID, timestamp }` | `{ message }` — review |
|
||||||
|
| `/items/{id}/questiion` | POST | `{ question, sessionID, timestamp }` | `{ message }` — **literal typo `questiion`, preserve it, matches the client** |
|
||||||
|
| `/purchase-email` | POST | `{ email, phone?, telegramUserId, items[] }` | `{ message }` |
|
||||||
|
| `/regions` | GET | — | `Region[]` — client falls back **silently** to 6 hardcoded regions on any error |
|
||||||
|
|
||||||
|
### 6.1 Products — the tolerance contract
|
||||||
|
|
||||||
|
The wire DTO `Item` (`src/app/models/item.model.ts`) is reconciled by `ApiService.normalizeItem()` — the single largest inline adapter in the codebase. It tolerates **two historical shapes at once**:
|
||||||
|
|
||||||
|
- `id` (string) ↔ `itemID` (numeric)
|
||||||
|
- `imgs[]` ↔ `photos[]`
|
||||||
|
- `names[]` ↔ `translations`
|
||||||
|
- `description` as a key/value array ↔ a plain string
|
||||||
|
- `comments` ↔ `callbacks` (reviews)
|
||||||
|
- color `0xRRGGBB` → normalized `#RRGGBB`
|
||||||
|
- `remaining` count → a stock band
|
||||||
|
|
||||||
|
**A real backend can send either historical shape — do not invent a third, cleaner shape.** `normalizeCategory()` does the same job for categories.
|
||||||
|
|
||||||
|
### 6.2 Categories — two parallel stacks exist
|
||||||
|
|
||||||
|
- **Clean stack (real, LIVE):** `GET /category` → `CategoryDto[]` (`{ categoryID, names: [{lang,name}], subcategories: [...] }`) → `CategoryMapper` flattens the tree, dedupes by id, normalizes `am→hy` → domain `Category`.
|
||||||
|
- **Legacy stack:** the same `/category` response also feeds `ApiService.normalizeCategory()` → a *different* `Category` type (`src/app/models/category.model.ts`). **Two unrelated `Category` types exist in the codebase with the same name** — a known duplication, not a bug to silently fix on the backend side; just be aware both consume the same wire shape.
|
||||||
|
|
||||||
|
```json
|
||||||
|
[{ "categoryID": 12, "names": [{ "lang": "ru", "name": "Электроника" }, { "lang": "en", "name": "Electronics" }], "subcategories": [{ "categoryID": 34, "names": [{ "lang": "en", "name": "Phones" }] }] }]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Cart / Orders / Payments (LIVE)
|
||||||
|
|
||||||
|
Cart **contents** are LOCAL-ONLY (localStorage `marketplace_cart`, + Telegram CloudStorage in-app) — there is no backend cart. Checkout produces real payment + order calls.
|
||||||
|
|
||||||
|
| Endpoint | Method | Base | Body | Response |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `/cart` | POST | marketplace | `CartPaymentRequest` | `QrCreateResponse` |
|
||||||
|
| `/orders` | POST | marketplace | `CreateOrderRequest` | `CreateOrderResponse` — fire-and-forget after payment succeeds, doesn't touch the payment call chain |
|
||||||
|
| `/qr` | POST | `qrApiUrl` | `QrCreateRequest` (headers `authorization-key`, `userid-value`) | `QrCreateResponse` |
|
||||||
|
| `/qr/dynamic/{partnerId}/{qrId}` | GET | `qrApiUrl` | — | `QrDynamicStatusResponse` |
|
||||||
|
| `/card/{partnerId}/{orderId}` | GET | `qrApiUrl` | — | `QrDynamicStatusResponse` |
|
||||||
|
|
||||||
|
Const `partnerId` = `web-97ec-9c57-4dde-9037-3a68f7f83750`.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface CartPaymentRequest {
|
||||||
|
amount: number; currency: 'RUB'; siteuserID: string; siteorderID: string; redirectUrl: string;
|
||||||
|
telegramUsername: string; paymentMethod: 'qr' | 'card'; qrDescription?: string; customerID?: string;
|
||||||
|
items: Array<{ itemID: number; price: number; name: string; quantity?: number }>;
|
||||||
|
}
|
||||||
|
interface CreateOrderRequest {
|
||||||
|
items: Array<{ productId: string; name: string; quantity: number; price: number }>;
|
||||||
|
customer: { name: string; email: string; phone: string };
|
||||||
|
payment?: { method: string; currency: string };
|
||||||
|
shipping?: { address: string; method: string; trackingNumber: string };
|
||||||
|
}
|
||||||
|
interface CreateOrderResponse { id: string; orderNumber: string; status: string; total: number; currency: string; }
|
||||||
|
```
|
||||||
|
|
||||||
|
`QrCreateResponse` is deliberately alias-tolerant — many casings accepted for id/url/partner fields (`qrId`/`qrID`, `nspkurl`/`nspkId`, `partnerID`/`partnerId`/`PartnerID`, etc). Pick one canonical casing on the backend; the client resolves whichever it gets.
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST https://api.dexarmarket.ru:445/cart
|
||||||
|
WebSessionID: 3f1c2a0e-…
|
||||||
|
{ "amount": 4990, "currency": "RUB", "siteuserID": "8823771", "siteorderID": "order-2026-0007", "redirectUrl": "https://marketplace.local/checkout/done", "telegramUsername": "buyer_ivan", "paymentMethod": "qr", "items": [{ "itemID": 101, "price": 4990, "name": "Wireless Keyboard", "quantity": 1 }] }
|
||||||
|
```
|
||||||
|
```json
|
||||||
|
{ "qrId": "QR-77f0", "nspkurl": "https://qr.nspk.ru/AD10…", "status": "created", "qrExpirationDate": "2026-07-26T04:10:00Z" }
|
||||||
|
```
|
||||||
|
|
||||||
|
**Payments are frozen** — this call chain is explicitly out of scope for changes; document only, don't modify.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Admin (backoffice) domains
|
||||||
|
|
||||||
|
**Structural finding, the single most important fact in this section:** of 11 admin gateway domains, only **Categories** and **Dashboard-metrics** are bound through a DI token — a real backend can be dropped in for those two with zero facade changes. **Every other admin domain's facade injects its mock `*LocalGateway` class directly**, so a token has to be added before a real backend can be wired in at all, regardless of whether the endpoint itself is easy to build. Only one real admin HTTP implementation exists anywhere: `AdminCategoriesApiGateway`.
|
||||||
|
|
||||||
|
| Domain | Interface | Real impl? | DI token? | Facade | Seam status |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Categories | `AdminCategoriesGateway` | **yes** (`admin-categories-api.gateway.ts`) | yes (`ADMIN_CATEGORIES_GATEWAY`) | `AdminCategoriesFacade` | MOCK-SWAPPABLE, done |
|
||||||
|
| Dashboard metrics | `AdminDashboardMetricsGateway` | no | yes (`ADMIN_DASHBOARD_METRICS_GATEWAY`) | `AdminDashboardFacade` | MOCK-SWAPPABLE, token only |
|
||||||
|
| Orders | `AdminOrdersGateway` | no | **none** | `AdminOrdersFacade` | MOCK-ONLY, no seam |
|
||||||
|
| Products | `AdminProductsGateway` | no | **none** | `AdminProductsFacade` | MOCK-ONLY, no seam |
|
||||||
|
| Users | `AdminUsersGateway` | no | **none** | `AdminUsersFacade` | MOCK-ONLY, no seam |
|
||||||
|
| Transactions | `AdminTransactionsGateway` | no | **none** | `AdminTransactionsFacade` | MOCK-ONLY, no seam |
|
||||||
|
| Monitoring | `AdminMonitoringGateway` | no | **none** | `AdminMonitoringFacade` | MOCK-ONLY, no seam |
|
||||||
|
| Moderation | `AdminModerationGateway` | no | **none** | `AdminModerationFacade` | MOCK-ONLY, no seam |
|
||||||
|
| Customers | *(none — derived)* | no | n/a | `AdminCustomersFacade` | derives from Orders' mock gateway |
|
||||||
|
| Analytics | *(none — derived)* | no | partial | `AdminAnalyticsFacade` | composes 5 other gateways, no data source |
|
||||||
|
| Media | abstract class `MediaRepository` | no | yes (class token) | `MediaLibraryFacade` | MOCK-SWAPPABLE |
|
||||||
|
|
||||||
|
### Gateway interface method contracts (what a real backend must satisfy)
|
||||||
|
|
||||||
|
- **Categories** — `loadCategories(filters)`, `loadCategory(id)`, `createCategory`, `updateCategory`, `deleteCategory`, `restoreCategory`, `isSlugTaken(slug, excludingId)`.
|
||||||
|
- **Dashboard metrics** — `loadMetrics(): AdminDashboardMetrics` (no params — a seller/scope filter would need a new parameter, no object to extend).
|
||||||
|
- **Orders** — `loadOrders(filters)`, `loadOrder(id)`, `updateStatus(id, status)`, `requestRefund(id)`, `addNote(id, note, internal)`, `archiveOrder`, `restoreOrder`, `deleteOrder`.
|
||||||
|
- **Products** — `loadProducts(filters)`, `loadProduct(id)`, `loadCategories()`, `createProduct`, `updateProduct`, `deleteProduct`, `duplicateProduct`, `archiveProduct`, `restoreProduct`.
|
||||||
|
- **Users** — `loadUsers`, `loadRoles`, `loadInvitations`, `loadSessions(userId)`, `loadAudit(userId)`, `setUserRole`, `setUserStatus`, `inviteUser(email, roleId, scope)`, `revokeInvitation`, `revokeSession`.
|
||||||
|
- **Transactions** — `loadTransactions(filters)`, `retryFailed(id)`, `setFraudFlag(id, flagged)`.
|
||||||
|
- **Monitoring** — `loadEvents(filters)`, `loadQueues()`, `loadWebhooks()`.
|
||||||
|
- **Moderation** — `loadReviews(filters)`, `loadReview(id)`, `setReviewStatus`, `setReviewVisible`, `setReviewPinned`, `setReviewFeatured`, `addModeratorNote`, `deleteReview`, `loadReports()`, `setReportStatus(id, status)`.
|
||||||
|
- **Media** — `list(params?)`, `upload(file, options?)`, `remove(id)`, `update(id, patch)`, `listFolders()`.
|
||||||
|
|
||||||
|
### The one real admin endpoint — Categories, exact paths
|
||||||
|
|
||||||
|
Base: `${apiConfig.getBaseUrl()}/backoffice/categories`. Mode-switched between this and the local mock via `getCategoryProviderMode()` — mock in local dev (no reachable backoffice API there), real API in production.
|
||||||
|
|
||||||
|
| Method | Path | Body | Response |
|
||||||
|
|---|---|---|---|
|
||||||
|
| GET | `/backoffice/categories?search=&visibility=&includeDeleted=` | — | `AdminCategory[]` |
|
||||||
|
| GET | `/backoffice/categories/{id}` | — | `AdminCategory \| null` (404 → null) |
|
||||||
|
| POST | `/backoffice/categories` | `AdminCategory` minus `{id, itemsCount, deletedAt, createdAt, updatedAt}` | `AdminCategory` |
|
||||||
|
| PUT | `/backoffice/categories/{id}` | full `AdminCategory` | `AdminCategory` |
|
||||||
|
| DELETE | `/backoffice/categories/{id}` | — | `void` — **soft delete only, no hard delete exists** |
|
||||||
|
| POST | `/backoffice/categories/{id}/restore` | `{}` | `AdminCategory \| null` |
|
||||||
|
| GET | `/backoffice/categories/slug-taken?slug=&excludingId=` | — | `{ taken: boolean }` |
|
||||||
|
|
||||||
|
Use this exact path shape as the template for every other admin domain in §8.5's build order — it's the only one proven end-to-end.
|
||||||
|
|
||||||
|
### Worked example — Admin Orders (no mapper exists yet, backend has freedom here)
|
||||||
|
|
||||||
|
Unlike Categories/Products (which have a wire DTO to match), admin domains other than Categories have **no wire DTO and no mapper today** — the mock gateways build view models directly in memory. This means the JSON shape below is a *proposal* the new `AdminOrdersApiGateway` would map into the existing `AdminOrder` view model, not a shape already fixed by an adapter:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "ord_1042", "status": "processing", "paymentStatus": "paid",
|
||||||
|
"customer": { "id": "cus_88", "name": "…", "email": "…" },
|
||||||
|
"items": [{ "productId": "…", "title": "…", "qty": 2, "unitPrice": 1990 }],
|
||||||
|
"shipping": { "method": "…", "address": "…" },
|
||||||
|
"timeline": [{ "event": "created", "at": "2026-07-01T10:00:00Z" }]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The same "no mapper exists, write one inside the new `*ApiGateway`" note applies to Products, Users, Transactions, Monitoring, Moderation.
|
||||||
|
|
||||||
|
### Widget manifest (LIVE — static/remote JSON, separate from admin CRUD)
|
||||||
|
|
||||||
|
`GET <bootstrap.widgetRegistry.manifestUrl>` (default `/assets/mock/bootstrap/widget-manifest.json`), falls back to `{ widgets: [] }` on any error, never throws to the UI.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{ "widgets": [{ "type": "hero", "version": "1.0.0", "componentKey": "HeroWidgetComponent", "supportedLayouts": ["hero"], "supportedDataSources": ["manual"], "settingsSchema": { "type": "object", "properties": { "title": { "type": "string" } } }, "defaultSettings": { "title": "Welcome" }, "enabled": true }] }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backoffice storefront cards (LIVE — distinct from admin CRUD above)
|
||||||
|
|
||||||
|
`GET /api/backoffice/products`, `GET /api/backoffice/categories` — feeds storefront product/category card widgets, not the admin panel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Everything that is LOCAL-ONLY (no backend call exists at all)
|
||||||
|
|
||||||
|
Worth knowing explicitly, so nobody assumes a gateway swap will "just work" for these:
|
||||||
|
|
||||||
|
- **Content management / static pages (CMS)** — reads/writes `BootstrapConfig.staticPages` in-memory. No dedicated backend call. Publishing = writing bootstrap back, for which no client write call exists.
|
||||||
|
- **Project editor / builder** — edits an in-memory `BootstrapConfig`, persists drafts to `localStorage` only. "Publish" today only promotes the local draft signal. A builder API is declared only as an empty `apiEndpoints.builder: {}` placeholder in bootstrap.
|
||||||
|
- **Search** — `SearchFacade` is a client-side orchestration over the product/category providers (history, trending, autocomplete, cache all local). The only real backend traffic underneath it is `GET /searchitems`.
|
||||||
|
- **User experience (wishlist/compare/recently-viewed/saved-searches)** — fully denormalized objects in `localStorage`, guest-first. A DI token exists for a future authenticated repository, but nothing is bound to it — comment in code notes it "can be switched to authenticated repository later."
|
||||||
|
- **Diagnostics** — inspects runtime/bootstrap/widget state locally; the one live-ish probe is a `/ping` health check.
|
||||||
|
- **Cart contents** — see §7, real payment/order calls exist, cart *state* never round-trips to a backend.
|
||||||
|
- **Currency conversion / display rates** — `CurrencyRatesService` holds RUB-based conversion rates in-memory, admin-editable via Admin Settings, persisted to `localStorage` only. `CurrencyConvertPipe` applies them client-side wherever a storefront price is rendered. The `Currency` request header (§6) is still sent on every call, but nothing round-trips a rate from the backend — see §12.7.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Backend build order (dependency-driven, not document order)
|
||||||
|
|
||||||
|
1. **Auth + session** — blocks everything admin-gated.
|
||||||
|
2. **Bootstrap content** (branding/theme/nav/seo) — transport (`GET /bootstrap`) already works; the *content* is still default stubs. Tenant resolution depends on it.
|
||||||
|
3. **Categories** — already LIVE both storefront and admin; products reference categories.
|
||||||
|
4. **Products / catalog** — storefront reads are LIVE; admin Products CRUD is the first no-seam admin domain to build.
|
||||||
|
5. **Media** — products/categories editors reference media assets.
|
||||||
|
6. **Cart / Orders / Transactions** — checkout is LIVE; admin Orders CRUD, then Transactions (derives from Orders).
|
||||||
|
7. **Reviews / Moderation** — customer writes are LIVE; admin Moderation gates them.
|
||||||
|
8. **Users / roles / invitations** — independent of commerce, needs auth.
|
||||||
|
9. **Dashboard metrics, then Monitoring** — operational visibility layers.
|
||||||
|
10. **Analytics — last.** Needs orders/products/moderation real *and* a tracking pipeline that doesn't exist yet anywhere (not just a missing endpoint — no data source at all).
|
||||||
|
11. **Builder draft/publish + CMS** — net-new write paths, can proceed in parallel once bootstrap content (step 2) is real.
|
||||||
|
12. **User-experience sync, search suggestions** — enhancements over already-working local features.
|
||||||
|
|
||||||
|
Per-domain migration pattern for the six no-seam admin domains (Orders, Products, Users, Transactions, Monitoring, Moderation): add a DI token → switch the facade to inject the token instead of the concrete mock class → implement the `*ApiGateway` (contains the DTO→view-model mapper) → bind the token → retire or keep the mock behind the existing `useMockData` flag. This is the exact pattern already proven by Categories — replicate it, don't redesign it per domain.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Known discrepancies to reconcile before/while building
|
||||||
|
|
||||||
|
- **`AdminRole` defined twice** with unrelated shapes (§2b) — auth string-union vs. Users-page display interface.
|
||||||
|
- **`Category` defined twice** (§6.2) — legacy vs. clean-stack, both fed by the same `/category` response.
|
||||||
|
- **Duplicate search models** under `features/search/models/` and `core/search/models/`.
|
||||||
|
- **`submitQuestion` endpoint path has a literal typo** (`questiion`, not `question`) — this matches the real backend spec, do not "fix" it.
|
||||||
|
- **The Ed25519 error-code bug** (§5) — `TOKEN_EXPIRED`/`INVALID_SIGNATURE` screens are fully built and unreachable from real HTTP responses today because the client only reads HTTP status, never a body code. Needs a coordinated backend + frontend fix, not backend alone.
|
||||||
|
- **`ADMIN_DASHBOARD_METRICS_GATEWAY` and `USER_EXPERIENCE_REPOSITORY` token factories return the mock/local class in every mode** — a real implementation must be written *and* explicitly bound; the seam existing does not mean a real backend is one line away.
|
||||||
|
|
||||||
|
For open product/business decisions this document deliberately does not resolve (rate limiting posture, refresh-token reuse detection, tenant-scoped auth, API versioning scheme, etc.), see [GAPS-AND-IMPROVEMENTS.md](GAPS-AND-IMPROVEMENTS.md).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Frontend-blocked TODOs — needs backend
|
||||||
|
|
||||||
|
Raised during the Phase 0 security hardening pass (see the sprint plan). Each of these has a client-side mitigation already in place where one exists, but none of them close the actual gap without a backend change.
|
||||||
|
|
||||||
|
### 12.1 Admin role claim on the session
|
||||||
|
|
||||||
|
**Gap:** `adminAuthGuard` (Mechanism A, Telegram/QR) only checks "is there an active session" — the session API has no concept of admin role at all, so the frontend cannot enforce permissions server-authoritatively. Client mitigation: `AdminPermissionsService` derives a cosmetic permission set by matching the Telegram username against the mock Users domain locally — this is UI-only and trivially bypassed by calling the API directly.
|
||||||
|
|
||||||
|
**Ask:** either (a) add a `role` field to the existing `GET /users/sessions/{id}` response when the session belongs to a registered admin, or (b) finish Mechanism B (Ed25519 challenge/response, already wired client-side, `/challenge` and `/verify` currently 404) so the JWT `role` claim becomes real. Whichever is chosen, every admin-mutating endpoint must independently authorize the request — a role claim on the session is necessary but not sufficient.
|
||||||
|
|
||||||
|
Proposed minimal shape for option (a), added to the existing poll response (§2a):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"webSessionID": "3f1c2a0e-4e21-4d3a-9e77-1e8f6a2d9c11",
|
||||||
|
"status": "active",
|
||||||
|
"user": { "id": 8823771, "username": "buyer_ivan", "firstName": "Ivan", "lastName": "P" },
|
||||||
|
"expiresAt": "2026-07-26T05:00:00Z",
|
||||||
|
"adminRole": "admin"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`adminRole` absent/null → treat as non-admin regardless of what `/backoffice/**` UI is reachable client-side.
|
||||||
|
|
||||||
|
### 12.2 HttpOnly session cookie
|
||||||
|
|
||||||
|
**Gap:** the customer session cookie (`webSessionID`, `services/auth.service.ts`) is set via `document.cookie` from the frontend, which means it cannot be `HttpOnly` — only a `Set-Cookie` response header from the backend can set that flag, and JS-set cookies are readable by any injected script. Client mitigation: CSP hardened on all three nginx tenant blocks (was missing entirely on two of three) as defense-in-depth, but this does not close the gap.
|
||||||
|
|
||||||
|
**Ask:** `POST /users/sessions` and `GET /users/sessions/{id}` issue the session id via `Set-Cookie: webSessionID=…; HttpOnly; Secure; SameSite=Lax; Max-Age=…` instead of (or in addition to, during migration) returning it in the JSON body. Once that ships, the frontend stops writing `document.cookie` itself and relies on the browser sending the cookie automatically; `credentials: 'include'` needs enabling on the relevant HTTP calls.
|
||||||
|
|
||||||
|
### 12.3 Server-side order pricing
|
||||||
|
|
||||||
|
**Gap:** `POST` order creation (§7) let the client send a computed, discount-applied `price` per line item with no server-side revalidation. Client fix already shipped: `CreateOrderRequest.items` no longer sends `price` — only `{ productId, name, quantity }`.
|
||||||
|
|
||||||
|
**Ask:** the order-creation endpoint must price every line item itself by looking up `productId` in its own catalog (applying whatever discount/promo logic is authoritative server-side), and reject/[400] if the resulting total doesn't reconcile with what the client displayed (or just recompute and use the server total as-of-record, ignoring any client total entirely). Example of the request shape now sent:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"items": [{ "productId": "prod_1042", "name": "Sample Product", "quantity": 2 }],
|
||||||
|
"customer": { "name": "Ivan P", "email": "ivan@example.com", "phone": "79991234567" },
|
||||||
|
"payment": { "method": "card", "currency": "RUB" }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Separately, `createCartPayment()` (payment-gateway charge creation) still sends a client-computed `amount` — that field can't simply be dropped, since it's what tells the payment provider how much to charge. That endpoint must independently revalidate `amount` against its own pricing before creating the charge, and reject on mismatch.
|
||||||
|
|
||||||
|
### 12.4 Real order audit trail
|
||||||
|
|
||||||
|
**Gap:** `AdminOrder` had no actor/audit field at all. Client fix already shipped: `AdminOrderTimelineEntry.actor` now exists and is populated from the signed-in admin's display name in the local mock gateway — but that's client-only bookkeeping with no server-side record.
|
||||||
|
|
||||||
|
**Ask:** when admin Orders CRUD gets a real backend (§10, step 6), every mutating endpoint (`updateStatus`, `requestRefund`, `addNote`, etc.) should record who performed the action server-side (from the authenticated session/JWT, not a client-supplied field) and return it in the order/timeline response:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"timeline": [
|
||||||
|
{ "status": "processing", "timestamp": "2026-08-13T10:15:00Z", "eventKey": "statusChanged", "actor": "anna@dexar.market" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
`actor` must be derived server-side from the authenticated caller, never trusted from the request body.
|
||||||
|
|
||||||
|
### 12.5 Back-in-stock ("Notify Me") subscription
|
||||||
|
|
||||||
|
**Gap:** the "Notify Me" button on out-of-stock products had no real subscription mechanism at all - it just toggled wishlist. Client fix already shipped: `notifyMe()` now calls `POST /items/{id}/notify-me` and, if that fails (today it always will - the endpoint doesn't exist), falls back to a local-only record in `localStorage['restockSubscriptions']` so the request isn't silently dropped while waiting on the backend. The shopper sees the same confirmation either way.
|
||||||
|
|
||||||
|
**Ask:** implement `POST /items/{id}/notify-me`, plus whatever mechanism actually sends the notification once the item restocks (Telegram message, most likely, given the rest of the auth stack). Request body sent today:
|
||||||
|
```json
|
||||||
|
{ "telegramUserId": "8823771" }
|
||||||
|
```
|
||||||
|
`telegramUserId` may be `null` for a non-Telegram web session - decide whether to also accept an email address as an alternative identifier (the frontend has no email capture on this flow today, so that would need a small frontend addition too). Once this ships, the frontend's localStorage fallback becomes purely a resilience path rather than the common case, and could optionally sync any locally-queued subscriptions on next successful call.
|
||||||
|
|
||||||
|
### 12.7 Currency conversion / FX rates
|
||||||
|
|
||||||
|
**Gap:** the backend has no per-currency pricing — it sends prices in one base currency (`RUB`) regardless of the `Currency` header (§6), and there's no exchange-rate endpoint. Client fix already shipped: admin manually enters a RUB-based rate per supported currency (Admin Settings → Currency rates), and every storefront price display converts client-side via that static, admin-typed number. Rates never update themselves and can drift from the real market rate.
|
||||||
|
|
||||||
|
**Ask:** this was raised as a real accounting concern (bank settlement totals not reconciling against order counts) — two options, not mutually exclusive:
|
||||||
|
1. Backend returns prices already converted per the `Currency` header (removes client-side conversion entirely, most correct).
|
||||||
|
2. Backend exposes a live/periodically-updated FX-rate endpoint (e.g. pegged to Rapira or another exchange) that the frontend polls instead of relying on an admin-typed static number — smaller change, keeps pricing display client-side but removes the manual-entry drift.
|
||||||
|
Either way, the *authoritative* amount charged (`createCartPayment`'s `amount`, §12.3) must be computed/validated server-side against whichever rate source is authoritative — a client-side conversion (current or future) must never be trusted for the actual charge amount.
|
||||||
|
|
||||||
|
### 12.8 Admin purchase notifications depend on Orders CRUD being real
|
||||||
|
|
||||||
|
**Gap:** `AdminOrderWatcherService` (new — polls for new orders to toast/badge the admin) polls `AdminOrdersGateway.loadOrders()` (§8), which is bound to the mock `AdminOrdersLocalGateway` — a static, 24-row in-memory seed with no create path (see §8's gateway table, "Orders … MOCK-ONLY, no seam"). No genuinely new order can ever appear today, so the feature is functionally inert until Orders CRUD gets a real backend (§10 step 6).
|
||||||
|
|
||||||
|
**Ask:** nothing new beyond what §10/§11 already ask for — once a real `AdminOrdersApiGateway` is bound, this feature starts working with no additional frontend change. Flagging here only so nobody spends time debugging "why doesn't the notification ever fire" against the mock.
|
||||||
|
|
||||||
|
### 12.9 Trending search terms
|
||||||
|
|
||||||
|
**Gap:** `SearchTrendingService.loadTrending()` is a stub returning `of(null)` - no trending-searches endpoint exists. It already degrades gracefully (UI hides the trending section rather than showing an error), so this is purely a missing-feature gap, not a bug.
|
||||||
|
|
||||||
|
**Ask:** an endpoint returning the top N search queries over some recent window, e.g.:
|
||||||
|
```json
|
||||||
|
{ "trending": [{ "query": "wireless earbuds", "count": 214 }, { "query": "winter jacket", "count": 187 }] }
|
||||||
|
```
|
||||||
|
Once it exists, wire `loadTrending()` to it and map `query` -> `SearchSuggestion.title/text`.
|
||||||
27
CHANGELOG.md
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
Recent work, newest first. Scoped to what changed and why — see `BACKEND-API-REFERENCE.md` for the backend-dependency detail on anything marked "depends on backend."
|
||||||
|
|
||||||
|
## 2026-08-15 — Admin purchase notifications
|
||||||
|
|
||||||
|
Admin gets notified when a new order lands: a toast (click-to-navigate) plus an unread badge + order list on the topbar bell icon (previously an unused "no notifications" placeholder). Poll-based — the backend has no WebSocket/SSE, so this follows the same polling pattern already used for payment status.
|
||||||
|
|
||||||
|
- `AdminOrderWatcherService` (new) polls `AdminOrdersGateway.loadOrders()` on an admin-editable interval (default 15s, editable in Admin Settings), diffs against persisted "last notified"/"last acknowledged" order pointers, and fires toasts only for genuinely new orders — never spams on first load.
|
||||||
|
- `UserNotificationService` gained an optional click-to-navigate `route` so a toast can jump straight to the order detail page.
|
||||||
|
- Reactively stops/starts polling off `AdminAuthService.isAuthenticated()` — a first attempt at this (tying it to component destruction) turned out not to work because logout never navigates or destroys the admin shell; caught and corrected before merge.
|
||||||
|
- **Depends on backend:** the mock `AdminOrdersLocalGateway` has no create path, so this feature is functionally inert until Orders CRUD gets a real backend gateway. See `BACKEND-API-REFERENCE.md` §12.8.
|
||||||
|
- Design/plan: `docs/superpowers/specs/2026-08-15-admin-purchase-notifications-design.md`, `docs/superpowers/plans/2026-08-15-admin-purchase-notifications.md`.
|
||||||
|
|
||||||
|
## 2026-08-14 — Currency conversion (client-side)
|
||||||
|
|
||||||
|
Switching currency (RUB/USD/EUR/AMD) now actually converts displayed prices, instead of just swapping the currency label next to an unchanged number.
|
||||||
|
|
||||||
|
- `CurrencyRatesService` (new) holds RUB-based conversion rates, admin-editable in Admin Settings → Currency rates, persisted to `localStorage`.
|
||||||
|
- `CurrencyConvertPipe` (new) applies the selected currency's rate wherever a storefront price renders: product cards, product detail page, quick-view dialog, delivery pricing, compare table, cart line items and totals, delivery selector.
|
||||||
|
- Admin backoffice screens intentionally keep showing raw stored-currency values — that's the existing convention for every other admin price display.
|
||||||
|
- Fixed a follow-on bug: the cart's delivery-selector component was using the item's *source* currency as both the display label and the conversion target, so the amount never actually converted (only the label matched) — same number shown in every currency.
|
||||||
|
- **Depends on backend:** rates are a manually-typed admin number today, not a live exchange rate, because the backend doesn't return per-currency pricing. See `BACKEND-API-REFERENCE.md` §12.7 for the two proposed backend directions (server-side conversion, or a live FX-rate endpoint) — raised because bank settlement totals weren't reconciling against order counts, which points at a real pricing-accuracy gap, not just a display one.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Earlier history: `git log`.
|
||||||
223
GAPS-AND-IMPROVEMENTS.md
Normal file
@@ -0,0 +1,223 @@
|
|||||||
|
# Gaps & Improvements
|
||||||
|
|
||||||
|
Findings only — nothing in this document has been fixed as part of writing it. Sourced from re-verifying prior audits against current source, plus a fresh automated review pass across the storefront (website) and backoffice (admin). Organized by the lens each finding matters most to; several findings matter to more than one role and are cross-referenced rather than duplicated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## As a Customer / End User
|
||||||
|
|
||||||
|
1. **Ed25519 admin-auth "session expired" and "invalid signature" recovery screens are dead UI.** Both are fully built and wired, but `toAuthErrorShape()` (`core/auth/services/auth.service.ts:110-118`) derives the error code from HTTP status only, never a body-level code — so a real backend 401 always shows the generic "Unauthorized" screen instead. Also an [Engineering](#as-backend--api-engineer) and [Backend](#as-backend--api-engineer) item.
|
||||||
|
2. **Dark mode selector does nothing.** The light/dark/system dropdown saves correctly, but no CSS anywhere reads the `data-theme-mode` attribute it sets — picking anything but Light changes nothing visually.
|
||||||
|
3. **"Site Layout" selector (Theme section) has no effect.** `layout.type` is edited but page rendering only ever reads each page's own `layout`, never the top-level selector.
|
||||||
|
4. **Footer "Contacts" link has nothing behind it.** No static-page content exists for it at all in the bootstrap data (unlike other footer legal pages, which are populated).
|
||||||
|
5. **Product pages get no per-product SEO.** `SeoService.setItemMeta(item)` — the method that would set per-product Open Graph/canonical tags — exists but is **never called anywhere in the codebase**. Every product page ships only the site-wide default meta tags.
|
||||||
|
6. **`og:locale` is hardcoded to `'ru_RU'`** in both SEO meta-tag code paths, regardless of the active locale — a real gap for EN/HY visitors' social-share previews.
|
||||||
|
7. **No structured data (JSON-LD) and no sitemap generation exist anywhere** — confirmed absent, not partially built. Sitemap is backend-only work; JSON-LD would need net-new frontend code.
|
||||||
|
8. **Checkout's payment-description fallback is a hardcoded Russian string** (`'Покупка на Маркетплейсе'`) used as a last resort when no brand name or hostname is available — single-tenant-framed wording in a multi-tenant product.
|
||||||
|
9. **Brand color contrast fails WCAG AA.** `--border-color` measures 1.24–1.42:1 against a 3:1 UI-component requirement in every theme; `--success`/`--warning`/`--error`/`--info-color` fail 4.5:1 when used as plain text. Real palette colors, not a token bug — see [Accessibility](#as-accessibility-reviewer).
|
||||||
|
10. **`stars.component.scss:10` uses a literal hex color** (`#cdd6d5`) with no design token behind it — any future palette change will silently miss this one glyph.
|
||||||
|
11. **No multi-vendor cart handling exists.** Checkout is one inline flow producing exactly one order from one payment popup; a cart with items from multiple sellers has no defined behavior (relevant the moment Seller Management ships beyond its current disabled-by-default placeholder).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## As Product Owner / Business
|
||||||
|
|
||||||
|
1. **Backend completion is ~10%.** Only Categories has a real HTTP implementation on the admin side; every other domain (Orders, Products, Users, Transactions, Monitoring, Moderation, Analytics, Customers) runs entirely on mock data today. The frontend is feature-complete against that mock data; production readiness is blocked entirely on backend work, not frontend polish.
|
||||||
|
2. **The admin role model is decorative.** `AdminRole`/permissions exist in code, but **nothing gates any button, page, or action on them anywhere in the app.** Anyone who passes admin authentication has full access regardless of their assigned role. This is a real authorization gap, not a display nicety, and should be scoped before any real admin backend goes live with multiple operators.
|
||||||
|
3. **Payment options are limited to QR and card via one custom flow** — no additional providers (wallets, buy-now-pay-later) are wired or planned; needs a business decision on which providers, if any, before integration work starts.
|
||||||
|
4. **Advanced analytics (traffic, funnels, heatmaps) has no data source at all** — not a missing endpoint, a missing tracking pipeline. Flagged as the single largest ("XL") remaining backend effort, deliberately last in the build order because it depends on every other commerce domain being real first.
|
||||||
|
5. **Seller Management has eight cross-linked documents for a capability that is disabled by default and has zero backend bytes.** Real risk if it proceeds: at least three of those documents independently restate the same undecided "Unified vs. Split Orders" question — a decision change means updating multiple documents in sync, not one. Worth a consolidation pass before backend implementation starts.
|
||||||
|
6. **Two competing "seller" type shapes exist with no conversion between them** (`SellerConfig` in bootstrap models vs. `Seller`/`SellerBranding` in the domain layer) — self-flagged during Seller Management design work, restated here as unresolved. Recommend resolving (pick one, or document a mapping) before real backend work on that capability begins.
|
||||||
|
7. **No reusable feature-flag/capability-guard service exists**, despite one being promised by an existing ADR. The one current consumer of `sellerManagement.enabled` hand-rolls the check inline; every future flag will either duplicate that pattern or need the promised service built retroactively under time pressure.
|
||||||
|
8. **The Seller Management "enabled" code path has never been manually exercised**, even once — every verification claim about it was tested with the flag at its real-world value, `false`. Low risk today (nothing renders differently yet), but worth a fixture-based test the first time any enabled-state UI is actually built.
|
||||||
|
9. **Two large lazy-loaded bundle chunks remain unaddressed**: `project-editor` (~1.0 MB) and `catalog-container` (~330–375 kB). No mechanical split has been found; needs a dedicated profiling pass, ideally under real backend latency rather than instant mock responses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## As QA / Test Engineering
|
||||||
|
|
||||||
|
1. **Automated test coverage is thin relative to the stated 80% target.** 11 spec files exist repo-wide (up from 5 before this cycle's test-foundation sprint); measured baseline is ~32% statements, ~19% branches, ~22% functions, ~33% lines. This is an honest foundation, not a coverage floor — no CI gate is set on it yet, deliberately, until a real floor number can be justified.
|
||||||
|
2. **Zero E2E tests exist anywhere in the repo.** No Playwright/Cypress/equivalent config found. Critical flows (storefront checkout, admin CRUD, builder draft→publish→live) have no automated regression coverage beyond the unit/facade specs added this cycle.
|
||||||
|
3. **Several "verified live" claims in prior audits were actually code-inspection only**, not real authenticated click-throughs — consistently because `/edit`, `/edit/:section`, and `/backoffice` require Telegram admin login, which cannot be completed in the automated environment those passes ran in. Worth flagging to a human tester before trusting those UI claims as fully proven: the manifest-aware layout picker (Sprint E), and multiple backoffice-auth-gated wording checks (Monitoring, Reports) among them.
|
||||||
|
4. **No real screen-reader software pass (NVDA/VoiceOver) has ever been performed anywhere in the app** — every accessibility claim in every audit to date is based on automated accessibility-tree inspection only (`role`, `aria-*` attribute presence), never an actual screen-reader session. This is a repo-wide gap, not specific to one page.
|
||||||
|
5. **A reactive-signal staleness bug was found and fixed in Seller Management's enabled-flag read** (it read the bootstrap snapshot once at construction instead of reactively) — worth a general regression-test pattern for any future flag/config read that should track `bootstrapRevision()`, since this bug class is easy to reintroduce and was invisible until specifically looked for.
|
||||||
|
6. **No facade-level tests exist yet for cart/checkout, moderation, or most admin domains** (Orders, Products, Users, Transactions, Monitoring were explicitly scoped out of this cycle's test-foundation sprint to stay within its time budget) — these are exactly the domains about to get real backends, so they carry the most regression risk with the least current coverage.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## As Backend / API Engineer
|
||||||
|
|
||||||
|
See [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md) for the full contract. Structural gaps worth flagging here specifically:
|
||||||
|
|
||||||
|
1. **Only 2 of 11 admin gateway domains (Categories, Dashboard-metrics) have a DI-token seam.** The other 9 — Orders, Products, Users, Transactions, Monitoring, Moderation, plus derived Customers/Analytics — inject their mock gateway class directly. A token has to be added to each before any real backend can be bound, independent of how easy that domain's actual endpoint is to build.
|
||||||
|
2. **`AdminRole` is defined twice with unrelated shapes** (auth string-union vs. a Users-page display interface) — needs a naming reconciliation before the real role table is built.
|
||||||
|
3. **Two unrelated `Category` types exist**, both fed by the same `/category` response, both still in active use.
|
||||||
|
4. **Duplicate search models** exist under two different module paths.
|
||||||
|
5. **The error envelope is entirely a proposal** — no interceptor in the app inspects error response bodies today; every error reaction happens at the raw HTTP-status level. Adopting an envelope is a net-new build for both sides, not a preservation of existing behavior.
|
||||||
|
6. **429 (rate limiting) has zero client-side handling anywhere** — no interceptor, facade, or component references it. If the backend rate-limits, today's frontend has no graceful path for that response.
|
||||||
|
7. **No API versioning scheme has been decided** — no version segment, no version header, anywhere in the client.
|
||||||
|
8. **Centralized error-handling scaffolding exists but was never built.** `src/app/core/error-handling/`, `src/app/core/guards/`, and `src/app/core/interceptors/` each contain only a `.gitkeep` file — someone planned a shared error-handling layer, and every caller still handles failures ad hoc at the call site instead. Worth building once real backends start returning the error envelope in [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md), rather than adding another one-off handler per facade.
|
||||||
|
9. **Admin Reports and Seller Management pages have zero data wiring of any kind** — not even a mock gateway call. Reports reuses `AdminAnalyticsFacade` (itself mock-derived) for its numbers; Seller Management is a static placeholder page with no `HttpClient` reference anywhere. Neither is currently a "swap the gateway" job — Reports inherits whatever Analytics becomes, Seller Management has no data layer to swap yet.
|
||||||
|
10. **Two per-domain provider tokens have a dead mock branch, silently.** `PRODUCT_DATA_PROVIDER` and `CATEGORY_REPOSITORY` always resolve to the real API implementation regardless of `useMockData` — there is no mock class bound to either token. Anyone toggling mock mode expecting storefront products/categories to mock out will be surprised; only Bootstrap, Backoffice-widget-data, and Admin-Categories actually respect the mock/api switch.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## As Accessibility Reviewer
|
||||||
|
|
||||||
|
1. **Brand color contrast genuinely fails WCAG AA** — see [Product Owner item 9 above](#as-product-owner--business) for the numbers. This requires a theme-owner sign-off before any fix ships, since it changes brand appearance, not just token values.
|
||||||
|
2. **No screen-reader software testing has ever been performed on this codebase** — every existing accessibility verification (including in this review) is automated accessibility-tree inspection, never a real NVDA/VoiceOver session. Recommend at least one manual pass on the highest-traffic flows (checkout, product page, admin login) before treating any part of the app as accessibility-verified end to end.
|
||||||
|
3. **Known past pattern worth re-checking elsewhere:** a raw `<textarea>` (no dedicated shared textarea component exists in the codebase) previously shipped without its `aria-label`/label association wired correctly in one place (Seller Management's Message field, since fixed). Any other raw `<textarea>` usage in the app should be checked for the same gap, since the shared `app-input` component handles this automatically but plain textareas do not.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## As Engineering / Tech Debt
|
||||||
|
|
||||||
|
1. **`navigation.header`** (header top-nav list) is editable in the builder but has zero runtime consumer — the header's actual menu comes from a different source entirely. Needs a product decision on positioning/behavior before it's real feature work, not a wiring fix.
|
||||||
|
2. **`catalog.navigationMode`** renders an intentional placeholder — confirmed not a bug, but the alternate nav UIs it implies (mega-menu, top-carousel, left-nav) don't exist yet if ever wanted.
|
||||||
|
3. **Angular 22 upgrade is researched but not started** (~2–3.5 days estimated, needs a dependency fix and Node version bump first). Explicitly recommended as its own dedicated session, never bundled with feature work.
|
||||||
|
4. **`MarketplaceRef` and `TenantConfig` both represent "a marketplace" from two different vantage points** — a deliberate, documented distinction today, but worth consolidating if a third marketplace-shaped type is ever proposed.
|
||||||
|
5. **`sellerId` fields are typed as bare `string` instead of the `UUID` alias** used everywhere else in the newer sellers domain — zero functional impact, pure convention drift, cheap to fix opportunistically.
|
||||||
|
6. **No shared breadcrumb component exists anywhere** — the only breadcrumb logic in the entire storefront is one local signal inside the catalog container, duplicated conceptually wherever a future breadcrumb might be needed.
|
||||||
|
7. **Bootstrap `apiEndpoints.{website,builder,backoffice}` are empty objects in the mock today** — meaning no builder or backoffice CRUD path exists as a literal anywhere in the client. Any concrete path documented for those domains is a proposal until this is populated.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-cutting / process
|
||||||
|
|
||||||
|
- **Documentation was spread across 60+ overlapping markdown files** at the time of this review (status trackers, sprint plans, multiple overlapping backend specs, several audit reports referencing each other) — consolidated as part of this same pass into this file, [BACKEND-API-REFERENCE.md](BACKEND-API-REFERENCE.md), and whatever the team chooses to keep going forward. Recommend a lighter-weight doc set than before: one living gaps list (this file), one backend contract, source-of-truth code — not a new pile of point-in-time sprint reports.
|
||||||
|
- **A running list of "Requires backend decision" items with recommended defaults already exists** inside `BACKEND-API-REFERENCE.md` and the source material behind it — treat those as the first thing to walk through with the client/backend team, since most already have a suggested default and don't need a meeting, only a sign-off.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Automated code review — website (storefront)
|
||||||
|
|
||||||
|
*Findings from a fresh source-level pass over `src/app/pages`, `src/app/features/website`, `src/app/features/search`, `src/app/widgets`, and the project-editor/builder. Each item includes a file:line reference and the role it matters most to.*
|
||||||
|
|
||||||
|
### Cart / Checkout
|
||||||
|
|
||||||
|
- `[Product Owner]` No dedicated checkout feature exists — `src/app/features/website/checkout/` and `src/app/features/website/cart/` are empty `.gitkeep` placeholders; the entire cart/payment flow lives in the legacy `src/app/pages/cart/`.
|
||||||
|
- `[Engineering]` The post-payment email/phone capture flow (`submitEmail`, `onEmailInput`, `onPhoneInput`, `validateEmail`, `validatePhone`) is dead code — the template has no matching `<input>` anywhere, so these methods are never invoked (`src/app/pages/cart/cart.component.ts:507-729`).
|
||||||
|
- `[Product Owner]` Because that form is unreachable, `recordOrder()` always submits the backoffice order with an empty `email`/`phone` for every purchase (`src/app/pages/cart/cart.component.ts:418-441`).
|
||||||
|
- `[Engineering]` `autoSubmitPurchase()` schedules navigation to home via `setTimeout(…, 0)` unconditionally at the top of the method, before checking for a Telegram user ID or waiting on the `submitPurchaseEmail` call — navigation fires regardless of submission success or failure (`cart.component.ts:443-454`).
|
||||||
|
- `[User]` When no Telegram user ID is available, `autoSubmitPurchase()` only logs to console and returns silently — no toast/notification, even as the popup is already closing (`cart.component.ts:450-454`).
|
||||||
|
- `[Engineering / Security]` Payment `currency` is hardcoded to `'RUB'` in both `createPayment()` and `recordOrder()`, ignoring `LanguageService.currentCurrency()` — the total shown to the user can diverge from the currency actually sent to the payment gateway (`cart.component.ts:257,436`).
|
||||||
|
- `[Security]` `amount` and per-item prices sent to `POST /cart` are computed entirely client-side from cart data in localStorage/Telegram CloudStorage, with no server round-trip to re-verify live prices before payment creation — a tampered local cart could request payment at an incorrect amount unless the backend independently revalidates (`cart.component.ts:253-266`, `services/cart.service.ts:133-190`).
|
||||||
|
- `[QA]` `copyPaymentLink()` failure path only does `console.error` — no visible feedback that "copy link" failed (`cart.component.ts:495-505`).
|
||||||
|
- `[Engineering]` Hardcoded Russian fallback string `'Покупка на Маркетплейсе'` used as the QR payment description when no brand name/hostname resolves — bypasses i18n entirely (`cart.component.ts:592-604`). See also [Customer item 8](#as-a-customer--end-user).
|
||||||
|
- `[QA]` Cart quantity increases (`increaseQuantity`, `CartService.updateQuantity`/`addItem`) never validate against the item's available stock — a user can raise a cart line past what's in stock with no cap or warning (`cart.component.ts:119-121`, `cart.service.ts:206-256`).
|
||||||
|
- `[Accessibility]` Swipe-to-reveal-delete on mobile cart rows is touch-only; a fallback always-visible delete button exists, but nothing makes the swipe *state itself* keyboard-reachable (`cart.component.ts:140-163`).
|
||||||
|
- `[Product Owner]` Terms-of-service links (public offer, return policy, guarantee, privacy policy) are plain placeholder text on the cart page, explicitly flagged in-code as needing real backend-configured links (`cart.component.html:147-152`).
|
||||||
|
|
||||||
|
### Auth
|
||||||
|
|
||||||
|
- `[Security]` The customer web session id is stored in a plain, non-`HttpOnly` cookie set via `document.cookie` — readable by any script, exfiltratable via XSS (`services/auth.service.ts:173-180`).
|
||||||
|
|
||||||
|
### Search
|
||||||
|
|
||||||
|
- `[i18n]` `SearchFacade.popularSearches` is a hardcoded English list ("Smartphones", "Sneakers", …) never routed through translation — renders in English regardless of active locale (`features/search/facade/search.facade.ts:58-91`).
|
||||||
|
- `[Product Owner]` `SearchTrendingService.loadTrending()` is a stub that always returns `null` ("Endpoint not available yet") — trending searches are non-functional end to end, and it overwrites the (already-hardcoded) fallback popular list with an empty one every time (`features/search/services/search-trending.service.ts:7-10`).
|
||||||
|
|
||||||
|
### Catalog
|
||||||
|
|
||||||
|
- `[Performance]` Catalog fetches at most 200 products per category in one call, then filters/sorts/paginates entirely client-side over that fixed batch — categories with more than 200 products silently truncate with no indication, and every filter/sort change re-processes the whole in-memory array instead of re-querying (`features/website/catalog/containers/catalog-container.component.ts:160,675-699`).
|
||||||
|
- `[Engineering / tech-debt]` `restoreContinueBrowsing()` is fully implemented (restores search/sort/layout/scroll) but never called anywhere — "continue browsing" state is saved on every interaction but never actually restored (`catalog-container.component.ts:901-929`).
|
||||||
|
- `[Engineering]` Non-numeric category route tokens are resolved by fetching the entire category tree and slugifying titles client-side — fragile against duplicate or renamed category titles, and loads the full tree just to resolve one slug (`catalog-container.component.ts:943-959`).
|
||||||
|
- `[Performance]` Price-range filter inputs trigger a full catalog recompute + URL sync on every keystroke, with no debounce (unlike the search box) (`features/website/catalog/components/filters-panel/filters-panel.component.ts:80-94`).
|
||||||
|
- `[QA]` Range filter min/max inputs have no validation preventing `min > max` — an inverted range silently yields zero results with no explicit error state (`filters-panel.component.ts:80-94`, `.html:56-76`).
|
||||||
|
|
||||||
|
### Product details
|
||||||
|
|
||||||
|
- `[User]` The "Notify Me" button (shown when out of stock) is wired to `toggleWishlist()` — it does not create any real back-in-stock subscription, just relabels the wishlist button (`features/website/product/containers/product-details-container.component.ts:356-358`, `product-actions.component.html:22-24`).
|
||||||
|
- `[Engineering]` "Buy Now" calls `addToCart()` (which resolves item data via an async, subscribed call inside `CartService.addItem`) and immediately navigates to `/cart` without awaiting completion — the cart page can render before the item has actually been added (`product-details-container.component.ts:306-320`, `cart.service.ts:206-243`).
|
||||||
|
|
||||||
|
### Product cards / search results
|
||||||
|
|
||||||
|
- `[Engineering / tech-debt]` The "Quick View" button on every search-results product card emits `quickViewPlaceholder`, but no parent component anywhere binds that output — clicking it is a dead end (`components/product-card/product-card.component.html:23-25`, `.ts:83-87`, `catalog/components/search-results/search-results.component.html:18-33`).
|
||||||
|
|
||||||
|
### Wishlist / Compare
|
||||||
|
|
||||||
|
- `[i18n]` The Compare table renders `product.name`/description fields/color/size straight off the raw product object instead of through `getTranslatedField()` — names and specs on the Compare page always show the source language regardless of active locale (`features/website/user-experience/compare/components/compare-table.component.ts:42-58`).
|
||||||
|
- `[Product Owner]` Wishlist, compare, recently-viewed, and saved searches are all localStorage-only via `USER_EXPERIENCE_REPOSITORY`, with no server sync to the authenticated Telegram session — data is lost on device change or storage clear despite the app having real login (`facades/platform/user-experience.facade.ts:1-137`).
|
||||||
|
|
||||||
|
### Widgets / Page builder
|
||||||
|
|
||||||
|
- `[Engineering]` `DataSourceResolverService.resolve()` has no `catchError`/fallback on any branch — an API failure while loading a widget's data propagates as an unhandled Observable error (`widgets/resolvers/data-source-resolver.service.ts:20-52`).
|
||||||
|
- `[User]` Because of the above, a single failing widget just silently fails to render (stays blank) — no retry, error message, or loading skeleton anywhere in the chain (`layouts/containers/dynamic-page-layout.component.ts:55-61`, `dynamic-renderer/widget-host/widget-host.service.ts:23-58`).
|
||||||
|
- `[Engineering / tech-debt]` `'html'`, `'banner'`, and `'partners'` widget types have full data-resolution logic written but no entry in `APPROVED_WIDGET_COMPONENTS` — configuring one of these renders `UnknownWidgetComponent` on the live storefront instead of real content (`widgets/registry/widget-registry.bootstrap.service.ts:9-17`, `data-source-resolver.service.ts:218-257`).
|
||||||
|
- `[Accessibility]` The hero widget carousel auto-advances every 5s whenever `data.autoplay` is set, with no pause/stop control exposed to the user — only the CSS entrance animation respects `prefers-reduced-motion`, the autoplay timer itself does not (WCAG 2.2.2 risk) (`widgets/ui/hero-widget.component.ts:292-301`).
|
||||||
|
|
||||||
|
### Static / CMS pages
|
||||||
|
|
||||||
|
- `[User]` `loadByKey`/`loadByPath` subscribe with only a `next` handler, no `error` callback — if bootstrap loading fails, `loading` stays `true` forever and the page shows an infinite spinner with no error state (`pages/static-page/static-page.component.ts:76-92`).
|
||||||
|
|
||||||
|
### i18n / Performance
|
||||||
|
|
||||||
|
- `[Performance]` `TranslatePipe` is declared `pure: false`, so every `| translate` binding (hundreds across templates — 40+ on the cart page alone) re-evaluates on every change-detection cycle instead of only when the language changes — a real cost at scale even under `OnPush` components (`i18n/translate.pipe.ts:4-14`).
|
||||||
|
|
||||||
|
## Automated code review — backoffice (admin)
|
||||||
|
|
||||||
|
*Findings from a fresh source-level pass over every `src/app/features/admin/*` module and `src/app/features/backoffice`. Each item includes a file:line reference and the role it matters most to.*
|
||||||
|
|
||||||
|
### Security / Authorization
|
||||||
|
|
||||||
|
- `[Security]` `adminAuthGuard` only checks `isAuthenticated()` — there is no role/permission check anywhere in the codebase (zero matches for a permission check project-wide). Any authenticated admin, including a seeded "viewer" role, can perform every destructive action: delete products/orders, change any user's role, refund orders (`core/admin-auth/admin-auth.guard.ts:6-15`). Same root cause as [Product Owner item 2](#as-product-owner--business).
|
||||||
|
- `[Security]` `AdminRole.permissions` arrays (owner/admin/editor/viewer) exist only as display labels — nothing gates a button, route, or action on them (`features/admin/users/services/admin-users-local.gateway.ts:7-12`, `admin-users-page.component.ts:59-64`).
|
||||||
|
- `[Security]` Nothing prevents suspending or demoting the last remaining `owner`-role user — `setStatus`/`setRole` apply unconditionally (`features/admin/users/facade/admin-users.facade.ts:35-41`).
|
||||||
|
- `[Security]` Category slug-uniqueness check **fails open**: on API error, `isSlugTaken` swallows the error and returns `false` ("not taken"), letting a duplicate/conflicting slug through silently instead of blocking submission (`features/admin/categories/services/admin-categories-api.gateway.ts:56-65`).
|
||||||
|
|
||||||
|
### Audit / Traceability
|
||||||
|
|
||||||
|
- `[Security]` Audit/timeline entries hardcode `actor: 'admin'` as a literal string instead of the real authenticated admin's identity — the "who did this" record is meaningless the moment more than one admin uses the system. Affects role/status changes, transaction retry/fraud-flag, and review moderation (`features/admin/users/services/admin-users-local.gateway.ts:93-94`; `transactions/services/admin-transactions-local.gateway.ts:41,50`; `moderation/services/admin-moderation-local.gateway.ts:64,74`).
|
||||||
|
- `[Security]` Orders have no actor/audit field at all — `AdminOrderTimelineEntry` has no `actor` property, so cancel/refund/status-change history records *what* changed but never *who* changed it, for the most financially sensitive module in the app (`features/admin/orders/models/admin-order.model.ts:32-36`).
|
||||||
|
|
||||||
|
### Destructive actions with missing/inconsistent confirmation
|
||||||
|
|
||||||
|
- `[Admin-operator]` Product delete (single row and bulk) removes the product permanently with **zero confirmation of any kind**, not even a native `confirm()` (`features/admin/products/components/admin-products-list.component.html:134,161` → `admin-products.facade.ts:316-317`; bulk: `admin-products-list-page.component.ts:36` → `facade.ts:175-178`).
|
||||||
|
- `[Admin-operator]` Order bulk-delete removes selected orders permanently with zero confirmation — destroys financial records in one click (`features/admin/orders/pages/admin-orders-list-page.component.html:48` → `admin-orders.facade.ts:134-140`).
|
||||||
|
- `[Admin-operator]` The review bulk-action button is labeled **"archive"** (implying reversible) but actually calls `deleteReview`, which permanently splices the review out — no confirmation dialog (`moderation/pages/admin-reviews-list-page.component.html:56` → `admin-moderation.facade.ts:148-154` → `admin-moderation-local.gateway.ts:94-97`).
|
||||||
|
- `[Admin-operator]` Category bulk-delete has no confirmation, inconsistent with the same module's single-delete flow, which does confirm (`categories/pages/admin-categories-list-page.component.ts:40` vs `76-84`).
|
||||||
|
- `[QA]` The order-detail "Change status" dropdown bypasses the confirm-gated Cancel/Refund buttons next to it — picking `cancelled`/`refunded` from the dropdown applies immediately with no confirmation (`orders/pages/admin-order-detail-page.component.html:49` vs `55-57`).
|
||||||
|
- `[QA]` Terminal order statuses aren't enforced in the UI — the status dropdown stays active after an order reaches `cancelled`/`refunded`, so a terminal order can be silently moved back to any other status (`admin-order-detail-page.component.html:47-58`).
|
||||||
|
- `[Accessibility / Engineering]` Confirmation UX is implemented three inconsistent ways across the app: native `window.confirm()`/`alert()` (Categories, Orders cancel/refund, Users suspend), a themed dialog component (Media Library only), and nothing at all (Products, Orders/Reviews bulk-delete) (`categories/pages/admin-categories-list-page.component.ts:78,81`; `orders/pages/admin-order-detail-page.component.ts:80,86`; `users/pages/admin-users-page.component.ts:43`; vs `features/backoffice/media/media-library-page.component.html:126-140`).
|
||||||
|
|
||||||
|
### Missing/incorrect loading, empty, error states
|
||||||
|
|
||||||
|
- `[QA]` Order-detail and Customer-detail pages collapse "loading," "not found," and "error" into one static "Loading…" string shown forever if the record isn't found or the request errors — `loadDetail` has no `error` handler (`orders/pages/admin-order-detail-page.component.html:89-91`, facade `admin-orders.facade.ts:98-100`; `customers/pages/admin-customer-detail-page.component.html:51-53`).
|
||||||
|
- `[QA]` Products, Categories, Orders, Customers, and Transactions facades swallow load errors into an empty array with no distinct `error` signal — a genuine API failure renders as "No results found" rather than an error-with-retry state, unlike Users/Monitoring/Analytics, which do track error separately (`products/facade/admin-products.facade.ts:101-114`; `customers/facade/admin-customers.facade.ts:42-54`; `transactions/facade/admin-transactions.facade.ts:15-29`).
|
||||||
|
- `[QA]` The Reports page never checks `facade.error()` even though `AdminAnalyticsFacade` exposes it — on load failure it silently renders 0%/0 stats instead of an error message (`admin/reports/pages/admin-reports-page.component.html:1-32`, facade `admin-analytics.facade.ts:41,77`).
|
||||||
|
- `[Engineering]` Save/mutate calls across Products, Categories, and Users subscribe with only a `next` handler — a failed save/delete/role-change fails completely silently with no user-facing feedback (`products/facade/admin-products.facade.ts:305-314`; `categories/facade/admin-categories.facade.ts:314-332`; `users/facade/admin-users.facade.ts:35-50`).
|
||||||
|
|
||||||
|
### Fake/stubbed data presented as real
|
||||||
|
|
||||||
|
- `[Engineering / Product Owner]` Monitoring's security/audit events, queue depths, and webhook deliveries are entirely synthetic (seeded fake generators, hardcoded queue states, modulo-based fake webhook statuses) with no connection to any real backend, yet presented as a live security/audit surface (`monitoring/services/admin-monitoring-local.gateway.ts:14-92`).
|
||||||
|
- `[Admin-operator]` The Monitoring page loads once on construction with no polling/auto-refresh and no manual refresh control (only a retry-on-error button) (`monitoring/pages/admin-monitoring-page.component.ts:39-41`; template `:79`).
|
||||||
|
- `[Admin-operator]` The topbar global search input has no `(input)`/`(keyup.enter)` binding and no handler anywhere — it is entirely decorative (`shell/admin-layout.component.html:118`).
|
||||||
|
- `[Admin-operator]` The notification bell always opens a panel showing a static "no notifications" message — no notification data source is wired up at all (`shell/admin-layout.component.html:141-157`).
|
||||||
|
- `[Product Owner]` The Dashboard's "images without alt text" health check is hardcoded to `status: 'unknown'` with no code path that could ever resolve it — a permanent placeholder sitting alongside real, resolvable checks (`dashboard/facade/admin-dashboard.facade.ts:132`).
|
||||||
|
- `[Product Owner]` "Request Refund" doesn't touch any payment processor — it only flips `payment.status` to `refund_requested`; actually completing the refund means separately picking "refunded" from the unrelated status dropdown, with no workflow linking the two (`orders/services/admin-orders-local.gateway.ts:43-50`).
|
||||||
|
|
||||||
|
### Incomplete modules
|
||||||
|
|
||||||
|
- `[Product Owner]` Settings (`/backoffice/settings`) is fully routed and linked in nav but contains exactly one control (density toggle to localStorage) — no store/payment/tax/shipping/notification settings exist despite the nav entry implying a general settings page.
|
||||||
|
- `[Product Owner]` Seller Management's "Request Access" form submission is mocked with a bare `setTimeout` — no backend call exists, "Learn more" is static copy. Confirms [Product Owner item 5-8](#as-product-owner--business) are still accurate against current source.
|
||||||
|
- `[Product Owner]` Customer-detail "Notes" card always shows a static "notes unavailable" message — no way to add customer notes at all, unlike Orders, which supports both customer-facing and internal notes (`customers/pages/admin-customer-detail-page.component.html:28-31`).
|
||||||
|
- `[Product Owner]` "Reports" duplicates "Analytics" 1:1 — both consume the same facade and independently re-run the same nested aggregation — but Reports exposes only 3 CSV export buttons. Unclear differentiation between the two nav entries for an operator (`reports/pages/admin-reports-page.component.ts:15-20`).
|
||||||
|
|
||||||
|
### Missing validation
|
||||||
|
|
||||||
|
- `[QA / Product Owner]` The product create/edit form has **zero client-side validation** — no required-field checks (name, price, SKU), no min/max on price/discount/quantity. A completely empty or negative-priced product can be saved with no warning (`products/components/admin-product-form.component.ts`, entire file; save path `admin-products.facade.ts:305-314`).
|
||||||
|
|
||||||
|
### Performance
|
||||||
|
|
||||||
|
- `[Performance]` `AdminAnalyticsFacade.load()` chains four nested subscriptions (orders → products → categories → reviews) instead of `forkJoin`/`combineLatest`, with no cancellation of in-flight requests — rapidly toggling the date-range filter can let a stale response overwrite a newer one (`analytics/facade/admin-analytics.facade.ts:65-130`).
|
||||||
|
- `[Performance]` Dashboard-stat computations across Orders, Customers, Transactions, and Analytics each independently re-fetch the entire order list with `pageSize: 100000` rather than sharing one cached read (`orders/facade/admin-orders.facade.ts:166-169`; `customers/facade/admin-customers.facade.ts:44,57`; `analytics/facade/admin-analytics.facade.ts:79,92`).
|
||||||
|
|
||||||
|
### Engineering / tech-debt
|
||||||
|
|
||||||
|
- `[Engineering]` `AdminCustomersFacade.buildCustomers()` treats `customerOrders[0]` as the customer's "latest" order with no local sort — correctness depends entirely on the orders gateway happening to already return descending-by-`createdAt` order; swapping in a differently-ordered real gateway would silently corrupt "last order" with no compiler or runtime signal (`customers/facade/admin-customers.facade.ts:24-39` vs `orders/services/admin-orders-local.gateway.ts:20`).
|
||||||
|
- `[Engineering]` Bulk operations (Orders, Categories, Products, Moderation) fire N independent gateway calls in a loop with no `forkJoin`, no per-item error handling, and no aggregate loading indicator — one failed item in a batch gives the user no signal at all (`moderation/facade/admin-moderation.facade.ts:133-154`; `orders/facade/admin-orders.facade.ts:119-140`; `categories/facade/admin-categories.facade.ts:391-397`).
|
||||||
|
|
||||||
|
### Doc-drift confirmed against the Seller-Management audit
|
||||||
|
|
||||||
|
- `[Engineering]` `Seller-Management-Backoffice-Readiness-Audit.md` stated Settings "No route exists... Skipped, nothing to audit" — now stale: `/backoffice/settings` is a real routed page with no `comingSoon` flag (`shell/admin-nav.model.ts:48`, `app.routes.ts:266-274`).
|
||||||
|
- `[Engineering]` Since that audit, `AdminOrder` and `AdminProduct` have both gained an explicit, unread `sellerId?: string` groundwork field — partially updates the audit's "no injection point" framing at the order level, though its core point (no per-item seller attribution on `AdminOrderItem`) remains accurate (`orders/models/admin-order.model.ts:54-59`; `products/models/admin-product.model.ts:115-120`).
|
||||||
374
README.md
@@ -1,374 +0,0 @@
|
|||||||
# Dexar Market (Multi-Brand Marketplace)
|
|
||||||
|
|
||||||
A modern, responsive marketplace application built with Angular 20 that supports multiple brands from a single codebase.
|
|
||||||
|
|
||||||
## 🎨 Multi-Brand Support
|
|
||||||
|
|
||||||
This project supports **two brands** with the same codebase:
|
|
||||||
- **Dexar Market** - Purple theme (`http://localhost:4200`)
|
|
||||||
- **Novo Market** - Green theme (`http://localhost:4201`)
|
|
||||||
|
|
||||||
Each brand has its own:
|
|
||||||
- Colors and themes
|
|
||||||
- Logos and branding
|
|
||||||
- Environment configuration
|
|
||||||
- Production builds
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- 🎨 **Multi-Brand Architecture** - Single codebase, multiple brands
|
|
||||||
- 📱 **Fully Responsive** - Optimized for desktop, tablet, and mobile devices
|
|
||||||
- 🏪 **Category Browsing** - Hierarchical category navigation
|
|
||||||
- ♾️ **Infinite Scroll** - Seamless product loading in categories and search
|
|
||||||
- 🔍 **Real-time Search** - Debounced search with live results
|
|
||||||
- 🛒 **Shopping Cart** - API-managed cart with quantity support
|
|
||||||
- 📞 **Phone Collection** - Russian phone number formatting and validation
|
|
||||||
- ⭐ **Product Reviews** - Display ratings, reviews, and Q&A
|
|
||||||
- 💳 **Payment Integration** - Telegram Web App payment flow
|
|
||||||
- 📧 **Email Notifications** - Purchase confirmation emails
|
|
||||||
- 📱 **PWA Support** - Progressive Web App with offline support
|
|
||||||
- 🔔 **Service Worker** - Smart caching for better performance
|
|
||||||
- 🎨 **Modern UI** - Clean, intuitive interface with smooth animations
|
|
||||||
|
|
||||||
## Tech Stack
|
|
||||||
|
|
||||||
- **Angular 21** - Latest Angular with standalone components and signals
|
|
||||||
- **TypeScript** - Type-safe development
|
|
||||||
- **SCSS** - Modular styling with theme-based architecture
|
|
||||||
- **RxJS** - Reactive programming for API calls
|
|
||||||
- **Signals** - Angular signals for reactive state management
|
|
||||||
- **Telegram Web App** - Integration with Telegram Mini Apps
|
|
||||||
- **PWA** - Service workers and offline support
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Development
|
|
||||||
|
|
||||||
**Run Dexar Market (Purple):**
|
|
||||||
```bash
|
|
||||||
npm start
|
|
||||||
# or
|
|
||||||
npm run start:dexar
|
|
||||||
```
|
|
||||||
Open: http://localhost:4200
|
|
||||||
|
|
||||||
**Run Novo Market (Green):**
|
|
||||||
```bash
|
|
||||||
npm run start:novo
|
|
||||||
```
|
|
||||||
Open: http://localhost:4201
|
|
||||||
|
|
||||||
### Production Build
|
|
||||||
|
|
||||||
**Build Dexar Market:**
|
|
||||||
```bash
|
|
||||||
npm run build:dexar
|
|
||||||
```
|
|
||||||
Output: `dist/dexarmarket/`
|
|
||||||
|
|
||||||
**Build Novo Market:**
|
|
||||||
```bash
|
|
||||||
npm run build:novo
|
|
||||||
```
|
|
||||||
Output: `dist/novomarket/`
|
|
||||||
|
|
||||||
## Project Structure
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── app/
|
|
||||||
│ ├── components/
|
|
||||||
│ │ ├── header/ # Brand-aware header
|
|
||||||
│ │ ├── footer/ # Brand-aware footer
|
|
||||||
│ │ └── logo/ # Dynamic logo component
|
|
||||||
│ ├── models/
|
|
||||||
│ │ ├── category.model.ts # Category interface
|
|
||||||
│ │ └── item.model.ts # Item, Photo, Callback, Question
|
|
||||||
│ ├── pages/
|
|
||||||
│ │ ├── home/ # Categories overview
|
|
||||||
│ │ ├── category/ # Product listing with infinite scroll
|
|
||||||
│ │ ├── item-detail/ # Product details
|
|
||||||
│ │ ├── search/ # Search with infinite scroll
|
|
||||||
│ │ ├── cart/ # Shopping cart with checkout
|
|
||||||
│ │ ├── info/ # About, contacts, FAQ, etc.
|
|
||||||
│ │ └── legal/ # Legal documents
|
|
||||||
│ ├── services/
|
|
||||||
│ │ ├── api.service.ts # HTTP API integration
|
|
||||||
│ │ ├── cart.service.ts # Cart state management (signals)
|
|
||||||
│ │ └── telegram.service.ts # Telegram WebApp integration
|
|
||||||
│ └── interceptors/
|
|
||||||
│ └── cache.interceptor.ts # API caching
|
|
||||||
├── environments/
|
|
||||||
│ ├── environment.ts # Dexar development
|
|
||||||
│ ├── environment.production.ts # Dexar production
|
|
||||||
│ ├── environment.novo.ts # Novo development
|
|
||||||
│ └── environment.novo.production.ts # Novo production
|
|
||||||
├── styles/
|
|
||||||
│ ├── themes/
|
|
||||||
│ │ ├── dexar.theme.scss # Purple theme
|
|
||||||
│ │ └── novo.theme.scss # Green theme
|
|
||||||
│ └── shared-legal.scss # Shared legal page styles
|
|
||||||
├── index.html # Dexar HTML
|
|
||||||
└── index.novo.html # Novo HTML
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
**Base URL:** Configured per environment
|
|
||||||
|
|
||||||
### Health Check
|
|
||||||
- `GET /ping` - Server availability check
|
|
||||||
|
|
||||||
### Categories
|
|
||||||
- `GET /category` - Get all categories (hierarchical)
|
|
||||||
|
|
||||||
### Items
|
|
||||||
- `GET /category/:categoryID?count=50&skip=100` - Get items in category (paginated)
|
|
||||||
- `GET /items?search=query&count=50&skip=100` - Search items (paginated)
|
|
||||||
|
|
||||||
### Cart
|
|
||||||
- `GET /cart` - Get cart items with quantities
|
|
||||||
- `POST /cart` - Add item `{ itemID: number, quantity?: number }`
|
|
||||||
- `PATCH /cart` - Update quantity `{ itemID: number, quantity: number }`
|
|
||||||
- `DELETE /cart` - Remove items `[itemID1, itemID2, ...]`
|
|
||||||
|
|
||||||
### Payment
|
|
||||||
- `POST /payment/create` - Create payment intent
|
|
||||||
- `POST /purchase-email` - Send purchase confirmation
|
|
||||||
|
|
||||||
See [docs/API_CHANGES_REQUIRED.md](docs/API_CHANGES_REQUIRED.md) for detailed API specifications.
|
|
||||||
|
|
||||||
## Environment Configuration
|
|
||||||
|
|
||||||
Each brand has development and production environments:
|
|
||||||
|
|
||||||
### Dexar Market
|
|
||||||
**Development** (`environment.ts`):
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
production: false,
|
|
||||||
brandName: 'Dexar Market',
|
|
||||||
apiUrl: '/api', // Uses proxy
|
|
||||||
// ... other config
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Production** (`environment.production.ts`):
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
production: true,
|
|
||||||
brandName: 'Dexar Market',
|
|
||||||
apiUrl: 'https://api.dexarmarket.ru',
|
|
||||||
// ... other config
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Novo Market
|
|
||||||
**Development** (`environment.novo.ts`):
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
production: false,
|
|
||||||
brandName: 'novo Market',
|
|
||||||
apiUrl: '/api', // Uses proxy
|
|
||||||
// ... other config
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Production** (`environment.novo.production.ts`):
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
production: true,
|
|
||||||
brandName: 'novo Market',
|
|
||||||
apiUrl: 'https://api.novomarket.ru', // To be configured
|
|
||||||
// ... other config
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Deployment
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
1. Node.js 18+ and npm installed
|
|
||||||
2. Backend API running and accessible
|
|
||||||
3. Domain names configured (dexarmarket.ru, novomarket.ru)
|
|
||||||
|
|
||||||
### Build for Production
|
|
||||||
|
|
||||||
**For Dexar Market:**
|
|
||||||
```bash
|
|
||||||
npm run build:dexar
|
|
||||||
```
|
|
||||||
Output: `dist/dexarmarket/`
|
|
||||||
|
|
||||||
**For Novo Market:**
|
|
||||||
```bash
|
|
||||||
npm run build:novo
|
|
||||||
```
|
|
||||||
Output: `dist/novomarket/`
|
|
||||||
|
|
||||||
### Nginx Configuration
|
|
||||||
|
|
||||||
When deploying to production, you **must** configure nginx to handle Angular routing properly.
|
|
||||||
|
|
||||||
**Example nginx config (Dexar):**
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name dexarmarket.ru www.dexarmarket.ru;
|
|
||||||
|
|
||||||
root /var/www/dexarmarket;
|
|
||||||
index index.html;
|
|
||||||
|
|
||||||
# Angular routing support
|
|
||||||
location / {
|
|
||||||
try_files $uri $uri/ /index.html;
|
|
||||||
}
|
|
||||||
|
|
||||||
# Gzip compression
|
|
||||||
gzip on;
|
|
||||||
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
|
|
||||||
|
|
||||||
# Cache static assets
|
|
||||||
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2)$ {
|
|
||||||
expires 1y;
|
|
||||||
add_header Cache-Control "public, immutable";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**For Novo Market**, use the same config with `novomarket.ru` and `/var/www/novomarket`.
|
|
||||||
|
|
||||||
### SSL Setup
|
|
||||||
|
|
||||||
Enable HTTPS with Let's Encrypt:
|
|
||||||
```bash
|
|
||||||
sudo certbot --nginx -d dexarmarket.ru -d www.dexarmarket.ru
|
|
||||||
sudo certbot --nginx -d novomarket.ru -d www.novomarket.ru
|
|
||||||
```
|
|
||||||
|
|
||||||
### Deploy Steps
|
|
||||||
|
|
||||||
1. Build the project:
|
|
||||||
```bash
|
|
||||||
npm run build:dexar
|
|
||||||
npm run build:novo
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Upload to server:
|
|
||||||
```bash
|
|
||||||
scp -r dist/dexarmarket/* user@server:/var/www/dexarmarket/
|
|
||||||
scp -r dist/novomarket/* user@server:/var/www/novomarket/
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Configure nginx (see above)
|
|
||||||
|
|
||||||
4. Reload nginx:
|
|
||||||
```bash
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### Important Notes
|
|
||||||
|
|
||||||
- The `try_files $uri $uri/ /index.html;` directive is **critical** for Angular routing
|
|
||||||
- Without it, direct URL access or page refreshes will cause 404 errors
|
|
||||||
- Each brand needs its own server block with separate domain
|
|
||||||
- Update API URLs in production environment files before building
|
|
||||||
|
|
||||||
## PWA (Progressive Web App)
|
|
||||||
|
|
||||||
The application includes PWA support with:
|
|
||||||
- Service worker for offline caching
|
|
||||||
- Install prompts on mobile devices
|
|
||||||
- Brand-specific app icons and manifests
|
|
||||||
- Background sync capabilities
|
|
||||||
|
|
||||||
**Manifests:**
|
|
||||||
- Dexar: `public/manifest.webmanifest`
|
|
||||||
- Novo: `public/manifest.novo.webmanifest`
|
|
||||||
|
|
||||||
**Configuration:** `ngsw-config.json`
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Angular CLI Commands
|
|
||||||
|
|
||||||
**Generate a new component:**
|
|
||||||
```bash
|
|
||||||
ng generate component component-name
|
|
||||||
```
|
|
||||||
|
|
||||||
**For a complete list of schematics:**
|
|
||||||
```bash
|
|
||||||
ng generate --help
|
|
||||||
```
|
|
||||||
|
|
||||||
### Running Tests
|
|
||||||
|
|
||||||
**Unit tests:**
|
|
||||||
```bash
|
|
||||||
ng test
|
|
||||||
```
|
|
||||||
|
|
||||||
**E2E tests:**
|
|
||||||
```bash
|
|
||||||
ng e2e
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Comprehensive documentation is available in the `docs/` folder:
|
|
||||||
|
|
||||||
- **[MULTI_BRAND.md](docs/MULTI_BRAND.md)** - Multi-brand architecture guide
|
|
||||||
- **[QUICK_START_NOVO.md](docs/QUICK_START_NOVO.md)** - Quick start for Novo brand
|
|
||||||
- **[API_CHANGES_REQUIRED.md](docs/API_CHANGES_REQUIRED.md)** - Backend API requirements
|
|
||||||
- **[DEPLOYMENT.md](docs/DEPLOYMENT.md)** - Deployment instructions
|
|
||||||
- **[PWA_SETUP.md](docs/PWA_SETUP.md)** - PWA configuration guide
|
|
||||||
- **[IMPLEMENTATION.md](docs/IMPLEMENTATION.md)** - Implementation details
|
|
||||||
- **[RECOMMENDATIONS.md](docs/RECOMMENDATIONS.md)** - Roadmap and improvements
|
|
||||||
- **[TROUBLESHOOTING.md](docs/TROUBLESHOOTING.md)** - Common issues and solutions
|
|
||||||
|
|
||||||
## Telegram Integration
|
|
||||||
|
|
||||||
The marketplace is designed to work as a Telegram Mini App:
|
|
||||||
|
|
||||||
1. Cart data is stored on backend per Telegram user
|
|
||||||
2. Payment flow uses Telegram's payment system
|
|
||||||
3. Deep linking support for sharing products
|
|
||||||
4. Telegram user info auto-collection
|
|
||||||
|
|
||||||
## Browser Compatibility
|
|
||||||
|
|
||||||
- Chrome/Edge 90+
|
|
||||||
- Firefox 88+
|
|
||||||
- Safari 14+
|
|
||||||
- Mobile browsers (iOS Safari, Chrome Mobile)
|
|
||||||
|
|
||||||
## Known Issues & Limitations
|
|
||||||
|
|
||||||
1. **Cart quantity support** - Backend needs to implement quantity fields (see [API_CHANGES_REQUIRED.md](docs/API_CHANGES_REQUIRED.md))
|
|
||||||
2. **Novo brand assets** - Logo and custom images need to be added
|
|
||||||
3. **Legal documents** - Need real company details for Novo brand before deployment
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
When contributing, please:
|
|
||||||
1. Follow the existing code style (use Prettier)
|
|
||||||
2. Write unit tests for new features
|
|
||||||
3. Update documentation as needed
|
|
||||||
4. Test both Dexar and Novo brands before committing
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
Proprietary - All rights reserved
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For technical support or questions:
|
|
||||||
- Email: dev@dexarmarket.ru
|
|
||||||
- Telegram: @dexarmarket
|
|
||||||
|
|
||||||
## Additional Resources
|
|
||||||
|
|
||||||
- [Angular CLI Documentation](https://angular.dev/tools/cli)
|
|
||||||
- [Angular Docs](https://angular.dev)
|
|
||||||
- [Telegram Web Apps](https://core.telegram.org/bots/webapps)
|
|
||||||
101
angular.json
@@ -28,6 +28,11 @@
|
|||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "public"
|
"input": "public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/assets",
|
||||||
|
"output": "assets"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
@@ -40,6 +45,10 @@
|
|||||||
{
|
{
|
||||||
"replace": "src/environments/environment.ts",
|
"replace": "src/environments/environment.ts",
|
||||||
"with": "src/environments/environment.production.ts"
|
"with": "src/environments/environment.production.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"replace": "src/app/interceptors/mock-data.interceptor.ts",
|
||||||
|
"with": "src/app/interceptors/mock-data.interceptor.production.ts"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
@@ -49,13 +58,13 @@
|
|||||||
"budgets": [
|
"budgets": [
|
||||||
{
|
{
|
||||||
"type": "initial",
|
"type": "initial",
|
||||||
"maximumWarning": "500kB",
|
"maximumWarning": "700kB",
|
||||||
"maximumError": "1MB"
|
"maximumError": "1.5MB"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "anyComponentStyle",
|
"type": "anyComponentStyle",
|
||||||
"maximumWarning": "25kB",
|
"maximumWarning": "40kB",
|
||||||
"maximumError": "35kB"
|
"maximumError": "50kB"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
@@ -82,78 +91,17 @@
|
|||||||
"optimization": false,
|
"optimization": false,
|
||||||
"extractLicenses": false,
|
"extractLicenses": false,
|
||||||
"sourceMap": true
|
"sourceMap": true
|
||||||
},
|
|
||||||
"novo": {
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "src/environments/environment.ts",
|
|
||||||
"with": "src/environments/environment.novo.ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"replace": "src/app/brands/brand-routes.ts",
|
|
||||||
"with": "src/app/brands/brand-routes.novo.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index": "src/index.novo.html",
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss",
|
|
||||||
"src/styles/themes/novo.theme.scss"
|
|
||||||
],
|
|
||||||
"outputPath": "dist/novomarket",
|
|
||||||
"optimization": false,
|
|
||||||
"extractLicenses": false,
|
|
||||||
"sourceMap": true
|
|
||||||
},
|
|
||||||
"novo-production": {
|
|
||||||
"fileReplacements": [
|
|
||||||
{
|
|
||||||
"replace": "src/environments/environment.ts",
|
|
||||||
"with": "src/environments/environment.novo.production.ts"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"replace": "src/app/brands/brand-routes.ts",
|
|
||||||
"with": "src/app/brands/brand-routes.novo.ts"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"index": "src/index.novo.html",
|
|
||||||
"styles": [
|
|
||||||
"src/styles.scss",
|
|
||||||
"src/styles/themes/novo.theme.scss"
|
|
||||||
],
|
|
||||||
"outputPath": "dist/novomarket",
|
|
||||||
"budgets": [
|
|
||||||
{
|
|
||||||
"type": "initial",
|
|
||||||
"maximumWarning": "500kB",
|
|
||||||
"maximumError": "1MB"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "anyComponentStyle",
|
|
||||||
"maximumWarning": "25kB",
|
|
||||||
"maximumError": "35kB"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputHashing": "all",
|
|
||||||
"optimization": {
|
|
||||||
"scripts": true,
|
|
||||||
"styles": {
|
|
||||||
"minify": true,
|
|
||||||
"inlineCritical": true
|
|
||||||
},
|
|
||||||
"fonts": {
|
|
||||||
"inline": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sourceMap": false,
|
|
||||||
"namedChunks": false,
|
|
||||||
"extractLicenses": true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "production"
|
"defaultConfiguration": "production"
|
||||||
},
|
},
|
||||||
"serve": {
|
"serve": {
|
||||||
"options": {
|
"options": {
|
||||||
"allowedHosts": ["novo.market", "dexarmarket.ru", "localhost"]
|
"allowedHosts": [
|
||||||
|
"dexarmarket.ru",
|
||||||
|
"dexar.market",
|
||||||
|
"localhost"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"builder": "@angular/build:dev-server",
|
"builder": "@angular/build:dev-server",
|
||||||
"configurations": {
|
"configurations": {
|
||||||
@@ -161,13 +109,8 @@
|
|||||||
"buildTarget": "Dexarmarket:build:production"
|
"buildTarget": "Dexarmarket:build:production"
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
|
"proxyConfig": "proxy.conf.json",
|
||||||
"buildTarget": "Dexarmarket:build:development"
|
"buildTarget": "Dexarmarket:build:development"
|
||||||
},
|
|
||||||
"novo": {
|
|
||||||
"buildTarget": "Dexarmarket:build:novo"
|
|
||||||
},
|
|
||||||
"novo-production": {
|
|
||||||
"buildTarget": "Dexarmarket:build:novo-production"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultConfiguration": "development"
|
"defaultConfiguration": "development"
|
||||||
@@ -183,11 +126,17 @@
|
|||||||
"zone.js/testing"
|
"zone.js/testing"
|
||||||
],
|
],
|
||||||
"tsConfig": "tsconfig.spec.json",
|
"tsConfig": "tsconfig.spec.json",
|
||||||
|
"karmaConfig": "karma.conf.js",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
{
|
{
|
||||||
"glob": "**/*",
|
"glob": "**/*",
|
||||||
"input": "public"
|
"input": "public"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"glob": "**/*",
|
||||||
|
"input": "src/assets",
|
||||||
|
"output": "assets"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"styles": [
|
"styles": [
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
# Backend API Changes Required
|
|
||||||
|
|
||||||
## Cart Quantity Support
|
|
||||||
|
|
||||||
### 1. Add Quantity to Cart Items
|
|
||||||
|
|
||||||
**Current GET /cart Response:**
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"itemID": 123,
|
|
||||||
"name": "Product Name",
|
|
||||||
"price": 100,
|
|
||||||
"currency": "RUB",
|
|
||||||
...other item fields
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
**NEW Required Response:**
|
|
||||||
```json
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"itemID": 123,
|
|
||||||
"name": "Product Name",
|
|
||||||
"price": 100,
|
|
||||||
"currency": "RUB",
|
|
||||||
"quantity": 2, // <-- ADD THIS FIELD
|
|
||||||
...other item fields
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. POST /cart - Add Item to Cart
|
|
||||||
|
|
||||||
**Current Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"itemID": 123
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**NEW Request (with optional quantity):**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"itemID": 123,
|
|
||||||
"quantity": 1 // Optional, defaults to 1 if not provided
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Behavior:**
|
|
||||||
- If item already exists in cart, **increment** the quantity by the provided amount
|
|
||||||
- If item doesn't exist, add it with the specified quantity
|
|
||||||
|
|
||||||
### 3. PATCH /cart - Update Item Quantity (NEW ENDPOINT)
|
|
||||||
|
|
||||||
**Request:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"itemID": 123,
|
|
||||||
"quantity": 5 // New quantity value (not increment, but absolute value)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"message": "Cart updated successfully"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Behavior:**
|
|
||||||
- Set the quantity to the exact value provided
|
|
||||||
- If quantity is 0 or negative, remove the item from cart
|
|
||||||
|
|
||||||
### 4. Payment Endpoints - Include Quantity
|
|
||||||
|
|
||||||
**POST /payment/create**
|
|
||||||
|
|
||||||
Update the items array to include quantity:
|
|
||||||
|
|
||||||
**Current:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"amount": 1000,
|
|
||||||
"currency": "RUB",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"itemID": 123,
|
|
||||||
"price": 500,
|
|
||||||
"name": "Product Name"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**NEW:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"amount": 1000,
|
|
||||||
"currency": "RUB",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"itemID": 123,
|
|
||||||
"price": 500,
|
|
||||||
"name": "Product Name",
|
|
||||||
"quantity": 2 // <-- ADD THIS FIELD
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Email Purchase Confirmation
|
|
||||||
|
|
||||||
**POST /purchase-email**
|
|
||||||
|
|
||||||
Update items to include quantity:
|
|
||||||
|
|
||||||
**NEW:**
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"email": "user@example.com",
|
|
||||||
"telegramUserId": "123456",
|
|
||||||
"items": [
|
|
||||||
{
|
|
||||||
"itemID": 123,
|
|
||||||
"name": "Product Name",
|
|
||||||
"price": 500,
|
|
||||||
"currency": "RUB",
|
|
||||||
"quantity": 2 // <-- ADD THIS FIELD
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Future: Filters & Sorting (To Be Discussed)
|
|
||||||
|
|
||||||
### GET /category/{categoryID}
|
|
||||||
|
|
||||||
Add query parameters for filtering and sorting:
|
|
||||||
|
|
||||||
**Proposed Query Parameters:**
|
|
||||||
- `sort`: Sort order (e.g., `price_asc`, `price_desc`, `rating_desc`, `name_asc`)
|
|
||||||
- `minPrice`: Minimum price filter
|
|
||||||
- `maxPrice`: Maximum price filter
|
|
||||||
- `minRating`: Minimum rating filter (1-5)
|
|
||||||
- `count`: Number of items per page (already exists)
|
|
||||||
- `skip`: Offset for pagination (already exists)
|
|
||||||
|
|
||||||
**Example:**
|
|
||||||
```
|
|
||||||
GET /category/5?sort=price_asc&minPrice=100&maxPrice=500&minRating=4&count=20&skip=0
|
|
||||||
```
|
|
||||||
|
|
||||||
**Response:** Same as current (array of items)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
**Required NOW:**
|
|
||||||
1. Add `quantity` field to cart item responses
|
|
||||||
2. Support `quantity` parameter in POST /cart
|
|
||||||
3. Create new PATCH /cart endpoint for updating quantities
|
|
||||||
4. Include `quantity` in payment and email endpoints
|
|
||||||
|
|
||||||
**Future (After Discussion):**
|
|
||||||
- Sorting and filtering query parameters for category items endpoint
|
|
||||||
@@ -1,156 +0,0 @@
|
|||||||
# Dexar Market - Deployment Guide
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
- Ubuntu/Debian server with root access
|
|
||||||
- Domain: dexarmarket.ru
|
|
||||||
- Node.js 18+ installed
|
|
||||||
|
|
||||||
## Quick Deployment
|
|
||||||
|
|
||||||
### 1. Build locally
|
|
||||||
```bash
|
|
||||||
npm install
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
Output: `dist/dexarmarket/browser/`
|
|
||||||
|
|
||||||
**VERIFY BUILD LOCALLY:**
|
|
||||||
```bash
|
|
||||||
cd dist/dexarmarket/browser
|
|
||||||
ls -la
|
|
||||||
```
|
|
||||||
You MUST see `index.html`, chunk files, `assets/` folder, etc.
|
|
||||||
|
|
||||||
### 2. Upload to server
|
|
||||||
```bash
|
|
||||||
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Set permissions on server
|
|
||||||
```bash
|
|
||||||
sudo chown -R www-data:www-data /var/www/dexarmarket
|
|
||||||
sudo chmod -R 755 /var/www/dexarmarket
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
## Initial Server Setup (one-time)
|
|
||||||
|
|
||||||
### Install and configure Nginx
|
|
||||||
```bash
|
|
||||||
sudo apt update
|
|
||||||
sudo apt install nginx -y
|
|
||||||
sudo mkdir -p /var/www/dexarmarket/browser
|
|
||||||
```
|
|
||||||
|
|
||||||
Copy `nginx.conf` content to `/etc/nginx/sites-available/dexarmarket`:
|
|
||||||
```bash
|
|
||||||
sudo nano /etc/nginx/sites-available/dexarmarket
|
|
||||||
```
|
|
||||||
|
|
||||||
Then enable it:
|
|
||||||
```bash
|
|
||||||
sudo ln -s /etc/nginx/sites-available/dexarmarket /etc/nginx/sites-enabled/
|
|
||||||
sudo rm /etc/nginx/sites-enabled/default
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### Setup SSL (recommended)
|
|
||||||
```bash
|
|
||||||
sudo apt install certbot python3-certbot-nginx -y
|
|
||||||
sudo certbot --nginx -d dexarmarket.ru -d www.dexarmarket.ru
|
|
||||||
```
|
|
||||||
|
|
||||||
## Common Issues & Solutions
|
|
||||||
|
|
||||||
### ❌ 404 Error - Files Not Found
|
|
||||||
|
|
||||||
**Check 1: Verify files on server**
|
|
||||||
```bash
|
|
||||||
ls -la /var/www/dexarmarket/browser/
|
|
||||||
```
|
|
||||||
Should show: `index.html`, `chunk-*.js`, `assets/`, etc.
|
|
||||||
|
|
||||||
**If empty:**
|
|
||||||
```bash
|
|
||||||
# Re-upload files
|
|
||||||
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check 2: Verify permissions**
|
|
||||||
```bash
|
|
||||||
namei -l /var/www/dexarmarket/browser/index.html
|
|
||||||
```
|
|
||||||
All directories need `x` (execute) permission.
|
|
||||||
|
|
||||||
**Fix permissions:**
|
|
||||||
```bash
|
|
||||||
sudo chown -R www-data:www-data /var/www/dexarmarket
|
|
||||||
sudo chmod -R 755 /var/www/dexarmarket
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check 3: Test nginx config**
|
|
||||||
```bash
|
|
||||||
sudo nginx -t
|
|
||||||
```
|
|
||||||
Should say "syntax is ok" and "test is successful".
|
|
||||||
|
|
||||||
**Check 4: View nginx error log**
|
|
||||||
```bash
|
|
||||||
sudo tail -f /var/log/nginx/error.log
|
|
||||||
```
|
|
||||||
This shows the actual error!
|
|
||||||
|
|
||||||
### ❌ 502 Bad Gateway - API Issues
|
|
||||||
|
|
||||||
**This means the API backend is down or unreachable.**
|
|
||||||
|
|
||||||
**Check 1: Is API accessible?**
|
|
||||||
```bash
|
|
||||||
curl -v https://api.dexarmarket.ru:445/ping
|
|
||||||
```
|
|
||||||
|
|
||||||
**Check 2: Port 445 problem**
|
|
||||||
Port 445 is unusual for HTTPS and may be blocked by firewalls. Standard HTTPS uses port 443.
|
|
||||||
|
|
||||||
**Check 3: CORS issues**
|
|
||||||
The API must allow requests from `https://dexarmarket.ru`. Check API CORS configuration.
|
|
||||||
|
|
||||||
**Check 4: SSL certificate**
|
|
||||||
```bash
|
|
||||||
curl -k https://api.dexarmarket.ru:445/ping
|
|
||||||
```
|
|
||||||
If this works but without `-k` doesn't, SSL cert is invalid.
|
|
||||||
|
|
||||||
### ✅ Final Verification Checklist
|
|
||||||
|
|
||||||
On server, run all these:
|
|
||||||
```bash
|
|
||||||
# 1. Files exist
|
|
||||||
ls -la /var/www/dexarmarket/browser/index.html
|
|
||||||
|
|
||||||
# 2. Nginx config is valid
|
|
||||||
sudo nginx -t
|
|
||||||
|
|
||||||
# 3. Nginx is running
|
|
||||||
sudo systemctl status nginx
|
|
||||||
|
|
||||||
# 4. Site is enabled
|
|
||||||
ls -la /etc/nginx/sites-enabled/ | grep dexarmarket
|
|
||||||
|
|
||||||
# 5. Test API from server
|
|
||||||
curl -v https://api.dexarmarket.ru:445/ping
|
|
||||||
|
|
||||||
# 6. Check logs
|
|
||||||
sudo tail -20 /var/log/nginx/error.log
|
|
||||||
sudo tail -20 /var/log/nginx/access.log
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Steps
|
|
||||||
|
|
||||||
If still having issues:
|
|
||||||
1. Check browser console (F12 → Console tab) - shows JavaScript errors
|
|
||||||
2. Check browser network tab (F12 → Network tab) - shows failed requests
|
|
||||||
3. Check exact error message in nginx logs
|
|
||||||
4. Test locally: `cd dist/dexarmarket/browser && python3 -m http.server 8000`
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# Dexar Market - Implementation Summary
|
|
||||||
|
|
||||||
## ✅ Completed Features
|
|
||||||
|
|
||||||
### 1. **Data Models** (`src/app/models/`)
|
|
||||||
- **Category Model**: Hierarchical category structure
|
|
||||||
- **Item Model**: Complete product data including photos/videos, pricing, reviews, Q&A
|
|
||||||
|
|
||||||
### 2. **Services** (`src/app/services/`)
|
|
||||||
- **API Service**: All endpoint integrations
|
|
||||||
- Health check (`/ping`)
|
|
||||||
- Categories (`/category`)
|
|
||||||
- Category items with pagination (`/category/:id`)
|
|
||||||
- Search with pagination (`/items`)
|
|
||||||
- Cart operations (GET, POST, DELETE)
|
|
||||||
- **Cart Service**: Reactive state management using Angular signals
|
|
||||||
- Add/remove items
|
|
||||||
- Real-time cart count
|
|
||||||
- Automatic total price calculation
|
|
||||||
|
|
||||||
### 3. **Pages** (`src/app/pages/`)
|
|
||||||
|
|
||||||
#### **Home Page** (`/`)
|
|
||||||
- Display all categories in grid layout
|
|
||||||
- Show subcategories
|
|
||||||
- Responsive category cards
|
|
||||||
|
|
||||||
#### **Category Page** (`/category/:id`)
|
|
||||||
- **Infinite Scroll**: Automatically loads more items on scroll
|
|
||||||
- Product grid with images, pricing, ratings
|
|
||||||
- Discount badges
|
|
||||||
- Stock status indicators
|
|
||||||
- Add to cart functionality
|
|
||||||
|
|
||||||
#### **Search Page** (`/search`)
|
|
||||||
- **Real-time search** with debounce (300ms)
|
|
||||||
- **Infinite Scroll** for results
|
|
||||||
- Same product display as category page
|
|
||||||
- Empty state handling
|
|
||||||
|
|
||||||
#### **Item Detail Page** (`/item/:id`)
|
|
||||||
- Photo/video gallery with thumbnails
|
|
||||||
- Full product information
|
|
||||||
- Pricing with discount display
|
|
||||||
- Reviews section with ratings
|
|
||||||
- Q&A section with voting counts (👍👎)
|
|
||||||
- Add to cart
|
|
||||||
|
|
||||||
#### **Cart Page** (`/cart`)
|
|
||||||
- List all cart items with details
|
|
||||||
- Remove individual items
|
|
||||||
- Clear entire cart
|
|
||||||
- Real-time total calculation
|
|
||||||
- Empty state with call-to-action
|
|
||||||
- Checkout button (placeholder)
|
|
||||||
|
|
||||||
### 4. **Components** (`src/app/components/`)
|
|
||||||
|
|
||||||
#### **Header Component**
|
|
||||||
- Sticky navigation
|
|
||||||
- Cart icon with badge showing item count
|
|
||||||
- Mobile-responsive hamburger menu
|
|
||||||
- Active route highlighting
|
|
||||||
|
|
||||||
### 5. **Routing & Configuration**
|
|
||||||
- Lazy-loaded routes for performance
|
|
||||||
- HTTP client configured
|
|
||||||
- All pages connected and navigable
|
|
||||||
|
|
||||||
### 6. **Responsive Design**
|
|
||||||
- Mobile-first approach
|
|
||||||
- Breakpoints at 768px and 968px
|
|
||||||
- Adaptive layouts for all screen sizes
|
|
||||||
- Touch-friendly interface
|
|
||||||
|
|
||||||
## 🎨 Design Features
|
|
||||||
|
|
||||||
- **Color Scheme**: Purple gradient theme (#667eea primary)
|
|
||||||
- **Smooth Animations**: Hover effects, transitions
|
|
||||||
- **Modern UI**: Card-based layouts, rounded corners
|
|
||||||
- **Custom Scrollbar**: Themed scrollbar styling
|
|
||||||
- **Loading States**: Spinners and skeleton states
|
|
||||||
- **Error Handling**: User-friendly error messages
|
|
||||||
|
|
||||||
## 📱 Performance Optimizations
|
|
||||||
|
|
||||||
1. **Infinite Scroll**: Loads 20 items at a time
|
|
||||||
2. **Lazy Loading**: Route-based code splitting
|
|
||||||
3. **Image Lazy Loading**: Native lazy loading for images
|
|
||||||
4. **Debounced Search**: Prevents excessive API calls
|
|
||||||
5. **Angular Signals**: Efficient reactivity
|
|
||||||
|
|
||||||
## 🔧 Technical Stack
|
|
||||||
|
|
||||||
- Angular 20 (standalone components)
|
|
||||||
- TypeScript
|
|
||||||
- RxJS for reactive programming
|
|
||||||
- SCSS for styling
|
|
||||||
- Angular Signals for state management
|
|
||||||
|
|
||||||
## 📦 API Integration
|
|
||||||
|
|
||||||
All endpoints from the provided documentation are integrated:
|
|
||||||
- ✅ GET /ping
|
|
||||||
- ✅ GET /category
|
|
||||||
- ✅ GET /category/:categoryID
|
|
||||||
- ✅ GET /items (search)
|
|
||||||
- ✅ GET /cart
|
|
||||||
- ✅ POST /cart
|
|
||||||
- ✅ DELETE /cart
|
|
||||||
|
|
||||||
## 🚀 How to Run
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install dependencies (if needed)
|
|
||||||
npm install
|
|
||||||
|
|
||||||
# Start development server
|
|
||||||
ng serve
|
|
||||||
|
|
||||||
# Open browser
|
|
||||||
http://localhost:4200
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📝 Notes
|
|
||||||
|
|
||||||
- **Item Detail Limitation**: Currently fetches items from cart for demo. In production, you may want to add a dedicated `/item/:id` endpoint or cache category results.
|
|
||||||
- **Checkout**: Placeholder button ready for payment integration
|
|
||||||
- **No Authentication**: As per requirements, no user management implemented
|
|
||||||
- **API Base URL**: Configured as `https://api.dexarmarket.ru`
|
|
||||||
|
|
||||||
## 🎯 Ready for Production
|
|
||||||
|
|
||||||
The application is production-ready with:
|
|
||||||
- Type-safe TypeScript
|
|
||||||
- Modular architecture
|
|
||||||
- Responsive design
|
|
||||||
- Error handling
|
|
||||||
- Performance optimizations
|
|
||||||
- Clean, maintainable code
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
# Multi-Brand Configuration
|
|
||||||
|
|
||||||
Этот проект поддерживает несколько брендов с разными темами и конфигурациями.
|
|
||||||
|
|
||||||
## Доступные бренды
|
|
||||||
|
|
||||||
### 1. Dexar Market (фиолетовый)
|
|
||||||
- **Цвета**: Фиолетовый/пурпурный (#667eea, #764ba2)
|
|
||||||
- **Домен**: dexarmarket.ru
|
|
||||||
- **Email**: info@dexarmarket.ru
|
|
||||||
|
|
||||||
### 2. novo Market (зеленый)
|
|
||||||
- **Цвета**: Зеленый (#10b981, #14b8a6)
|
|
||||||
- **Домен**: novomarket.ru (будет настроено)
|
|
||||||
- **Email**: info@novomarket.ru (будет настроено)
|
|
||||||
|
|
||||||
## Команды запуска
|
|
||||||
|
|
||||||
### Dexar Market (разработка)
|
|
||||||
```bash
|
|
||||||
ng serve
|
|
||||||
# или
|
|
||||||
ng serve --configuration=development
|
|
||||||
```
|
|
||||||
|
|
||||||
### novo Market (разработка)
|
|
||||||
```bash
|
|
||||||
ng serve --configuration=novo
|
|
||||||
```
|
|
||||||
|
|
||||||
### Сборка для продакшена
|
|
||||||
|
|
||||||
#### Dexar Market
|
|
||||||
```bash
|
|
||||||
ng build --configuration=production
|
|
||||||
```
|
|
||||||
Результат: `dist/dexarmarket/`
|
|
||||||
|
|
||||||
#### novo Market
|
|
||||||
```bash
|
|
||||||
ng build --configuration=novo-production
|
|
||||||
```
|
|
||||||
Результат: `dist/novomarket/`
|
|
||||||
|
|
||||||
## Структура файлов
|
|
||||||
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── environments/
|
|
||||||
│ ├── environment.ts # Dexar Development
|
|
||||||
│ ├── environment.production.ts # Dexar Production
|
|
||||||
│ ├── environment.novo.ts # novo Development
|
|
||||||
│ └── environment.novo.production.ts # novo Production
|
|
||||||
├── styles/
|
|
||||||
│ └── themes/
|
|
||||||
│ ├── dexar.theme.scss # Dexar цвета (фиолетовый)
|
|
||||||
│ └── novo.theme.scss # novo цвета (зеленый)
|
|
||||||
```
|
|
||||||
|
|
||||||
## Что настраивается через Environment
|
|
||||||
|
|
||||||
В файлах environment можно настроить:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
{
|
|
||||||
brandName: 'Название бренда',
|
|
||||||
brandFullName: 'Полное название бренда',
|
|
||||||
theme: 'dexar' | 'novo',
|
|
||||||
apiUrl: 'URL API',
|
|
||||||
logo: 'Путь к логотипу',
|
|
||||||
contactEmail: 'Email контактов',
|
|
||||||
supportEmail: 'Email поддержки',
|
|
||||||
domain: 'Домен сайта',
|
|
||||||
telegram: 'Telegram канал',
|
|
||||||
phones: {
|
|
||||||
russia: 'Телефон в России',
|
|
||||||
armenia: 'Телефон в Армении'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## CSS Переменные
|
|
||||||
|
|
||||||
Темы используют CSS переменные, которые можно изменить:
|
|
||||||
|
|
||||||
```scss
|
|
||||||
:root {
|
|
||||||
--primary-color: #10b981; // Основной цвет
|
|
||||||
--primary-hover: #059669; // Hover эффект
|
|
||||||
--secondary-color: #14b8a6; // Вторичный цвет
|
|
||||||
--gradient-primary: linear-gradient(...);
|
|
||||||
--gradient-hero: linear-gradient(...);
|
|
||||||
// и другие...
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Обновление для нового бренда
|
|
||||||
|
|
||||||
### Что нужно обновить для novo Market:
|
|
||||||
|
|
||||||
1. ✅ **Environment файлы** - созданы
|
|
||||||
2. ✅ **Темы (SCSS)** - созданы (зеленые цвета)
|
|
||||||
3. ✅ **Angular.json конфигурации** - настроены
|
|
||||||
4. ⏳ **Логотипы и изображения** - добавить в `public/assets/images/`
|
|
||||||
5. ⏳ **Реквизиты компании** - обновить когда будут готовы
|
|
||||||
6. ⏳ **Домен и SSL** - настроить при деплое
|
|
||||||
7. ⏳ **API endpoint** - обновить когда будет готов
|
|
||||||
|
|
||||||
## Деплой
|
|
||||||
|
|
||||||
### Dexar Market
|
|
||||||
```bash
|
|
||||||
ng build --configuration=production
|
|
||||||
# Deploy dist/dexarmarket/ to dexarmarket.ru
|
|
||||||
```
|
|
||||||
|
|
||||||
### novo Market
|
|
||||||
```bash
|
|
||||||
ng build --configuration=novo-production
|
|
||||||
# Deploy dist/novomarket/ to novomarket.ru
|
|
||||||
```
|
|
||||||
|
|
||||||
## Отличия брендов
|
|
||||||
|
|
||||||
| Параметр | Dexar Market | novo Market |
|
|
||||||
|----------|--------------|-------------|
|
|
||||||
| Основной цвет | Фиолетовый (#667eea) | Зеленый (#10b981) |
|
|
||||||
| Название | Dexar Market | novo Market |
|
|
||||||
| Домен | dexarmarket.ru | novomarket.ru |
|
|
||||||
| Email | info@dexarmarket.ru | info@novomarket.ru |
|
|
||||||
| Telegram | @dexarmarket | @novomarket |
|
|
||||||
| Реквизиты | Текущие | Будут обновлены |
|
|
||||||
|
|
||||||
## Следующие шаги для novo Market
|
|
||||||
|
|
||||||
1. Добавить логотип novo Market (`public/assets/images/novo-logo.svg`)
|
|
||||||
2. Обновить реквизиты компании в правовых документах
|
|
||||||
3. Настроить API endpoint для novo
|
|
||||||
4. Настроить домен и SSL сертификаты
|
|
||||||
5. Обновить контактную информацию (телефоны, адреса)
|
|
||||||
|
|
||||||
## Примечания
|
|
||||||
|
|
||||||
- Оба бренда используют одну кодовую базу
|
|
||||||
- Все компоненты автоматически адаптируются под выбранный бренд
|
|
||||||
- Легко добавить новые бренды по той же схеме
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
# PWA Setup Guide
|
|
||||||
|
|
||||||
## ✅ Implemented Features
|
|
||||||
|
|
||||||
### 1. Service Worker
|
|
||||||
- **Caching Strategy**: Aggressive prefetch for app shell
|
|
||||||
- **API Caching**: Freshness strategy with 1-hour cache (max 100 requests)
|
|
||||||
- **Image Caching**: Performance strategy with 7-day cache (max 50 images)
|
|
||||||
- **Configuration**: `ngsw-config.json`
|
|
||||||
|
|
||||||
### 2. Web App Manifests
|
|
||||||
- **Dexar**: `public/manifest.webmanifest` (purple theme #a855f7)
|
|
||||||
- **Novo**: `public/manifest.novo.webmanifest` (green theme #10b981)
|
|
||||||
- **Features**:
|
|
||||||
- Installable on mobile/desktop
|
|
||||||
- Standalone display mode
|
|
||||||
- 8 icon sizes (72px to 512px)
|
|
||||||
- Russian language metadata
|
|
||||||
|
|
||||||
### 3. Offline Support
|
|
||||||
- App shell loads instantly from cache
|
|
||||||
- API responses cached for 1 hour
|
|
||||||
- Product images cached for 7 days
|
|
||||||
- Automatic background updates
|
|
||||||
|
|
||||||
## 🚀 Testing PWA Functionality
|
|
||||||
|
|
||||||
### Local Testing with Production Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build for production
|
|
||||||
npm run build -- --configuration=production
|
|
||||||
|
|
||||||
# Serve the production build
|
|
||||||
npx http-server dist/dexarmarket -p 4200 -c-1
|
|
||||||
|
|
||||||
# For Novo brand
|
|
||||||
npx http-server dist/novomarket -p 4201 -c-1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Chrome DevTools Testing
|
|
||||||
|
|
||||||
1. Open `http://localhost:4200`
|
|
||||||
2. Open DevTools (F12)
|
|
||||||
3. Go to **Application** tab
|
|
||||||
4. Check:
|
|
||||||
- **Service Workers**: Should show registered worker
|
|
||||||
- **Cache Storage**: Should show `ngsw:/:db`, `ngsw:/:assets`
|
|
||||||
- **Manifest**: Should show app details
|
|
||||||
|
|
||||||
### Install Prompt Testing
|
|
||||||
|
|
||||||
1. Open app in Chrome/Edge
|
|
||||||
2. Click the **install icon** in address bar (➕)
|
|
||||||
3. Confirm installation
|
|
||||||
4. App opens as standalone window
|
|
||||||
5. Check Start Menu/Home Screen for app icon
|
|
||||||
|
|
||||||
### Offline Testing
|
|
||||||
|
|
||||||
1. Open app while online
|
|
||||||
2. Navigate through pages (loads assets)
|
|
||||||
3. Open DevTools → Network → Toggle **Offline**
|
|
||||||
4. Refresh page - should still work!
|
|
||||||
5. Navigate to cached pages - should load instantly
|
|
||||||
|
|
||||||
## 📱 Mobile Testing
|
|
||||||
|
|
||||||
### Android Chrome
|
|
||||||
1. Open app URL
|
|
||||||
2. Chrome shows "Add to Home Screen" banner
|
|
||||||
3. Install and open - works like native app
|
|
||||||
4. Splash screen with your logo/colors
|
|
||||||
|
|
||||||
### iOS Safari
|
|
||||||
1. Open app URL
|
|
||||||
2. Tap Share → "Add to Home Screen"
|
|
||||||
3. Icon appears on home screen
|
|
||||||
4. Opens in full-screen mode
|
|
||||||
|
|
||||||
## 🔧 Configuration Details
|
|
||||||
|
|
||||||
### Service Worker Caching Strategy
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"app": {
|
|
||||||
"installMode": "prefetch", // Download immediately
|
|
||||||
"updateMode": "prefetch" // Auto-update in background
|
|
||||||
},
|
|
||||||
"assets": {
|
|
||||||
"installMode": "lazy", // Load on-demand
|
|
||||||
"updateMode": "prefetch"
|
|
||||||
},
|
|
||||||
"api-cache": {
|
|
||||||
"strategy": "freshness", // Network first, fallback to cache
|
|
||||||
"maxAge": "1h" // Keep for 1 hour
|
|
||||||
},
|
|
||||||
"product-images": {
|
|
||||||
"strategy": "performance", // Cache first, update in background
|
|
||||||
"maxAge": "7d" // Keep for 7 days
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### Manifest Differences
|
|
||||||
|
|
||||||
| Property | Dexar | Novo |
|
|
||||||
|----------|-------|------|
|
|
||||||
| Theme Color | #a855f7 (purple) | #10b981 (green) |
|
|
||||||
| Name | Dexar Market | Novo Market |
|
|
||||||
| Icons | Default Angular | Default Angular |
|
|
||||||
| Background | White (#ffffff) | White (#ffffff) |
|
|
||||||
|
|
||||||
## 🎨 Custom Icons (Recommended)
|
|
||||||
|
|
||||||
Replace the default Angular icons with brand-specific ones:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
public/icons/
|
|
||||||
├── icon-72x72.png # Smallest (splash screen)
|
|
||||||
├── icon-96x96.png
|
|
||||||
├── icon-128x128.png
|
|
||||||
├── icon-144x144.png
|
|
||||||
├── icon-152x152.png # iOS home screen
|
|
||||||
├── icon-192x192.png # Android home screen
|
|
||||||
├── icon-384x384.png
|
|
||||||
└── icon-512x512.png # Largest (splash, install prompt)
|
|
||||||
```
|
|
||||||
|
|
||||||
**Design Guidelines**:
|
|
||||||
- Use solid background color (purple for Dexar, green for Novo)
|
|
||||||
- Center white logo/icon
|
|
||||||
- Keep design simple (shows at small sizes)
|
|
||||||
- Export as PNG with transparency or solid background
|
|
||||||
|
|
||||||
## 🔄 Update Strategy
|
|
||||||
|
|
||||||
### How Updates Work
|
|
||||||
1. User visits app
|
|
||||||
2. Service worker checks for updates
|
|
||||||
3. New version downloads in background
|
|
||||||
4. User refreshes → gets updated version
|
|
||||||
5. Old cache automatically cleared
|
|
||||||
|
|
||||||
### Force Update (Development)
|
|
||||||
```bash
|
|
||||||
# Clear all caches
|
|
||||||
chrome://serviceworker-internals/ # Unregister worker
|
|
||||||
chrome://settings/clearBrowserData # Clear cache
|
|
||||||
|
|
||||||
# Or in code (add to app.config.ts)
|
|
||||||
navigator.serviceWorker.getRegistrations().then(registrations => {
|
|
||||||
registrations.forEach(reg => reg.unregister());
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 Performance Benefits
|
|
||||||
|
|
||||||
### Before PWA
|
|
||||||
- Initial load: ~2-3s (network dependent)
|
|
||||||
- Subsequent loads: ~1-2s
|
|
||||||
- Offline: ❌ Not available
|
|
||||||
|
|
||||||
### After PWA
|
|
||||||
- Initial load: ~2-3s (first visit)
|
|
||||||
- Subsequent loads: **~200-500ms** (cached)
|
|
||||||
- Offline: ✅ **Fully functional**
|
|
||||||
- Install: ✅ **Native app experience**
|
|
||||||
|
|
||||||
## 🐛 Troubleshooting
|
|
||||||
|
|
||||||
### Service Worker Not Registering
|
|
||||||
- Check console for errors
|
|
||||||
- Ensure HTTPS (or localhost)
|
|
||||||
- Clear browser cache and reload
|
|
||||||
|
|
||||||
### Old Version Not Updating
|
|
||||||
- Hard refresh: `Ctrl+Shift+R` (Windows) or `Cmd+Shift+R` (Mac)
|
|
||||||
- Unregister worker in DevTools
|
|
||||||
- Wait 24 hours (automatic update)
|
|
||||||
|
|
||||||
### Manifest Not Loading
|
|
||||||
- Check `index.html` has `<link rel="manifest">`
|
|
||||||
- Verify manifest path is correct
|
|
||||||
- Check manifest JSON is valid (no syntax errors)
|
|
||||||
|
|
||||||
### Icons Not Showing
|
|
||||||
- Check icon paths in manifest
|
|
||||||
- Ensure icons exist in `public/icons/`
|
|
||||||
- Verify icon sizes match manifest
|
|
||||||
|
|
||||||
## 📚 Next Steps
|
|
||||||
|
|
||||||
1. **Custom Icons**: Create brand-specific icons for both themes
|
|
||||||
2. **Push Notifications**: Add user engagement (requires backend)
|
|
||||||
3. **Background Sync**: Queue offline orders, sync when online
|
|
||||||
4. **Analytics**: Track PWA installs, offline usage
|
|
||||||
5. **A2HS Prompt**: Show custom "Install App" banner
|
|
||||||
|
|
||||||
## 🔗 Resources
|
|
||||||
|
|
||||||
- [PWA Checklist](https://web.dev/pwa-checklist/)
|
|
||||||
- [Angular PWA Guide](https://angular.dev/ecosystem/service-workers)
|
|
||||||
- [Manifest Generator](https://www.simicart.com/manifest-generator.html/)
|
|
||||||
- [Icon Generator](https://realfavicongenerator.net/)
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
# Рекомендации по работе с платежными ссылками
|
|
||||||
|
|
||||||
## Требования Райффайзенбанка для оплаты по ссылке
|
|
||||||
|
|
||||||
### ✅ Что уже реализовано:
|
|
||||||
|
|
||||||
1. **Реквизиты организации** - полностью заполнены
|
|
||||||
2. **Правила оплаты** - подробная страница с требованиями ЦБ РФ, PCI DSS, 3D-Secure
|
|
||||||
3. **Политика возврата** - полная информация о возврате физических и цифровых товаров
|
|
||||||
4. **Публичная оферта** - модель маркетплейса, разграничение ответственности
|
|
||||||
5. **Политика конфиденциальности** - обработка персональных данных (152-ФЗ)
|
|
||||||
6. **Чекбокс согласия в корзине** - со ссылками на:
|
|
||||||
- Публичную оферту
|
|
||||||
- Политику возврата
|
|
||||||
- Условия гарантии
|
|
||||||
- Политику конфиденциальности
|
|
||||||
7. **Логотипы платежных систем**:
|
|
||||||
- МИР (обязательно!)
|
|
||||||
- Visa
|
|
||||||
- Mastercard
|
|
||||||
- Размещены в футере и на странице оплаты
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📧 Рекомендации при отправке платежной ссылки покупателю
|
|
||||||
|
|
||||||
### Шаблон письма/сообщения:
|
|
||||||
|
|
||||||
```
|
|
||||||
Здравствуйте, [Имя покупателя]!
|
|
||||||
|
|
||||||
Ваш заказ №[НОМЕР] оформлен.
|
|
||||||
|
|
||||||
Для оплаты перейдите по ссылке:
|
|
||||||
[ПЛАТЕЖНАЯ ССЫЛКА]
|
|
||||||
|
|
||||||
Сумма к оплате: [СУММА] ₽
|
|
||||||
|
|
||||||
Перед оплатой, пожалуйста, ознакомьтесь с условиями:
|
|
||||||
• Публичная оферта: https://dexarmarket.ru/public-offer
|
|
||||||
• Политика возврата: https://dexarmarket.ru/return-policy
|
|
||||||
• Условия гарантии: https://dexarmarket.ru/guarantee
|
|
||||||
• Политика конфиденциальности: https://dexarmarket.ru/privacy-policy
|
|
||||||
|
|
||||||
Оплачивая заказ, вы подтверждаете, что ознакомились и согласны с данными условиями.
|
|
||||||
|
|
||||||
---
|
|
||||||
С уважением,
|
|
||||||
Команда Dexarmarket
|
|
||||||
Техподдержка: Info@dexarmarket.ru
|
|
||||||
Телефон: +7 (926) 459-31-57
|
|
||||||
```
|
|
||||||
|
|
||||||
### ✅ Важно получить подтверждение от покупателя!
|
|
||||||
|
|
||||||
**Вариант 1 - Автоматическое подтверждение:**
|
|
||||||
После оплаты отправить покупателю:
|
|
||||||
```
|
|
||||||
Спасибо за оплату заказа №[НОМЕР]!
|
|
||||||
|
|
||||||
Вы подтвердили согласие с:
|
|
||||||
✓ Публичной офертой
|
|
||||||
✓ Политикой возврата
|
|
||||||
✓ Условиями гарантии
|
|
||||||
✓ Политикой конфиденциальности
|
|
||||||
|
|
||||||
Чек отправлен на email: [EMAIL]
|
|
||||||
Статус заказа можно отслеживать в личном кабинете.
|
|
||||||
```
|
|
||||||
|
|
||||||
**Вариант 2 - Ручное подтверждение (желательно):**
|
|
||||||
Перед отправкой ссылки запросить:
|
|
||||||
```
|
|
||||||
Для оформления заказа подтвердите, пожалуйста, что вы ознакомились с условиями
|
|
||||||
(https://dexarmarket.ru/public-offer) и согласны с ними.
|
|
||||||
|
|
||||||
Ответьте "Согласен" или "Подтверждаю" для продолжения.
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛡️ Защита от оспаривания платежей (Chargeback)
|
|
||||||
|
|
||||||
### Что сохранять для доказательной базы:
|
|
||||||
|
|
||||||
1. **Переписка с покупателем:**
|
|
||||||
- Скриншоты чатов
|
|
||||||
- Email переписка
|
|
||||||
- SMS/WhatsApp сообщения с подтверждением
|
|
||||||
|
|
||||||
2. **Логи действий покупателя:**
|
|
||||||
- IP-адрес при оформлении заказа
|
|
||||||
- Timestamp (дата и время)
|
|
||||||
- Согласие с чекбоксом (если есть личный кабинет)
|
|
||||||
|
|
||||||
3. **Документы об отправке:**
|
|
||||||
- Трек-номер посылки
|
|
||||||
- Подтверждение доставки
|
|
||||||
- Подпись получателя (если есть)
|
|
||||||
|
|
||||||
4. **Платежная информация:**
|
|
||||||
- Номер транзакции
|
|
||||||
- Дата и время оплаты
|
|
||||||
- Сумма платежа
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🔒 Дополнительные меры безопасности
|
|
||||||
|
|
||||||
### 1. Двухфакторное подтверждение
|
|
||||||
Для крупных заказов (>10 000 ₽) рекомендуется:
|
|
||||||
- Звонок покупателю для подтверждения заказа
|
|
||||||
- Запись разговора (с уведомлением клиента)
|
|
||||||
|
|
||||||
### 2. Проверка благонадежности
|
|
||||||
Для новых покупателей:
|
|
||||||
- Проверить совпадение адреса доставки с регионом телефона
|
|
||||||
- При подозрительных заказах запросить фото документа
|
|
||||||
|
|
||||||
### 3. Страхование рисков
|
|
||||||
- Оформить договор с платежным провайдером на защиту от мошенничества
|
|
||||||
- Использовать холдирование средств (72 часа на проверку)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📊 Статистика оспариваний
|
|
||||||
|
|
||||||
**Риски по категориям товаров:**
|
|
||||||
- Электроника: ~2-5% оспариваний
|
|
||||||
- Одежда: ~1-3%
|
|
||||||
- Цифровые товары: ~0.5-2%
|
|
||||||
- Продукты питания: ~0.1-0.5%
|
|
||||||
|
|
||||||
**Причины оспариваний:**
|
|
||||||
1. "Не получил товар" (40%)
|
|
||||||
2. "Товар не соответствует описанию" (30%)
|
|
||||||
3. "Не заказывал" (20%)
|
|
||||||
4. "Дубликат платежа" (10%)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Чек-лист готовности к работе с Райффайзенбанком
|
|
||||||
|
|
||||||
- [x] Реквизиты организации заполнены
|
|
||||||
- [x] Правила оплаты на русском языке
|
|
||||||
- [x] Политика возврата опубликована
|
|
||||||
- [x] Публичная оферта опубликована
|
|
||||||
- [x] Политика конфиденциальности опубликована
|
|
||||||
- [x] Логотип МИР размещен на сайте
|
|
||||||
- [x] Чекбокс согласия с условиями в корзине
|
|
||||||
- [x] Ссылки на все документы в чекбоксе
|
|
||||||
- [ ] Настроен процесс отправки платежных ссылок с условиями
|
|
||||||
- [ ] Настроен процесс получения подтверждений от покупателей
|
|
||||||
- [ ] Настроена система логирования действий пользователей
|
|
||||||
- [ ] Подготовлена база для работы с оспариваниями
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📞 Контакты для связи с банком
|
|
||||||
|
|
||||||
**АО "Райффайзенбанк"**
|
|
||||||
- Сайт: https://www.raiffeisen.ru
|
|
||||||
- Требования к сайтам: https://www.raiffeisen.ru/common/img/uploaded/files/business/treb_k_saity.pdf
|
|
||||||
- Техподдержка эквайринга: указывается при подключении
|
|
||||||
|
|
||||||
**Платежная система МИР**
|
|
||||||
- Требования к использованию логотипа: https://mironline.ru/support/merchantam/brand/
|
|
||||||
- Обязательно размещение логотипа при приеме карт МИР
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Статус проекта
|
|
||||||
|
|
||||||
**Готовность к подключению эквайринга: 95%**
|
|
||||||
|
|
||||||
Осталось реализовать:
|
|
||||||
1. Автоматизацию отправки ссылок с условиями
|
|
||||||
2. Систему получения подтверждений от покупателей
|
|
||||||
3. Логирование действий для доказательной базы
|
|
||||||
|
|
||||||
**Все юридические и информационные требования выполнены!** ✅
|
|
||||||
@@ -1,423 +0,0 @@
|
|||||||
# Project Recommendations & Roadmap
|
|
||||||
|
|
||||||
## 📊 Current Status: 9.2/10
|
|
||||||
|
|
||||||
Your project is production-ready with excellent architecture! Here's what to focus on next:
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## ✅ Recently Completed (January 2026)
|
|
||||||
|
|
||||||
1. **Phone Number Collection**
|
|
||||||
- Real-time formatting (+7 XXX XXX-XX-XX)
|
|
||||||
- Comprehensive validation (11 digits)
|
|
||||||
- Raw digits sent to API
|
|
||||||
|
|
||||||
2. **HTML Structure Unification**
|
|
||||||
- Single template for both themes
|
|
||||||
- CSS-only differentiation (Novo/Dexar)
|
|
||||||
- Eliminated code duplication
|
|
||||||
|
|
||||||
3. **PWA Implementation**
|
|
||||||
- Service worker with smart caching
|
|
||||||
- Dual manifests (brand-specific)
|
|
||||||
- Offline support
|
|
||||||
- Installable app
|
|
||||||
|
|
||||||
4. **Code Quality**
|
|
||||||
- Removed 3 duplicate methods
|
|
||||||
- Fixed SCSS syntax errors
|
|
||||||
- Optimized cart component
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎯 Priority Roadmap
|
|
||||||
|
|
||||||
### 🔥 HIGH PRIORITY (Next 2 Weeks)
|
|
||||||
|
|
||||||
#### 1. Custom PWA Icons
|
|
||||||
**Why**: Branding, professionalism
|
|
||||||
**Effort**: 2-3 hours
|
|
||||||
**Impact**: High visibility
|
|
||||||
|
|
||||||
**Action Items**:
|
|
||||||
```bash
|
|
||||||
# Create 8 icon sizes for each brand:
|
|
||||||
# Dexar: Purple (#a855f7) background + white logo
|
|
||||||
# Novo: Green (#10b981) background + white logo
|
|
||||||
|
|
||||||
public/icons/dexar/
|
|
||||||
├── icon-72x72.png
|
|
||||||
├── icon-512x512.png
|
|
||||||
└── ...
|
|
||||||
|
|
||||||
public/icons/novo/
|
|
||||||
├── icon-72x72.png
|
|
||||||
└── ...
|
|
||||||
|
|
||||||
# Update manifests to point to brand folders
|
|
||||||
```
|
|
||||||
|
|
||||||
**Tools**: Figma, Photoshop, or [RealFaviconGenerator](https://realfavicongenerator.net/)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 2. Unit Testing
|
|
||||||
**Why**: Code reliability, easier refactoring
|
|
||||||
**Effort**: 1-2 weeks
|
|
||||||
**Impact**: Development velocity, bug reduction
|
|
||||||
|
|
||||||
**Target Coverage**: 80%+
|
|
||||||
|
|
||||||
**Priority Test Files**:
|
|
||||||
```typescript
|
|
||||||
// 1. Services (highest ROI)
|
|
||||||
cart.service.spec.ts // Test signal updates, cart logic
|
|
||||||
api.service.spec.ts // Mock HTTP calls
|
|
||||||
telegram.service.spec.ts // Test WebApp initialization
|
|
||||||
|
|
||||||
// 2. Components (critical paths)
|
|
||||||
cart.component.spec.ts // Payment flow, validation
|
|
||||||
header.component.spec.ts // Cart count, navigation
|
|
||||||
item-detail.component.spec.ts // Add to cart, variant selection
|
|
||||||
|
|
||||||
// 3. Interceptors
|
|
||||||
cache.interceptor.spec.ts // Verify caching logic
|
|
||||||
```
|
|
||||||
|
|
||||||
**Quick Start**:
|
|
||||||
```bash
|
|
||||||
# Generate test with Angular CLI
|
|
||||||
ng test --code-coverage
|
|
||||||
|
|
||||||
# Write first test
|
|
||||||
describe('CartService', () => {
|
|
||||||
it('should add item to cart', () => {
|
|
||||||
service.addToCart(mockItem, mockVariant);
|
|
||||||
expect(service.cartItems().length).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 3. Error Boundary & User Feedback
|
|
||||||
**Why**: Graceful failures, better UX
|
|
||||||
**Effort**: 1 day
|
|
||||||
**Impact**: User trust, reduced support tickets
|
|
||||||
|
|
||||||
**Implementation**:
|
|
||||||
```typescript
|
|
||||||
// src/app/services/error-handler.service.ts
|
|
||||||
@Injectable({ providedIn: 'root' })
|
|
||||||
export class ErrorHandlerService {
|
|
||||||
showError(message: string) {
|
|
||||||
// Show toast notification
|
|
||||||
// Log to analytics
|
|
||||||
// Optionally send to backend
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Usage in cart.component.ts
|
|
||||||
this.apiService.createPayment(data).subscribe({
|
|
||||||
next: (response) => { /* handle success */ },
|
|
||||||
error: (err) => {
|
|
||||||
this.errorHandler.showError(
|
|
||||||
'Не удалось создать платеж. Попробуйте позже.'
|
|
||||||
);
|
|
||||||
console.error(err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Add Toast Library**:
|
|
||||||
```bash
|
|
||||||
npm install ngx-toastr --save
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### ⚡ MEDIUM PRIORITY (Next Month)
|
|
||||||
|
|
||||||
#### 4. E2E Testing
|
|
||||||
**Why**: Catch integration bugs, confidence in releases
|
|
||||||
**Effort**: 3-5 days
|
|
||||||
**Impact**: Release quality
|
|
||||||
|
|
||||||
**Recommended**: [Playwright](https://playwright.dev/) (better than Cypress for modern apps)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm install @playwright/test --save-dev
|
|
||||||
npx playwright install
|
|
||||||
```
|
|
||||||
|
|
||||||
**Critical Test Scenarios**:
|
|
||||||
1. Browse categories → View item → Add to cart → Checkout
|
|
||||||
2. Search product → Filter results → Add to cart
|
|
||||||
3. Empty cart → Add items → Remove items
|
|
||||||
4. Payment flow (mock SBP QR code response)
|
|
||||||
5. Email/phone validation on success screen
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 5. Analytics Integration
|
|
||||||
**Why**: Data-driven decisions, understand users
|
|
||||||
**Effort**: 1 day
|
|
||||||
**Impact**: Business insights
|
|
||||||
|
|
||||||
**Recommended Setup**:
|
|
||||||
```typescript
|
|
||||||
// Yandex Metrica (best for Russian market)
|
|
||||||
<!-- index.html -->
|
|
||||||
<script>
|
|
||||||
(function(m,e,t,r,i,k,a){
|
|
||||||
// Yandex Metrica snippet
|
|
||||||
})(window, document, "yandex_metrica_callbacks2");
|
|
||||||
</script>
|
|
||||||
|
|
||||||
// Track events
|
|
||||||
yaCounter12345678.reachGoal('ADD_TO_CART', {
|
|
||||||
product_id: item.id,
|
|
||||||
price: variant.price
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
**Key Metrics to Track**:
|
|
||||||
- Product views
|
|
||||||
- Add to cart events
|
|
||||||
- Checkout initiation
|
|
||||||
- Payment success/failure
|
|
||||||
- Search queries
|
|
||||||
- PWA installs
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 6. Performance Optimization
|
|
||||||
**Why**: Better UX, SEO, conversion rates
|
|
||||||
**Effort**: 2-3 days
|
|
||||||
**Impact**: User satisfaction
|
|
||||||
|
|
||||||
**Action Items**:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 1. Image Optimization
|
|
||||||
// Use WebP format with fallbacks
|
|
||||||
<picture>
|
|
||||||
<source srcset="image.webp" type="image/webp">
|
|
||||||
<img src="image.jpg" alt="Product">
|
|
||||||
</picture>
|
|
||||||
|
|
||||||
// 2. Lazy Load Images
|
|
||||||
<img loading="lazy" src="product.jpg">
|
|
||||||
|
|
||||||
// 3. Preload Critical Assets
|
|
||||||
// index.html
|
|
||||||
<link rel="preload" href="logo.svg" as="image">
|
|
||||||
|
|
||||||
// 4. Virtual Scrolling for Long Lists
|
|
||||||
// npm install @angular/cdk
|
|
||||||
<cdk-virtual-scroll-viewport itemSize="150">
|
|
||||||
@for (item of items; track item.id) {
|
|
||||||
<div>{{ item.title }}</div>
|
|
||||||
}
|
|
||||||
</cdk-virtual-scroll-viewport>
|
|
||||||
```
|
|
||||||
|
|
||||||
**Measure First**:
|
|
||||||
```bash
|
|
||||||
# Lighthouse audit
|
|
||||||
npm install -g lighthouse
|
|
||||||
lighthouse http://localhost:4200 --view
|
|
||||||
|
|
||||||
# Target scores:
|
|
||||||
# Performance: 90+
|
|
||||||
# Accessibility: 95+
|
|
||||||
# Best Practices: 100
|
|
||||||
# SEO: 90+
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### 🔮 FUTURE ENHANCEMENTS (Next Quarter)
|
|
||||||
|
|
||||||
#### 7. Push Notifications
|
|
||||||
**Why**: Re-engage users, promote offers
|
|
||||||
**Effort**: 1 week (needs backend)
|
|
||||||
**Impact**: Retention, sales
|
|
||||||
|
|
||||||
**Requirements**:
|
|
||||||
- Firebase Cloud Messaging (FCM)
|
|
||||||
- Backend endpoint to send notifications
|
|
||||||
- User permission flow
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 8. Background Sync
|
|
||||||
**Why**: Queue orders offline, sync when online
|
|
||||||
**Effort**: 2-3 days
|
|
||||||
**Impact**: Offline-first experience
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// Register background sync
|
|
||||||
navigator.serviceWorker.ready.then(registration => {
|
|
||||||
registration.sync.register('sync-orders');
|
|
||||||
});
|
|
||||||
|
|
||||||
// ngsw-config.json - already set up!
|
|
||||||
// Your PWA is ready for this
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
#### 9. Advanced Features
|
|
||||||
**Effort**: Varies
|
|
||||||
**Impact**: Competitive advantage
|
|
||||||
|
|
||||||
- **Product Recommendations**: "You might also like..."
|
|
||||||
- **Recently Viewed**: Track browsing history
|
|
||||||
- **Wishlist**: Save items for later
|
|
||||||
- **Price Alerts**: Notify when price drops
|
|
||||||
- **Social Sharing**: Share products on Telegram/VK
|
|
||||||
- **Dark Mode**: Theme switcher
|
|
||||||
- **Multi-language**: Support English, etc.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🛠️ Technical Debt & Improvements
|
|
||||||
|
|
||||||
### Quick Wins (< 1 hour each)
|
|
||||||
|
|
||||||
1. **Environment Variables for API URLs**
|
|
||||||
```typescript
|
|
||||||
// Don't hardcode API URLs
|
|
||||||
// Use environment.apiUrl consistently
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Content Security Policy (CSP)**
|
|
||||||
```nginx
|
|
||||||
# nginx.conf
|
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline';";
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Rate Limiting**
|
|
||||||
```typescript
|
|
||||||
// Prevent API spam
|
|
||||||
import { debounceTime } from 'rxjs';
|
|
||||||
|
|
||||||
searchQuery$.pipe(
|
|
||||||
debounceTime(300)
|
|
||||||
).subscribe(/* search */);
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Loading States**
|
|
||||||
```html
|
|
||||||
<!-- Show skeletons while loading -->
|
|
||||||
@if (loading()) {
|
|
||||||
<div class="skeleton"></div>
|
|
||||||
} @else {
|
|
||||||
<div>{{ content }}</div>
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **SEO Meta Tags**
|
|
||||||
```typescript
|
|
||||||
// Use Angular's Meta service
|
|
||||||
constructor(private meta: Meta) {}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.meta.updateTag({
|
|
||||||
name: 'description',
|
|
||||||
content: this.product.description
|
|
||||||
});
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 📈 Success Metrics
|
|
||||||
|
|
||||||
### Before Optimizations
|
|
||||||
- Test Coverage: ~10%
|
|
||||||
- Lighthouse Score: ~85
|
|
||||||
- Error Tracking: Console only
|
|
||||||
- Analytics: None
|
|
||||||
- PWA: ❌
|
|
||||||
|
|
||||||
### After Optimizations (Target)
|
|
||||||
- Test Coverage: **80%+**
|
|
||||||
- Lighthouse Score: **95+**
|
|
||||||
- Error Tracking: ✅ Centralized
|
|
||||||
- Analytics: ✅ Yandex Metrica
|
|
||||||
- PWA: ✅ **Fully functional**
|
|
||||||
- User Engagement: **+30%** (with push notifications)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🎓 Learning Resources
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- [Angular Testing Guide](https://angular.dev/guide/testing)
|
|
||||||
- [Testing Library](https://testing-library.com/docs/angular-testing-library/intro/)
|
|
||||||
|
|
||||||
### Performance
|
|
||||||
- [Web.dev Performance](https://web.dev/performance/)
|
|
||||||
- [Angular Performance Checklist](https://github.com/mgechev/angular-performance-checklist)
|
|
||||||
|
|
||||||
### PWA
|
|
||||||
- [PWA Workshop](https://web.dev/learn/pwa/)
|
|
||||||
- [Workbox](https://developer.chrome.com/docs/workbox/) (service worker library)
|
|
||||||
|
|
||||||
### Analytics
|
|
||||||
- [Yandex Metrica Guide](https://yandex.ru/support/metrica/)
|
|
||||||
- [Google Analytics 4](https://developers.google.com/analytics/devguides/collection/ga4)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💡 Pro Tips
|
|
||||||
|
|
||||||
1. **Ship Frequently**: Deploy small updates often
|
|
||||||
2. **Monitor Production**: Set up error tracking (Sentry, Rollbar)
|
|
||||||
3. **User Feedback**: Add feedback button in app
|
|
||||||
4. **A/B Testing**: Test different checkout flows
|
|
||||||
5. **Mobile First**: 70%+ of e-commerce is mobile
|
|
||||||
6. **Accessibility**: Test with screen readers
|
|
||||||
7. **Security**: Regular dependency updates (`npm audit fix`)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 🚀 Next Actions (This Week)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Day 1: PWA Icons
|
|
||||||
1. Design icons for both brands
|
|
||||||
2. Update manifests
|
|
||||||
3. Test installation on mobile
|
|
||||||
|
|
||||||
# Day 2-3: Error Handling
|
|
||||||
1. Install ngx-toastr
|
|
||||||
2. Add ErrorHandlerService
|
|
||||||
3. Update all API calls with error handling
|
|
||||||
|
|
||||||
# Day 4-5: First Unit Tests
|
|
||||||
1. Set up testing utilities
|
|
||||||
2. Write tests for CartService
|
|
||||||
3. Write tests for cart validation logic
|
|
||||||
4. Run coverage report: npm test -- --code-coverage
|
|
||||||
|
|
||||||
# Weekend: Analytics
|
|
||||||
1. Set up Yandex Metrica
|
|
||||||
2. Add tracking to key events
|
|
||||||
3. Monitor dashboard
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 💬 Questions?
|
|
||||||
|
|
||||||
If you need help with any of these tasks:
|
|
||||||
1. Ask for specific code examples
|
|
||||||
2. Request architectural guidance
|
|
||||||
3. Need library recommendations
|
|
||||||
4. Want code reviews
|
|
||||||
|
|
||||||
Your project is already excellent - these improvements will make it world-class! 🌟
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
# 🔧 Troubleshooting Guide for 404 and 502 Errors
|
|
||||||
|
|
||||||
## Quick Diagnosis
|
|
||||||
|
|
||||||
Run these commands on your Ubuntu server to diagnose the issue:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Check if files exist
|
|
||||||
ls -la /var/www/dexarmarket/browser/index.html
|
|
||||||
|
|
||||||
# 2. Check nginx config syntax
|
|
||||||
sudo nginx -t
|
|
||||||
|
|
||||||
# 3. Check nginx error logs (THIS IS MOST IMPORTANT!)
|
|
||||||
sudo tail -30 /var/log/nginx/error.log
|
|
||||||
|
|
||||||
# 4. Check if nginx is running
|
|
||||||
sudo systemctl status nginx
|
|
||||||
|
|
||||||
# 5. Test API from server
|
|
||||||
curl -v https://api.dexarmarket.ru:445/ping
|
|
||||||
```
|
|
||||||
|
|
||||||
## Error: 404 Not Found
|
|
||||||
|
|
||||||
### Cause: Files not uploaded or wrong path
|
|
||||||
|
|
||||||
**Solution 1: Verify files are on server**
|
|
||||||
```bash
|
|
||||||
ls -la /var/www/dexarmarket/browser/
|
|
||||||
```
|
|
||||||
|
|
||||||
Should show:
|
|
||||||
- `index.html`
|
|
||||||
- `main-*.js`
|
|
||||||
- `chunk-*.js`
|
|
||||||
- `polyfills-*.js`
|
|
||||||
- `styles-*.css`
|
|
||||||
- `assets/` folder
|
|
||||||
|
|
||||||
**If files are missing:**
|
|
||||||
```bash
|
|
||||||
# From your local machine:
|
|
||||||
cd F:\dx\marketplace\Dexarmarket
|
|
||||||
npm run build
|
|
||||||
scp -r dist/dexarmarket/browser/* user@your-server:/var/www/dexarmarket/browser/
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution 2: Fix permissions**
|
|
||||||
```bash
|
|
||||||
sudo chown -R www-data:www-data /var/www/dexarmarket
|
|
||||||
sudo chmod -R 755 /var/www/dexarmarket
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution 3: Check nginx config is loaded**
|
|
||||||
```bash
|
|
||||||
# Check which config is active
|
|
||||||
ls -la /etc/nginx/sites-enabled/
|
|
||||||
|
|
||||||
# Should show symlink to dexarmarket config
|
|
||||||
# If not:
|
|
||||||
sudo ln -s /etc/nginx/sites-available/dexarmarket /etc/nginx/sites-enabled/
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution 4: Verify nginx root path**
|
|
||||||
```bash
|
|
||||||
sudo cat /etc/nginx/sites-available/dexarmarket | grep root
|
|
||||||
```
|
|
||||||
|
|
||||||
Should show: `root /var/www/dexarmarket/browser;`
|
|
||||||
|
|
||||||
## Error: 502 Bad Gateway
|
|
||||||
|
|
||||||
### This means the API backend (https://api.dexarmarket.ru:445) is unreachable
|
|
||||||
|
|
||||||
**Solution 1: Check if API is running**
|
|
||||||
```bash
|
|
||||||
# From Ubuntu server:
|
|
||||||
curl -v https://api.dexarmarket.ru:445/ping
|
|
||||||
|
|
||||||
# If this fails, your API backend is down!
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution 2: Port 445 is blocked**
|
|
||||||
Port 445 is typically blocked by many firewalls because it's used for SMB file sharing.
|
|
||||||
|
|
||||||
**Check from browser console (F12):**
|
|
||||||
- Open browser Developer Tools (F12)
|
|
||||||
- Go to Console tab
|
|
||||||
- Look for errors like: `net::ERR_CONNECTION_REFUSED` or `net::ERR_SSL_PROTOCOL_ERROR`
|
|
||||||
|
|
||||||
**Possible fixes:**
|
|
||||||
- Use standard port 443 for HTTPS
|
|
||||||
- Or use port 8443, 8080, or other non-standard but common ports
|
|
||||||
- Configure firewall to allow port 445
|
|
||||||
|
|
||||||
**Solution 3: CORS issues**
|
|
||||||
The API must have CORS headers allowing requests from `https://dexarmarket.ru`
|
|
||||||
|
|
||||||
Check API response headers:
|
|
||||||
```bash
|
|
||||||
curl -v -H "Origin: https://dexarmarket.ru" https://api.dexarmarket.ru:445/ping
|
|
||||||
```
|
|
||||||
|
|
||||||
Should include headers like:
|
|
||||||
```
|
|
||||||
Access-Control-Allow-Origin: https://dexarmarket.ru
|
|
||||||
```
|
|
||||||
|
|
||||||
**Solution 4: SSL Certificate issues**
|
|
||||||
```bash
|
|
||||||
# Test with SSL verification disabled
|
|
||||||
curl -k https://api.dexarmarket.ru:445/ping
|
|
||||||
|
|
||||||
# If this works but normal curl doesn't, SSL cert is invalid
|
|
||||||
```
|
|
||||||
|
|
||||||
## Still Not Working?
|
|
||||||
|
|
||||||
### Get detailed error information:
|
|
||||||
|
|
||||||
**1. Browser Console (JavaScript errors)**
|
|
||||||
```
|
|
||||||
F12 → Console tab
|
|
||||||
Look for red errors
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Browser Network Tab (Failed requests)**
|
|
||||||
```
|
|
||||||
F12 → Network tab
|
|
||||||
Reload page
|
|
||||||
Look for red (failed) requests
|
|
||||||
Click on failed request to see details
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Nginx Error Log (Server-side errors)**
|
|
||||||
```bash
|
|
||||||
sudo tail -50 /var/log/nginx/error.log
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Nginx Access Log (See what requests come in)**
|
|
||||||
```bash
|
|
||||||
sudo tail -50 /var/log/nginx/access.log
|
|
||||||
```
|
|
||||||
|
|
||||||
**5. Test Build Locally**
|
|
||||||
```bash
|
|
||||||
cd F:\dx\marketplace\Dexarmarket\dist\dexarmarket\browser
|
|
||||||
python -m http.server 8000
|
|
||||||
# Visit http://localhost:8000
|
|
||||||
```
|
|
||||||
|
|
||||||
If local test works, the issue is with deployment, not the build.
|
|
||||||
|
|
||||||
## Common Mistakes
|
|
||||||
|
|
||||||
❌ **Uploading to wrong directory**
|
|
||||||
- Correct: `/var/www/dexarmarket/browser/`
|
|
||||||
- Wrong: `/var/www/dexarmarket/` (missing browser/)
|
|
||||||
|
|
||||||
❌ **Wrong permissions**
|
|
||||||
```bash
|
|
||||||
# Must be readable by www-data
|
|
||||||
sudo chown -R www-data:www-data /var/www/dexarmarket
|
|
||||||
sudo chmod -R 755 /var/www/dexarmarket
|
|
||||||
```
|
|
||||||
|
|
||||||
❌ **Nginx config not reloaded**
|
|
||||||
```bash
|
|
||||||
# After ANY change to nginx config:
|
|
||||||
sudo nginx -t
|
|
||||||
sudo systemctl reload nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
❌ **Old files cached**
|
|
||||||
```bash
|
|
||||||
# Clear browser cache: Ctrl+Shift+R (hard refresh)
|
|
||||||
```
|
|
||||||
|
|
||||||
❌ **API port blocked**
|
|
||||||
- Port 445 is unusual and often blocked
|
|
||||||
- Consider using port 443 (standard HTTPS)
|
|
||||||
|
|
||||||
## Contact Information for Support
|
|
||||||
|
|
||||||
When asking for help, provide:
|
|
||||||
1. Output of `sudo nginx -t`
|
|
||||||
2. Last 30 lines of nginx error log: `sudo tail -30 /var/log/nginx/error.log`
|
|
||||||
3. Browser console errors (F12 → Console)
|
|
||||||
4. Result of `curl -v https://api.dexarmarket.ru:445/ping` from server
|
|
||||||
5. Screenshot of browser Network tab showing failed request
|
|
||||||
2
docs/context/features/media-manager/FACTS.jsonl
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
{"id":"MM-20260715T000000Z-0001","subject":"media-backend","predicate":"is","object":"not implemented yet; /media routes to BackofficeComingSoonPageComponent; GET /media, POST /media/upload, DELETE /media/:id, PATCH /media/:id are the documented backend gap","src":["docs/context/adrs/ADR-0002-media-manager-contract.md","src/app/app.routes.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-15T00:00:00Z","tags":["media-manager","backend-gap"]}
|
||||||
|
{"id":"MM-20260715T000000Z-0002","subject":"media-storage","predicate":"is-implemented-by","object":"MediaRepository interface with MockMediaRepository (IndexedDB-backed, interim) and HttpMediaRepository (future) selected via DI token; media assets never enter the Bootstrap model","src":["docs/context/adrs/ADR-0002-media-manager-contract.md"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-15T00:00:00Z","tags":["media-manager","repository-pattern"]}
|
||||||
6
docs/context/features/platform-vision/FACTS.jsonl
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{"id":"PV-20260713T000000Z-0001","subject":"platform","predicate":"is-architected-as","object":"multi-tenant marketplace platform powering unlimited marketplaces from one codebase, driven entirely by backend bootstrap configuration","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"decision","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["architecture","multi-tenant"]}
|
||||||
|
{"id":"PV-20260713T000000Z-0002","subject":"frontend","predicate":"must-not","object":"contain marketplace-specific code, hardcoded marketplace data, or environment-flag-driven UI","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["frontend","constraint"]}
|
||||||
|
{"id":"PV-20260713T000000Z-0003","subject":"bootstrap","predicate":"must-only-contain","object":"data needed before app start (branding, languages, homepage layout, navigation, enabled widgets, footer pages) and must never contain products, orders, cart, or users","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["bootstrap","constraint"]}
|
||||||
|
{"id":"PV-20260713T000000Z-0004","subject":"translatable-fields","predicate":"must-be-modeled-as","object":"generic translations.{lang} map so adding/removing a language automatically exposes/removes translation fields across all translatable objects","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["i18n","constraint"]}
|
||||||
|
{"id":"PV-20260713T000000Z-0005","subject":"admin-app","predicate":"is-isolated-from","object":"marketplace storefront bundle: admin code never ships to storefront and vice versa, though they may share a domain","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["admin","security"]}
|
||||||
|
{"id":"PV-20260713T000000Z-0006","subject":"widgets","predicate":"must-not-own","object":"page spacing or page width; the renderer owns sections, spacing, and page width, widgets own only their internal layout","src":["docs/context/adrs/ADR-0001-marketplace-platform-vision.md"],"status":"active","kind":"constraint","updated_at":"2026-07-13T00:00:00Z","confidence":"high","tags":["widgets","layout"]}
|
||||||
6
docs/context/features/project-editor/FACTS.jsonl
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{"id":"PE-20260713T010000Z-0001","subject":"project-editor-routing","predicate":"is","object":"flat routes under /edit/:section (no projectId — a project is the domain-resolved tenant); /builder and /project-editor redirect to /edit/general","src":["docs/superpowers/specs/2026-07-13-marketplace-project-editor-sprint16-design.md","src/app/app.routes.ts"],"status":"active","kind":"decision","updated_at":"2026-07-13T01:00:00Z","confidence":"high","tags":["project-editor","routing"]}
|
||||||
|
{"id":"PE-20260713T010000Z-0002","subject":"locale-sync","predicate":"is-implemented-by","object":"LocaleSyncService, which generically adds/removes a locale key across static page translations and navigation labels without per-field hardcoding","src":["src/app/features/project-editor/services/locale-sync.service.ts"],"status":"active","kind":"implemented","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","i18n"]}
|
||||||
|
{"id":"PE-20260713T010000Z-0003","subject":"draft-publish-flow","predicate":"is","object":"client-side only (ProjectEditorFacade.status/dirty/save/publish) because no backend draft/publish endpoint exists yet; PUT /builder/bootstrap/draft and POST /builder/bootstrap/publish are the documented backend gap","src":["docs/Project-Editor.md","src/app/features/project-editor/facade/project-editor.facade.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","backend-gap"]}
|
||||||
|
{"id":"PE-20260713T010000Z-0004","subject":"html-editing","predicate":"uses","object":"MarketplaceHtmlEditorComponent, a contentEditable + toolbar component with no external rich-text dependency; emits raw HTML, never sanitizes during editing","src":["src/app/features/project-editor/components/html-editor/marketplace-html-editor.component.ts"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","html-editor"]}
|
||||||
|
{"id":"PE-20260713T010000Z-0005","subject":"navigation-tab","predicate":"supports","object":"header navigation and flat-list footer navigation (add/remove/reorder/edit); grouped-column footer navigation is read-only until a future sprint","src":["src/app/features/project-editor/sections/navigation-section.component.ts"],"status":"active","kind":"constraint","confidence":"high","updated_at":"2026-07-13T01:00:00Z","tags":["project-editor","navigation"]}
|
||||||
|
{"id":"PE-20260716T220000Z-0006","subject":"config-schema-and-validation","predicate":"is-implemented-by","object":"a field-schema registry (schema/editor-schema.ts, EditorSchemaService) driving centralized, severity-tagged validation (ProjectValidator composing pure schema/validators/primitives functions) and debounced undo/redo (schema/history.util) in ProjectEditorFacade; section templates stay hand-authored (metadata-augmented, not schema-rendered)","src":["docs/context/adrs/ADR-0002-project-editor-config-schema-and-validation-engine.md","src/app/features/project-editor/schema/editor-schema.ts","src/app/features/project-editor/services/project-validator.service.ts","src/app/features/project-editor/facade/project-editor.facade.ts"],"status":"active","kind":"decision","confidence":"high","updated_at":"2026-07-16T22:00:00Z","tags":["project-editor","schema","validation","undo-redo"]}
|
||||||
@@ -0,0 +1,944 @@
|
|||||||
|
# Admin Purchase Notifications Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Notify admin (toast + topbar bell badge/panel) when a new order lands on the marketplace, poll-based since the backend has no WebSocket/SSE.
|
||||||
|
|
||||||
|
**Architecture:** A single `AdminOrderWatcherService` polls `AdminOrdersLocalGateway.loadOrders()` on an editable interval (default 15s), diffs against a persisted "last notified" order id to fire toasts for genuinely new orders, and exposes a `recentOrders`/`unreadCount` signal pair that the existing (currently-empty) topbar bell panel renders. Poll interval is editable in the admin settings page, same pattern as the currency-rates section added previously.
|
||||||
|
|
||||||
|
**Tech Stack:** Angular 17+ signals, RxJS, Jasmine/Karma (`ng test`), existing `LocalStorageService`/`UserNotificationService`/`TranslateService` patterns.
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- No WebSocket/SSE available — polling only (confirmed `BACKEND-API-REFERENCE.md:20`).
|
||||||
|
- Persist state via `LocalStorageService` (`getItem`/`setItem`), never raw `localStorage`.
|
||||||
|
- Reuse the existing topbar bell (`admin-layout.component.html:141-157`) instead of adding a new nav badge.
|
||||||
|
- Admin backoffice price/amount displays stay in the order's raw stored currency (no `currencyConvert` pipe) — consistent with every other admin screen.
|
||||||
|
- Route arrays for admin navigation use the pattern `[languageService.currentLanguage(), 'backoffice', 'orders', id]` (no leading `/`), matching `admin-orders-list-page.component.ts:52`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: `UserNotificationService` gains an optional click-to-navigate route
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/features/website/user-experience/services/user-notification.service.ts`
|
||||||
|
- Modify: `src/app/features/website/user-experience/components/floating-notifications/floating-notifications.component.ts`
|
||||||
|
- Modify: `src/app/features/website/user-experience/components/floating-notifications/floating-notifications.component.html`
|
||||||
|
- Test: `src/app/features/website/user-experience/services/user-notification.service.spec.ts` (new)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Produces: `UserNotificationService.show(message: string, type?: UserNotificationType, durationMs?: number, route?: string[]): void`
|
||||||
|
- Produces: `UserNotification.route?: string[]`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `src/app/features/website/user-experience/services/user-notification.service.spec.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { UserNotificationService } from './user-notification.service';
|
||||||
|
|
||||||
|
describe('UserNotificationService', () => {
|
||||||
|
let service: UserNotificationService;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({});
|
||||||
|
service = TestBed.inject(UserNotificationService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('stores the route on the notification when provided', () => {
|
||||||
|
service.show('New order #1042', 'info', 4000, ['en', 'backoffice', 'orders', 'ord_1']);
|
||||||
|
|
||||||
|
const [note] = service.notifications();
|
||||||
|
expect(note.message).toBe('New order #1042');
|
||||||
|
expect(note.route).toEqual(['en', 'backoffice', 'orders', 'ord_1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves route undefined when not provided', () => {
|
||||||
|
service.show('Saved');
|
||||||
|
|
||||||
|
const [note] = service.notifications();
|
||||||
|
expect(note.route).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/user-notification.service.spec.ts'`
|
||||||
|
Expected: FAIL — `show` has no fourth parameter, `route` does not exist on `UserNotification`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement**
|
||||||
|
|
||||||
|
Replace the full contents of `src/app/features/website/user-experience/services/user-notification.service.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable, signal } from '@angular/core';
|
||||||
|
|
||||||
|
export type UserNotificationType = 'success' | 'info' | 'warning';
|
||||||
|
|
||||||
|
export interface UserNotification {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
type: UserNotificationType;
|
||||||
|
/** Route to navigate to when the notification is clicked. Absent means not clickable. */
|
||||||
|
route?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class UserNotificationService {
|
||||||
|
private readonly state = signal<UserNotification[]>([]);
|
||||||
|
|
||||||
|
readonly notifications = this.state.asReadonly();
|
||||||
|
|
||||||
|
show(message: string, type: UserNotificationType = 'info', durationMs: number = 2500, route?: string[]): void {
|
||||||
|
const next: UserNotification = {
|
||||||
|
id: `note-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||||
|
message,
|
||||||
|
type,
|
||||||
|
...(route ? { route } : {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
this.state.update(items => [next, ...items].slice(0, 4));
|
||||||
|
|
||||||
|
setTimeout(() => this.dismiss(next.id), durationMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
dismiss(id: string): void {
|
||||||
|
this.state.update(items => items.filter(item => item.id !== id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Modify `src/app/features/website/user-experience/components/floating-notifications/floating-notifications.component.ts` — replace full contents:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { UserNotification, UserNotificationService } from '../../services/user-notification.service';
|
||||||
|
import { TranslatePipe } from '../../../../../i18n/translate.pipe';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-floating-notifications',
|
||||||
|
standalone: true,
|
||||||
|
imports: [TranslatePipe],
|
||||||
|
templateUrl: './floating-notifications.component.html',
|
||||||
|
styleUrls: ['./floating-notifications.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class FloatingNotificationsComponent {
|
||||||
|
private readonly notificationsService = inject(UserNotificationService);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
|
||||||
|
readonly notifications = this.notificationsService.notifications;
|
||||||
|
|
||||||
|
dismiss(id: string): void {
|
||||||
|
this.notificationsService.dismiss(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
navigate(note: UserNotification): void {
|
||||||
|
if (note.route) {
|
||||||
|
void this.router.navigate(note.route);
|
||||||
|
}
|
||||||
|
this.dismiss(note.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace full contents of `src/app/features/website/user-experience/components/floating-notifications/floating-notifications.component.html`:
|
||||||
|
|
||||||
|
```html
|
||||||
|
@if (notifications().length > 0) {
|
||||||
|
<aside class="floating-notifications" aria-live="polite" aria-atomic="true">
|
||||||
|
@for (note of notifications(); track note.id) {
|
||||||
|
<article
|
||||||
|
class="floating-note"
|
||||||
|
[class]="'floating-note floating-note-' + note.type"
|
||||||
|
[class.floating-note-clickable]="!!note.route"
|
||||||
|
(click)="note.route && navigate(note)"
|
||||||
|
>
|
||||||
|
<p>{{ note.message }}</p>
|
||||||
|
<button type="button" (click)="$event.stopPropagation(); dismiss(note.id)" [attr.aria-label]="'common.dismiss' | translate">×</button>
|
||||||
|
</article>
|
||||||
|
}
|
||||||
|
</aside>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/user-notification.service.spec.ts'`
|
||||||
|
Expected: PASS (2 specs)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/features/website/user-experience/services/user-notification.service.ts src/app/features/website/user-experience/services/user-notification.service.spec.ts src/app/features/website/user-experience/components/floating-notifications/floating-notifications.component.ts src/app/features/website/user-experience/components/floating-notifications/floating-notifications.component.html
|
||||||
|
git commit -m "feat: UserNotificationService supports click-to-navigate toasts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: `AdminOrderWatcherService` — polling, diffing, toast firing
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/app/features/admin/shell/services/admin-order-watcher.service.ts`
|
||||||
|
- Test: `src/app/features/admin/shell/services/admin-order-watcher.service.spec.ts`
|
||||||
|
- Modify: `src/app/i18n/translations.ts`, `src/app/i18n/en.ts`, `src/app/i18n/ru.ts`, `src/app/i18n/hy.ts` (one new key)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `AdminOrdersLocalGateway.loadOrders(filters: AdminOrderListFilters): Observable<AdminOrdersListResult>` (existing)
|
||||||
|
- Consumes: `UserNotificationService.show(message, type?, durationMs?, route?)` (Task 1)
|
||||||
|
- Consumes: `LocalStorageService.getItem(key): string | null`, `.setItem(key, value): void` (existing)
|
||||||
|
- Produces: `AdminOrderWatcherService.recentOrders: Signal<AdminOrder[]>`
|
||||||
|
- Produces: `AdminOrderWatcherService.unreadCount: Signal<number>`
|
||||||
|
- Produces: `AdminOrderWatcherService.intervalMs: Signal<number>`
|
||||||
|
- Produces: `AdminOrderWatcherService.start(): void`
|
||||||
|
- Produces: `AdminOrderWatcherService.markAllSeen(): void`
|
||||||
|
- Produces: `AdminOrderWatcherService.setIntervalSeconds(seconds: number): void`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the i18n key first (needed by the test's translated toast message)**
|
||||||
|
|
||||||
|
In `src/app/i18n/translations.ts`, inside the `topbar:` block under `adminShell` (next to `notificationsEmpty: string;`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
notificationsEmpty: string;
|
||||||
|
notificationNewOrder: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/i18n/en.ts`, inside `adminShell.topbar` (next to `notificationsEmpty:`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
notificationsEmpty: 'No new notifications',
|
||||||
|
notificationNewOrder: 'New order #{{orderNumber}}',
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/i18n/ru.ts`, inside `adminShell.topbar`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
notificationsEmpty: 'Нет новых уведомлений',
|
||||||
|
notificationNewOrder: 'Новый заказ №{{orderNumber}}',
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/i18n/hy.ts`, inside `adminShell.topbar`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
notificationsEmpty: 'Նոր ծանուցումներ չկան',
|
||||||
|
notificationNewOrder: 'Նոր պատվեր #{{orderNumber}}',
|
||||||
|
```
|
||||||
|
|
||||||
|
(Match each file's existing `notificationsEmpty` value/indentation exactly — only add the new line after it.)
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing test**
|
||||||
|
|
||||||
|
Create `src/app/features/admin/shell/services/admin-order-watcher.service.spec.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { AdminOrderWatcherService } from './admin-order-watcher.service';
|
||||||
|
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
||||||
|
import { AdminOrder, AdminOrdersListResult } from '../../orders/models/admin-order.model';
|
||||||
|
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
||||||
|
|
||||||
|
function makeOrder(id: string, orderNumber: string, createdAt: string): AdminOrder {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
orderNumber,
|
||||||
|
status: 'pending',
|
||||||
|
customer: { name: 'Test Customer', email: 't@example.com', phone: '+70000000000' },
|
||||||
|
payment: { method: 'card', status: 'paid', amount: 1000, currency: 'RUB' },
|
||||||
|
shipping: { address: '', method: '', trackingNumber: '' },
|
||||||
|
items: [],
|
||||||
|
total: 1000,
|
||||||
|
currency: 'RUB',
|
||||||
|
notes: '',
|
||||||
|
internalNotes: '',
|
||||||
|
timeline: [],
|
||||||
|
archived: false,
|
||||||
|
createdAt,
|
||||||
|
updatedAt: createdAt,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AdminOrderWatcherService', () => {
|
||||||
|
let ordersByPoll: AdminOrder[][];
|
||||||
|
let pollIndex: number;
|
||||||
|
let notifications: UserNotificationService;
|
||||||
|
let service: AdminOrderWatcherService;
|
||||||
|
|
||||||
|
function fakeGateway() {
|
||||||
|
return {
|
||||||
|
loadOrders: () => {
|
||||||
|
const items = ordersByPoll[pollIndex] ?? ordersByPoll[ordersByPoll.length - 1];
|
||||||
|
const result: AdminOrdersListResult = { items, total: items.length, page: 1, pageSize: 20 };
|
||||||
|
return of(result);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
pollIndex = 0;
|
||||||
|
ordersByPoll = [
|
||||||
|
[makeOrder('o2', '1002', '2026-08-15T10:00:00.000Z'), makeOrder('o1', '1001', '2026-08-15T09:00:00.000Z')],
|
||||||
|
];
|
||||||
|
|
||||||
|
localStorage.clear();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: AdminOrdersLocalGateway, useValue: fakeGateway() as unknown as AdminOrdersLocalGateway },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
notifications = TestBed.inject(UserNotificationService);
|
||||||
|
service = TestBed.inject(AdminOrderWatcherService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('does not toast on the very first poll and marks everything as acknowledged', fakeAsync(() => {
|
||||||
|
service.start();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
expect(notifications.notifications().length).toBe(0);
|
||||||
|
expect(service.unreadCount()).toBe(0);
|
||||||
|
expect(service.recentOrders().map(o => o.id)).toEqual(['o2', 'o1']);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('toasts and increments unreadCount for orders newer than the last-notified one', fakeAsync(() => {
|
||||||
|
service.start();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
pollIndex = 1;
|
||||||
|
ordersByPoll.push([
|
||||||
|
makeOrder('o3', '1003', '2026-08-15T11:00:00.000Z'),
|
||||||
|
makeOrder('o2', '1002', '2026-08-15T10:00:00.000Z'),
|
||||||
|
makeOrder('o1', '1001', '2026-08-15T09:00:00.000Z'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
tick(service.intervalMs());
|
||||||
|
|
||||||
|
expect(notifications.notifications().length).toBe(1);
|
||||||
|
expect(notifications.notifications()[0].message).toContain('1003');
|
||||||
|
expect(notifications.notifications()[0].route).toEqual(['ru', 'backoffice', 'orders', 'o3']);
|
||||||
|
expect(service.unreadCount()).toBe(1);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('markAllSeen resets unreadCount without clearing recentOrders', fakeAsync(() => {
|
||||||
|
service.start();
|
||||||
|
tick(0);
|
||||||
|
|
||||||
|
pollIndex = 1;
|
||||||
|
ordersByPoll.push([
|
||||||
|
makeOrder('o3', '1003', '2026-08-15T11:00:00.000Z'),
|
||||||
|
makeOrder('o2', '1002', '2026-08-15T10:00:00.000Z'),
|
||||||
|
makeOrder('o1', '1001', '2026-08-15T09:00:00.000Z'),
|
||||||
|
]);
|
||||||
|
tick(service.intervalMs());
|
||||||
|
|
||||||
|
expect(service.unreadCount()).toBe(1);
|
||||||
|
|
||||||
|
service.markAllSeen();
|
||||||
|
|
||||||
|
expect(service.unreadCount()).toBe(0);
|
||||||
|
expect(service.recentOrders().map(o => o.id)).toEqual(['o3', 'o2', 'o1']);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('setIntervalSeconds updates intervalMs and rejects invalid values', () => {
|
||||||
|
service.setIntervalSeconds(30);
|
||||||
|
expect(service.intervalMs()).toBe(30000);
|
||||||
|
|
||||||
|
service.setIntervalSeconds(0);
|
||||||
|
expect(service.intervalMs()).toBe(30000);
|
||||||
|
|
||||||
|
service.setIntervalSeconds(-5);
|
||||||
|
expect(service.intervalMs()).toBe(30000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: `LanguageService` defaults to `'ru'` (see `language.service.ts:23`), which is why the expected route in the second test starts with `'ru'`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/admin-order-watcher.service.spec.ts'`
|
||||||
|
Expected: FAIL — `admin-order-watcher.service.ts` does not exist yet.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement**
|
||||||
|
|
||||||
|
Create `src/app/features/admin/shell/services/admin-order-watcher.service.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { Injectable, Signal, computed, inject, signal } from '@angular/core';
|
||||||
|
import { AdminOrder } from '../../orders/models/admin-order.model';
|
||||||
|
import { AdminOrdersLocalGateway } from '../../orders/services/admin-orders-local.gateway';
|
||||||
|
import { LocalStorageService } from '../../../../core/storage/local-storage.service';
|
||||||
|
import { UserNotificationService } from '../../../website/user-experience/services/user-notification.service';
|
||||||
|
import { LanguageService } from '../../../../services/language.service';
|
||||||
|
import { TranslateService } from '../../../../i18n/translate.service';
|
||||||
|
|
||||||
|
const LAST_NOTIFIED_KEY = 'adminOrderWatcher.lastNotifiedOrderId.v1';
|
||||||
|
const LAST_ACKNOWLEDGED_KEY = 'adminOrderWatcher.lastAcknowledgedOrderId.v1';
|
||||||
|
const POLL_INTERVAL_KEY = 'adminOrderWatcher.pollIntervalMs.v1';
|
||||||
|
export const DEFAULT_POLL_INTERVAL_MS = 15000;
|
||||||
|
const MIN_POLL_INTERVAL_MS = 1000;
|
||||||
|
const RECENT_ORDERS_LIMIT = 20;
|
||||||
|
const TOAST_DURATION_MS = 4000;
|
||||||
|
|
||||||
|
@Injectable({ providedIn: 'root' })
|
||||||
|
export class AdminOrderWatcherService {
|
||||||
|
private readonly gateway = inject(AdminOrdersLocalGateway);
|
||||||
|
private readonly storage = inject(LocalStorageService);
|
||||||
|
private readonly notifications = inject(UserNotificationService);
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
private readonly i18n = inject(TranslateService);
|
||||||
|
|
||||||
|
private readonly recentOrdersSignal = signal<AdminOrder[]>([]);
|
||||||
|
readonly recentOrders: Signal<AdminOrder[]> = this.recentOrdersSignal.asReadonly();
|
||||||
|
|
||||||
|
private readonly lastAcknowledgedOrderIdSignal = signal<string | null>(this.storage.getItem(LAST_ACKNOWLEDGED_KEY));
|
||||||
|
|
||||||
|
readonly unreadCount = computed(() => {
|
||||||
|
const orders = this.recentOrdersSignal();
|
||||||
|
if (orders.length === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
const ackId = this.lastAcknowledgedOrderIdSignal();
|
||||||
|
if (ackId === null) {
|
||||||
|
return orders.length;
|
||||||
|
}
|
||||||
|
const idx = orders.findIndex(order => order.id === ackId);
|
||||||
|
return idx === -1 ? orders.length : idx;
|
||||||
|
});
|
||||||
|
|
||||||
|
private readonly intervalMsSignal = signal<number>(this.readStoredIntervalMs());
|
||||||
|
readonly intervalMs: Signal<number> = this.intervalMsSignal.asReadonly();
|
||||||
|
|
||||||
|
private lastNotifiedOrderId: string | null = this.storage.getItem(LAST_NOTIFIED_KEY);
|
||||||
|
private timerId: ReturnType<typeof setInterval> | null = null;
|
||||||
|
private started = false;
|
||||||
|
|
||||||
|
start(): void {
|
||||||
|
if (this.started) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.started = true;
|
||||||
|
this.poll();
|
||||||
|
this.scheduleNext();
|
||||||
|
}
|
||||||
|
|
||||||
|
setIntervalSeconds(seconds: number): void {
|
||||||
|
if (!Number.isFinite(seconds) || seconds < MIN_POLL_INTERVAL_MS / 1000) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ms = Math.round(seconds * 1000);
|
||||||
|
this.intervalMsSignal.set(ms);
|
||||||
|
this.storage.setItem(POLL_INTERVAL_KEY, String(ms));
|
||||||
|
if (this.started) {
|
||||||
|
this.scheduleNext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
markAllSeen(): void {
|
||||||
|
const newestId = this.recentOrdersSignal()[0]?.id ?? null;
|
||||||
|
this.lastAcknowledgedOrderIdSignal.set(newestId);
|
||||||
|
if (newestId) {
|
||||||
|
this.storage.setItem(LAST_ACKNOWLEDGED_KEY, newestId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private scheduleNext(): void {
|
||||||
|
if (this.timerId !== null) {
|
||||||
|
clearInterval(this.timerId);
|
||||||
|
}
|
||||||
|
this.timerId = setInterval(() => this.poll(), this.intervalMsSignal());
|
||||||
|
}
|
||||||
|
|
||||||
|
private poll(): void {
|
||||||
|
this.gateway.loadOrders({ search: '', status: 'all', page: 1, pageSize: RECENT_ORDERS_LIMIT }).subscribe({
|
||||||
|
next: result => this.handleOrders(result.items),
|
||||||
|
error: err => console.error('Error polling for new orders:', err),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleOrders(items: AdminOrder[]): void {
|
||||||
|
this.recentOrdersSignal.set(items);
|
||||||
|
|
||||||
|
if (items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFirstPoll = this.lastNotifiedOrderId === null;
|
||||||
|
const notifyIndex = isFirstPoll ? -1 : items.findIndex(order => order.id === this.lastNotifiedOrderId);
|
||||||
|
const newOrders = isFirstPoll ? [] : (notifyIndex === -1 ? items : items.slice(0, notifyIndex));
|
||||||
|
|
||||||
|
this.lastNotifiedOrderId = items[0].id;
|
||||||
|
this.storage.setItem(LAST_NOTIFIED_KEY, this.lastNotifiedOrderId);
|
||||||
|
|
||||||
|
if (isFirstPoll) {
|
||||||
|
// Nothing existed to compare against yet - treat current orders as already
|
||||||
|
// acknowledged so a fresh admin session doesn't see the whole history as unread.
|
||||||
|
if (this.lastAcknowledgedOrderIdSignal() === null) {
|
||||||
|
this.lastAcknowledgedOrderIdSignal.set(items[0].id);
|
||||||
|
this.storage.setItem(LAST_ACKNOWLEDGED_KEY, items[0].id);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = newOrders.length - 1; i >= 0; i--) {
|
||||||
|
const order = newOrders[i];
|
||||||
|
this.notifications.show(
|
||||||
|
this.i18n.t('adminShell.topbar.notificationNewOrder', { orderNumber: order.orderNumber }),
|
||||||
|
'info',
|
||||||
|
TOAST_DURATION_MS,
|
||||||
|
[this.languageService.currentLanguage(), 'backoffice', 'orders', order.id]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readStoredIntervalMs(): number {
|
||||||
|
const stored = Number(this.storage.getItem(POLL_INTERVAL_KEY));
|
||||||
|
return Number.isFinite(stored) && stored >= MIN_POLL_INTERVAL_MS ? stored : DEFAULT_POLL_INTERVAL_MS;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/admin-order-watcher.service.spec.ts'`
|
||||||
|
Expected: PASS (4 specs)
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/features/admin/shell/services/admin-order-watcher.service.ts src/app/features/admin/shell/services/admin-order-watcher.service.spec.ts src/app/i18n/translations.ts src/app/i18n/en.ts src/app/i18n/ru.ts src/app/i18n/hy.ts
|
||||||
|
git commit -m "feat: AdminOrderWatcherService polls for new orders and toasts"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Wire the watcher into the admin topbar bell
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/features/admin/shell/admin-layout.component.ts`
|
||||||
|
- Modify: `src/app/features/admin/shell/admin-layout.component.html`
|
||||||
|
- Modify: `src/app/features/admin/shell/admin-layout.component.scss`
|
||||||
|
- Test: `src/app/features/admin/shell/admin-layout.component.spec.ts` (new)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `AdminOrderWatcherService.{recentOrders, unreadCount, start, markAllSeen}` (Task 2)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `src/app/features/admin/shell/admin-layout.component.spec.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { signal } from '@angular/core';
|
||||||
|
import { AdminLayoutComponent } from './admin-layout.component';
|
||||||
|
import { AdminOrderWatcherService } from './services/admin-order-watcher.service';
|
||||||
|
import { AdminOrder } from '../orders/models/admin-order.model';
|
||||||
|
|
||||||
|
function makeOrder(id: string, orderNumber: string): AdminOrder {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
orderNumber,
|
||||||
|
status: 'pending',
|
||||||
|
customer: { name: 'Test Customer', email: 't@example.com', phone: '' },
|
||||||
|
payment: { method: 'card', status: 'paid', amount: 500, currency: 'RUB' },
|
||||||
|
shipping: { address: '', method: '', trackingNumber: '' },
|
||||||
|
items: [],
|
||||||
|
total: 500,
|
||||||
|
currency: 'RUB',
|
||||||
|
notes: '',
|
||||||
|
internalNotes: '',
|
||||||
|
timeline: [],
|
||||||
|
archived: false,
|
||||||
|
createdAt: '2026-08-15T10:00:00.000Z',
|
||||||
|
updatedAt: '2026-08-15T10:00:00.000Z',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AdminLayoutComponent notifications bell', () => {
|
||||||
|
let watcherStub: {
|
||||||
|
recentOrders: ReturnType<typeof signal<AdminOrder[]>>;
|
||||||
|
unreadCount: ReturnType<typeof signal<number>>;
|
||||||
|
start: jasmine.Spy;
|
||||||
|
markAllSeen: jasmine.Spy;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
watcherStub = {
|
||||||
|
recentOrders: signal<AdminOrder[]>([makeOrder('o1', '1001')]),
|
||||||
|
unreadCount: signal(1),
|
||||||
|
start: jasmine.createSpy('start'),
|
||||||
|
markAllSeen: jasmine.createSpy('markAllSeen'),
|
||||||
|
};
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [AdminLayoutComponent],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: AdminOrderWatcherService, useValue: watcherStub },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('starts the watcher once on construction', () => {
|
||||||
|
TestBed.createComponent(AdminLayoutComponent);
|
||||||
|
expect(watcherStub.start).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('exposes unreadCount and recentOrders from the watcher', () => {
|
||||||
|
const fixture = TestBed.createComponent(AdminLayoutComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
expect(component.unreadCount()).toBe(1);
|
||||||
|
expect(component.recentOrders().map(o => o.id)).toEqual(['o1']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('marks orders seen when the notifications panel opens', () => {
|
||||||
|
const fixture = TestBed.createComponent(AdminLayoutComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
component.toggleNotifications();
|
||||||
|
expect(component.notificationsOpen()).toBe(true);
|
||||||
|
expect(watcherStub.markAllSeen).toHaveBeenCalledTimes(1);
|
||||||
|
|
||||||
|
component.toggleNotifications();
|
||||||
|
expect(component.notificationsOpen()).toBe(false);
|
||||||
|
expect(watcherStub.markAllSeen).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/admin-layout.component.spec.ts'`
|
||||||
|
Expected: FAIL — `AdminOrderWatcherService` not referenced by the component yet, `unreadCount`/`recentOrders` don't exist on `AdminLayoutComponent`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement**
|
||||||
|
|
||||||
|
In `src/app/features/admin/shell/admin-layout.component.ts`, add the import and field (place near the other service injections):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AdminOrderWatcherService } from './services/admin-order-watcher.service';
|
||||||
|
```
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
private readonly orderWatcher = inject(AdminOrderWatcherService);
|
||||||
|
|
||||||
|
readonly unreadCount = this.orderWatcher.unreadCount;
|
||||||
|
readonly recentOrders = this.orderWatcher.recentOrders;
|
||||||
|
```
|
||||||
|
|
||||||
|
In the constructor, after `this.readRouteData();`, add:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
this.orderWatcher.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the `toggleNotifications` method:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
toggleNotifications(): void {
|
||||||
|
this.notificationsOpen.update(open => !open);
|
||||||
|
if (this.notificationsOpen()) {
|
||||||
|
this.orderWatcher.markAllSeen();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add a navigation helper next to `adminLinkFor`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
goToOrder(orderId: string): void {
|
||||||
|
void this.router.navigate([this.currentLang(), 'backoffice', 'orders', orderId]);
|
||||||
|
this.notificationsOpen.set(false);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/features/admin/shell/admin-layout.component.html`, replace the notifications block (lines 141-157):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="admin-layout__notifications">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="admin-layout__icon-button"
|
||||||
|
aria-haspopup="true"
|
||||||
|
[attr.aria-expanded]="notificationsOpen()"
|
||||||
|
[attr.aria-label]="'adminShell.topbar.notifications' | translate"
|
||||||
|
(click)="toggleNotifications()"
|
||||||
|
>
|
||||||
|
<app-icon name="bell" [size]="18" />
|
||||||
|
@if (unreadCount() > 0) {
|
||||||
|
<span class="admin-layout__notifications-badge">{{ unreadCount() }}</span>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
|
@if (notificationsOpen()) {
|
||||||
|
<div class="admin-layout__notifications-panel" role="menu">
|
||||||
|
@if (recentOrders().length === 0) {
|
||||||
|
<p>{{ 'adminShell.topbar.notificationsEmpty' | translate }}</p>
|
||||||
|
} @else {
|
||||||
|
@for (order of recentOrders(); track order.id) {
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="admin-layout__notification-item"
|
||||||
|
role="menuitem"
|
||||||
|
(click)="goToOrder(order.id)"
|
||||||
|
>
|
||||||
|
<span class="admin-layout__notification-order">#{{ order.orderNumber }}</span>
|
||||||
|
<span class="admin-layout__notification-customer">{{ order.customer.name }}</span>
|
||||||
|
<span class="admin-layout__notification-amount">{{ order.total }} {{ order.currency }}</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/features/admin/shell/admin-layout.component.scss`, add (near other `.admin-layout__notifications*` rules if any exist, otherwise at the end):
|
||||||
|
|
||||||
|
```scss
|
||||||
|
.admin-layout__notifications {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-layout__notifications-badge {
|
||||||
|
position: absolute;
|
||||||
|
top: 2px;
|
||||||
|
right: 2px;
|
||||||
|
min-width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
padding: 0 4px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--color-danger, #ef4444);
|
||||||
|
color: #fff;
|
||||||
|
font-size: 10px;
|
||||||
|
line-height: 16px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-layout__notification-item {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 2px;
|
||||||
|
width: 100%;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-sm, 4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-layout__notification-item:hover {
|
||||||
|
background: var(--bg-secondary, #f4f6f5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-layout__notification-order { font-weight: var(--font-weight-medium, 500); }
|
||||||
|
.admin-layout__notification-customer { color: var(--text-secondary, #6b7280); font-size: var(--font-size-sm, 0.8125rem); }
|
||||||
|
.admin-layout__notification-amount { font-size: var(--font-size-sm, 0.8125rem); }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/admin-layout.component.spec.ts'`
|
||||||
|
Expected: PASS (3 specs)
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run full build to catch template errors**
|
||||||
|
|
||||||
|
Run: `npx ng build --configuration development`
|
||||||
|
Expected: build succeeds, no template compile errors.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/features/admin/shell/admin-layout.component.ts src/app/features/admin/shell/admin-layout.component.html src/app/features/admin/shell/admin-layout.component.scss src/app/features/admin/shell/admin-layout.component.spec.ts
|
||||||
|
git commit -m "feat: wire order watcher into admin topbar bell (badge + panel)"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Editable poll interval in admin settings
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/app/features/admin/settings/pages/admin-settings-page.component.ts`
|
||||||
|
- Modify: `src/app/features/admin/settings/pages/admin-settings-page.component.html`
|
||||||
|
- Modify: `src/app/i18n/translations.ts`, `src/app/i18n/en.ts`, `src/app/i18n/ru.ts`, `src/app/i18n/hy.ts`
|
||||||
|
- Test: `src/app/features/admin/settings/pages/admin-settings-page.component.spec.ts` (new)
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: `AdminOrderWatcherService.{intervalMs, setIntervalSeconds}` (Task 2)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add i18n keys**
|
||||||
|
|
||||||
|
In `src/app/i18n/translations.ts`, inside `adminSettings:` (after `currencyRatesSaved: string;`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
notificationInterval: string;
|
||||||
|
notificationIntervalExplain: string;
|
||||||
|
notificationIntervalSave: string;
|
||||||
|
notificationIntervalSaved: string;
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/i18n/en.ts`, inside `adminSettings` (after `currencyRatesSaved: 'Rates saved',`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
notificationInterval: 'New-order check interval (seconds)',
|
||||||
|
notificationIntervalExplain: 'How often the admin panel polls for new orders to show a notification.',
|
||||||
|
notificationIntervalSave: 'Save interval',
|
||||||
|
notificationIntervalSaved: 'Interval saved',
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/i18n/ru.ts`, inside `adminSettings` (after `currencyRatesSaved: 'Курсы сохранены',`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
notificationInterval: 'Интервал проверки новых заказов (сек)',
|
||||||
|
notificationIntervalExplain: 'Как часто админ-панель проверяет новые заказы для уведомления.',
|
||||||
|
notificationIntervalSave: 'Сохранить интервал',
|
||||||
|
notificationIntervalSaved: 'Интервал сохранён',
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/i18n/hy.ts`, inside `adminSettings` (after `currencyRatesSaved: 'Փոխարժեքները պահպանվեցին',`):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
notificationInterval: 'Նոր պատվերների ստուգման ինտերվալ (վրկ)',
|
||||||
|
notificationIntervalExplain: 'Որքան հաճախ է ադմին վահանակը ստուգում նոր պատվերներ ծանուցման համար։',
|
||||||
|
notificationIntervalSave: 'Պահպանել ինտերվալը',
|
||||||
|
notificationIntervalSaved: 'Ինտերվալը պահպանվեց',
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the failing test**
|
||||||
|
|
||||||
|
Create `src/app/features/admin/settings/pages/admin-settings-page.component.spec.ts`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { provideRouter } from '@angular/router';
|
||||||
|
import { signal } from '@angular/core';
|
||||||
|
import { AdminSettingsPageComponent } from './admin-settings-page.component';
|
||||||
|
import { AdminOrderWatcherService } from '../../shell/services/admin-order-watcher.service';
|
||||||
|
|
||||||
|
describe('AdminSettingsPageComponent notification interval', () => {
|
||||||
|
let watcherStub: {
|
||||||
|
intervalMs: ReturnType<typeof signal<number>>;
|
||||||
|
setIntervalSeconds: jasmine.Spy;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
watcherStub = {
|
||||||
|
intervalMs: signal(15000),
|
||||||
|
setIntervalSeconds: jasmine.createSpy('setIntervalSeconds'),
|
||||||
|
};
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [AdminSettingsPageComponent],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: AdminOrderWatcherService, useValue: watcherStub },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('initializes the draft from the current interval in seconds', () => {
|
||||||
|
const fixture = TestBed.createComponent(AdminSettingsPageComponent);
|
||||||
|
expect(fixture.componentInstance.notificationIntervalSecondsDraft()).toBe(15);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('saveNotificationInterval calls setIntervalSeconds with the draft value', () => {
|
||||||
|
const fixture = TestBed.createComponent(AdminSettingsPageComponent);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
|
||||||
|
component.notificationIntervalSecondsDraft.set(30);
|
||||||
|
component.saveNotificationInterval();
|
||||||
|
|
||||||
|
expect(watcherStub.setIntervalSeconds).toHaveBeenCalledWith(30);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Run test to verify it fails**
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/admin-settings-page.component.spec.ts'`
|
||||||
|
Expected: FAIL — `notificationIntervalSecondsDraft`/`saveNotificationInterval` don't exist yet.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement**
|
||||||
|
|
||||||
|
In `src/app/features/admin/settings/pages/admin-settings-page.component.ts`, add the import:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { AdminOrderWatcherService } from '../../shell/services/admin-order-watcher.service';
|
||||||
|
```
|
||||||
|
|
||||||
|
Add the field and methods to the class (alongside the currency-rates fields):
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
readonly orderWatcher = inject(AdminOrderWatcherService);
|
||||||
|
readonly notificationIntervalSecondsDraft = signal(Math.round(this.orderWatcher.intervalMs() / 1000));
|
||||||
|
readonly showNotificationIntervalSaved = signal(false);
|
||||||
|
|
||||||
|
saveNotificationInterval(): void {
|
||||||
|
this.orderWatcher.setIntervalSeconds(this.notificationIntervalSecondsDraft());
|
||||||
|
this.showNotificationIntervalSaved.set(true);
|
||||||
|
setTimeout(() => this.showNotificationIntervalSaved.set(false), SAVED_MESSAGE_DURATION_MS);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In `src/app/features/admin/settings/pages/admin-settings-page.component.html`, add a new `.settings-card` block after the currency-rates one (before the closing `</section>`):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<div class="settings-card">
|
||||||
|
<h2>{{ 'adminSettings.notificationInterval' | translate }}</h2>
|
||||||
|
<p class="settings-explain">{{ 'adminSettings.notificationIntervalExplain' | translate }}</p>
|
||||||
|
<div class="rate-row">
|
||||||
|
<input
|
||||||
|
class="rate-input"
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
step="1"
|
||||||
|
[ngModel]="notificationIntervalSecondsDraft()"
|
||||||
|
(ngModelChange)="notificationIntervalSecondsDraft.set($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="rate-actions">
|
||||||
|
<button type="button" class="save-button" (click)="saveNotificationInterval()">{{ 'adminSettings.notificationIntervalSave' | translate }}</button>
|
||||||
|
<span class="saved-message" *ngIf="showNotificationIntervalSaved()">{{ 'adminSettings.notificationIntervalSaved' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run test to verify it passes**
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/admin-settings-page.component.spec.ts'`
|
||||||
|
Expected: PASS (2 specs)
|
||||||
|
|
||||||
|
- [ ] **Step 6: Full verification**
|
||||||
|
|
||||||
|
Run: `npx tsc --noEmit -p tsconfig.json`
|
||||||
|
Expected: no errors.
|
||||||
|
|
||||||
|
Run: `npx ng build --configuration development`
|
||||||
|
Expected: build succeeds.
|
||||||
|
|
||||||
|
Run: `npm run test -- --include='**/admin-order-watcher.service.spec.ts' --include='**/admin-layout.component.spec.ts' --include='**/admin-settings-page.component.spec.ts' --include='**/user-notification.service.spec.ts'`
|
||||||
|
Expected: all specs PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/app/features/admin/settings/pages/admin-settings-page.component.ts src/app/features/admin/settings/pages/admin-settings-page.component.html src/app/features/admin/settings/pages/admin-settings-page.component.spec.ts src/app/i18n/translations.ts src/app/i18n/en.ts src/app/i18n/ru.ts src/app/i18n/hy.ts
|
||||||
|
git commit -m "feat: editable new-order poll interval in admin settings"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Manual Verification (after all tasks)
|
||||||
|
|
||||||
|
1. `npm run barry -- kb search --source cq --query "admin order notifications"` if KB sharing is enabled (skip if local-only — see project `CLAUDE.md`).
|
||||||
|
2. Start the dev server, log into `/backoffice`, leave the tab open.
|
||||||
|
3. In another tab (or via `admin-orders-local.gateway.ts`'s seed data timing), wait for the poll interval — confirm no toast fires on first load.
|
||||||
|
4. Trigger a new order (checkout flow in `cart.component.ts`, or temporarily lower `SEED_COUNT`/seed timing in `admin-orders-local.gateway.ts` to simulate one — revert after testing) and confirm: toast appears with the order number, bell badge shows `1`, clicking either navigates to `/backoffice/orders/:id`.
|
||||||
|
5. Open the bell panel without clicking a row — confirm badge clears but the order still lists in the panel.
|
||||||
|
6. Change the interval in Admin Settings, save, confirm the toast "Interval saved" message. no crash on next poll cycle.
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# Admin product view count column — design
|
||||||
|
|
||||||
|
**Status:** Approved
|
||||||
|
**Date:** 2026-08-15
|
||||||
|
**Related backlog item:** #1 (site traffic counter)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
User reported "site traffic isn't visible, counter shows low." Investigation found two separate things already exist and are working as intended, neither of which is the actual gap:
|
||||||
|
|
||||||
|
- Admin Analytics → Traffic tab already shows an honest `"Unknown - available after backend"` badge (`admin-analytics-page.component.html:231`) — no fake data, correctly reflects that no traffic-tracking pipeline exists at all (`BACKEND-API-REFERENCE.md` §10 step 10).
|
||||||
|
- The storefront `Item.visits` field is wired end-to-end from the live backend (`api.service.ts:438`) but is never rendered anywhere in the UI, and the backend mock always seeds it `0`.
|
||||||
|
|
||||||
|
User confirmed (via clarifying question) the actual complaint is: **no per-product view count visible in Admin Products.**
|
||||||
|
|
||||||
|
Further investigation found Admin Products runs on a fully separate mock domain (`AdminProduct` model, `admin-products-local.gateway.ts`, seeded from `list.json`) that has no relationship to the storefront's live `Item.visits` pipeline at all. So a "Views" column here cannot show real per-product traffic today — there is no data source for it in the admin domain. This mirrors the currency/FX and order-notification gaps already documented this session: build the honest client-side piece, document the backend gap explicitly, never fabricate numbers.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
**Model:** add `visits: number` to `AdminProduct` (`src/app/features/admin/products/models/admin-product.model.ts`), alongside the other stat-like fields (`priority`, `quantity`).
|
||||||
|
|
||||||
|
**Mock gateway:** `admin-products-local.gateway.ts` defaults `visits: 0` when building the in-memory seed from `list.json` — no fabricated numbers, matches the field's actual state (nothing increments it yet).
|
||||||
|
|
||||||
|
**List column:** `ALL_PRODUCT_COLUMNS` (`admin-products.facade.ts:39`) gains `'visits'`. Rendered in `admin-products-list.component.html` table view only (grid view is out of scope per user's placement choice), following the exact existing `isColumnVisible('stock')`/`isColumnVisible('price')` pattern — toggleable via the same column-picker UI, persisted the same way (`LocalStorageService`, `COLUMNS_KEY`).
|
||||||
|
|
||||||
|
**i18n:** one new key, `adminProducts.views` (label for the column header), added to `en.ts`/`ru.ts`/`hy.ts`/`translations.ts`.
|
||||||
|
|
||||||
|
## Backend doc update
|
||||||
|
|
||||||
|
New `BACKEND-API-REFERENCE.md` §12.x ask (numbered after the existing 12.8, following the established "Gap / Ask" format): the admin Products domain has no view-count source. Two options to raise:
|
||||||
|
1. Once admin Products gets a real backend (§10 step 4), include a view/visit count per product in the response.
|
||||||
|
2. Alternatively, bridge to the storefront's already-live `Item.visits` (§6, `/items/{id}`) by product id — smaller change if a unified product identity exists between the storefront and admin domains.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Storefront customer-facing "N people viewed this" display — not requested, deferred (was offered as a placement option, not chosen).
|
||||||
|
- Product edit/detail page display — not requested (list column only, per user's placement choice).
|
||||||
|
- Any client-side view tracking/incrementing — explicitly rejected in favor of the honest display-only approach; a client-only counter would only reflect the admin's own browser, not real shoppers, same trap already avoided for currency rates.
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Admin purchase notifications — design
|
||||||
|
|
||||||
|
**Status:** Approved
|
||||||
|
**Date:** 2026-08-15
|
||||||
|
**Related backlog item:** #7 (marked ВАЖНО — important)
|
||||||
|
|
||||||
|
## Problem
|
||||||
|
|
||||||
|
Admin has no signal when a purchase happens on the marketplace. Orders only surface if
|
||||||
|
someone manually opens the Orders list and refreshes. Backend exposes no WebSocket/SSE
|
||||||
|
(confirmed in `BACKEND-API-REFERENCE.md:20` — every "live" feature today, e.g. payment
|
||||||
|
status, is plain polling), so this has to be poll-based like the rest of the app.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
**`AdminOrderWatcherService`** (new, `providedIn: root`, admin-scoped)
|
||||||
|
|
||||||
|
- Polls `AdminOrdersLocalGateway.loadOrders()` (sorted `createdAt` desc, already the
|
||||||
|
default sort) on an interval.
|
||||||
|
- Diffs the newest order's `id`/`createdAt` against the last-seen value, kept in memory
|
||||||
|
and persisted via `LocalStorageService` (survives page reload, same pattern as
|
||||||
|
`AdminPreferencesService`).
|
||||||
|
- On finding order(s) newer than last-seen: fires one toast per new order and
|
||||||
|
increments an `unreadCount` signal.
|
||||||
|
- Started once at the admin shell root, so it keeps polling regardless of which admin
|
||||||
|
page is open.
|
||||||
|
|
||||||
|
**Poll interval**
|
||||||
|
|
||||||
|
- Editable by admin, default 15s.
|
||||||
|
- Setting lives in the same admin-settings page as currency rates
|
||||||
|
(`admin-settings-page.component.ts`), persisted via `LocalStorageService`.
|
||||||
|
|
||||||
|
**Toast delivery**
|
||||||
|
|
||||||
|
- Reuses the existing `UserNotificationService` / `FloatingNotificationsComponent`
|
||||||
|
(already global — `providedIn: root`, mounted once in `app.html`). No new toast UI.
|
||||||
|
- `UserNotification` gains an optional `route: string[]` field.
|
||||||
|
- `FloatingNotificationsComponent` gets a click handler: navigate to `route` (if set)
|
||||||
|
then dismiss.
|
||||||
|
|
||||||
|
**Badge — reuses existing topbar bell**
|
||||||
|
|
||||||
|
`admin-layout.component.html:141-157` already has an unused bell icon +
|
||||||
|
dropdown panel (currently hardcoded to always show "no notifications").
|
||||||
|
Wire the watcher's data into it instead of adding a new indicator:
|
||||||
|
|
||||||
|
- `unreadCount` signal (from `AdminOrderWatcherService`) rendered as a badge
|
||||||
|
on the bell icon (`admin-layout__icon-button`).
|
||||||
|
- Opening the panel (`notificationsOpen()`, already wired to the bell click)
|
||||||
|
lists the unread new orders instead of the static "notificationsEmpty"
|
||||||
|
text.
|
||||||
|
- Opening the panel marks all currently-known orders as seen → badge resets
|
||||||
|
to 0 (same trigger `AdminLayoutComponent.toggleNotifications()` already
|
||||||
|
has).
|
||||||
|
|
||||||
|
**Click behavior**
|
||||||
|
|
||||||
|
- Toast click → `/admin/orders/:id` (the new order's detail page).
|
||||||
|
- Clicking an order row inside the bell panel → same, then closes the panel.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```
|
||||||
|
AdminOrderWatcherService (interval timer)
|
||||||
|
-> AdminOrdersLocalGateway.loadOrders()
|
||||||
|
-> diff against last-seen order id/createdAt (LocalStorageService)
|
||||||
|
-> new order(s) found?
|
||||||
|
-> UserNotificationService.show(message, 'info', { route: ['/admin/orders', id] })
|
||||||
|
-> unreadCount.update(n => n + 1)
|
||||||
|
-> admin clicks toast/badge -> router navigate -> orders-list visit resets unreadCount
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
Poll failures are silent/logged only (`console.error`), consistent with existing
|
||||||
|
polling code (payment status polling in `cart.component.ts`). No toast spam on
|
||||||
|
transient network errors — watcher just retries on the next interval.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Native OS push notifications (tab not focused) — user explicitly chose in-app
|
||||||
|
toast/badge only, not browser Notification API.
|
||||||
|
- Sound alerts — not selected.
|
||||||
|
- Telegram/email alerts to staff — not selected, would need backend bot/mail
|
||||||
|
integration.
|
||||||
24
files/changes.txt
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
bro please read carefully.
|
||||||
|
this must be for all projects.
|
||||||
|
At first here is the API for only auth process:
|
||||||
|
https://users.vitanova.network:456/ping
|
||||||
|
|
||||||
|
and here are other stuff regarding it:
|
||||||
|
//Logout user by sessionID
|
||||||
|
r.DELETE("/users/sessions/:webSessionID", Logout)
|
||||||
|
|
||||||
|
//creates new session for user and send code for activation
|
||||||
|
r.POST("/users/sessions", newWebSession)
|
||||||
|
|
||||||
|
r.GET("/users/sessions/:webSessionID", getWebSession)
|
||||||
|
|
||||||
|
As you got all the info, keep all the structure of api above.
|
||||||
|
Now when the user clicks on login, we must show the QR and the link of the telegram bot, which we already have (btw sho me, so i see wheter it is true or not)
|
||||||
|
and add a query param "?start=GUID" and generate a guid there.
|
||||||
|
after we post it ad a websession, we have to get it like this " r.GET("/users/sessions/:webSessionID", getWebSession)" every 5 secs untill we get a status true
|
||||||
|
if we will be loged in, we have to keep that webSessionID in the cookies for an hour
|
||||||
|
1. if we open our website, we have to check the cookies and do a request for that websession
|
||||||
|
2. if we are not loged in, then we will loge in one more time
|
||||||
|
|
||||||
|
|
||||||
|
any questions?
|
||||||
32
karma.conf.js
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// Karma configuration for `ng test` (@angular/build:karma builder).
|
||||||
|
// A headless, sandbox-free Chrome launcher so the suite runs in CI and in
|
||||||
|
// restricted/dev environments where Chrome isn't on PATH. CHROME_BIN falls
|
||||||
|
// back to the default Windows install path when the env var isn't set.
|
||||||
|
process.env.CHROME_BIN =
|
||||||
|
process.env.CHROME_BIN || 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe';
|
||||||
|
|
||||||
|
module.exports = function (config) {
|
||||||
|
config.set({
|
||||||
|
frameworks: ['jasmine'],
|
||||||
|
plugins: [
|
||||||
|
require('karma-jasmine'),
|
||||||
|
require('karma-chrome-launcher'),
|
||||||
|
require('karma-jasmine-html-reporter'),
|
||||||
|
require('karma-coverage'),
|
||||||
|
],
|
||||||
|
browsers: ['ChromeHeadlessNoSandbox'],
|
||||||
|
customLaunchers: {
|
||||||
|
ChromeHeadlessNoSandbox: {
|
||||||
|
base: 'ChromeHeadless',
|
||||||
|
flags: ['--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
reporters: ['progress', 'coverage'],
|
||||||
|
coverageReporter: {
|
||||||
|
dir: require('path').join(__dirname, 'coverage'),
|
||||||
|
subdir: '.',
|
||||||
|
reporters: [{ type: 'text-summary' }, { type: 'html' }, { type: 'lcovonly' }],
|
||||||
|
},
|
||||||
|
restartOnFileChange: true,
|
||||||
|
});
|
||||||
|
};
|
||||||
143
nginx.conf
@@ -7,7 +7,7 @@ server {
|
|||||||
|
|
||||||
# Angular routing - serve index.html for all routes
|
# Angular routing - serve index.html for all routes
|
||||||
location / {
|
location / {
|
||||||
try_files $uri $uri/ /index.html =404;
|
try_files $uri $uri/ /index.html;
|
||||||
}
|
}
|
||||||
|
|
||||||
# Static assets caching
|
# Static assets caching
|
||||||
@@ -36,9 +36,150 @@ server {
|
|||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://telegram.org; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https:; frame-src https://telegram.org;" always;
|
||||||
|
|
||||||
# Brotli compression (if available)
|
# Brotli compression (if available)
|
||||||
# brotli on;
|
# brotli on;
|
||||||
# brotli_comp_level 6;
|
# brotli_comp_level 6;
|
||||||
# brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
# brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name lovero.store www.lovero.store;
|
||||||
|
|
||||||
|
root /var/www/loveromarket/browser;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Angular routing
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API calls to backend
|
||||||
|
location /api {
|
||||||
|
proxy_pass https://api.lovero.store:555;
|
||||||
|
proxy_set_header Host api.lovero.store;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
rewrite ^/api(/.*)$ $1 break;
|
||||||
|
proxy_ssl_verify off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static assets caching
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Don't cache index.html
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Expires "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://telegram.org; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https:; frame-src https://telegram.org;" always;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Template for onboarding a new marketplace tenant.
|
||||||
|
# Replace NEWMARKETPLACE.EXAMPLE.COM, /var/www/newmarketplace, and the
|
||||||
|
# api.newmarketplace.example.com:443 proxy target with the real values,
|
||||||
|
# then rename this block's server_name/root before deploying.
|
||||||
|
#
|
||||||
|
# --- SPA routing (read before you skip this) ---
|
||||||
|
# This is an Angular app with client-side routing (all page navigation - the
|
||||||
|
# admin dashboard, project editor, catalog, product pages, etc. - happens in
|
||||||
|
# the browser, not via new server requests). Every URL the app owns
|
||||||
|
# (/:lang/backoffice/dashboard, /:lang/edit/general, /:lang/catalog/5, ...)
|
||||||
|
# must fall through to index.html on a fresh request (page refresh, typed
|
||||||
|
# URL, browser back/forward after a full reload) so Angular's router can take
|
||||||
|
# over client-side. `try_files $uri $uri/ /index.html;` below is what makes
|
||||||
|
# that work: nginx tries the literal file, then the directory, then falls
|
||||||
|
# back to index.html for anything that isn't a real static asset. If you ever
|
||||||
|
# see a raw nginx 404 page on refresh/back-navigation (not a blank app, an
|
||||||
|
# actual nginx error page), this fallback is missing or misconfigured for
|
||||||
|
# that server block - it is NOT an Angular or JS problem.
|
||||||
|
#
|
||||||
|
# --- Two ways the frontend talks to its API - pick one per tenant ---
|
||||||
|
# 1) Proxied (what this template and the lovero.store block above do):
|
||||||
|
# the frontend calls a relative `/api/...` path, and nginx proxies it to
|
||||||
|
# the real backend below. Browser never sees the backend host/port.
|
||||||
|
# 2) Direct (what the dexarmarket.ru production build does): the frontend's
|
||||||
|
# `environment.production.ts` sets `apiUrl`/`authApiUrl` to an absolute
|
||||||
|
# URL (e.g. `https://api.dexarmarket.ru:445`) and calls that directly -
|
||||||
|
# this nginx config is never involved in API calls at all for that tenant.
|
||||||
|
# If a tenant using pattern (2) reports 502/504 Bad Gateway on refresh or
|
||||||
|
# back-navigation, it is NOT this file - the app re-fires session-check and
|
||||||
|
# bootstrap-load calls on every route change/refresh, and a 502/504 means the
|
||||||
|
# *backend's own* reverse proxy/app server (the one fronting that absolute
|
||||||
|
# apiUrl/authApiUrl host) is down, overloaded, or timing out. Check that
|
||||||
|
# backend's own nginx/app logs, not this one.
|
||||||
|
server {
|
||||||
|
listen 80;
|
||||||
|
server_name newmarketplace.example.com www.newmarketplace.example.com;
|
||||||
|
|
||||||
|
root /var/www/newmarketplace/browser;
|
||||||
|
index index.html;
|
||||||
|
|
||||||
|
# Angular routing - serve index.html for all routes (client-side router
|
||||||
|
# handles /edit, /:lang/edit/:section, etc. once index.html is served)
|
||||||
|
location / {
|
||||||
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Proxy API calls to backend - only needed if this tenant uses the
|
||||||
|
# relative `/api` pattern (see comment above); delete this block if the
|
||||||
|
# tenant's environment.*.ts uses an absolute apiUrl instead.
|
||||||
|
location /api {
|
||||||
|
proxy_pass https://api.newmarketplace.example.com:443;
|
||||||
|
proxy_set_header Host api.newmarketplace.example.com;
|
||||||
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
|
rewrite ^/api(/.*)$ $1 break;
|
||||||
|
proxy_ssl_verify off;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Static assets caching
|
||||||
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||||
|
expires 1y;
|
||||||
|
add_header Cache-Control "public, immutable";
|
||||||
|
try_files $uri =404;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Don't cache index.html
|
||||||
|
location = /index.html {
|
||||||
|
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||||
|
add_header Pragma "no-cache";
|
||||||
|
add_header Expires "0";
|
||||||
|
}
|
||||||
|
|
||||||
|
gzip on;
|
||||||
|
gzip_vary on;
|
||||||
|
gzip_proxied any;
|
||||||
|
gzip_comp_level 6;
|
||||||
|
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript image/svg+xml;
|
||||||
|
gzip_min_length 1000;
|
||||||
|
|
||||||
|
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||||
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
|
add_header X-XSS-Protection "1; mode=block" always;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
|
add_header Permissions-Policy "camera=(), microphone=(), geolocation=()" always;
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval' https://telegram.org; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; font-src 'self' https://fonts.gstatic.com; img-src 'self' data: https:; connect-src 'self' https:; frame-src https://telegram.org;" always;
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
"resources": {
|
"resources": {
|
||||||
"files": [
|
"files": [
|
||||||
"/favicon.ico",
|
"/favicon.ico",
|
||||||
"/index.csr.html",
|
|
||||||
"/index.html",
|
"/index.html",
|
||||||
"/manifest.webmanifest",
|
"/manifest.webmanifest",
|
||||||
"/*.css",
|
"/*.css",
|
||||||
@@ -31,7 +30,9 @@
|
|||||||
{
|
{
|
||||||
"name": "api-cache",
|
"name": "api-cache",
|
||||||
"urls": [
|
"urls": [
|
||||||
"/api/**"
|
"/api/**",
|
||||||
|
"https://api.dexarmarket.ru:445/**",
|
||||||
|
"https://api.novo.market:444/**"
|
||||||
],
|
],
|
||||||
"cacheConfig": {
|
"cacheConfig": {
|
||||||
"maxSize": 100,
|
"maxSize": 100,
|
||||||
@@ -48,7 +49,7 @@
|
|||||||
"https://**/*.webp"
|
"https://**/*.webp"
|
||||||
],
|
],
|
||||||
"cacheConfig": {
|
"cacheConfig": {
|
||||||
"maxSize": 50,
|
"maxSize": 200,
|
||||||
"maxAge": "7d",
|
"maxAge": "7d",
|
||||||
"strategy": "performance"
|
"strategy": "performance"
|
||||||
}
|
}
|
||||||
|
|||||||
3719
package-lock.json
generated
46
package.json
@@ -5,41 +5,49 @@
|
|||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve",
|
"start": "ng serve",
|
||||||
"dexar": "ng serve --configuration=development --port 4200",
|
"dexar": "ng serve --configuration=development --port 4200",
|
||||||
"novo": "ng serve --configuration=novo --port 4201",
|
|
||||||
"start:dexar": "ng serve --configuration=development --port 4200",
|
"start:dexar": "ng serve --configuration=development --port 4200",
|
||||||
"start:novo": "ng serve --configuration=novo --port 4201",
|
|
||||||
"build": "ng build",
|
"build": "ng build",
|
||||||
"build:dexar": "ng build --configuration=production",
|
"build:dexar": "ng build --configuration=production",
|
||||||
"build:novo": "ng build --configuration=novo-production",
|
"test": "ng test --watch=false --browsers=ChromeHeadlessNoSandbox",
|
||||||
|
"test:coverage": "ng test --watch=false --browsers=ChromeHeadlessNoSandbox --code-coverage",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"arch:check:boundaries": "node tools/architecture/check-boundaries.mjs",
|
||||||
|
"arch:check:cycles": "npx --yes madge --circular --extensions ts src/app --ts-config tsconfig.app.json",
|
||||||
|
"arch:check": "npm run arch:check:boundaries ; npm run arch:check:cycles",
|
||||||
|
"barry": "barry-cache",
|
||||||
|
"barry:validate": "barry-cache validate",
|
||||||
|
"barry:resume": "barry-cache resume",
|
||||||
|
"barry:finalize": "barry-cache finalize",
|
||||||
|
"barry:failure": "barry-cache failure"
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/common": "^21.0.6",
|
"@angular/animations": "22.0.8",
|
||||||
"@angular/compiler": "^21.0.6",
|
"@angular/cdk": "22.0.6",
|
||||||
"@angular/core": "^21.0.6",
|
"@angular/common": "22.0.8",
|
||||||
"@angular/forms": "^21.0.6",
|
"@angular/compiler": "22.0.8",
|
||||||
"@angular/platform-browser": "^21.0.6",
|
"@angular/core": "22.0.8",
|
||||||
"@angular/router": "^21.0.6",
|
"@angular/forms": "22.0.8",
|
||||||
"@angular/service-worker": "^21.0.6",
|
"@angular/platform-browser": "22.0.8",
|
||||||
"primeicons": "^7.0.0",
|
"@angular/router": "22.0.8",
|
||||||
"primeng": "^21.0.3",
|
"@angular/service-worker": "22.0.8",
|
||||||
"rxjs": "~7.8.0",
|
"rxjs": "~7.8.0",
|
||||||
"tslib": "^2.8.0",
|
"tslib": "^2.8.0",
|
||||||
"zone.js": "~0.16.0"
|
"zone.js": "~0.16.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular/build": "^21.0.6",
|
"@angular/build": "22.0.8",
|
||||||
"@angular/cli": "^21.0.6",
|
"@angular/cli": "22.0.8",
|
||||||
"@angular/compiler-cli": "^21.0.6",
|
"@angular/compiler-cli": "22.0.8",
|
||||||
"@types/jasmine": "~5.1.0",
|
"@types/jasmine": "~5.1.0",
|
||||||
"jasmine-core": "~5.13.0",
|
"barry-cache": "^0.9.3",
|
||||||
|
"istanbul-lib-instrument": "^6.0.3",
|
||||||
|
"jasmine-core": "~5.5.0",
|
||||||
"karma": "~6.4.0",
|
"karma": "~6.4.0",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage": "~2.2.0",
|
"karma-coverage": "^2.2.1",
|
||||||
"karma-jasmine": "~5.1.0",
|
"karma-jasmine": "~5.1.0",
|
||||||
"karma-jasmine-html-reporter": "~2.1.0",
|
"karma-jasmine-html-reporter": "~2.1.0",
|
||||||
"typescript": "~5.9.3"
|
"typescript": "~6.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
{
|
{
|
||||||
"/api": {
|
"/api": {
|
||||||
"target": "https://api.dexarmarket.ru:445",
|
"target": "https://novo.market",
|
||||||
"secure": false,
|
"secure": false,
|
||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
"pathRewrite": {
|
|
||||||
"^/api": ""
|
|
||||||
},
|
|
||||||
"logLevel": "debug"
|
"logLevel": "debug"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
public/assets/images/dexar-logo-small.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108.43 100.53"><path d="M101.66 15.71c-4.16-.3-8.34-.35-12.51-.46-3.85-.1-7.69-.15-11.54-.21-9.14-.15-18.29-.32-27.44-.44-7.84-.11-15.68-.18-23.53-.21-.83 0-1.17-.3-1.33-1.01-.81-3.51-1.64-7.02-2.44-10.53-.31-1.33-1.42-2.36-2.68-2.41C18.6.37 17.01.27 15.42.23 11.37.13 7.31.06 3.25 0 1.27-.03 0 1.13 0 2.92 0 4.7 1.38 6.06 3.26 6.09c4.28.08 8.56.17 12.84.2.89 0 1.34.26 1.56 1.17 1.2 4.99 2.47 9.95 3.69 14.93 2.3 9.38 4.58 18.77 6.88 28.15 1.11 4.54 2.21 9.07 3.36 13.6.28 1.11.15 1.73-1.02 2.31-3.76 1.85-5.33 5.91-4.45 9.93.91 4.11 4.58 6.95 9.07 7.02h1.38c-2.97 1.75-4.68 4.13-4.95 7.42-.27 3.32 1.42 5.8 3.95 7.96-4.85.74-6.27.75-9.41 1.23.8.23 1.31.11 1.98.12 4.46.05 8.92.17 13.37.01 4.94-.17 8.86-5.16 7.57-10.63-.63-2.66-2.21-4.7-5.04-5.9h39.73c-2.87 1.74-4.53 4.14-4.85 7.36-.32 3.29 1.08 5.9 3.89 8.11-9.01.38-17.71.47-26.34 1.09l30.02.35c1.84-.07 3.73.03 5.49-.97 4.82-2.75 6.23-8.3 3.26-12.73-.84-1.26-2.17-2.19-3.21-3.2 1.3 0 2.83.03 4.35 0 1.66-.04 2.81-1.34 2.78-3.08-.02-1.56-1.25-2.77-2.82-2.79-6.68-.07-13.36-.18-20.04-.2-9.37-.04-18.74-.01-28.11-.02H35.44c-2.17 0-3.72-1.47-3.62-3.37.09-1.79 1.73-3.16 3.83-3.15 8.39.04 16.77.1 25.16.13 8.61.04 17.21.06 25.82.07.97 0 1.94-.09 2.9-.21 3.83-.52 6.67-3.16 7.69-6.89 1.84-6.75 3.76-13.47 5.65-20.21 1.36-4.84 2.79-9.66 4.08-14.52.59-2.2 1.13-4.45 1.32-6.7.29-3.53-2.89-6.7-6.6-6.96Zm-13.8 71.86c2.2-.07 4.11 1.95 4.1 4.15-.18 2.67-1.84 3.97-4.24 4.07-2.17.08-4.06-1.98-4.03-4.18.03-2.3 1.72-3.96 4.17-4.04m-47.43-.03c2.45-.06 4.19 1.8 4.15 4.03-.05 2.63-2.02 3.98-4.06 4.02-2.23.04-4.05-1.86-4.15-4.07-.1-2.22 2.05-4.07 4.06-3.98m30.45-67.01v12.33c-1.89 0-3.69.02-5.48 0-3.15-.05-6.3-.18-9.45-.18-.98 0-1.2-.35-1.27-1.24-.22-2.76-.55-5.5-.82-8.25-.09-.93-.15-1.86-.21-2.66zm-.14 17.64v12.64c-4.47 0-8.88.02-13.29-.04-.26 0-.71-.63-.75-1.01-.35-3.18-.62-6.37-.91-9.55v-.11c-.15-1.98-.15-1.95 1.83-1.94 4.35.02 8.69 0 13.13 0Zm-41.31-8.1c-.62-2.71-1.26-5.41-1.88-8.12-.15-.65-.27-1.32-.43-2.1 7.05.12 13.97.24 21.04.37.41 4.15.81 8.23 1.19 12.14H32.48c-.11 0-.22-.02-.32-.03-2.25-.14-2.24-.14-2.73-2.26m5.02 20.67c-1.01-4.24-2.02-8.49-3.03-12.7h18.64c.47 4.3.93 8.46 1.39 12.7H34.44Zm57.74 8.57c-.3 1.1-.54 2.23-.89 3.31-.51 1.58-1.87 2.54-3.47 2.54-16.08-.01-32.17-.04-48.25 0-1.26 0-1.71-.36-1.95-1.57-.44-2.27-1.1-4.5-1.65-6.75-.04-.17 0-.35 0-.67l56.99.39c-.29 1.03-.53 1.89-.77 2.76Zm4.75-16.54c-.7 2.51-1.41 5.02-2.17 7.51-.09.29-.56.65-.85.65q-8.385.06-16.77 0c-.29 0-.83-.42-.84-.64-.05-3.87-.04-7.75-.04-11.6h21.71c-.38 1.5-.69 2.8-1.05 4.08Zm5.38-19.31c-.83 2.95-1.7 5.89-2.49 8.85-.19.73-.47 1.01-1.23.99-6.45-.16-12.91-.28-19.36-.41-.94-.02-1.88 0-2.97 0 0-3.91.01-7.67 0-11.43 0-.76.45-.78 1-.77 2.83.08 5.65.17 8.48.22 4.93.09 9.86.15 14.79.22 1.49.02 2.18.94 1.78 2.34Z" style="fill:#477470;stroke-width:0"/></svg>
|
||||||
|
After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 727 B After Width: | Height: | Size: 6.3 KiB |
BIN
public/assets/images/footer_bg.webp
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
public/assets/images/footer_bg_mobile.webp
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/assets/images/hero_background_desktop.webp
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
public/assets/images/hero_background_mobile.webp
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
public/assets/images/lavero/lavero-logo.png
Normal file
|
After Width: | Height: | Size: 306 KiB |
8
public/assets/images/placeholder.svg
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300" role="img" aria-label="No image available">
|
||||||
|
<rect width="400" height="300" fill="#e5e7eb"/>
|
||||||
|
<g fill="none" stroke="#9ca3af" stroke-width="2">
|
||||||
|
<rect x="40" y="40" width="320" height="220" rx="8"/>
|
||||||
|
<path d="M40 220 L140 130 L200 180 L260 110 L360 220" />
|
||||||
|
<circle cx="140" cy="100" r="20"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 380 B |
325
public/assets/mock/bootstrap/bootstrap.json
Normal file
@@ -0,0 +1,325 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": "1.0.0",
|
||||||
|
"generatedAt": "2026-07-03T00:00:00Z",
|
||||||
|
"tenant": {
|
||||||
|
"id": "tenant-default-001",
|
||||||
|
"slug": "default",
|
||||||
|
"code": "DEFAULT",
|
||||||
|
"host": "default.local",
|
||||||
|
"name": "Marketplace",
|
||||||
|
"websiteBaseUrl": "https://marketplace.local",
|
||||||
|
"builderBaseUrl": "https://builder.marketplace.local",
|
||||||
|
"backofficeBaseUrl": "https://backoffice.marketplace.local",
|
||||||
|
"defaultLocale": "ru",
|
||||||
|
"supportedLocales": ["ru", "en", "hy"],
|
||||||
|
"defaultCurrency": "RUB",
|
||||||
|
"supportedCurrencies": ["RUB", "USD", "EUR", "AMD"],
|
||||||
|
"timezone": "Europe/Moscow"
|
||||||
|
},
|
||||||
|
"branding": {
|
||||||
|
"brandName": "Marketplace",
|
||||||
|
"legalName": "Marketplace LLC",
|
||||||
|
"slogan": "Digital commerce marketplace",
|
||||||
|
"logoUrl": "/icons/icon-192x192.png",
|
||||||
|
"logoCompactUrl": "/icons/icon-192x192.png",
|
||||||
|
"faviconUrl": "/favicon.ico",
|
||||||
|
"appIconUrl": "/icons/icon-192x192.png",
|
||||||
|
"supportEmail": "support@marketplace.local",
|
||||||
|
"supportPhone": "+7-900-000-00-00"
|
||||||
|
},
|
||||||
|
"theme": {
|
||||||
|
"themeId": "default-light",
|
||||||
|
"mode": "light",
|
||||||
|
"palette": {
|
||||||
|
"primary": "#497671",
|
||||||
|
"secondary": "#a1b4b5",
|
||||||
|
"accent": "#a7ceca",
|
||||||
|
"success": "#10b981",
|
||||||
|
"warning": "#f59e0b",
|
||||||
|
"danger": "#ef4444",
|
||||||
|
"info": "#3b82f6",
|
||||||
|
"textPrimary": "#1e3c38",
|
||||||
|
"textSecondary": "#667a77",
|
||||||
|
"backgroundPrimary": "#ffffff",
|
||||||
|
"backgroundSecondary": "#f5f5f5",
|
||||||
|
"border": "#d3dad9"
|
||||||
|
},
|
||||||
|
"typography": {
|
||||||
|
"primaryFontFamily": "DM Sans, sans-serif",
|
||||||
|
"headingFontFamily": "DM Sans, sans-serif",
|
||||||
|
"baseFontSize": 16
|
||||||
|
},
|
||||||
|
"spacing": {
|
||||||
|
"unit": 4,
|
||||||
|
"scale": [0, 4, 8, 12, 16, 24, 32, 48]
|
||||||
|
},
|
||||||
|
"borderRadiusScale": {
|
||||||
|
"sm": "8px",
|
||||||
|
"md": "12px",
|
||||||
|
"lg": "16px",
|
||||||
|
"xl": "22px"
|
||||||
|
},
|
||||||
|
"shadows": {
|
||||||
|
"sm": "0 2px 8px rgba(0,0,0,0.1)",
|
||||||
|
"md": "0 4px 12px rgba(0,0,0,0.15)",
|
||||||
|
"lg": "0 12px 32px rgba(73,118,113,0.2)"
|
||||||
|
},
|
||||||
|
"iconSet": "default"
|
||||||
|
},
|
||||||
|
"company": {
|
||||||
|
"companyName": "Marketplace LLC",
|
||||||
|
"registrationNumber": "1027700000000",
|
||||||
|
"taxId": "7700000000",
|
||||||
|
"address": {
|
||||||
|
"country": "Russia",
|
||||||
|
"region": "Moscow",
|
||||||
|
"city": "Moscow",
|
||||||
|
"street": "Tverskaya 1",
|
||||||
|
"postalCode": "125009"
|
||||||
|
},
|
||||||
|
"contacts": {
|
||||||
|
"email": "support@marketplace.local",
|
||||||
|
"phone": "+7-900-000-00-00",
|
||||||
|
"telegram": "@marketplace_support",
|
||||||
|
"website": "https://marketplace.local"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"featureFlags": {
|
||||||
|
"wishlist": true,
|
||||||
|
"compare": true,
|
||||||
|
"reviews": true,
|
||||||
|
"blog": false,
|
||||||
|
"chat": false,
|
||||||
|
"analytics": true,
|
||||||
|
"notifications": true,
|
||||||
|
"coupons": true,
|
||||||
|
"loyalty": false,
|
||||||
|
"giftCards": false,
|
||||||
|
"invoices": true
|
||||||
|
},
|
||||||
|
"apiEndpoints": {
|
||||||
|
"bootstrap": {
|
||||||
|
"path": "/bootstrap",
|
||||||
|
"method": "GET",
|
||||||
|
"timeoutMs": 10000
|
||||||
|
},
|
||||||
|
"website": {},
|
||||||
|
"builder": {},
|
||||||
|
"backoffice": {}
|
||||||
|
},
|
||||||
|
"localization": {
|
||||||
|
"defaultLocale": "ru",
|
||||||
|
"supportedLocales": ["ru", "en", "hy"],
|
||||||
|
"currencyByLocale": {
|
||||||
|
"ru": "RUB",
|
||||||
|
"en": "USD",
|
||||||
|
"hy": "AMD"
|
||||||
|
},
|
||||||
|
"dictionaries": [
|
||||||
|
{
|
||||||
|
"locale": "ru",
|
||||||
|
"dictionaryUrl": "/assets/i18n/ru.json",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"locale": "en",
|
||||||
|
"dictionaryUrl": "/assets/i18n/en.json",
|
||||||
|
"version": "1.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"locale": "hy",
|
||||||
|
"dictionaryUrl": "/assets/i18n/hy.json",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"seo": {
|
||||||
|
"default": {
|
||||||
|
"title": "Marketplace",
|
||||||
|
"description": "Digital commerce marketplace",
|
||||||
|
"robots": "index,follow"
|
||||||
|
},
|
||||||
|
"byPageKey": {
|
||||||
|
"home": {
|
||||||
|
"title": "Marketplace - Home",
|
||||||
|
"description": "Digital commerce marketplace",
|
||||||
|
"canonicalUrl": "https://marketplace.local/",
|
||||||
|
"robots": "index,follow"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"permissions": {
|
||||||
|
"definitions": [
|
||||||
|
{
|
||||||
|
"key": "builder.pages.edit",
|
||||||
|
"description": "Edit pages in builder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "backoffice.products.read",
|
||||||
|
"description": "Read products in backoffice"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "builder_admin",
|
||||||
|
"permissions": ["builder.pages.edit"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "backoffice_manager",
|
||||||
|
"permissions": ["backoffice.products.read"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"id": "nav-home",
|
||||||
|
"labelKey": "nav.home",
|
||||||
|
"route": "/",
|
||||||
|
"icon": "home",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nav-search",
|
||||||
|
"labelKey": "nav.search",
|
||||||
|
"route": "/search",
|
||||||
|
"icon": "search",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "nav-cart",
|
||||||
|
"labelKey": "nav.cart",
|
||||||
|
"route": "/cart",
|
||||||
|
"icon": "cart",
|
||||||
|
"order": 3
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"footer": [
|
||||||
|
{
|
||||||
|
"id": "footer-about",
|
||||||
|
"labelKey": "nav.about",
|
||||||
|
"route": "/about",
|
||||||
|
"order": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "footer-contacts",
|
||||||
|
"labelKey": "nav.contacts",
|
||||||
|
"route": "/contacts",
|
||||||
|
"order": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "footer-privacy",
|
||||||
|
"labelKey": "nav.privacy",
|
||||||
|
"route": "/privacy-policy",
|
||||||
|
"order": 3
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"pages": [
|
||||||
|
{
|
||||||
|
"id": "page-home",
|
||||||
|
"key": "home",
|
||||||
|
"title": "Home",
|
||||||
|
"route": {
|
||||||
|
"path": "/",
|
||||||
|
"exact": true
|
||||||
|
},
|
||||||
|
"layout": "default-public",
|
||||||
|
"seoKey": "home",
|
||||||
|
"visible": true,
|
||||||
|
"sections": [
|
||||||
|
{
|
||||||
|
"id": "section-hero",
|
||||||
|
"type": "hero",
|
||||||
|
"order": 1,
|
||||||
|
"layout": {
|
||||||
|
"strategy": "hero",
|
||||||
|
"columns": 1,
|
||||||
|
"gap": "1.5rem",
|
||||||
|
"align": "stretch"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"desktop": true,
|
||||||
|
"tablet": true,
|
||||||
|
"mobile": true
|
||||||
|
},
|
||||||
|
"visible": true,
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"id": "widget-hero-main",
|
||||||
|
"type": "hero",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"visible": true,
|
||||||
|
"props": {
|
||||||
|
"title": "Welcome to Marketplace Platform",
|
||||||
|
"subtitle": "Configuration-driven multi-tenant commerce",
|
||||||
|
"ctaLabel": "Start Shopping"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "section-categories",
|
||||||
|
"type": "categories",
|
||||||
|
"order": 2,
|
||||||
|
"layout": {
|
||||||
|
"strategy": "grid",
|
||||||
|
"columns": 1,
|
||||||
|
"gap": "1.5rem",
|
||||||
|
"align": "stretch"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"desktop": true,
|
||||||
|
"tablet": true,
|
||||||
|
"mobile": true
|
||||||
|
},
|
||||||
|
"visible": true,
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"id": "widget-categories-root",
|
||||||
|
"type": "categories",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"visible": true,
|
||||||
|
"props": {
|
||||||
|
"title": "Categories",
|
||||||
|
"source": "root",
|
||||||
|
"emptyMessage": "No categories available"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "section-featured-products",
|
||||||
|
"type": "product-collection",
|
||||||
|
"order": 3,
|
||||||
|
"layout": {
|
||||||
|
"strategy": "carousel",
|
||||||
|
"columns": 1,
|
||||||
|
"gap": "1rem",
|
||||||
|
"align": "stretch"
|
||||||
|
},
|
||||||
|
"visibility": {
|
||||||
|
"desktop": true,
|
||||||
|
"tablet": true,
|
||||||
|
"mobile": true
|
||||||
|
},
|
||||||
|
"visible": true,
|
||||||
|
"widgets": [
|
||||||
|
{
|
||||||
|
"id": "widget-featured-products",
|
||||||
|
"type": "product-collection",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"visible": true,
|
||||||
|
"props": {
|
||||||
|
"title": "Featured Products",
|
||||||
|
"source": "featured",
|
||||||
|
"count": 8,
|
||||||
|
"actionLabel": "Select"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -1 +1 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><linearGradient id="a" x1="0%" y1="0%" x2="100%" y2="100%"><stop offset="0%" style="stop-color:#667eea;stop-opacity:1"/><stop offset="100%" style="stop-color:#764ba2;stop-opacity:1"/></linearGradient></defs><path d="m20 35-5 50q0 10 10 10h50q10 0 10-10l-5-50Z" fill="url(#a)" stroke="#4a5cd6" stroke-width="2"/><path d="M30 35q0-20 20-20t20 20" fill="none" stroke="#4a5cd6" stroke-width="3" stroke-linecap="round"/><circle cx="70" cy="25" r="4" fill="gold"/><circle cx="30" cy="70" r="3" fill="#fff" opacity=".7"/></svg>
|
<svg data-name="Слой 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 108.43 100.53"><path d="M101.66 15.71c-4.16-.3-8.34-.35-12.51-.46-3.85-.1-7.69-.15-11.54-.21-9.14-.15-18.29-.32-27.44-.44-7.84-.11-15.68-.18-23.53-.21-.83 0-1.17-.3-1.33-1.01-.81-3.51-1.64-7.02-2.44-10.53-.31-1.33-1.42-2.36-2.68-2.41C18.6.37 17.01.27 15.42.23 11.37.13 7.31.06 3.25 0 1.27-.03 0 1.13 0 2.92 0 4.7 1.38 6.06 3.26 6.09c4.28.08 8.56.17 12.84.2.89 0 1.34.26 1.56 1.17 1.2 4.99 2.47 9.95 3.69 14.93 2.3 9.38 4.58 18.77 6.88 28.15 1.11 4.54 2.21 9.07 3.36 13.6.28 1.11.15 1.73-1.02 2.31-3.76 1.85-5.33 5.91-4.45 9.93.91 4.11 4.58 6.95 9.07 7.02h1.38c-2.97 1.75-4.68 4.13-4.95 7.42-.27 3.32 1.42 5.8 3.95 7.96-4.85.74-6.27.75-9.41 1.23.8.23 1.31.11 1.98.12 4.46.05 8.92.17 13.37.01 4.94-.17 8.86-5.16 7.57-10.63-.63-2.66-2.21-4.7-5.04-5.9h39.73c-2.87 1.74-4.53 4.14-4.85 7.36-.32 3.29 1.08 5.9 3.89 8.11-9.01.38-17.71.47-26.34 1.09l30.02.35c1.84-.07 3.73.03 5.49-.97 4.82-2.75 6.23-8.3 3.26-12.73-.84-1.26-2.17-2.19-3.21-3.2 1.3 0 2.83.03 4.35 0 1.66-.04 2.81-1.34 2.78-3.08-.02-1.56-1.25-2.77-2.82-2.79-6.68-.07-13.36-.18-20.04-.2-9.37-.04-18.74-.01-28.11-.02H35.44c-2.17 0-3.72-1.47-3.62-3.37.09-1.79 1.73-3.16 3.83-3.15 8.39.04 16.77.1 25.16.13 8.61.04 17.21.06 25.82.07.97 0 1.94-.09 2.9-.21 3.83-.52 6.67-3.16 7.69-6.89 1.84-6.75 3.76-13.47 5.65-20.21 1.36-4.84 2.79-9.66 4.08-14.52.59-2.2 1.13-4.45 1.32-6.7.29-3.53-2.89-6.7-6.6-6.96Zm-13.8 71.86c2.2-.07 4.11 1.95 4.1 4.15-.18 2.67-1.84 3.97-4.24 4.07-2.17.08-4.06-1.98-4.03-4.18.03-2.3 1.72-3.96 4.17-4.04m-47.43-.03c2.45-.06 4.19 1.8 4.15 4.03-.05 2.63-2.02 3.98-4.06 4.02-2.23.04-4.05-1.86-4.15-4.07-.1-2.22 2.05-4.07 4.06-3.98m30.45-67.01v12.33c-1.89 0-3.69.02-5.48 0-3.15-.05-6.3-.18-9.45-.18-.98 0-1.2-.35-1.27-1.24-.22-2.76-.55-5.5-.82-8.25-.09-.93-.15-1.86-.21-2.66zm-.14 17.64v12.64c-4.47 0-8.88.02-13.29-.04-.26 0-.71-.63-.75-1.01-.35-3.18-.62-6.37-.91-9.55v-.11c-.15-1.98-.15-1.95 1.83-1.94 4.35.02 8.69 0 13.13 0Zm-41.31-8.1c-.62-2.71-1.26-5.41-1.88-8.12-.15-.65-.27-1.32-.43-2.1 7.05.12 13.97.24 21.04.37.41 4.15.81 8.23 1.19 12.14H32.48c-.11 0-.22-.02-.32-.03-2.25-.14-2.24-.14-2.73-2.26m5.02 20.67c-1.01-4.24-2.02-8.49-3.03-12.7h18.64c.47 4.3.93 8.46 1.39 12.7H34.44Zm57.74 8.57c-.3 1.1-.54 2.23-.89 3.31-.51 1.58-1.87 2.54-3.47 2.54-16.08-.01-32.17-.04-48.25 0-1.26 0-1.71-.36-1.95-1.57-.44-2.27-1.1-4.5-1.65-6.75-.04-.17 0-.35 0-.67l56.99.39c-.29 1.03-.53 1.89-.77 2.76Zm4.75-16.54c-.7 2.51-1.41 5.02-2.17 7.51-.09.29-.56.65-.85.65q-8.385.06-16.77 0c-.29 0-.83-.42-.84-.64-.05-3.87-.04-7.75-.04-11.6h21.71c-.38 1.5-.69 2.8-1.05 4.08Zm5.38-19.31c-.83 2.95-1.7 5.89-2.49 8.85-.19.73-.47 1.01-1.23.99-6.45-.16-12.91-.28-19.36-.41-.94-.02-1.88 0-2.97 0 0-3.91.01-7.67 0-11.43 0-.76.45-.78 1-.77 2.83.08 5.65.17 8.48.22 4.93.09 9.86.15 14.79.22 1.49.02 2.18.94 1.78 2.34Z" style="fill:#477470;stroke-width:0"/></svg>
|
||||||
|
Before Width: | Height: | Size: 588 B After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 3.2 KiB |
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 547 B |
|
Before Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 1.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"$schema": "./node_modules/@angular/service-worker/config/schema.json",
|
"name": "Marketplace - Интернет-магазин",
|
||||||
"name": "Novo Market - Интернет-магазин",
|
"short_name": "Marketplace",
|
||||||
"short_name": "Novo",
|
"description": "Интернет-магазин цифровых товаров и услуг",
|
||||||
"description": "Novo Market - ваш онлайн магазин качественных товаров с доставкой",
|
|
||||||
"theme_color": "#10b981",
|
"theme_color": "#10b981",
|
||||||
"background_color": "#ffffff",
|
"background_color": "#ffffff",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
@@ -12,34 +11,10 @@
|
|||||||
"categories": ["shopping", "lifestyle"],
|
"categories": ["shopping", "lifestyle"],
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "icons/icon-72x72.png",
|
"src": "icons/icon-192x192.png",
|
||||||
"sizes": "72x72",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable any"
|
"purpose": "any"
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-128x128.png",
|
|
||||||
"sizes": "128x128",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-152x152.png",
|
|
||||||
"sizes": "152x152",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-192x192.png",
|
"src": "icons/icon-192x192.png",
|
||||||
@@ -47,12 +22,6 @@
|
|||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable any"
|
"purpose": "maskable any"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"src": "icons/icon-384x384.png",
|
|
||||||
"sizes": "384x384",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"src": "icons/icon-512x512.png",
|
"src": "icons/icon-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Dexar Market - Интернет-магазин",
|
"name": "Marketplace - Интернет-магазин",
|
||||||
"short_name": "Dexar Market",
|
"short_name": "Marketplace",
|
||||||
"description": "Интернет-магазин цифровых товаров и услуг",
|
"description": "Интернет-магазин цифровых товаров и услуг",
|
||||||
"display": "standalone",
|
"display": "standalone",
|
||||||
"orientation": "portrait-primary",
|
"orientation": "portrait-primary",
|
||||||
@@ -11,34 +11,10 @@
|
|||||||
"categories": ["shopping", "marketplace"],
|
"categories": ["shopping", "marketplace"],
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "icons/icon-72x72.png",
|
"src": "icons/icon-192x192.png",
|
||||||
"sizes": "72x72",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable any"
|
"purpose": "any"
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-96x96.png",
|
|
||||||
"sizes": "96x96",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-128x128.png",
|
|
||||||
"sizes": "128x128",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-144x144.png",
|
|
||||||
"sizes": "144x144",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "icons/icon-152x152.png",
|
|
||||||
"sizes": "152x152",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "icons/icon-192x192.png",
|
"src": "icons/icon-192x192.png",
|
||||||
@@ -46,12 +22,6 @@
|
|||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "maskable any"
|
"purpose": "maskable any"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"src": "icons/icon-384x384.png",
|
|
||||||
"sizes": "384x384",
|
|
||||||
"type": "image/png",
|
|
||||||
"purpose": "maskable any"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"src": "icons/icon-512x512.png",
|
"src": "icons/icon-512x512.png",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
|
|||||||
@@ -1,9 +1,20 @@
|
|||||||
User-agent: *
|
User-agent: *
|
||||||
Allow: /
|
Allow: /
|
||||||
Sitemap: https://dexarmarket.ru/sitemap.xml
|
|
||||||
|
|
||||||
# Block access to cart (user-specific data)
|
# Block access to cart (user-specific data)
|
||||||
Disallow: /cart
|
Disallow: /cart
|
||||||
|
|
||||||
|
# Block admin/backoffice and internal diagnostics
|
||||||
|
Disallow: /*/backoffice
|
||||||
|
Disallow: /*/edit
|
||||||
|
Disallow: /*/project-editor
|
||||||
|
Disallow: /__diagnostics
|
||||||
|
|
||||||
# Crawl delay for polite crawling
|
# Crawl delay for polite crawling
|
||||||
Crawl-delay: 1
|
Crawl-delay: 1
|
||||||
|
|
||||||
|
# Static baseline sitemap (home/catalog/search/wishlist/compare only) - see
|
||||||
|
# public/sitemap.xml's own header comment for what this does and does not
|
||||||
|
# cover (no per-tenant product/category/static-page URLs yet - needs a
|
||||||
|
# backend/build-time generator, documented in docs/backend/BACKEND-INTEGRATION.md#619-sitemap-future--static-baseline-only-today).
|
||||||
|
Sitemap: /sitemap.xml
|
||||||
|
|||||||
44
public/sitemap.xml
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
Static baseline sitemap - top-level, statically-known marketplace routes
|
||||||
|
only (home, catalog, search, wishlist, compare) for the site's default
|
||||||
|
locale segment ('ru', see app.routes.ts's `redirectTo: 'ru'` fallback).
|
||||||
|
|
||||||
|
Known limitation (documented, not faked): this is a multi-tenant,
|
||||||
|
config-driven platform (docs/ARCHITECTURE.md) - supported locales,
|
||||||
|
categories, products, and static pages are all resolved at runtime from
|
||||||
|
the tenant's bootstrap config, not enumerable at build time from the
|
||||||
|
frontend alone. A real per-tenant sitemap covering
|
||||||
|
/:lang/product/:id, /:lang/catalog/:categoryId, and /:lang/:staticPath
|
||||||
|
needs a backend/build-time job that reads the same bootstrap data source
|
||||||
|
and regenerates this file (or serves it dynamically) per tenant/domain -
|
||||||
|
see docs/backend/BACKEND-INTEGRATION.md#619-sitemap-future--static-baseline-only-today. Until that exists, this static file is a reasonable
|
||||||
|
floor, not the full picture.
|
||||||
|
-->
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
<url>
|
||||||
|
<loc>/ru</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>1.0</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>/ru/catalog</loc>
|
||||||
|
<changefreq>daily</changefreq>
|
||||||
|
<priority>0.9</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>/ru/search</loc>
|
||||||
|
<changefreq>weekly</changefreq>
|
||||||
|
<priority>0.5</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>/ru/wishlist</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.3</priority>
|
||||||
|
</url>
|
||||||
|
<url>
|
||||||
|
<loc>/ru/compare</loc>
|
||||||
|
<changefreq>monthly</changefreq>
|
||||||
|
<priority>0.3</priority>
|
||||||
|
</url>
|
||||||
|
</urlset>
|
||||||
59
skills-lock.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"skills": {
|
||||||
|
"angular-developer": {
|
||||||
|
"source": "angular/skills",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "angular-developer/SKILL.md",
|
||||||
|
"computedHash": "62e087c9cf0dc17f4ca4fed9f451f65605f43e4427016eb799409d6da39a0a87"
|
||||||
|
},
|
||||||
|
"cavecrew": {
|
||||||
|
"source": "JuliusBrussee/caveman",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/cavecrew/SKILL.md",
|
||||||
|
"computedHash": "9633c1391fa246091ce68ea522c0e424b2bc93aeb69fc44221a30b53e8a2c23d"
|
||||||
|
},
|
||||||
|
"caveman": {
|
||||||
|
"source": "JuliusBrussee/caveman",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/caveman/SKILL.md",
|
||||||
|
"computedHash": "723fb2a8bec1156c0f0b5bf020cc739ed09702b7726ec6377480038871339f6e"
|
||||||
|
},
|
||||||
|
"caveman-commit": {
|
||||||
|
"source": "JuliusBrussee/caveman",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/caveman-commit/SKILL.md",
|
||||||
|
"computedHash": "f028652defd5fdeddcce2994083cb1a7b201ee827bba8e2495546ee159fca3de"
|
||||||
|
},
|
||||||
|
"caveman-compress": {
|
||||||
|
"source": "JuliusBrussee/caveman",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/caveman-compress/SKILL.md",
|
||||||
|
"computedHash": "1055abaf7cb2f8c0ca78b64101b84dfc910d9819733ed9f0277b661441797aeb"
|
||||||
|
},
|
||||||
|
"caveman-help": {
|
||||||
|
"source": "JuliusBrussee/caveman",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/caveman-help/SKILL.md",
|
||||||
|
"computedHash": "4dba39eea07a050108d47940b39600bc8f45489201ecff0ccf03627180fd8e50"
|
||||||
|
},
|
||||||
|
"caveman-review": {
|
||||||
|
"source": "JuliusBrussee/caveman",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/caveman-review/SKILL.md",
|
||||||
|
"computedHash": "b9091dbc51de0f3710ea818fd4d638539f8c1784f8fda931eb159c44861e702e"
|
||||||
|
},
|
||||||
|
"caveman-stats": {
|
||||||
|
"source": "JuliusBrussee/caveman",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/caveman-stats/SKILL.md",
|
||||||
|
"computedHash": "331f720e2fa97b68cacdae44384878071e8cac6013479edea68f4c8eca308852"
|
||||||
|
},
|
||||||
|
"design-taste-frontend": {
|
||||||
|
"source": "Leonxlnx/taste-skill",
|
||||||
|
"sourceType": "github",
|
||||||
|
"skillPath": "skills/taste-skill/SKILL.md",
|
||||||
|
"computedHash": "899b84384f74f540ea5284d9b2e9234e050998b42eacc805410b518d4226c0b3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +1,35 @@
|
|||||||
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode } from '@angular/core';
|
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection, isDevMode } from '@angular/core';
|
||||||
import { PreloadAllModules, provideRouter, withPreloading, withInMemoryScrolling } from '@angular/router';
|
import { provideRouter, withInMemoryScrolling } from '@angular/router';
|
||||||
import { provideHttpClient, withInterceptors } from '@angular/common/http';
|
import { provideHttpClient, withInterceptors, withXhr } from '@angular/common/http';
|
||||||
|
|
||||||
import { routes } from './app.routes';
|
import { routes } from './app.routes';
|
||||||
import { cacheInterceptor } from './interceptors/cache.interceptor';
|
import { cacheInterceptor } from './interceptors/cache.interceptor';
|
||||||
|
import { apiBaseUrlInterceptor } from './interceptors/api-base-url.interceptor';
|
||||||
|
import { apiHeadersInterceptor } from './interceptors/api-headers.interceptor';
|
||||||
|
import { mockDataInterceptor } from './interceptors/mock-data.interceptor';
|
||||||
|
import { adminAuthHeadersInterceptor } from './core/admin-auth/admin-auth-headers.interceptor';
|
||||||
|
import { Ed25519VerificationService } from './core/admin-auth/ed25519-verification.model';
|
||||||
|
import { NoopEd25519VerificationService } from './core/admin-auth/noop-ed25519-verification.service';
|
||||||
import { provideServiceWorker } from '@angular/service-worker';
|
import { provideServiceWorker } from '@angular/service-worker';
|
||||||
|
import { MediaRepository } from './core/media/media-repository';
|
||||||
|
import { MockMediaRepository } from './core/media/mock-media-repository.service';
|
||||||
|
|
||||||
export const appConfig: ApplicationConfig = {
|
export const appConfig: ApplicationConfig = {
|
||||||
providers: [
|
providers: [
|
||||||
provideBrowserGlobalErrorListeners(),
|
provideBrowserGlobalErrorListeners(),
|
||||||
provideZoneChangeDetection({ eventCoalescing: true }),
|
provideZoneChangeDetection({ eventCoalescing: true }),
|
||||||
provideRouter(
|
provideRouter(
|
||||||
routes,
|
routes,
|
||||||
withPreloading(PreloadAllModules),
|
|
||||||
withInMemoryScrolling({ scrollPositionRestoration: 'top' })
|
withInMemoryScrolling({ scrollPositionRestoration: 'top' })
|
||||||
),
|
),
|
||||||
provideHttpClient(
|
provideHttpClient(withXhr(),
|
||||||
withInterceptors([cacheInterceptor])
|
withInterceptors([mockDataInterceptor, apiBaseUrlInterceptor, apiHeadersInterceptor, adminAuthHeadersInterceptor, cacheInterceptor])
|
||||||
), provideServiceWorker('ngsw-worker.js', {
|
),
|
||||||
enabled: !isDevMode(),
|
{ provide: Ed25519VerificationService, useClass: NoopEd25519VerificationService },
|
||||||
registrationStrategy: 'registerWhenStable:30000'
|
{ provide: MediaRepository, useClass: MockMediaRepository },
|
||||||
})
|
provideServiceWorker('ngsw-worker.js', {
|
||||||
|
enabled: !isDevMode(),
|
||||||
|
registrationStrategy: 'registerWhenStable:30000'
|
||||||
|
})
|
||||||
]
|
]
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,23 +1,33 @@
|
|||||||
@if (checkingServer()) {
|
@if (checkingServer()) {
|
||||||
<div class="server-check-overlay">
|
<div class="server-check-overlay">
|
||||||
<div class="server-check-content">
|
<div class="spinner-large"></div>
|
||||||
<div class="spinner-large"></div>
|
<p>{{ 'app.connecting' | translate }}</p>
|
||||||
<h2>Проверка соединения с сервером...</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
} @else if (!serverAvailable()) {
|
} @else if (!serverAvailable()) {
|
||||||
<div class="server-error-overlay">
|
<div class="server-error-overlay">
|
||||||
<div class="server-error-content">
|
<div class="error-icon">⚠️</div>
|
||||||
<div class="error-icon">⚠️</div>
|
<h2>{{ 'app.serverUnavailable' | translate }}</h2>
|
||||||
<h1>Извините, возникла проблема</h1>
|
<p>{{ 'app.serverError' | translate }}</p>
|
||||||
<p>Не удается подключиться к серверу. Пожалуйста, проверьте подключение к интернету или попробуйте позже.</p>
|
<button class="retry-btn" (click)="retryConnection()">{{ 'app.retryConnection' | translate }}</button>
|
||||||
<button class="retry-btn" (click)="retryConnection()">Попробовать снова</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
} @else {
|
} @else if (isAdminRoute()) {
|
||||||
|
<router-outlet></router-outlet>
|
||||||
|
<app-telegram-login mode="admin" />
|
||||||
|
} @else {
|
||||||
|
<a class="skip-link" href="#main-content">{{ 'app.skipToContent' | translate }}</a>
|
||||||
<app-header></app-header>
|
<app-header></app-header>
|
||||||
<main class="main-content">
|
<main id="main-content" class="main-content" tabindex="-1">
|
||||||
|
@if (!isHomePage()) {
|
||||||
|
<app-back-button />
|
||||||
|
}
|
||||||
<router-outlet></router-outlet>
|
<router-outlet></router-outlet>
|
||||||
</main>
|
</main>
|
||||||
<app-footer></app-footer>
|
<app-floating-notifications />
|
||||||
|
@defer (on viewport) {
|
||||||
|
<app-footer></app-footer>
|
||||||
|
} @placeholder {
|
||||||
|
<div class="footer-placeholder" aria-hidden="true"></div>
|
||||||
|
}
|
||||||
|
<!-- <app-telegram-login /> -->
|
||||||
|
<app-telegram-login mode="admin" />
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Routes } from '@angular/router';
|
import { Routes } from '@angular/router';
|
||||||
import { brandInfoRoutes, brandLegalRoutes } from './brands/brand-routes';
|
import { languageGuard } from './guards/language.guard';
|
||||||
|
import { projectEditorDirtyGuard } from './features/project-editor/guards/project-editor-dirty.guard';
|
||||||
|
import { adminAuthGuard, requireAdminPermission } from './core/admin-auth/admin-auth.guard';
|
||||||
|
import { authRoutes } from './core/auth/auth.routes';
|
||||||
|
import { adminCategoryDirtyGuard } from './features/admin/categories/guards/admin-category-dirty.guard';
|
||||||
|
import { adminProductDirtyGuard } from './features/admin/products/guards/admin-product-dirty.guard';
|
||||||
|
import { environment } from '../environments/environment';
|
||||||
|
|
||||||
// Core routes (same across all brands)
|
// Core routes (same across all brands)
|
||||||
const coreRoutes: Routes = [
|
const coreRoutes: Routes = [
|
||||||
@@ -7,35 +13,331 @@ const coreRoutes: Routes = [
|
|||||||
path: '',
|
path: '',
|
||||||
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent)
|
loadComponent: () => import('./pages/home/home.component').then(m => m.HomeComponent)
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'catalog',
|
||||||
|
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'catalog/:id',
|
||||||
|
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'category/:id',
|
path: 'category/:id',
|
||||||
loadComponent: () => import('./pages/category/subcategories.component').then(m => m.SubcategoriesComponent)
|
redirectTo: 'catalog/:id',
|
||||||
|
pathMatch: 'full'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'category/:id/items',
|
path: 'category/:id/items',
|
||||||
loadComponent: () => import('./pages/category/category.component').then(m => m.CategoryComponent)
|
redirectTo: 'catalog/:id',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'product/:id',
|
||||||
|
loadComponent: () => import('./features/website/product/containers/product-details-container.component').then(m => m.ProductDetailsContainerComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'item/:id',
|
path: 'item/:id',
|
||||||
loadComponent: () => import('./pages/item-detail/item-detail.component').then(m => m.ItemDetailComponent)
|
redirectTo: 'product/:id',
|
||||||
|
pathMatch: 'full'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'search',
|
path: 'search',
|
||||||
loadComponent: () => import('./pages/search/search.component').then(m => m.SearchComponent)
|
loadComponent: () => import('./features/website/catalog/containers/catalog-container.component').then(m => m.CatalogContainerComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit',
|
||||||
|
canActivate: [adminAuthGuard],
|
||||||
|
loadComponent: () => import('./features/project-editor/pages/builder-overview-page.component').then(m => m.BuilderOverviewPageComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'edit/:section',
|
||||||
|
canActivate: [adminAuthGuard],
|
||||||
|
loadComponent: () => import('./features/project-editor/pages/project-editor-page.component').then(m => m.ProjectEditorPageComponent),
|
||||||
|
canDeactivate: [projectEditorDirtyGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'backoffice',
|
||||||
|
canActivate: [adminAuthGuard],
|
||||||
|
loadComponent: () => import('./features/admin/shell/admin-layout.component').then(m => m.AdminLayoutComponent),
|
||||||
|
children: [
|
||||||
|
{ path: '', redirectTo: 'dashboard', pathMatch: 'full' },
|
||||||
|
{
|
||||||
|
path: 'dashboard',
|
||||||
|
loadComponent: () => import('./features/admin/dashboard/pages/admin-dashboard-page.component').then(m => m.AdminDashboardPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.dashboard.title',
|
||||||
|
descriptionKey: 'adminShell.pages.dashboard.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.pages.dashboard.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'products',
|
||||||
|
loadComponent: () => import('./features/admin/products/pages/admin-products-list-page.component').then(m => m.AdminProductsListPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.products.title',
|
||||||
|
descriptionKey: 'adminShell.pages.products.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.products' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'products/create',
|
||||||
|
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
|
||||||
|
canDeactivate: [adminProductDirtyGuard],
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.productCreate.title',
|
||||||
|
descriptionKey: 'adminShell.pages.productCreate.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.products', path: ['products'] }, { labelKey: 'adminShell.pages.productCreate.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'products/:id/edit',
|
||||||
|
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
|
||||||
|
canDeactivate: [adminProductDirtyGuard],
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.productEdit.title',
|
||||||
|
descriptionKey: 'adminShell.pages.productEdit.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.products', path: ['products'] }, { labelKey: 'adminShell.pages.productEdit.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'products/:id/duplicate',
|
||||||
|
loadComponent: () => import('./features/admin/products/pages/admin-product-editor-page.component').then(m => m.AdminProductEditorPageComponent),
|
||||||
|
canDeactivate: [adminProductDirtyGuard],
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.productDuplicate.title',
|
||||||
|
descriptionKey: 'adminShell.pages.productDuplicate.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.products', path: ['products'] }, { labelKey: 'adminShell.pages.productDuplicate.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'categories',
|
||||||
|
loadComponent: () => import('./features/admin/categories/pages/admin-categories-list-page.component').then(m => m.AdminCategoriesListPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.categories.title',
|
||||||
|
descriptionKey: 'adminShell.pages.categories.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.categories' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'categories/create',
|
||||||
|
loadComponent: () => import('./features/admin/categories/pages/admin-category-editor-page.component').then(m => m.AdminCategoryEditorPageComponent),
|
||||||
|
canDeactivate: [adminCategoryDirtyGuard],
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.categoryCreate.title',
|
||||||
|
descriptionKey: 'adminShell.pages.categoryCreate.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.categories', path: ['categories'] }, { labelKey: 'adminShell.pages.categoryCreate.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'categories/:id/edit',
|
||||||
|
loadComponent: () => import('./features/admin/categories/pages/admin-category-editor-page.component').then(m => m.AdminCategoryEditorPageComponent),
|
||||||
|
canDeactivate: [adminCategoryDirtyGuard],
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.categoryEdit.title',
|
||||||
|
descriptionKey: 'adminShell.pages.categoryEdit.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.categories', path: ['categories'] }, { labelKey: 'adminShell.pages.categoryEdit.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Static Pages is a first-class Project Editor module (Sprint X+2), not a
|
||||||
|
// separate backoffice CRUD surface - redirect here rather than build a
|
||||||
|
// second UI over the same bootstrap.staticPages data.
|
||||||
|
path: 'static-pages',
|
||||||
|
redirectTo: '/edit/static-pages',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'transactions',
|
||||||
|
loadComponent: () => import('./features/admin/transactions/pages/admin-transactions-list-page.component').then(m => m.AdminTransactionsListPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.transactions.title',
|
||||||
|
descriptionKey: 'adminShell.pages.transactions.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.transactions' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'orders',
|
||||||
|
loadComponent: () => import('./features/admin/orders/pages/admin-orders-list-page.component').then(m => m.AdminOrdersListPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.orders.title',
|
||||||
|
descriptionKey: 'adminShell.pages.orders.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.orders' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'orders/:id',
|
||||||
|
loadComponent: () => import('./features/admin/orders/pages/admin-order-detail-page.component').then(m => m.AdminOrderDetailPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.orderDetail.title',
|
||||||
|
descriptionKey: 'adminShell.pages.orderDetail.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.orders', path: ['orders'] }, { labelKey: 'adminShell.pages.orderDetail.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'customers',
|
||||||
|
loadComponent: () => import('./features/admin/customers/pages/admin-customers-list-page.component').then(m => m.AdminCustomersListPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.customers.title',
|
||||||
|
descriptionKey: 'adminShell.pages.customers.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.customers' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'customers/:email',
|
||||||
|
loadComponent: () => import('./features/admin/customers/pages/admin-customer-detail-page.component').then(m => m.AdminCustomerDetailPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.customerDetail.title',
|
||||||
|
descriptionKey: 'adminShell.pages.customerDetail.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.customers', path: ['customers'] }, { labelKey: 'adminShell.pages.customerDetail.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'moderation',
|
||||||
|
loadComponent: () => import('./features/admin/moderation/pages/admin-reviews-list-page.component').then(m => m.AdminReviewsListPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.moderation.title',
|
||||||
|
descriptionKey: 'adminShell.pages.moderation.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.moderation' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'moderation/reports',
|
||||||
|
loadComponent: () => import('./features/admin/moderation/pages/admin-reports-list-page.component').then(m => m.AdminReportsListPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.reportsQueue.title',
|
||||||
|
descriptionKey: 'adminShell.pages.reportsQueue.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.moderation', path: ['moderation'] }, { labelKey: 'adminShell.pages.reportsQueue.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'moderation/:id',
|
||||||
|
loadComponent: () => import('./features/admin/moderation/pages/admin-review-detail-page.component').then(m => m.AdminReviewDetailPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.reviewDetail.title',
|
||||||
|
descriptionKey: 'adminShell.pages.reviewDetail.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.moderation', path: ['moderation'] }, { labelKey: 'adminShell.pages.reviewDetail.title' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'media',
|
||||||
|
loadComponent: () => import('./features/backoffice/media/media-library-page.component').then(m => m.MediaLibraryPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.media.title',
|
||||||
|
descriptionKey: 'adminShell.pages.media.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.mediaLibrary' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'users',
|
||||||
|
canActivate: [requireAdminPermission('users.manage')],
|
||||||
|
loadComponent: () => import('./features/admin/users/pages/admin-users-page.component').then(m => m.AdminUsersPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.users.title',
|
||||||
|
descriptionKey: 'adminShell.pages.users.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.users' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'monitoring',
|
||||||
|
loadComponent: () => import('./features/admin/monitoring/pages/admin-monitoring-page.component').then(m => m.AdminMonitoringPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.monitoring.title',
|
||||||
|
descriptionKey: 'adminShell.pages.monitoring.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.monitoring' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'analytics',
|
||||||
|
loadComponent: () => import('./features/admin/analytics/pages/admin-analytics-page.component').then(m => m.AdminAnalyticsPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.analytics.title',
|
||||||
|
descriptionKey: 'adminShell.pages.analytics.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.analytics' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'reports',
|
||||||
|
loadComponent: () => import('./features/admin/reports/pages/admin-reports-page.component').then(m => m.AdminReportsPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.reports.title',
|
||||||
|
descriptionKey: 'adminShell.pages.reports.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.reports' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'settings',
|
||||||
|
loadComponent: () => import('./features/admin/settings/pages/admin-settings-page.component').then(m => m.AdminSettingsPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.settings.title',
|
||||||
|
descriptionKey: 'adminShell.pages.settings.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.settings' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'partners/seller-management',
|
||||||
|
loadComponent: () => import('./features/admin/seller-management/pages/admin-seller-management-page.component').then(m => m.AdminSellerManagementPageComponent),
|
||||||
|
data: {
|
||||||
|
titleKey: 'adminShell.pages.sellerManagement.title',
|
||||||
|
descriptionKey: 'adminShell.pages.sellerManagement.description',
|
||||||
|
breadcrumb: [{ labelKey: 'adminShell.nav.sellerManagement' }]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ path: '**', redirectTo: 'dashboard' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'builder',
|
||||||
|
redirectTo: 'edit',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'project-editor',
|
||||||
|
redirectTo: 'edit',
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'wishlist',
|
||||||
|
loadComponent: () => import('./features/website/user-experience/wishlist/containers/wishlist-page.component').then(m => m.WishlistPageComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'compare',
|
||||||
|
loadComponent: () => import('./features/website/user-experience/compare/containers/compare-page.component').then(m => m.ComparePageComponent)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'cart',
|
path: 'cart',
|
||||||
loadComponent: () => import('./pages/cart/cart.component').then(m => m.CartComponent)
|
loadComponent: () => import('./pages/cart/cart.component').then(m => m.CartComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'page/:key',
|
||||||
|
loadComponent: () => import('./pages/static-page/static-page.component').then(m => m.StaticPageComponent)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':staticPath',
|
||||||
|
loadComponent: () => import('./pages/static-page/static-page.component').then(m => m.StaticPageComponent)
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// Combine core routes with brand-specific routes
|
// TODO(CMS): Resolve informational/legal pages from backend content configuration here.
|
||||||
|
// Disabled hardcoded pages: about, contacts, faq, delivery, guarantee,
|
||||||
|
// company-details, payment-terms, return-policy, public-offer, privacy-policy.
|
||||||
|
const cmsContentRoutes: Routes = [];
|
||||||
|
|
||||||
|
// All routes sit under a :lang prefix (e.g. /ru/cart, /en/product/5)
|
||||||
export const routes: Routes = [
|
export const routes: Routes = [
|
||||||
...coreRoutes,
|
...(environment.production ? [] : [{
|
||||||
...brandInfoRoutes,
|
path: '__diagnostics',
|
||||||
...brandLegalRoutes,
|
loadComponent: () => import('./features/diagnostics/components/diagnostics-page.component').then(m => m.DiagnosticsPageComponent)
|
||||||
|
}]),
|
||||||
|
...authRoutes,
|
||||||
{
|
{
|
||||||
path: '**',
|
path: ':lang',
|
||||||
redirectTo: ''
|
canActivate: [languageGuard],
|
||||||
}
|
children: [
|
||||||
|
...coreRoutes,
|
||||||
|
...cmsContentRoutes,
|
||||||
|
{ path: '**', redirectTo: '' }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
// URLs without a language prefix → redirect to default language
|
||||||
|
{ path: '**', redirectTo: 'ru' }
|
||||||
];
|
];
|
||||||
@@ -5,83 +5,64 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-check-overlay,
|
.footer-placeholder {
|
||||||
.server-error-overlay {
|
min-height: 1px;
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
background: #f8f9fa;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 9999;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-check-content,
|
.server-check-overlay,
|
||||||
.server-error-content {
|
.server-error-overlay {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 100vh;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 40px;
|
padding: 2rem;
|
||||||
max-width: 500px;
|
background: var(--surface-ground, #f8f9fa);
|
||||||
|
color: var(--text-color, #333);
|
||||||
}
|
}
|
||||||
|
|
||||||
.spinner-large {
|
.spinner-large {
|
||||||
width: 60px;
|
width: 48px;
|
||||||
height: 60px;
|
height: 48px;
|
||||||
border: 6px solid #f3f3f3;
|
border: 4px solid var(--surface-border, #dee2e6);
|
||||||
border-top: 6px solid var(--primary-color);
|
border-top-color: var(--primary-color, #007bff);
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
animation: spin 1s linear infinite;
|
animation: spin 0.8s linear infinite;
|
||||||
margin: 0 auto 24px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% { transform: rotate(0deg); }
|
to { transform: rotate(360deg); }
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.server-check-content h2 {
|
|
||||||
color: #333;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.error-icon {
|
.error-icon {
|
||||||
font-size: 5rem;
|
font-size: 3rem;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-error-content h1 {
|
.server-error-overlay h2 {
|
||||||
font-size: 2rem;
|
margin: 0 0 0.5rem;
|
||||||
color: #333;
|
font-size: var(--font-size-2xl, 1.25rem);
|
||||||
margin: 0 0 16px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.server-error-content p {
|
.server-error-overlay p {
|
||||||
font-size: 1.1rem;
|
margin: 0 0 1.5rem;
|
||||||
color: #333;
|
opacity: 0.7;
|
||||||
line-height: 1.6;
|
max-width: 300px;
|
||||||
margin: 0 0 32px 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.retry-btn {
|
.retry-btn {
|
||||||
padding: 14px 32px;
|
padding: 0.75rem 2rem;
|
||||||
background: var(--primary-color);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 8px;
|
border-radius: var(--radius-sm, 8px);
|
||||||
font-size: 1.1rem;
|
background: var(--primary-color, #007bff);
|
||||||
font-weight: 600;
|
color: #fff;
|
||||||
|
font-size: var(--font-size-lg, 1rem);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background 0.2s;
|
transition: opacity 0.2s;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: var(--primary-hover);
|
opacity: 0.85;
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
transform: scale(0.98);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { App } from './app';
|
|
||||||
import { provideRouter } from '@angular/router';
|
|
||||||
|
|
||||||
describe('App', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [App],
|
|
||||||
providers: [provideRouter([])]
|
|
||||||
}).compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the app', () => {
|
|
||||||
const fixture = TestBed.createComponent(App);
|
|
||||||
const app = fixture.componentInstance;
|
|
||||||
expect(app).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
171
src/app/app.ts
@@ -1,93 +1,138 @@
|
|||||||
|
|
||||||
import { Component, OnInit, OnDestroy, signal, ApplicationRef } from '@angular/core';
|
import { Component, OnInit, signal, ApplicationRef, inject, DestroyRef, ChangeDetectionStrategy } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { Router, RouterOutlet, NavigationEnd } from '@angular/router';
|
||||||
import { RouterOutlet } from '@angular/router';
|
|
||||||
import { Title } from '@angular/platform-browser';
|
import { Title } from '@angular/platform-browser';
|
||||||
import { HeaderComponent } from './components/header/header.component';
|
import { HeaderComponent } from './components/header/header.component';
|
||||||
import { FooterComponent } from './components/footer/footer.component';
|
import { FooterComponent } from './components/footer/footer.component';
|
||||||
import { ApiService } from './services';
|
import { BackButtonComponent } from './components/back-button/back-button.component';
|
||||||
import { Subscription, interval, concat } from 'rxjs';
|
import { interval, concat } from 'rxjs';
|
||||||
import { first } from 'rxjs/operators';
|
import { filter, first } from 'rxjs/operators';
|
||||||
import { environment } from '../environments/environment';
|
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||||
import { SwUpdate } from '@angular/service-worker';
|
import { SwUpdate } from '@angular/service-worker';
|
||||||
|
import { TranslatePipe } from './i18n/translate.pipe';
|
||||||
|
import { TranslateService } from './i18n/translate.service';
|
||||||
|
import { PlatformRuntimeService } from './core/runtime/platform-runtime.service';
|
||||||
|
import { UiRuntimeFacade } from './facades/runtime/ui-runtime.facade';
|
||||||
|
import { ApiHealthService } from './services/api-health.service';
|
||||||
|
import { SeoService } from './services/seo.service';
|
||||||
|
import { FloatingNotificationsComponent } from './features/website/user-experience/components/floating-notifications/floating-notifications.component';
|
||||||
|
import { AdminAuthService } from './core/admin-auth/admin-auth.service';
|
||||||
|
import { AuthService } from './services/auth.service';
|
||||||
|
import { TelegramLoginComponent } from './components/telegram-login/telegram-login.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'app-root',
|
selector: 'app-root',
|
||||||
imports: [RouterOutlet, HeaderComponent, FooterComponent, CommonModule],
|
imports: [RouterOutlet, HeaderComponent, FooterComponent, BackButtonComponent, TranslatePipe, FloatingNotificationsComponent, TelegramLoginComponent],
|
||||||
templateUrl: './app.html',
|
templateUrl: './app.html',
|
||||||
styleUrl: './app.scss'
|
styleUrl: './app.scss',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
})
|
})
|
||||||
export class App implements OnInit, OnDestroy {
|
export class App implements OnInit {
|
||||||
protected title = environment.brandName;
|
protected title = '';
|
||||||
serverAvailable = signal(true);
|
isHomePage = signal(true);
|
||||||
|
isAdminRoute = signal(false);
|
||||||
checkingServer = signal(true);
|
checkingServer = signal(true);
|
||||||
private pingSubscription?: Subscription;
|
serverAvailable = signal(false);
|
||||||
private updateSubscription?: Subscription;
|
|
||||||
|
|
||||||
constructor(
|
private destroyRef = inject(DestroyRef);
|
||||||
private apiService: ApiService,
|
private titleService = inject(Title);
|
||||||
private titleService: Title,
|
private swUpdate = inject(SwUpdate);
|
||||||
private swUpdate: SwUpdate,
|
private appRef = inject(ApplicationRef);
|
||||||
private appRef: ApplicationRef
|
private router = inject(Router);
|
||||||
) {}
|
private i18n = inject(TranslateService);
|
||||||
|
private platformRuntime = inject(PlatformRuntimeService);
|
||||||
|
private uiRuntime = inject(UiRuntimeFacade);
|
||||||
|
private apiHealth = inject(ApiHealthService);
|
||||||
|
private seoService = inject(SeoService);
|
||||||
|
private authService = inject(AuthService);
|
||||||
|
private adminAuthService = inject(AdminAuthService);
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// Устанавливаем заголовок страницы в зависимости от бренда
|
this.platformRuntime.initialize();
|
||||||
this.titleService.setTitle(`${environment.brandFullName} - Маркетплейс товаров и услуг`);
|
this.title = this.uiRuntime.marketplaceName();
|
||||||
|
this.titleService.setTitle(`${this.uiRuntime.marketplaceDisplayName()} - ${this.i18n.t('app.pageTitle')}`);
|
||||||
this.checkServerHealth();
|
this.checkServerHealth();
|
||||||
this.setupAutoUpdates();
|
this.setupAutoUpdates();
|
||||||
|
this.openLoginDialogsFromTestModeQueryParams();
|
||||||
|
|
||||||
|
// Track route changes to show/hide back button
|
||||||
|
this.router.events
|
||||||
|
.pipe(
|
||||||
|
filter(event => event instanceof NavigationEnd),
|
||||||
|
takeUntilDestroyed(this.destroyRef)
|
||||||
|
)
|
||||||
|
.subscribe((event) => {
|
||||||
|
const navEnd = event as NavigationEnd;
|
||||||
|
const url = navEnd.urlAfterRedirects || navEnd.url;
|
||||||
|
// Home pages: /ru, /en, /hy (with or without trailing slash)
|
||||||
|
this.isHomePage.set(/^\/[a-z]{2}\/?$/.test(url) || url === '/' || url === '');
|
||||||
|
// Admin backoffice and the Marketplace Builder (/edit) own their own
|
||||||
|
// shells (AdminLayoutComponent / ProjectEditorPageComponent's sidebar) -
|
||||||
|
// the storefront header/back-button/footer never render on either.
|
||||||
|
this.isAdminRoute.set(/^\/[a-z]{2}\/(backoffice|edit)(\/|$|\?)/.test(url));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
checkServerHealth(): void {
|
private checkServerHealth(): void {
|
||||||
this.pingSubscription = this.apiService.ping().subscribe({
|
this.checkingServer.set(true);
|
||||||
next: (response) => {
|
this.apiHealth.ping()
|
||||||
// Server is available
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
this.serverAvailable.set(true);
|
.subscribe({
|
||||||
this.checkingServer.set(false);
|
next: () => {
|
||||||
},
|
this.serverAvailable.set(true);
|
||||||
error: (err) => {
|
this.checkingServer.set(false);
|
||||||
console.error('Server health check failed:', err);
|
},
|
||||||
// Allow app to continue even if server is unreachable
|
error: () => {
|
||||||
this.serverAvailable.set(true);
|
this.serverAvailable.set(false);
|
||||||
this.checkingServer.set(false);
|
this.checkingServer.set(false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setupAutoUpdates(): void {
|
retryConnection(): void {
|
||||||
|
this.checkServerHealth();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ?login=true / ?adminLogin=true open the respective login dialog for
|
||||||
|
* manual testing. ?devBypassAdmin=true skips the QR flow entirely and
|
||||||
|
* activates a fake local admin session - dev builds only, no effect (and
|
||||||
|
* no-ops server-side too, see AdminAuthService.devBypassLogin) in
|
||||||
|
* production. No effect when the params are absent.
|
||||||
|
*/
|
||||||
|
private openLoginDialogsFromTestModeQueryParams(): void {
|
||||||
|
if (typeof window === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
if (params.get('login') === 'true') {
|
||||||
|
this.authService.requestLogin();
|
||||||
|
}
|
||||||
|
if (params.get('adminLogin') === 'true') {
|
||||||
|
this.adminAuthService.requestLogin();
|
||||||
|
}
|
||||||
|
if (params.get('devBypassAdmin') === 'true') {
|
||||||
|
this.adminAuthService.devBypassLogin();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private setupAutoUpdates(): void {
|
||||||
if (!this.swUpdate.isEnabled) {
|
if (!this.swUpdate.isEnabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for updates every 6 hours
|
|
||||||
const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
|
const appIsStable$ = this.appRef.isStable.pipe(first(isStable => isStable === true));
|
||||||
const every6Hours$ = interval(6 * 60 * 60 * 1000);
|
const every6Hours$ = interval(6 * 60 * 60 * 1000);
|
||||||
const checkInterval$ = concat(appIsStable$, every6Hours$);
|
const checkInterval$ = concat(appIsStable$, every6Hours$);
|
||||||
|
|
||||||
this.updateSubscription = checkInterval$.subscribe(async () => {
|
checkInterval$
|
||||||
try {
|
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||||
await this.swUpdate.checkForUpdate();
|
.subscribe(async () => {
|
||||||
} catch (err) {
|
try {
|
||||||
console.error('Update check failed:', err);
|
await this.swUpdate.checkForUpdate();
|
||||||
}
|
} catch (err) {
|
||||||
});
|
console.error('Update check failed:', err);
|
||||||
|
}
|
||||||
// Silently activate updates when ready
|
});
|
||||||
this.swUpdate.versionUpdates.subscribe(event => {
|
|
||||||
if (event.type === 'VERSION_READY') {
|
|
||||||
// Update will activate on next navigation/reload automatically
|
|
||||||
console.log('New app version ready');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.pingSubscription?.unsubscribe();
|
|
||||||
this.updateSubscription?.unsubscribe();
|
|
||||||
}
|
|
||||||
|
|
||||||
retryConnection(): void {
|
|
||||||
this.checkingServer.set(true);
|
|
||||||
this.checkServerHealth();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
// Novo brand routes
|
|
||||||
// Loaded via angular.json fileReplacements when building for novo
|
|
||||||
import { Routes } from '@angular/router';
|
|
||||||
|
|
||||||
export const brandInfoRoutes: Routes = [
|
|
||||||
{
|
|
||||||
path: 'about',
|
|
||||||
loadComponent: () => import('./novo/pages/info/about/about.component').then(m => m.AboutNovoComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'contacts',
|
|
||||||
loadComponent: () => import('./novo/pages/info/contacts/contacts.component').then(m => m.ContactsNovoComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'faq',
|
|
||||||
loadComponent: () => import('./novo/pages/info/faq/faq.component').then(m => m.FaqNovoComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'delivery',
|
|
||||||
loadComponent: () => import('./novo/pages/info/delivery/delivery.component').then(m => m.DeliveryNovoComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'guarantee',
|
|
||||||
loadComponent: () => import('./novo/pages/info/guarantee/guarantee.component').then(m => m.GuaranteeNovoComponent)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const brandLegalRoutes: Routes = [
|
|
||||||
{
|
|
||||||
path: 'company-details',
|
|
||||||
loadComponent: () => import('./novo/pages/legal/company-details/company-details.component').then(m => m.CompanyDetailsNovoComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'payment-terms',
|
|
||||||
loadComponent: () => import('./novo/pages/legal/payment-terms/payment-terms.component').then(m => m.PaymentTermsNovoComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'return-policy',
|
|
||||||
loadComponent: () => import('./novo/pages/legal/return-policy/return-policy.component').then(m => m.ReturnPolicyNovoComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'public-offer',
|
|
||||||
loadComponent: () => import('./novo/pages/legal/public-offer/public-offer.component').then(m => m.PublicOfferNovoComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'privacy-policy',
|
|
||||||
loadComponent: () => import('./novo/pages/legal/privacy-policy/privacy-policy.component').then(m => m.PrivacyPolicyNovoComponent)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
// Default brand routes (Dexar)
|
|
||||||
// This file is swapped via angular.json fileReplacements for each brand
|
|
||||||
import { Routes } from '@angular/router';
|
|
||||||
|
|
||||||
export const brandInfoRoutes: Routes = [
|
|
||||||
{
|
|
||||||
path: 'about',
|
|
||||||
loadComponent: () => import('../pages/info/about/about.component').then(m => m.AboutComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'contacts',
|
|
||||||
loadComponent: () => import('../pages/info/contacts/contacts.component').then(m => m.ContactsComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'faq',
|
|
||||||
loadComponent: () => import('../pages/info/faq/faq.component').then(m => m.FaqComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'delivery',
|
|
||||||
loadComponent: () => import('../pages/info/delivery/delivery.component').then(m => m.DeliveryComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'guarantee',
|
|
||||||
loadComponent: () => import('../pages/info/guarantee/guarantee.component').then(m => m.GuaranteeComponent)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
export const brandLegalRoutes: Routes = [
|
|
||||||
{
|
|
||||||
path: 'company-details',
|
|
||||||
loadComponent: () => import('../pages/legal/company-details/company-details.component').then(m => m.CompanyDetailsComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'payment-terms',
|
|
||||||
loadComponent: () => import('../pages/legal/payment-terms/payment-terms.component').then(m => m.PaymentTermsComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'return-policy',
|
|
||||||
loadComponent: () => import('../pages/legal/return-policy/return-policy.component').then(m => m.ReturnPolicyComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'public-offer',
|
|
||||||
loadComponent: () => import('../pages/legal/public-offer/public-offer.component').then(m => m.PublicOfferComponent)
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'privacy-policy',
|
|
||||||
loadComponent: () => import('../pages/legal/privacy-policy/privacy-policy.component').then(m => m.PrivacyPolicyComponent)
|
|
||||||
}
|
|
||||||
];
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>О компании ООО «ИНТ ФИН ЛОГИСТИК»</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>О нас</h2>
|
|
||||||
<p>Компания DexarMarket действительно представляет собой быстроразвивающийся маркетплейс, активно функционирующий в области торговли различными товарами и услугами. Регистрация юридического лица ООО "ИНТ ФИН ЛОГИСТИК", осуществленная согласно законодательству Армении, подчеркивает международную направленность бизнеса, поскольку компания также успешно работает на российском рынке, имея необходимые реквизиты для легальной деятельности в РФ.</p>
|
|
||||||
|
|
||||||
<p>Начало своей деятельности DEXARMARKET положил именно в Армении, однако расширение произошло стремительно. Уже летом 2025 года площадка вышла на российский рынок, показывая значительный рост популярности среди партнеров и покупателей из разных регионов мира, включая Россию, Объединённые Арабские Эмираты, Турцию, Китай, Армению, Казахстан, Кыргызстан и другие государства.</p>
|
|
||||||
|
|
||||||
<p>Основная цель компании заключается в предоставлении качественного сервиса своим партнерам и покупателям, обеспечивая комфорт и надежность сделок, расширяя ассортимент товаров и услуг, а также поддерживая высокие стандарты обслуживания клиентов.</p>
|
|
||||||
|
|
||||||
<p>Таким образом, DEXARMARKET является ярким примером успешного международного проекта, демонстрирующего успешное развитие и интеграцию на глобальном уровне.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Наша миссия</h2>
|
|
||||||
<p>Мы стремимся создать уникальную экосистему, обеспечивающую максимальный комфорт и выгоду нашим партнёрам при размещении товаров на платформе DEXARMARKET. Мы предлагаем широкий спектр дополнительных услуг и сервисов, помогающих оптимизировать бизнес-процессы наших продавцов.</p>
|
|
||||||
|
|
||||||
<p>Для покупателей мы предоставляем удобный доступ к разнообразному ассортименту качественных товаров по привлекательным ценам непосредственно от производителей и поставщиков. Это позволяет клиентам экономить время и средства, выбирая лучшие предложения на рынке.</p>
|
|
||||||
|
|
||||||
<h3>Нашими основными приоритетами являются:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Создание прозрачной и эффективной системы взаимодействия между продавцами и покупателями.</li>
|
|
||||||
<li>Предоставление инновационных решений для повышения конкурентоспособности и увеличения продаж наших партнёров.</li>
|
|
||||||
<li>Обеспечение высокого уровня клиентского сервиса и поддержки пользователей на всех этапах сотрудничества.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Мы постоянно работаем над улучшением функционала нашей платформы, внедряя новые технологии и инструменты, чтобы сделать процесс покупки и продажи ещё более удобным и выгодным для всех участников рынка.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>История компании DEXARMARKET</h2>
|
|
||||||
|
|
||||||
<p>Компания DEXARMARKET возникла благодаря усилиям команды профессионалов, объединённых общей целью и опытом в ключевых областях: информационно-коммуникационных технологиях (IT), экономике и торговой отрасли. Основатели понимали важность интеграции передовых технологических решений и глубокого понимания рыночных процессов, что позволило сформировать эффективную концепцию развития маркетплейса.</p>
|
|
||||||
|
|
||||||
<h3>Основные этапы становления:</h3>
|
|
||||||
|
|
||||||
<h4>Зарождение идеи</h4>
|
|
||||||
<p>Команда, обладающая глубокими познаниями в цифровой трансформации бизнеса и экономических процессах, объединилась вокруг амбициозной цели: создание площадки, способствующей развитию предпринимательства и коммерции на международном уровне.</p>
|
|
||||||
|
|
||||||
<h4>Разработка концепции</h4>
|
|
||||||
<p>Осознавая необходимость предоставления удобных инструментов для эффективного управления бизнесом онлайн, команда приступила к разработке оригинальной модели маркетплейса, основанной на последних достижениях в области электронной коммерции и аналитики больших данных.</p>
|
|
||||||
|
|
||||||
<h4>Запуск платформы</h4>
|
|
||||||
<p>Используя инновационные решения в информационной инфраструктуре и надёжные механизмы защиты данных, команда создала стабильную цифровую среду, привлекательную для предпринимателей и конечных потребителей.</p>
|
|
||||||
|
|
||||||
<h4>Выход на международный уровень</h4>
|
|
||||||
<p>Получив признание на национальном рынке, DEXARMARKET начал своё активное продвижение на международные рынки, постепенно охватывая разные регионы и страны, создавая условия для роста малого и среднего бизнеса, а также крупных торговых компаний.</p>
|
|
||||||
|
|
||||||
<p><strong>Сегодня DEXARMARKET продолжает развиваться, совершенствуя свои технологические возможности и предлагая пользователям всё больше преимуществ и возможностей для успешной коммерческой деятельности.</strong></p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Контактная информация</h2>
|
|
||||||
<p><strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></p>
|
|
||||||
<p><strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></p>
|
|
||||||
<p><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></p>
|
|
||||||
<p><strong>Часы работы:</strong><br>Техподдержка: 24/7<br>Ответы на вопросы: 10:00 - 19:00 (МСК)</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Реквизиты организации</h2>
|
|
||||||
<p><strong>Полное наименование организации:</strong><br>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФИН ЛОГИСТИК»</p>
|
|
||||||
<p><strong>Директор:</strong> Оганнисян Ашот Рафикович</p>
|
|
||||||
<p><strong>Юридический адрес:</strong><br>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
|
|
||||||
<p><strong>Офис в Армении:</strong><br>0033, Ереван, улица Братьев Орбели, 47</p>
|
|
||||||
<p><strong>Офис в России:</strong><br>121059, Москва, наб. Тараса Шевченко, 3к2</p>
|
|
||||||
<p><strong>Основные реквизиты:</strong><br>ИНН (РФ): 9909697628<br>ИНН (Армения): 03033502<br>КПП: 770287001<br>ОГРН: 85.110.1408711</p>
|
|
||||||
<p><strong>Банковские реквизиты:</strong><br>Банк: АО "Райффайзенбанк"<br>Расчетный счет: 40807810500000002376<br>Корр. счет: 30101810200000000700<br>БИК: 044525700</p>
|
|
||||||
<p><strong>Контактная информация:</strong><br>Телефон (Россия): +7 (926) 459-31-57<br>Телефон (Армения): +374 94 86 18 16<br>Email: info@dexarmarket.ru<br>Сайт: www.dexarmarket.ru</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>Контакты</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Контактная информация</h2>
|
|
||||||
<p>Свяжитесь с нами по любым вопросам, связанным с работой маркетплейса, оформлением заказов или возвратом товаров.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Реквизиты организации</h2>
|
|
||||||
<p><strong>Наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p>
|
|
||||||
<p><strong>ИНН:</strong> 9909697628</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Телефоны</h2>
|
|
||||||
<p><strong>Россия:</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></p>
|
|
||||||
<p><strong>Армения:</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Email</h2>
|
|
||||||
<p>По всем вопросам пишите на: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></p>
|
|
||||||
<p>Мы стараемся отвечать на все письма в течение 24 часов в рабочие дни.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Часы работы</h2>
|
|
||||||
<p><strong>Техническая поддержка:</strong> Круглосуточно (24/7)</p>
|
|
||||||
<p><strong>Ответы на вопросы:</strong> Понедельник - Пятница: 10:00 - 19:00 (МСК)</p>
|
|
||||||
<p>Суббота - Воскресенье: выходной</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Адреса офисов</h2>
|
|
||||||
<p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p>
|
|
||||||
<p><strong>Офис в России:</strong> 121059, Москва, наб. Тараса Шевченко, 3к2</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Техническая поддержка</h2>
|
|
||||||
<p>При возникновении технических проблем с работой сайта или вопросов по оформлению заказа обращайтесь на <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> с подробным описанием проблемы.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>Доставка</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>1. Способы доставки</h2>
|
|
||||||
<p>1.1. <strong>Цифровые товары:</strong> Платформа DexarMarket осуществляет доставку цифровых товаров (лицензионные ключи, электронные сертификаты, цифровой контент и т.д.) на указанную Покупателем электронную почту или в личный кабинет на платформе незамедлительно после подтверждения оплаты.</p>
|
|
||||||
<p>1.2. <strong>Материальные товары:</strong> Продавец самостоятельно осуществляет доставку материальных Товаров с привлечением транспортных компаний и почтовых операторов, включая, но не ограничиваясь: <strong>АО «СДЭК»</strong>, <strong>ФГУП «Почта России»</strong>, <strong>Boxberry</strong>, <strong>DPD</strong>, <strong>Яндекс.Доставка</strong> и иных перевозчиков по согласованию с Покупателем.</p>
|
|
||||||
<p>1.3. Способы доставки материальных товаров, доступные для конкретного Заказа, определяются Продавцом на момент оформления Заказа и могут включать:</p>
|
|
||||||
<ul>
|
|
||||||
<li>курьерскую доставку (АО «СДЭК», DPD, Яндекс.Доставка, иные курьерские службы);</li>
|
|
||||||
<li>доставку в пункт выдачи (Boxberry, СДЭК, Почта России);</li>
|
|
||||||
<li>почтовую отправку (ФГУП «Почта России»);</li>
|
|
||||||
<li>самовывоз (если применимо для данного Товара).</li>
|
|
||||||
</ul>
|
|
||||||
<p>1.4. Выбор конкретного способа доставки осуществляется Покупателем в процессе оформления Заказа в пределах доступных вариантов, предложенных Продавцом.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>2. Стоимость доставки</h2>
|
|
||||||
<p>2.1. <strong>Цифровые товары:</strong> Доставка цифровых товаров осуществляется бесплатно.</p>
|
|
||||||
<p>2.2. <strong>Материальные товары:</strong> Стоимость доставки материальных товаров определяется Продавцом в зависимости от выбранного способа доставки, веса и габаритов отправления, адреса доставки и действующих тарифов перевозчика на момент оформления Заказа.</p>
|
|
||||||
<p>2.3. Итоговая стоимость доставки материальных товаров отображается Покупателю до подтверждения Заказа и включается в общую сумму к оплате.</p>
|
|
||||||
<p>2.4. В случае проведения акций и специальных предложений доставка может быть осуществлена бесплатно при выполнении условий, указанных в описании акции.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>3. Сроки доставки</h2>
|
|
||||||
<p>3.1. <strong>Цифровые товары:</strong> Доставка цифровых товаров производится мгновенно (в течение нескольких минут) после подтверждения оплаты. В отдельных случаях срок может составлять до 24 часов.</p>
|
|
||||||
<p>3.2. <strong>Материальные товары:</strong> Ориентировочные сроки доставки материальных товаров по территории Российской Федерации составляют от 1 (одного) до 14 (четырнадцати) рабочих дней с момента передачи отправления перевозчику Продавцом.</p>
|
|
||||||
<p>3.3. Указанные сроки являются приблизительными и могут изменяться в зависимости от региона доставки, работы перевозчика, погодных условий, выходных и праздничных дней.</p>
|
|
||||||
<p>3.4. Точный срок доставки материальных товаров рассчитывается автоматически при оформлении Заказа на основании данных выбранного перевозчика и доводится до сведения Покупателя.</p>
|
|
||||||
<p>3.5. Продавец не несёт ответственности за нарушение сроков доставки, вызванное действиями или бездействием перевозчика, обстоятельствами непреодолимой силы либо иными независящими от Продавца причинами.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>4. Условия передачи и приёмки Товара</h2>
|
|
||||||
<p>4.1. Товар передаётся Покупателю или указанному им лицу перевозчиком в соответствии с условиями выбранного способа доставки.</p>
|
|
||||||
<p>4.2. Риск случайной гибели или повреждения Товара переходит к Покупателю с момента фактической передачи Товара Покупателю или уполномоченному им лицу.</p>
|
|
||||||
<p>4.3. При получении Товара Покупатель обязан:</p>
|
|
||||||
<ul>
|
|
||||||
<li>проверить целостность упаковки и соответствие наименования и количества Товара данным, указанным в сопроводительных документах;</li>
|
|
||||||
<li>в случае обнаружения повреждений упаковки, недостачи или несоответствия Товара — составить акт в присутствии представителя перевозчика;</li>
|
|
||||||
<li>при отсутствии претензий — расписаться в документах перевозчика о получении Товара в надлежащем состоянии.</li>
|
|
||||||
</ul>
|
|
||||||
<p>4.4. Отправка Товара осуществляется Продавцом после подтверждения Заказа и получения оплаты (полной или частичной) в соответствии с условиями конкретного Заказа.</p>
|
|
||||||
<p>4.5. Продавец не несёт ответственности за утрату, повреждение или задержку Товара в период транспортировки, если указанные обстоятельства вызваны действиями или бездействием перевозчика. В таких случаях Покупатель вправе предъявить претензии непосредственно перевозчику в установленном законодательством порядке, а Продавец обязуется оказать содействие в урегулировании спора в разумных пределах.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>5. Контактная информация</h2>
|
|
||||||
<p>По вопросам доставки обращайтесь:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
|
|
||||||
<li><strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></li>
|
|
||||||
<li><strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>Часто задаваемые вопросы (FAQ) 📌</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Общие вопросы</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Что такое DexarMarket?</h3>
|
|
||||||
<p>DexarMarket — это онлайн-площадка, где независимые продавцы размещают свои товары и услуги. Наша задача — обеспечить удобную и безопасную среду для совершения покупок, мы сами не производим и не продаем товары.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как зарегистрироваться на платформе?</h3>
|
|
||||||
<p>Зарегистрироваться легко: откройте наше приложение внутри Telegram. Ваш профиль будет создан автоматически сразу же после входа.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Безопасно ли покупать на сайте?</h3>
|
|
||||||
<p>Абсолютно да! Все транзакции защищены современными технологиями шифрования (PCI DSS и 3D Secure). Ваши банковские данные надежно хранятся отдельно от нашей системы.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Оформление заказа</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как правильно оформить покупку?</h3>
|
|
||||||
<p>
|
|
||||||
1️⃣ Найдите понравившийся товар и добавьте его в корзину.<br>
|
|
||||||
2️⃣ Открыв корзину, кликните «Оформить заказ».<br>
|
|
||||||
3️⃣ Укажите адрес доставки и контактную информацию.<br>
|
|
||||||
4️⃣ Выберите удобный способ оплаты.<br>
|
|
||||||
5️⃣ Ознакомьтесь и подтвердите публичную оферту.<br>
|
|
||||||
6️⃣ Завершите оплату вашего заказа.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Можно ли внести изменения в заказ после завершения покупки?</h3>
|
|
||||||
<p>Если продавец ещё не отправил товар, напишите нам в поддержку — мы поможем скорректировать заказ. Но после отправки товара изменение невозможно.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как отказаться от заказа?</h3>
|
|
||||||
<p>Свяжитесь с нами или непосредственно с продавцом через чаты заказов. Пока посылка не отправлена, отказ возможен с полным возвратом денег.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Оплата 💳</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Какие способы оплаты доступны?</h3>
|
|
||||||
<p>Принимаются:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Банковские карты Visa, MasterCard, МИР,</li>
|
|
||||||
<li>Система быстрых платежей (СБП),</li>
|
|
||||||
<li>Электронные кошельки ЮMoney, QIWI,</li>
|
|
||||||
<li>Наличные при получении (если предусмотрено продавцом).</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Когда спишутся средства с моей карты?</h3>
|
|
||||||
<p>Операция проходит мгновенно, но деньги временно замораживаются до момента получения вами товара. Как только вы подтверждаете получение, средства отправляются продавцу.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Почему мой платёж мог быть отклонён?</h3>
|
|
||||||
<p>Возможные причины отказа:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Недостаточно средств на счёте,</li>
|
|
||||||
<li>Превышение лимита операций по карте,</li>
|
|
||||||
<li>Карта заблокирована банком,</li>
|
|
||||||
<li>Технические сбои.</li>
|
|
||||||
</ul>
|
|
||||||
<p>Рекомендуем обратиться в банк для выяснения деталей.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Получу ли я чек об оплате?</h3>
|
|
||||||
<p>Да, электронная квитанция придёт на указанный вами e-mail сразу после успешного платежа согласно закону № 54-ФЗ.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Доставка 🚚</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Через какие службы осуществляется доставка?</h3>
|
|
||||||
<p><strong>Цифровые товары:</strong> Платформа DexarMarket автоматически отправляет цифровые товары (лицензии, ключи, сертификаты) на вашу электронную почту или в личный кабинет мгновенно после оплаты.</p>
|
|
||||||
<p><strong>Материальные товары:</strong> Продавец самостоятельно работает с ведущими службами доставки:</p>
|
|
||||||
<ul>
|
|
||||||
<li>СДЭК,</li>
|
|
||||||
<li>Почта России,</li>
|
|
||||||
<li>Boxberry,</li>
|
|
||||||
<li>DPD,</li>
|
|
||||||
<li>Яндекс.Доставка.</li>
|
|
||||||
</ul>
|
|
||||||
<p>Способ доставки материальных товаров выбирается исходя из предпочтений продавца и места назначения.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Сколько стоит доставка моего заказа?</h3>
|
|
||||||
<p><strong>Цифровые товары:</strong> Доставка бесплатная — товар придёт на email мгновенно.</p>
|
|
||||||
<p><strong>Материальные товары:</strong> Цена определяется продавцом и зависит от веса, габаритов товара, выбранного способа и региона доставки. Окончательная стоимость видна при оформлении заказа.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Какой срок доставки моего заказа?</h3>
|
|
||||||
<p><strong>Цифровые товары:</strong> Мгновенная доставка на email (в течение нескольких минут после оплаты).</p>
|
|
||||||
<p><strong>Материальные товары</strong> (примерные сроки):</p>
|
|
||||||
<ul>
|
|
||||||
<li>СДЭК: 2–7 рабочих дней,</li>
|
|
||||||
<li>Почта России: 5–14 рабочих дней,</li>
|
|
||||||
<li>Boxberry: 2–5 рабочих дней,</li>
|
|
||||||
<li>DPD: 1–3 рабочих дня,</li>
|
|
||||||
<li>Яндекс.Доставка: в тот же день (при наличии возможности в вашем городе).</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как отслеживать мою посылку?</h3>
|
|
||||||
<p>Вы получите трек-код на свою электронную почту и сможете увидеть статус заказа в личном кабинете. Отслеживать посылку можно на официальном сайте выбранной курьерской службы.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Что делать, если товар пришёл повреждённым? ⛑</h3>
|
|
||||||
<p>Осмотрите товар прямо при курьере. Если обнаружились дефекты — оформляйте акт отказа от приёмки и незамедлительно сообщайте продавцу и службе поддержки.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Возврат и обмен ✅</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Можно ли вернуть товар?</h3>
|
|
||||||
<p>Да, закон позволяет вернуть качественный товар в течение 7 дней после получения. Бракованные изделия возвращаются по особым правилам.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Какие товары вернуть нельзя?</h3>
|
|
||||||
<p>Нельзя вернуть товары, перечисленные в Постановлении Правительства РФ №2463: медикаменты, косметику, бельё, активированные цифровые продукты и индивидуальные заказы. Подробности смотрите в разделе <a routerLink="/return-policy">«Политика возврата»</a>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как вернуть деньги?</h3>
|
|
||||||
<p>
|
|
||||||
1. Сообщите продавцу о намерении вернуть товар.<br>
|
|
||||||
2. Верните товар в оригинальной упаковке.<br>
|
|
||||||
3. Продавец проверит состояние товара и вернёт средства таким же способом, каким была произведена покупка (до 30 дней ожидания возврата).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Кто оплатит обратный путь?</h3>
|
|
||||||
<p>Покупатель оплачивает обратную доставку качественного товара. Если обнаружен брак — расходы несёт продавец.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Гарантия 🔧</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Есть ли гарантия на товары?</h3>
|
|
||||||
<p>Большинство наших товаров имеют официальную гарантию производителя. Срок варьируется от 12 до 36 месяцев и указывается в карточке каждого продукта.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как воспользоваться гарантией?</h3>
|
|
||||||
<p>Сообщите продавцу о неисправности, приложив фотографии дефектов и гарантийный талон. Продавец займётся ремонтом или заменой товара.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Что не покрыто гарантией?</h3>
|
|
||||||
<p>Механические повреждения, последствия неправильной эксплуатации, самостоятельный ремонт, влияние влаги (если нет защиты), естественный износ — всё это не является страховым случаем.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Безопасность и конфиденциальность 🔐</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как вы защищаете мои личные данные?</h3>
|
|
||||||
<p>Используем современные методы шифрования SSL/TLS, не храним реквизиты карт, выполняем требования закона №152-ФЗ о защите персональной информации. Подробности — в <a routerLink="/privacy-policy">Политике конфиденциальности</a>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Кому предоставляются мои данные?</h3>
|
|
||||||
<p>Только вашему продавцу для обработки заказа и службам доставки. Данные не используются третьими лицами для маркетинга без вашего разрешения.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как удалить свой аккаунт?</h3>
|
|
||||||
<p>Напишите нам в службу поддержки запрос на удаление учётной записи. Аккаунт будет ликвидирован вместе с личной информацией в течение месяца.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Информация для продавцов 📋</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как начать продавать на площадке?</h3>
|
|
||||||
<p>Чтобы присоединиться к нашим продавцам, обратитесь в службу поддержки по электронной почте <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a>.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Какова комиссия площадки?</h3>
|
|
||||||
<p>Комиссия зависит от типа товара и объёма продаж. Узнать точные условия можно при регистрации.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Когда поступят деньги за продажу?</h3>
|
|
||||||
<p>Продавец получает средства после подтверждения клиентом получения товара либо спустя две недели с момента доставки, если клиент не указал проблем с покупкой.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Служба поддержки 💬</h2>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Как связаться с поддержкой?</h3>
|
|
||||||
<p>
|
|
||||||
✉️ <strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a><br>
|
|
||||||
📞 <strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a><br>
|
|
||||||
📞 <strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a><br>
|
|
||||||
🏢 <strong>Время работы офиса:</strong> 10:00—19:00 (МСК)<br>
|
|
||||||
❄️ <strong>Техподдержка доступна круглосуточно.</strong>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="faq-item">
|
|
||||||
<h3>Сколько ждать ответа?</h3>
|
|
||||||
<p>Во время рабочего дня ответ поступает в течение двух часов. В праздники и выходные возможны задержки до суток.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Нужна помощь?</h2>
|
|
||||||
<p>Если возникнут дополнительные вопросы, обращайтесь на <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> — мы оперативно решим любые ваши вопросы!</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>Гарантия 🔨</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>1. Основные положения о гарантии</h2>
|
|
||||||
<p>Настоящий раздел устанавливает порядок предоставления гарантийных услуг на товары, купленные на маркетплейсе DexarMarket.</p>
|
|
||||||
<ul>
|
|
||||||
<li>Обязательства по гарантии исполняет сам Продавец товара, в строгом соответствии с российским законодательством.</li>
|
|
||||||
<li>Платформа DexarMarket выступает лишь информационным посредником и не принимает участие в исполнении гарантийных условий.</li>
|
|
||||||
<li>Гарантия действует исключительно на заводские дефекты и недостатки, возникшие не по вине покупателя.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>2. Срок гарантии 🏷</h2>
|
|
||||||
<p>Срок гарантии устанавливается Продавцом или производителем товара и публикуется:</p>
|
|
||||||
<ul>
|
|
||||||
<li>На страницах товаров нашего сайта.</li>
|
|
||||||
<li>В гарантийном талоне, вложенном в упаковку.</li>
|
|
||||||
<li>В сопроводительной документации товара.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>Типичные сроки гарантии по категориям товаров:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Электроника и бытовая техника:</strong> от 12 до 24 месяцев.</li>
|
|
||||||
<li><strong>Компьютерная техника и комплектующие:</strong> от 12 до 36 месяцев.</li>
|
|
||||||
<li><strong>Одежда и обувь:</strong> от 30 дней до полугода (зависит от сезонности).</li>
|
|
||||||
<li><strong>Мебель:</strong> от 12 до 18 месяцев.</li>
|
|
||||||
<li><strong>Цифровая продукция:</strong> поддержка определяется самим Продавцом.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Начало срока гарантии отсчитывается с момента передачи товара покупателю.</p>
|
|
||||||
<p>Замена товара продлевает гарантийный срок заново с момента выдачи замены.</p>
|
|
||||||
<p>Если срок гарантии не обозначен Продавцом, покупатель имеет право предъявлять претензии в течение 2-х лет с момента приобретения товара (согласно ст. 19 Закона РФ «О защите прав потребителей»).</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>3. Условия предоставления гарантии 📝</h2>
|
|
||||||
<p>Гарантия действительна при выполнении следующих требований:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Использование товара строго по инструкции.</li>
|
|
||||||
<li>Отсутствие самостоятельной разборки, ремонта или модификации устройства.</li>
|
|
||||||
<li>Нет механических повреждений внешнего корпуса и внутренних элементов.</li>
|
|
||||||
<li>Сохранены оригинальные пломбы и серийные номера (если предусмотрены).</li>
|
|
||||||
<li>Устройство не подвергалось влиянию высоких температур, влажности или химикатов.</li>
|
|
||||||
<li>Имеются гарантийный талон и подтверждение покупки (чек, квитанция).</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>Документы для обращения по гарантии:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li>Сам товар с полной комплектацией.</li>
|
|
||||||
<li>Гарантийный талон (если прилагался).</li>
|
|
||||||
<li>Документ, подтверждающий приобретение (чек, кассовый ордер).</li>
|
|
||||||
<li>Удостоверение личности владельца товара (например, паспорт).</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>4. Гарантийный ремонт и замена 🛠</h2>
|
|
||||||
|
|
||||||
<h3>Права покупателя при выявлении брака:</h3>
|
|
||||||
<p>Если недостаток найден в пределах гарантийного периода, вы имеете право:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Бесплатно устранить неисправность.</li>
|
|
||||||
<li>Получить аналогичный товар взамен испорченного.</li>
|
|
||||||
<li>Потребовать замену на другой товар с перерасчетом стоимости.</li>
|
|
||||||
<li>Снизить цену товара пропорционально дефекту.</li>
|
|
||||||
<li>Вернуть полную сумму за товар.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>Сроки ремонта:</h3>
|
|
||||||
<p>Ремонт выполняется быстро, но максимальный срок составляет 45 дней (статья 20 Закона РФ «О защите прав потребителей»). Если срок нарушен, вы можете попросить заменить товар или вернуть деньги.</p>
|
|
||||||
|
|
||||||
<h3>Временная замена товара:</h3>
|
|
||||||
<p>Если срок ремонта превышает неделю, продавец обязан предоставить временный заменитель для технически сложных товаров.</p>
|
|
||||||
|
|
||||||
<h3>Доставка для ремонта:</h3>
|
|
||||||
<p>Расходы на транспортировку товара в сервисный центр и обратно берет на себя продавец или специализированный сервисный центр.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>5. Случаи, не подлежащие гарантии 🔍</h2>
|
|
||||||
<p>Гарантия не работает, если выявлены следующие обстоятельства:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Механическое повреждение (удары, падение, трещины, царапины);</li>
|
|
||||||
<li>Нарушения правил эксплуатации (неправильное подключение, превышение нагрузки, нестандартное применение);</li>
|
|
||||||
<li>Повреждения из-за внешних воздействий (жидкость, грязь, высокие температуры, сырость);</li>
|
|
||||||
<li>Самостоятельный ремонт (разборка, модернизация, замена комплектующих);</li>
|
|
||||||
<li>Действие форс-мажорных обстоятельств (пожар, затопление, кража, погодные катаклизмы);</li>
|
|
||||||
<li>Естественное старение материалов (потеря цвета, блеск, незначительный износ);</li>
|
|
||||||
<li>Незаконное нарушение заводских пломб или уничтожение серийных номеров.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>Также не относятся к гарантийному случаю:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li>Косметические изъяны, не влияющие на работу (поверхностные царапинки, небольшие пятна);</li>
|
|
||||||
<li>Изменения внешнего вида вследствие обычной эксплуатации;</li>
|
|
||||||
<li>Программные неполадки, вызванные установками постороннего программного обеспечения;</li>
|
|
||||||
<li>Проблемы совместимости с устройствами или ПО других производителей.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Отдельно оговорено ограничение гарантии на расходники (батарейки, лампочки, фильтры), указанные в описании товара.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>6. Процедура подачи заявки на гарантийное обслуживание 🗒</h2>
|
|
||||||
<p>Чтобы воспользоваться гарантийным сервисом, выполните следующие шаги:</p>
|
|
||||||
<ol>
|
|
||||||
<li>Связаться с продавцом через контактные данные, указанные в вашем заказе.</li>
|
|
||||||
<li>Объяснить суть проблемы и приложить фотоматериалы или видеозапись (если необходимо).</li>
|
|
||||||
<li>Получить от продавца инструкцию по обращению в сервисный центр или адрес, куда отправить товар.</li>
|
|
||||||
<li>Доставить товар в сервис с документами, подтверждающими покупку (гарантийный талон, чек).</li>
|
|
||||||
<li>Получить акт приёма товара с указанием срока ремонтных работ.</li>
|
|
||||||
<li>Забрать восстановленный товар после оповещения о завершении ремонта.</li>
|
|
||||||
</ol>
|
|
||||||
|
|
||||||
<h3>Правила отправки товара почтой или курьером:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Надежно упакуйте устройство, предотвращая возможные повреждения при перевозке.</li>
|
|
||||||
<li>Положите копии документов о покупке и подробное описание проблемы внутрь упаковки.</li>
|
|
||||||
<li>Оформите почтовое отправление с оценочной стоимостью.</li>
|
|
||||||
<li>Обязательно сохраняйте номер трека для контроля местонахождения груза.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Если возникли трудности, покупатели также могут обращаться в нашу службу поддержки через email: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a>.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>7. Дополнительные права покупателя 🎯</h2>
|
|
||||||
<p>Если товар имеет серьёзный недостаток, вы вправе:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Требовать полного возврата денег.</li>
|
|
||||||
<li>Просить замену на товар другой модели с соответствующим перерасчётом стоимости.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>Серьёзный недостаток</strong> — это ситуация, когда:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Невозможно исправить поломку.</li>
|
|
||||||
<li>Устранение поломки требует больших затрат или долгого времени.</li>
|
|
||||||
<li>Недостаток появляется снова после ремонта.</li>
|
|
||||||
<li>Одна и та же проблема возникает многократно.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>Кроме того, вы можете взыскать убытки, понесённые из-за продажи некачественного товара.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>8. Контактная информация 📞</h2>
|
|
||||||
<p>По любым вопросам гарантийного обслуживания обращайтесь сначала к продавцу (см. страницу товара или ваше уведомление о доставке).</p>
|
|
||||||
|
|
||||||
<p><strong>Если возникла необходимость решения спора:</strong></p>
|
|
||||||
<p>Отправьте письмо на email Маркетплейса: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> с темой: «Гарантийный вопрос — Заказ №[номер заказа]».</p>
|
|
||||||
|
|
||||||
<p>В случае отказа продавца принять претензию, вы имеете право инициировать независимую экспертизу качества товара и подать иск в судебные органы.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>Реквизиты организации</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Полное наименование организации</h2>
|
|
||||||
<p>ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ИНТ ФИН ЛОГИСТИК»</p>
|
|
||||||
<p><strong>Сокращенное наименование:</strong> ООО «ИНТ ФИН ЛОГИСТИК»</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Юридический адрес</h2>
|
|
||||||
<p>АРМЕНИЯ, 2301, КОТАЙКСКАЯ ОБЛАСТЬ, РАЗДАН, ХАЧАТРЯНА ул, 31, 4</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Фактический адрес</h2>
|
|
||||||
<p><strong>Офис в Армении:</strong> 0033, Ереван, улица Братьев Орбели, 47</p>
|
|
||||||
<p><strong>Офис в России:</strong> 121059, Москва, наб. Тараса Шевченко, 3к2</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Основные реквизиты</h2>
|
|
||||||
<div class="details-grid">
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>ИНН (RUS):</strong>
|
|
||||||
<span>9909697628</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>ИНН (ARM):</strong>
|
|
||||||
<span>03033502</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>КПП:</strong>
|
|
||||||
<span>770287001</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>ОГРН:</strong>
|
|
||||||
<span>85.110.1408711</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Банковские реквизиты</h2>
|
|
||||||
<div class="details-grid">
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Банк:</strong>
|
|
||||||
<span>АО "Райффайзенбанк"</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Расчетный счет:</strong>
|
|
||||||
<span>40807810500000002376</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Корр. счет:</strong>
|
|
||||||
<span>30101810200000000700</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>БИК:</strong>
|
|
||||||
<span>044525700</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Контактная информация</h2>
|
|
||||||
<div class="details-grid">
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Телефон (RUS):</strong>
|
|
||||||
<span><a href="tel:+79264593157">+7 (926) 459-31-57</a></span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Телефон (ARM):</strong>
|
|
||||||
<span><a href="tel:+37494861816">+374 94 86 18 16</a></span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Email:</strong>
|
|
||||||
<span><a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Сайт:</strong>
|
|
||||||
<span><a href="https://dexarmarket.ru" target="_blank">dexarmarket.ru</a></span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Часы работы офиса:</strong>
|
|
||||||
<span>10:00 - 19:00 (МСК)</span>
|
|
||||||
</div>
|
|
||||||
<div class="detail-item">
|
|
||||||
<strong>Техподдержка:</strong>
|
|
||||||
<span>24/7</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Руководство</h2>
|
|
||||||
<p><strong>Генеральный директор:</strong> Оганнисян Ашот Рафикович</p>
|
|
||||||
<p><strong>Основание действий:</strong> Устав</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>Правила оплаты</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>1. Общие положения</h2>
|
|
||||||
<p>1.1. Настоящие Правила определяют порядок оплаты Товаров и Услуг, приобретаемых Покупателями через Маркетплейс DexarMarket.</p>
|
|
||||||
<p>1.2. Оплата производится за Товары/Услуги, размещенные независимыми Продавцами. Маркетплейс выступает в качестве информационного посредника и обеспечивает техническую инфраструктуру для проведения платежей.</p>
|
|
||||||
<p>1.3. Оплата товаров и услуг на Маркетплейсе осуществляется в российских рублях (RUB).</p>
|
|
||||||
<p>1.4. Цены на Товары/Услуги устанавливаются Продавцами самостоятельно и указываются на странице соответствующего Товара/Услуги.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>2. Способы оплаты</h2>
|
|
||||||
<p>2.1. Маркетплейс поддерживает следующие способы оплаты:</p>
|
|
||||||
|
|
||||||
<div class="payment-methods">
|
|
||||||
<div class="payment-logos">
|
|
||||||
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="80" height="50" />
|
|
||||||
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="80" height="50" />
|
|
||||||
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="80" height="50" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><strong>Банковские карты:</strong> Visa, Mastercard, МИР</li>
|
|
||||||
<li><strong>Системы быстрых платежей (СБП):</strong> мгновенный перевод через мобильное приложение банка</li>
|
|
||||||
<li><strong>Электронные кошельки:</strong> ЮMoney, QIWI (при наличии)</li>
|
|
||||||
<li><strong>Оплата по ссылке:</strong> генерация уникальной платежной ссылки для каждого заказа</li>
|
|
||||||
</ul>
|
|
||||||
<p>2.2. Доступные способы оплаты могут различаться в зависимости от Продавца и типа Товара/Услуги.</p>
|
|
||||||
<p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>3. Процесс оплаты</h2>
|
|
||||||
<p>3.1. Процедура оплаты заказа включает следующие этапы:</p>
|
|
||||||
<ol>
|
|
||||||
<li>Выбор Товаров/Услуг и добавление их в корзину</li>
|
|
||||||
<li>Оформление заказа с указанием контактных данных и способа доставки (при необходимости)</li>
|
|
||||||
<li>Выбор способа оплаты из доступных вариантов</li>
|
|
||||||
<li>Перенаправление на защищенную страницу платежной системы или получение платежной ссылки</li>
|
|
||||||
<li>Ввод платежных данных и подтверждение оплаты</li>
|
|
||||||
<li>Получение уведомления об успешной оплате</li>
|
|
||||||
</ol>
|
|
||||||
<p>3.2. При оплате банковской картой Покупатель может быть перенаправлен на страницу банка-эмитента для прохождения дополнительной аутентификации (3D-Secure).</p>
|
|
||||||
<p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>4. Безопасность платежей</h2>
|
|
||||||
<p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p>
|
|
||||||
<p>4.2. Маркетплейс не хранит полные данные банковских карт Покупателей. Обработка платежных данных осуществляется сертифицированными платежными агрегаторами.</p>
|
|
||||||
<p>4.3. Для защиты от мошенничества применяется технология 3D-Secure, требующая подтверждения платежа через SMS-код или push-уведомление от банка.</p>
|
|
||||||
<p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>5. Подтверждение оплаты</h2>
|
|
||||||
<p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p>
|
|
||||||
<p>5.2. Подтверждение содержит следующую информацию:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Номер заказа</li>
|
|
||||||
<li>Дата и время оплаты</li>
|
|
||||||
<li>Сумма платежа</li>
|
|
||||||
<li>Состав заказа</li>
|
|
||||||
<li>Контактные данные Продавца</li>
|
|
||||||
</ul>
|
|
||||||
<p>5.3. Информация о заказе также отображается в личном кабинете Покупателя на Маркетплейсе (при наличии регистрации).</p>
|
|
||||||
<p>5.4. Фискальный чек направляется Продавцом в соответствии с требованиями законодательства РФ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>6. Возврат средств</h2>
|
|
||||||
<p>6.1. Порядок возврата денежных средств регулируется <a routerLink="/return-policy">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p>
|
|
||||||
<p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p>
|
|
||||||
<p>6.3. Срок возврата денежных средств составляет:</p>
|
|
||||||
<ul>
|
|
||||||
<li>На банковскую карту: от 3 до 30 банковских дней (в зависимости от банка-эмитента)</li>
|
|
||||||
<li>На электронный кошелек: от 1 до 5 рабочих дней</li>
|
|
||||||
<li>Через СБП: от 1 до 3 рабочих дней</li>
|
|
||||||
</ul>
|
|
||||||
<p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>7. Неуспешные платежи</h2>
|
|
||||||
<p>7.1. Платеж может быть отклонен по следующим причинам:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Недостаточно средств на счете</li>
|
|
||||||
<li>Неверно введены платежные данные</li>
|
|
||||||
<li>Карта заблокирована или просрочена</li>
|
|
||||||
<li>Превышены лимиты на операции, установленные банком</li>
|
|
||||||
<li>Отказ в проведении транзакции системой безопасности</li>
|
|
||||||
</ul>
|
|
||||||
<p>7.2. В случае неуспешной оплаты Покупатель получает уведомление с указанием причины отказа.</p>
|
|
||||||
<p>7.3. При возникновении проблем с оплатой рекомендуется:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Проверить правильность введенных данных</li>
|
|
||||||
<li>Связаться с банком-эмитентом карты для уточнения причины отказа</li>
|
|
||||||
<li>Попробовать альтернативный способ оплаты</li>
|
|
||||||
<li>Обратиться в службу поддержки: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>8. Контакты для вопросов по оплате</h2>
|
|
||||||
<p>По вопросам, связанным с оплатой заказов, вы можете обратиться:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
|
|
||||||
<li><strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></li>
|
|
||||||
<li><strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></li>
|
|
||||||
<li><strong>Время работы:</strong> Круглосуточно (техническая поддержка)</li>
|
|
||||||
<li><strong>Среднее время ответа:</strong> До 24 часов в рабочие дни</li>
|
|
||||||
</ul>
|
|
||||||
<p>При обращении указывайте номер заказа и краткое описание проблемы для более быстрого решения вопроса.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>ПОЛИТИКА В ОТНОШЕНИИ ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>1. ОБЩИЕ ПОЛОЖЕНИЯ</h2>
|
|
||||||
|
|
||||||
<p>1.1. Настоящая политика ООО "ИНТ ФИН ЛОГИСТИК" (ИНН 9909697628), именуемого далее как «Оператор», описывает порядок обработки персональных данных и направлена на защиту прав и законных интересов субъектов данных. Документ разработан в соответствии с Федеральным законом №152-ФЗ от 27 июля 2006 года «О персональных данных».</p>
|
|
||||||
|
|
||||||
<p>1.2. Политика определяет порядок и меры обеспечения безопасности обработки персональных данных на сайте <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>, ставя своей задачей защитить права и свободы человека и гражданина, включая право на неприкосновенность частной жизни, личную и семейную тайны.</p>
|
|
||||||
|
|
||||||
<p>1.3. Документ охватывает все процессы, осуществляемые Оператором, касающиеся обработки персональных данных.</p>
|
|
||||||
|
|
||||||
<p>1.4. Политика обязательна для изучения и выполнения всеми лицами, допущенными к обработке персональных данных.</p>
|
|
||||||
|
|
||||||
<p>1.5. Действует в отношении всех действий, связанных с обработкой персональных данных на сайте <a href="https://dexarmarket.ru">https://dexarmarket.ru</a> и в информационных системах Оператора.</p>
|
|
||||||
|
|
||||||
<p>1.6. Пользователь, оформляющий заказ, открывающий личный кабинет или иным образом взаимодействующий с Оператором, выражает согласие на обработку своих персональных данных в соответствии с Политикой и законодательством РФ. Продолжительное использование сайта свидетельствует о согласии с Положениями Политики. Пользователь, не готовый согласиться с условиями, должен воздержаться от использования ресурса.</p>
|
|
||||||
|
|
||||||
<p><strong>Дополнительно:</strong> Настоящая Политика распространяется на персональные данные, собранные как до вступления документа в силу, так и после.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>2. ТЕРМИНЫ И ОПРЕДЕЛЕНИЯ</h2>
|
|
||||||
|
|
||||||
<p>В данной Политике используются следующие основные термины и определения:</p>
|
|
||||||
|
|
||||||
<p><strong>Персональные данные (ПДн)</strong> — любая информация, прямо или косвенно относящаяся к определённому физическому лицу (субъекту персональных данных).</p>
|
|
||||||
|
|
||||||
<p><strong>Информационная система персональных данных (ИСПДн)</strong> — совокупность персональных данных, хранимых в базах данных, а также технологий и средств для их обработки.</p>
|
|
||||||
|
|
||||||
<p><strong>Автоматизированная обработка ПДн</strong> — обработка данных с использованием компьютерных средств.</p>
|
|
||||||
|
|
||||||
<p><strong>Блокировка ПДн</strong> — временная приостановка обработки данных (за исключением случаев уточнения данных).</p>
|
|
||||||
|
|
||||||
<p><strong>Обезличивание ПДн</strong> — действия, ведущие к невозможности установления принадлежности данных конкретному лицу без дополнительной информации.</p>
|
|
||||||
|
|
||||||
<p><strong>Интернет-сайт (Сайт)</strong> — автоматизированная информационная система, доступная в сети Интернет по адресу: <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p>
|
|
||||||
|
|
||||||
<p><strong>Обработка ПДн</strong> — любые действия с персональными данными, включая сбор, запись, хранение, обновление, использование, передачу, уничтожение и другие операции.</p>
|
|
||||||
|
|
||||||
<p><strong>Оператор</strong> — государственный или частный орган, самостоятельно или совместно организующий обработку персональных данных.</p>
|
|
||||||
|
|
||||||
<p><strong>Предоставление ПДн</strong> — передача данных определённому лицу или группе лиц.</p>
|
|
||||||
|
|
||||||
<p><strong>Распространение ПДн</strong> — открытие данных неопределённому кругу лиц, включая публикацию в СМИ или сети Интернет.</p>
|
|
||||||
|
|
||||||
<p><strong>Трансграничная передача ПДн</strong> — передача данных зарубежным властям, компаниям или физическим лицам.</p>
|
|
||||||
|
|
||||||
<p><strong>Уничтожение ПДн</strong> — действия, приводящие к утрате возможности восстановления данных или уничтожения материальных носителей.</p>
|
|
||||||
|
|
||||||
<p><strong>Субъект ПДн</strong> — физическое лицо, чья информация обрабатывается.</p>
|
|
||||||
|
|
||||||
<p><strong>Конфиденциальность ПДн</strong> — обязанность Оператора защищать данные от распространения без согласия субъекта или законного основания.</p>
|
|
||||||
|
|
||||||
<p><strong>Продавец (Исполнитель)</strong> — лицо, предлагающее товары или услуги на сайте <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p>
|
|
||||||
|
|
||||||
<p><strong>Пользователь</strong> — лицо, посещающее или использующее ресурсы, управляемые Оператором, включая сайт <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>.</p>
|
|
||||||
|
|
||||||
<p><strong>Заказ</strong> — оформленный Пользователем заказ товаров или услуг на сайте.</p>
|
|
||||||
|
|
||||||
<p><strong>Файлы cookie</strong> — небольшие файлы, сохраняемые на устройстве пользователя для запоминания предпочтений и действий при последующих посещениях сайта.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>3. ПРАВОВЫЕ ОСНОВАНИЯ ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
|
|
||||||
|
|
||||||
<p>3.1. Правовым основанием Обработки ПДн в зависимости от целей процесса, предусматривающего Обработку ПДн может являться:</p>
|
|
||||||
|
|
||||||
<h3>3.1.1. Конституция Российской Федерации, а также совокупность правовых актов:</h3>
|
|
||||||
<ul>
|
|
||||||
<li>Налоговый кодекс Российской Федерации;</li>
|
|
||||||
<li>Гражданский Кодекс Российской Федерации;</li>
|
|
||||||
<li>ст. 86 - 90 Трудового кодекса Российской Федерации;</li>
|
|
||||||
<li>Федеральный закон от 07.08.2001 № 115-ФЗ «О противодействии легализации (отмыванию) доходов, полученных преступным путем, и финансированию терроризма»;</li>
|
|
||||||
<li>Федеральный закон от 27.07.2006 г. № 152-ФЗ «О персональных данных»;</li>
|
|
||||||
<li>Федеральный закон от 22.04.1996 № 39-ФЗ «О рынке ценных бумаг»;</li>
|
|
||||||
<li>Федеральный закон от 26.12.1995 №208-ФЗ «Об акционерных обществах»;</li>
|
|
||||||
<li>Федеральный закон от 27.07.2006 № 149-ФЗ «Об информации, информационных технологиях и о защите информации»;</li>
|
|
||||||
<li>Федеральный закон от 01.04.1996 № 27-ФЗ «Об индивидуальном (персонифицированном) учете в системе обязательного пенсионного страхования»;</li>
|
|
||||||
<li>Федеральный закон от 06.04.2011 № 63-ФЗ «Об электронной подписи»;</li>
|
|
||||||
<li>Федеральный закон от 06.12.2011 № 402-ФЗ «О бухгалтерском учете»;</li>
|
|
||||||
<li>Федеральный закон от 27.06.2011 № 161-ФЗ «О национальной платежной системе»;</li>
|
|
||||||
<li>Постановление Правительства Российской Федерации от 15.09.2008 № 687 «Об утверждении положения об особенностях обработки персональных данных, осуществляемой без использования средств автоматизации»;</li>
|
|
||||||
<li>Постановлением Правительства РФ от 01.11.2012 № 1119 "Об утверждении требований к защите персональных данных при их обработке в информационных системах персональных данных"</li>
|
|
||||||
<li>иные нормативные правовые акты Российской Федерации и нормативные документы исполнительных органов государственной власти.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>3.1.2. Устав Оператора.</p>
|
|
||||||
|
|
||||||
<p>3.1.3. Договоры, заключаемые между Оператором и Субъектом Персональных данных, в том числе в случае реализации Оператором своего права на уступку прав (требований) по таким договорам, между Оператором и иным лицом, поручившим Оператору Обработку ПДн, а также для заключения договоров, стороной которых являются Субъекты Персональных данных.</p>
|
|
||||||
|
|
||||||
<p>3.1.4. Согласие на Обработку ПДн (в случаях, прямо не предусмотренных законодательством Российской Федерации, но соответствующих полномочиям Оператора), в т.ч. согласие соискателей на замещение вакантных должностей на Обработку ПДн, согласие практиканта на Обработку ПДн, согласие работников на Обработку ПДн; согласие клиентов на Обработку ПДн, согласие Пользователей соответствующего Сайта, согласие иных Субъектов Персональных данных.</p>
|
|
||||||
|
|
||||||
<p>3.1.5. Договор между оператором и третьим лицом, где последнее поручает Оператору обработку персональных данных Субъекта Персональных данных или передает на основании заключенного договора персональные данные Субъекта Персональных данных.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>4. КАТЕГОРИИ СУБЪЕКТОВ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
|
|
||||||
|
|
||||||
<p>4.1. Оператором осуществляется Обработка полученных в установленном законом порядке ПДн, принадлежащих:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Кандидатам на работу и работникам Оператора;</li>
|
|
||||||
<li>Уволенным работникам Оператора;</li>
|
|
||||||
<li>Близким родственникам/членам семьи работников Оператора, практикантам;</li>
|
|
||||||
<li>Потенциальным клиентам, клиентам - физическим лицам, клиентам - индивидуальным предпринимателям;</li>
|
|
||||||
<li>Физическим лицам, заключившим с Оператором гражданско-правовые договоры;</li>
|
|
||||||
<li>Пользователям Сайтов Оператора, получателям Заказов;</li>
|
|
||||||
<li>Клиентам других юридических лиц;</li>
|
|
||||||
<li>Собственникам Оператора;</li>
|
|
||||||
<li>Иным субъектам, вступившим или намеревающимися вступить в договорные отношения с Оператором.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>5. КАТЕГОРИИ ОБРАБАТЫВАЕМЫХ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
|
|
||||||
|
|
||||||
<p>5.1. Оператор обрабатывает следующие категории ПДн Пользователей:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>Сведения, полученные при регистрации и/или оформлении Заказа (фамилию, имя, фактический адрес, номер телефона, адрес электронной почты, cookie);</li>
|
|
||||||
<li>Сведения, полученные при взаимодействии с Пользователями (фамилия, имя, отчество, пол, место рождения, дата рождения, паспортные данные, адрес, контакты);</li>
|
|
||||||
<li>Сведения о способе доставки и оплаты Заказа;</li>
|
|
||||||
<li>Сведения о претензиях Пользователя;</li>
|
|
||||||
<li>Сведения о геолокации (местонахождении).</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>5.2. Персональные данные могут быть получены Оператором путем:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Предоставления Субъектами через формы на Сайте или по email;</li>
|
|
||||||
<li>Получения от третьих лиц (Продавцов, контрагентов) в рамках законодательства РФ.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>6. ПРИНЦИПЫ, ПОРЯДОК И УСЛОВИЯ ОБРАБОТКИ</h2>
|
|
||||||
|
|
||||||
<h3>6.1. Принципы обработки Персональных данных</h3>
|
|
||||||
<p>Обработка ПДн у Оператора осуществляется на основе следующих принципов:</p>
|
|
||||||
<ul>
|
|
||||||
<li>законности и справедливой основы;</li>
|
|
||||||
<li>ограничения Обработки ПДн достижением конкретных, заранее определенных и законных целей;</li>
|
|
||||||
<li>недопущения Обработки ПДн, несовместимой с целями сбора ПДн;</li>
|
|
||||||
<li>обеспечения точности, достаточности и актуальности ПДн;</li>
|
|
||||||
<li>хранение ПДн не дольше, чем этого требуют цели Обработки;</li>
|
|
||||||
<li>уничтожения либо обезличивания ПДн по достижении целей их Обработки.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>6.2. Обязанности работников Оператора</h3>
|
|
||||||
<p>Работники Оператора, допущенные к Обработке Персональных данных, обязаны:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Знать и выполнять положения законодательства РФ в области ПДн;</li>
|
|
||||||
<li>Знать и выполнять положения настоящей Политики;</li>
|
|
||||||
<li>Обрабатывать ПДн только в рамках должностных обязанностей;</li>
|
|
||||||
<li>Не разглашать ПДн, обрабатываемые Оператором;</li>
|
|
||||||
<li>Сообщать о действиях, которые могут привести к нарушению Политики.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>7. ПРАВА СУБЪЕКТА ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
|
|
||||||
|
|
||||||
<h3>7.1. Согласие Субъекта</h3>
|
|
||||||
<p>Субъект ПДн дает согласие на Обработку свободно, своей волей и в своем интересе.</p>
|
|
||||||
|
|
||||||
<h3>7.2. Права Субъекта</h3>
|
|
||||||
<p>Субъект ПДн имеет право на получение информации о:</p>
|
|
||||||
<ul>
|
|
||||||
<li>подтверждение факта Обработки ПДн;</li>
|
|
||||||
<li>правовые основания и цели Обработки;</li>
|
|
||||||
<li>применяемые способы Обработки;</li>
|
|
||||||
<li>сроки Обработки и хранения данных;</li>
|
|
||||||
<li>уточнение, блокирование или уничтожение данных.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>Субъект ПДн имеет право отозвать свое Согласие</strong> и потребовать удалить свои ПДн, направив сообщение на: <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></p>
|
|
||||||
|
|
||||||
<p>Субъект также имеет право на защиту своих прав, возмещение убытков и компенсацию морального вреда.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>8. ОБЯЗАННОСТИ ОПЕРАТОРА</h2>
|
|
||||||
|
|
||||||
<p>8.1. Оператор обязан предоставить Субъекту информацию о обработке его данных при обращении.</p>
|
|
||||||
|
|
||||||
<p>8.2. Оператор обеспечивает хранение данных граждан РФ на территории Российской Федерации.</p>
|
|
||||||
|
|
||||||
<p>8.3. Оператор несет иные обязанности, установленные ФЗ-152.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>9. ОБЕСПЕЧЕНИЕ БЕЗОПАСНОСТИ</h2>
|
|
||||||
|
|
||||||
<p>9.1-9.3. Безопасность обеспечивается комплексом организационных и технических мер:</p>
|
|
||||||
<ul>
|
|
||||||
<li>назначение ответственных лиц;</li>
|
|
||||||
<li>ограничение доступа к ПДн;</li>
|
|
||||||
<li>обучение сотрудников;</li>
|
|
||||||
<li>учет и хранение носителей с ПДн;</li>
|
|
||||||
<li>использование антивирусных средств;</li>
|
|
||||||
<li>средства шифрования и межсетевого экранирования;</li>
|
|
||||||
<li>физическая защита помещений;</li>
|
|
||||||
<li>контроль мер безопасности.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>10. ОТВЕТСТВЕННОСТЬ</h2>
|
|
||||||
|
|
||||||
<p>10.1. Лица, виновные в нарушении норм обработки и защиты Персональных данных, несут ответственность согласно законодательству РФ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>11. ЦЕЛИ ОБРАБОТКИ</h2>
|
|
||||||
|
|
||||||
<p>11.1. Оператор обрабатывает ПДн для следующих целей:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Предоставления доступа к Сайту и личному кабинету;</li>
|
|
||||||
<li>Исполнения договоров купли-продажи и оказания услуг;</li>
|
|
||||||
<li>Доставки товаров;</li>
|
|
||||||
<li>Урегулирования претензий;</li>
|
|
||||||
<li>Обработки платежей;</li>
|
|
||||||
<li>Улучшения качества обслуживания;</li>
|
|
||||||
<li>Получения обратной связи;</li>
|
|
||||||
<li>Проведения маркетинговых исследований;</li>
|
|
||||||
<li>Направления рекламных сообщений;</li>
|
|
||||||
<li>В иных случаях, предусмотренных законодательством РФ.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>12. АВТОМАТИЧЕСКИ СОБИРАЕМАЯ ИНФОРМАЦИЯ</h2>
|
|
||||||
|
|
||||||
<p>12.1. Оператор собирает и обрабатывает:</p>
|
|
||||||
<ul>
|
|
||||||
<li>информацию об интересах на основе поисковых запросов;</li>
|
|
||||||
<li>информацию для формирования рейтинга (отзывы, данные об исполнении Заказов);</li>
|
|
||||||
<li>статистику об использовании Сайта.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>12.2. Оператор использует cookies, веб-отметки и другие технологии мониторинга. Эти технологии не дают возможность автоматически получать ПДн.</p>
|
|
||||||
|
|
||||||
<p>12.3. Если собранные сведения можно соотнести с личным кабинетом Пользователя, они могут обрабатываться совместно с ПДн.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>13. КОНТАКТНАЯ ИНФОРМАЦИЯ</h2>
|
|
||||||
|
|
||||||
<p>По всем вопросам обработки персональных данных обращайтесь:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Email:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
|
|
||||||
<li><strong>Телефон (Россия):</strong> <a href="tel:+79264593157">+7 (926) 459-31-57</a></li>
|
|
||||||
<li><strong>Телефон (Армения):</strong> <a href="tel:+37494861816">+374 94 86 18 16</a></li>
|
|
||||||
</ul>
|
|
||||||
<p>Мы ответим в течение 30 дней согласно законодательству РФ.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>СОГЛАШЕНИЕ ПУБЛИЧНОЙ ОФЕРТЫ</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>Основные понятия</h2>
|
|
||||||
|
|
||||||
<p>В данном документе приводятся ключевые термины и пояснения, применяемые в публичном соглашении:</p>
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li><strong>Маркетплейс, Интернет-сайт или Сайт</strong> — это комплекс технических и программных средств, предназначенный для функционирования веб-ресурса по адресу <a href="https://dexarmarket.ru">https://dexarmarket.ru</a>. Сайт принадлежит владельцу и служит платформой для организации взаимодействия между независимыми продавцами и покупателями. Сам владелец сайта не занимается продажей товаров и услуг, а предоставляет пространство для размещения объявлений и заключения сделок.</li>
|
|
||||||
|
|
||||||
<li><strong>Пользователь</strong> — физлицо, которое пользуется сайтом, регистрируясь и принимая условия данного соглашения.</li>
|
|
||||||
|
|
||||||
<li><strong>Личный кабинет</strong> — персональная страница Пользователя, защищённая индивидуальными регистрационными данными (логином и паролем). Логином может выступать электронная почта или мобильный телефон, используемый для авторизации.</li>
|
|
||||||
|
|
||||||
<li><strong>Учетные данные</strong> — комбинация имени пользователя (логина) и пароля, создаваемая при регистрации.</li>
|
|
||||||
|
|
||||||
<li><strong>Покупатель</strong> — зарегистрированный Пользователь, который сделал заказ через сайт.</li>
|
|
||||||
|
|
||||||
<li><strong>Администратор или Владелец Сайта</strong> — юридическое лицо ООО «ИНТ ФИН ЛОГИСТИК», идентификационный налоговый номер (ИНН) 9909697628.</li>
|
|
||||||
|
|
||||||
<li><strong>Продавец или Селлер</strong> — физические или юридические лица, предприниматели, предлагающие товары и услуги на ресурсе. Продавцы несут личную ответственность за качество, безопасность и соответствие описания товаров.</li>
|
|
||||||
|
|
||||||
<li><strong>Контент Сайта</strong> — совокупные материалы, находящиеся на ресурсе, включая дизайн, тексты, графику, видео, аудиозаписи и прочие объекты.</li>
|
|
||||||
|
|
||||||
<li><strong>Товар</strong> — материальные и цифровые продукты, а также услуги, предлагаемые на продажу через сайт.</li>
|
|
||||||
|
|
||||||
<li><strong>Цифровой товар</strong> — товары, распространяемые в электронной форме, такие как ПО, подписка на онлайн-курсы, виртуальные активы, музыкальные треки, электронные книги и подобные виды продукции.</li>
|
|
||||||
|
|
||||||
<li><strong>Заказ</strong> — оформленное заявление на приобретение товаров или услуг от Покупателя.</li>
|
|
||||||
|
|
||||||
<li><strong>Сервис</strong> — специальное программное обеспечение, интегрированное в ресурс, дающее возможность пользоваться функциональностью сайта.</li>
|
|
||||||
|
|
||||||
<li><strong>Продукты Владельца Сайта</strong> — информационные и сопутствующие услуги, предоставляемые владельцем сайта.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>1. Общие положения</h2>
|
|
||||||
|
|
||||||
<p>1.1. Настоящим документом устанавливается порядок использования ресурсов, принадлежащих администратору, включая сайт, мобильные версии и приложения, управляемые Администрацией.</p>
|
|
||||||
|
|
||||||
<p>1.2. Документ является обязательным соглашением между Пользователем и Владельцем ресурса.</p>
|
|
||||||
|
|
||||||
<p>1.3. Настоящее соглашение и информация о продуктах, размещённая на сайте, соответствуют определению публичной оферты согласно статье 435 и пункту 2 статьи 437 ГК РФ.</p>
|
|
||||||
|
|
||||||
<p>1.4. Пользователь соглашается с условиями договора автоматически, начиная с первого посещения сайта, регистрации или оформления заказа без авторизации, нажатия кнопок «Оформить заказ», «Отправить сообщение» или «Купить».</p>
|
|
||||||
|
|
||||||
<p>1.5. Заключение соглашения путём акцептования оферты не требует подписи сторон и признается действительным в электронном виде.</p>
|
|
||||||
|
|
||||||
<p>1.6. Использование сайта подразумевает согласие с условиями соглашения, вступающего в силу сразу после выражения согласия пользователем.</p>
|
|
||||||
|
|
||||||
<p>1.7. В случае несогласия с условиями Пользователь обязуется незамедлительно прекратить пользование ресурсом.</p>
|
|
||||||
|
|
||||||
<p>1.8. Дополнительно регулирование использования сайта осуществляется <a routerLink="/privacy-policy">Политикой обработки персональных данных</a>.</p>
|
|
||||||
|
|
||||||
<p>1.9. Изменения в соглашение могут вноситься Владельцем без предварительного уведомления и становятся обязательными с момента публикации изменений.</p>
|
|
||||||
|
|
||||||
<p>1.10. Во время проведения рекламных кампаний могут устанавливаться особые условия оформления заказов, возврата или обмена товаров.</p>
|
|
||||||
|
|
||||||
<p>1.14. Оферта предназначена для любых пользователей ресурса, включая юридических лиц и индивидуальных предпринимателей.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>2. Предмет соглашения</h2>
|
|
||||||
|
|
||||||
<p>2.1. Целью соглашения является предоставление возможности Пользователям покупать товары и услуги, представляемые на ресурсе, а также использование продуктов, предоставляемых Владельцем сайта.</p>
|
|
||||||
|
|
||||||
<p>2.2. Соглашение регулирует порядок использования сайта и функций, предоставляемых Владельцем.</p>
|
|
||||||
|
|
||||||
<p>2.3. Действие документа распространяется на все типы товаров, услуг и продуктов, имеющихся на сайте.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>3. Условия продажи товаров и оказания услуг</h2>
|
|
||||||
|
|
||||||
<p><strong>3.1. Принятие условий продажи</strong></p>
|
|
||||||
<p>Пользователь, делая заказы через маркетплейс, выражает полное согласие с условиями продажи товаров и оказания услуг, определёнными в данном соглашении.</p>
|
|
||||||
|
|
||||||
<p><strong>3.2. Заключение договоров</strong></p>
|
|
||||||
<p>Договор розничной купли-продажи или договор оказания услуг заключается непосредственно между Продавцом и Покупателем с момента выдачи Продавцом кассовых или товарных чеков, подтверждающих оплату. Маркетплейс играет роль информационного посредника, предоставляя инфраструктуру для совершения сделок, но не является участником данного договора. Ответственность за выполнение договора, качество товаров и услуг лежит на Продавце.</p>
|
|
||||||
|
|
||||||
<p><strong>3.3. Согласие на обработку контактов</strong></p>
|
|
||||||
<p>Пользователь даёт согласие на использование его контактных данных (адрес электронной почты, номер телефона) администрацией сайта и Продавцом, а также привлечёнными третьими сторонами для выполнения обязательств перед Покупателем, включая рассылки рекламной и иной информации.</p>
|
|
||||||
|
|
||||||
<p><strong>3.4. Привлечение третьих лиц</strong></p>
|
|
||||||
<p>Пользователь соглашается с возможностью привлечения Продавцом третьих лиц для исполнения договора купли-продажи или оказания услуг, сохраняя ответственность Продавца за выполнение обязательств.</p>
|
|
||||||
|
|
||||||
<p><strong>3.5. Права и обязательства по договорам</strong></p>
|
|
||||||
<p>Права и обязанности по договорам купли-продажи возникают непосредственно у Продавца. Пользователь осознаёт, что Владелец сайта исполняет роль оператора маркетплейса и не несёт ответственности за действия Продавцов, качество товаров, законное основание Продавца осуществлять сделки и фактическое исполнение обязательств.</p>
|
|
||||||
|
|
||||||
<p><strong>3.6. Функции владельца сайта</strong></p>
|
|
||||||
<p>Владелец сайта предоставляет информационное и техническое сопровождение: координирует взаимодействие между Покупателем и Продавцом, передает информацию о заказе, может помогать в урегулировании споров. Тем не менее, ответственность за исполнение договора остаётся за Продавцом.</p>
|
|
||||||
|
|
||||||
<p><strong>3.7. Переуступка прав Продавцом</strong></p>
|
|
||||||
<p>Покупатель осведомлён и соглашается с правом Продавца уступать или передавать свои права и обязанности третьим сторонам.</p>
|
|
||||||
|
|
||||||
<p><strong>3.8. Применение законов</strong></p>
|
|
||||||
<p>Отношения между Пользователем и Продавцом регулируются положениями Федерального закона «О защите прав потребителей» (№ 2300-1 от 07 февраля 1992 года) и Гражданского кодекса РФ.</p>
|
|
||||||
|
|
||||||
<p><strong>3.10. Рекламные сообщения</strong></p>
|
|
||||||
<p>Пользователь дает согласие на получение рекламных сообщений в соответствии с Федеральным законом «О рекламе».</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>4. Регистрация на сайте и Личный кабинет Пользователя</h2>
|
|
||||||
|
|
||||||
<p><strong>4.1. Процедуры регистрации</strong></p>
|
|
||||||
<p>Пользователю предлагается зарегистрироваться на сайте для полноценного использования отдельных функций и сервисов. Хотя регистрация необязательна для оформления заказа, она создаёт уникальные возможности, включая Личный кабинет.</p>
|
|
||||||
|
|
||||||
<p><strong>4.2. Обязанности Пользователя при регистрации</strong></p>
|
|
||||||
<p>Регистрируясь, Пользователь берёт обязательство предоставлять точную и актуальную информацию, заполняя анкету регистрации. Необходимо поддерживать данные в актуальном состоянии.</p>
|
|
||||||
|
|
||||||
<p><strong>4.3. Идентификация пользователя</strong></p>
|
|
||||||
<p>Любые действия, совершённые с использованием индивидуального логина и пароля, считаются действиями самого Пользователя, пока не доказана противоположная позиция.</p>
|
|
||||||
|
|
||||||
<p><strong>4.4. Конфиденциальность учетных данных</strong></p>
|
|
||||||
<p>Пользователь обязуется держать в тайне логин и пароль, предоставленные при регистрации. При подозрении на нарушение безопасности необходимо срочно проинформировать администрацию сайта письмом на адрес <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a>.</p>
|
|
||||||
|
|
||||||
<p><strong>4.7. Подтверждение данных</strong></p>
|
|
||||||
<p>Владелец сайта имеет право запросить подтверждение предоставленной информации в любой момент.</p>
|
|
||||||
|
|
||||||
<p><strong>4.11. Завершение сеанса работы</strong></p>
|
|
||||||
<p>Пользователь обязан самостоятельно завершать работу в Личном кабинете («Выход»), покидая сайт, для обеспечения безопасности аккаунта.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>5. Права и обязанности Владельца сайта</h2>
|
|
||||||
|
|
||||||
<p><strong>5.1. Права Владельца сайта</strong></p>
|
|
||||||
<p>Владелец сайта наделён следующими полномочиями:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Устанавливать ограничения в использовании ресурса для всех или отдельных групп пользователей.</li>
|
|
||||||
<li>Направлять Пользователям информацию о нововведениях и изменениях на сайте.</li>
|
|
||||||
<li>Передавать полномочия третьим лицам без одобрения Покупателя.</li>
|
|
||||||
<li>Изменять условия проводимых акций в одностороннем порядке.</li>
|
|
||||||
<li>Ограничивать действия Пользователей, создающих риски для работоспособности сайта.</li>
|
|
||||||
<li>Проводить технические работы без предупреждения пользователей.</li>
|
|
||||||
<li>Использовать статистику, собранную на сайте, в собственных интересах.</li>
|
|
||||||
<li>Менять список товаров и услуг, ценники и условия в любое время.</li>
|
|
||||||
<li>Отказывать в обслуживании Пользователям, подозреваемым в неправомерных действиях.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>5.2. Обязанности Владельца сайта</strong></p>
|
|
||||||
<p>Основной обязанностью Владельца сайта является оказание Пользователям услуг, предусмотренных пунктом 2.1 настоящего соглашения.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>6. Права и обязанности Пользователя</h2>
|
|
||||||
|
|
||||||
<p><strong>6.1. Обязанности Пользователя</strong></p>
|
|
||||||
<p>Пользователь обязуется соблюдать ряд важных обязанностей:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Ознакомляться с текстом соглашения до момента заключения договора купли-продажи или оказания услуг.</li>
|
|
||||||
<li>Придерживаться этического поведения при написании отзывов и взаимодействии с персоналом сайта.</li>
|
|
||||||
<li>Общаться вежливо и уважительно с сотрудниками и коллегами.</li>
|
|
||||||
<li>Не допускать распространение ненормативной лексики, грубостей и оскорблений.</li>
|
|
||||||
<li>Не создавать помех нормальной работе сайта и его сервисов.</li>
|
|
||||||
<li>Не заниматься загрузкой вирусов, вредоносных программ и других опасных файлов.</li>
|
|
||||||
<li>Не применять запрещённые автоматизированные программы для сбора информации без разрешения Администрации.</li>
|
|
||||||
<li>Не пытаться получить доступ к чужим учётным данным.</li>
|
|
||||||
<li>Законно использовать контент сайта и избегать незаконного копирования материалов.</li>
|
|
||||||
<li>Зарегистрироваться только от собственного имени и не маскироваться под других лиц.</li>
|
|
||||||
<li>Своевременно оплачивать заказанные товары и услуги в соответствии с соглашением.</li>
|
|
||||||
<li>Использовать приобретённые товары исключительно для личных нужд, не преследуя коммерческих целей.</li>
|
|
||||||
<li>Беречь информационную безопасность ресурса, воздерживаясь от попыток взлома или нарушения целостности сайта.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>6.2. Запрещённые действия Пользователя</strong></p>
|
|
||||||
<p>Пользователю категорически запрещается:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Загрузка и публикация контента, противоречащего закону, содержащего вирусные программы, ложную информацию или уничижительную критику.</li>
|
|
||||||
<li>Нарушение прав других пользователей и нанесение вреда несовершеннолетним гражданам.</li>
|
|
||||||
<li>Любое поведение, нарушающее российское законодательство, включая нормы международного права.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>6.3. Права Пользователя</strong></p>
|
|
||||||
<p>Пользователь имеет право отказаться от получения рекламных сообщений, воспользовавшись соответствующим инструментом на сайте или направив заявку по электронной почте <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a> или письмом по официальному адресу Владельца сайта.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>7. Исключительные права на контент сайта</h2>
|
|
||||||
|
|
||||||
<p><strong>7.1. Объекты интеллектуальной собственности</strong></p>
|
|
||||||
<p>Весь контент, доступный на сайте, включая:</p>
|
|
||||||
<ul>
|
|
||||||
<li>элементы дизайна,</li>
|
|
||||||
<li>текстовые материалы,</li>
|
|
||||||
<li>графики и изображения,</li>
|
|
||||||
<li>видеоролики,</li>
|
|
||||||
<li>компьютерные программы,</li>
|
|
||||||
<li>базы данных,</li>
|
|
||||||
<li>другие материалы —</li>
|
|
||||||
</ul>
|
|
||||||
<p>является объектом исключительных авторских прав Владельца сайта и других правообладателей.</p>
|
|
||||||
|
|
||||||
<p><strong>7.2. Использование контента</strong></p>
|
|
||||||
<p>Применение контента разрешено только в рамках функциональных возможностей сайта. Любое другое использование элементов сайта или размещённого контента без предварительного разрешения владельцев недопустимо.</p>
|
|
||||||
|
|
||||||
<p><strong>7.3. Использование для личного потребления</strong></p>
|
|
||||||
<p>Пользователь вправе использовать контент сайта для личного некоммерческого использования при условии обязательного сохранения обозначений авторских прав, смежных прав, товарных знаков и прочих уведомлений об авторстве.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>8. Сайты и контент третьих лиц</h2>
|
|
||||||
|
|
||||||
<p><strong>8.1. Наличие ссылок на внешние ресурсы</strong></p>
|
|
||||||
<p>Сайт может включать гиперссылки на сторонние ресурсы в Интернете. Эти ресурсы не подвергаются проверке Владельцем сайта на предмет соответствия законодательным нормам или другим критериям. Соответственно, ответственность за содержание и материалы на сайтах третьих лиц не относится к компетенции Владельца сайта.</p>
|
|
||||||
|
|
||||||
<p><strong>8.2. Характер ссылок</strong></p>
|
|
||||||
<p>Ссылки на сторонние сайты, а также упоминания продуктов, услуг или иной информации коммерческого или некоммерческого характера, присутствующие на сайте, не означают одобрения или рекомендаций со стороны Владельца сайта.</p>
|
|
||||||
|
|
||||||
<p><strong>8.3. Требования к цитированию</strong></p>
|
|
||||||
<p>При любом воспроизведении материалов сайта необходимо указывать активную ссылку на источник.</p>
|
|
||||||
|
|
||||||
<p><strong>8.4. Интеллектуальная собственность</strong></p>
|
|
||||||
<p>Логотип, фирменное название, визуальное оформление и общий облик сайта принадлежат интеллектуальным правам Владельца сайта. Их использование без прямой явной санкции от Владельца запрещено.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>9. Ответственность сторон</h2>
|
|
||||||
|
|
||||||
<p><strong>9.1. Общая ответственность</strong></p>
|
|
||||||
<p>Пользователь использует сайт на свой риск. Ресурсы предоставляются "как есть", без гарантий соответствия ожиданиям пользователя.</p>
|
|
||||||
|
|
||||||
<p><strong>9.2. Ограничения ответственности</strong></p>
|
|
||||||
<p>Владелец сайта не гарантирует:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Соответствие сайта потребностям пользователя.</li>
|
|
||||||
<li>Постоянную доступность, скорость и отсутствие ошибок в сервисе.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>9.3. Последствия использования информации</strong></p>
|
|
||||||
<p>Вся информация и материалы, получаемые через сайт, используются пользователем на его усмотрение и риск. Пользователь несёт ответственность за возможные негативные последствия использования этих сведений.</p>
|
|
||||||
|
|
||||||
<p><strong>9.4. Ответственность за недостоверную информацию</strong></p>
|
|
||||||
<p>Владелец сайта не несёт ответственности за неверные или неточные данные, предоставленные пользователем при регистрации.</p>
|
|
||||||
|
|
||||||
<p><strong>9.5. Безопасность аккаунта</strong></p>
|
|
||||||
<p>Пользователь самостоятельно отвечает за сохранение секретности используемых средств доступа к своему профилю.</p>
|
|
||||||
|
|
||||||
<p><strong>9.6. Ответственность Продавца</strong></p>
|
|
||||||
<p>Продавец несёт полную ответственность за качество, безопасность и соответствие реализуемой продукции заявленным характеристикам, а также за убытки, вызванные нарушением обязательств перед Покупателем.</p>
|
|
||||||
|
|
||||||
<p><strong>9.7. Освобождение Владельца сайта от ответственности</strong></p>
|
|
||||||
<p>Владелец сайта не несёт ответственности за:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Качество, безопасность и правомерность товаров и услуг, предлагаемых Продавцами.</li>
|
|
||||||
<li>Выполнение или ненадлежащее выполнение Продавцами своих обязательств перед Покупателями.</li>
|
|
||||||
<li>Истинность информации, представляемой Продавцами о товаре.</li>
|
|
||||||
<li>Повреждение прав третьих лиц, включая интеллектуальную собственность.</li>
|
|
||||||
<li>Вопросы доставки, комплектации и состояния товаров.</li>
|
|
||||||
</ul>
|
|
||||||
<p>Покупатель соглашается, что претензии по качеству, количеству, комплектности товаров и услугам следует направлять непосредственно Продавцу. Администрация сайта может способствовать решению споров, но сама ответственности не несёт.</p>
|
|
||||||
|
|
||||||
<p><strong>9.8. Ответственность за доставку</strong></p>
|
|
||||||
<p>Ответственность за сроки, условия и качество доставки товаров несут транспортные компании и курьерские службы. Владелец сайта выступает только в качестве информационного посредника и не несёт ответственности за действия служб доставки.</p>
|
|
||||||
|
|
||||||
<p><strong>9.9. Ответственность Пользователя перед третьими лицами</strong></p>
|
|
||||||
<p>Пользователь лично отвечает перед третьими лицами за свои действия, связанные с использованием сайта.</p>
|
|
||||||
|
|
||||||
<p><strong>9.10. Форс-мажорные обстоятельства</strong></p>
|
|
||||||
<p>Работа сайта может быть приостановлена в случае непредвиденных обстоятельств, аварий, сбоев оборудования или вмешательств третьих лиц без предварительных уведомлений.</p>
|
|
||||||
|
|
||||||
<p><strong>9.11. Ответственность за рекламу</strong></p>
|
|
||||||
<p>Ответственность за содержание рекламных объявлений несут Продавцы, размещающие информацию о своих товарах и услугах. Владелец сайта предоставляет только техническую платформу для размещения информации.</p>
|
|
||||||
|
|
||||||
<p><strong>9.12. Стихийные бедствия и чрезвычайные события</strong></p>
|
|
||||||
<p>Ни одна сторона не несёт ответственности за невыполнение обязательств, вызванное обстоятельствами непреодолимой силы, такими как природные катастрофы, войны или техногенные аварии.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>10. Товар и порядок совершения покупки</h2>
|
|
||||||
|
|
||||||
<p><strong>10.1. Оформление заказа</strong></p>
|
|
||||||
<p>Заказ оформляется Покупателем в строгом соответствии с процедурой, установленной на сайте.</p>
|
|
||||||
|
|
||||||
<p><strong>10.2. Ответственность за некорректные данные</strong></p>
|
|
||||||
<p>Покупатель принимает на себя риски неполучения заказа, связанные с ошибочной или неполной информацией, предоставленной при оформлении заказа.</p>
|
|
||||||
|
|
||||||
<p><strong>10.3. Предварительная информация о доставке</strong></p>
|
|
||||||
<p>После формирования заказа Покупатель получает уведомление о примерной дате доставки через электронную почту или телефон, указанный при регистрации.</p>
|
|
||||||
|
|
||||||
<p><strong>10.4. Конкретизация времени доставки</strong></p>
|
|
||||||
<p>Перед доставкой сотрудник магазина свяжется с Покупателем по телефону для уточнения точного времени доставки.</p>
|
|
||||||
|
|
||||||
<p><strong>10.5. Уточнение ожидаемого времени доставки</strong></p>
|
|
||||||
<p>Менеджер по заказу уведомляет Покупателя о планируемой дате передачи заказа в службу доставки через электронную почту или звонок.</p>
|
|
||||||
|
|
||||||
<p><strong>10.6. Возможность нехватки товара</strong></p>
|
|
||||||
<p>Продавец не гарантирует постоянное наличие заказанного товара на складе на момент обработки заказа.</p>
|
|
||||||
|
|
||||||
<p><strong>10.11. Законодательные рамки поставок</strong></p>
|
|
||||||
<p>Поставка товаров осуществляется в соответствии с законами Российской Федерации и страны происхождения Продавца.</p>
|
|
||||||
|
|
||||||
<p><strong>10.12. Применимость российского законодательства</strong></p>
|
|
||||||
<p>Физические товары поступают к покупателю в соответствии с законодательством Российской Федерации.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>11. Доставка заказа</h2>
|
|
||||||
|
|
||||||
<p><strong>11.1. Способы и сроки доставки</strong></p>
|
|
||||||
<p>Способы и ориентировочные сроки доставки товаров указаны на сайте. Фактические сроки могут быть уточнены при подтверждении заказа сотрудником call-центра или представителем Продавца.</p>
|
|
||||||
|
|
||||||
<p><strong>11.2. Подтверждение поставки цифровых товаров</strong></p>
|
|
||||||
<p>Факт поставки цифровых товаров подтверждается отправкой электронного письма на указанный при регистрации адрес с приложением цифрового файла или кода. Получение такого письма означает выполнение услуги по поставке цифровых товаров.</p>
|
|
||||||
<p>Физический товар считается доставленным в момент его реальной передачи Покупателю или назначенному им получателю согласно условиям выбранной доставки.</p>
|
|
||||||
|
|
||||||
<p><strong>11.3. Территория доставки</strong></p>
|
|
||||||
<p>Территориальные границы доставки товаров, предлагаемых Продавцом, опубликованы на сайте.</p>
|
|
||||||
|
|
||||||
<p><strong>11.4. Прием товара</strong></p>
|
|
||||||
<p>При доставке товар передается Покупателю или указанному в заказе третьему лицу.</p>
|
|
||||||
|
|
||||||
<p><strong>11.5. Проверка личности</strong></p>
|
|
||||||
<p>При передаче предварительно оплаченного заказа служба доставки вправе попросить предъявить удостоверение личности для исключения риска мошенничества.</p>
|
|
||||||
|
|
||||||
<p><strong>11.6. Переход риска утраты товара</strong></p>
|
|
||||||
<p>Риск случайной потери или порчи товара переходит к Покупателю в момент непосредственной передачи заказа.</p>
|
|
||||||
|
|
||||||
<p><strong>11.7. Расчёт стоимости доставки</strong></p>
|
|
||||||
<p>Стоимость доставки рассчитывается индивидуально для каждого заказа, учитывая вес, регион и выбранный способ транспортировки.</p>
|
|
||||||
|
|
||||||
<p><strong>11.8. Завершение обязательств Продавца</strong></p>
|
|
||||||
<p>Продавец выполнил свои обязательства по передаче товара, если товар передан Покупателю курьером или выдан Покупателем в почтовом отделении или другом согласованном месте.</p>
|
|
||||||
|
|
||||||
<p><strong>11.9. Осмотр товара при получении</strong></p>
|
|
||||||
<p>При получении товара Покупатель вправе визуально оценить его состояние и при необходимости провести осмотр в присутствии сотрудника Продавца или курьера.</p>
|
|
||||||
|
|
||||||
<p><strong>11.10. Проверка товара при приеме от курьера</strong></p>
|
|
||||||
<p>Покупатель может осмотреть полученный товар и убедиться в его соответствии заявленному количеству, ассортименту и комплектации.</p>
|
|
||||||
|
|
||||||
<p><strong>11.11. Лимиты времени для курьерской доставки</strong></p>
|
|
||||||
<p>Курьеры имеют ограниченное время пребывания по адресу Покупателя — максимум 20 минут.</p>
|
|
||||||
|
|
||||||
<p><strong>11.12. Сертификация товара</strong></p>
|
|
||||||
<p>Товары, размещённые на сайте, соответствуют государственным стандартам (ГОСТ) и техническим условиям (ТУ), что подтверждено сертификатами и прочими необходимыми документами.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>12. Оплата товара</h2>
|
|
||||||
|
|
||||||
<p><strong>12.1. Валюта и структура цены</strong></p>
|
|
||||||
<p>Цена товаров, размещённых на сайте, выражается в рублях Российской Федерации и включает все налоговые отчисления и обязательные сборы.</p>
|
|
||||||
|
|
||||||
<p><strong>12.2. Возможные ошибки в ценах</strong></p>
|
|
||||||
<p>Иногда на сайте могут возникать технические ошибки, приводящие к неверному отражению цен на товары или стоимости доставки. При обнаружении таких ошибок Продавец уведомляет Покупателя и уточняет цену, оставляя Покупателю выбор подтвердить заказ по обновлённым расценкам или отменить сделку.</p>
|
|
||||||
|
|
||||||
<p><strong>12.3. Изменение цен</strong></p>
|
|
||||||
<p>Продавец вправе изменять цены на товары в одностороннем порядке. Однако если товар уже оплачен Покупателем, изменение цены на этот товар не допускается.</p>
|
|
||||||
|
|
||||||
<p><strong>12.4. Способы оплаты</strong></p>
|
|
||||||
<p>Варианты оплаты, доступные Покупателю, приведены на сайте в процессе оформления заказа. Метод оплаты принимается на основании выбора Пользователя из числа доступных опций.</p>
|
|
||||||
|
|
||||||
<p><strong>12.6. Особенности оплаты с помощью банковских карт</strong></p>
|
|
||||||
<p>При оплате товаров с использованием банковских карт действуют следующие правила:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Платежи осуществляются держателем карты или лицом, имеющим доверенность.</li>
|
|
||||||
<li>Авторизация операций проходит через банковскую организацию.</li>
|
|
||||||
<li>Продавец вправе запросить документ, удостоверяющий личность, для подтверждения полномочий на использование карты.</li>
|
|
||||||
<li>При осуществлении оплаты банковской картой на сайте Покупатель соглашается на получение кассового чека в электронной форме по электронной почте.</li>
|
|
||||||
<li>Заказы, оплаченные картой, проверяются Продавцом для предотвращения мошеннических действий.</li>
|
|
||||||
<li>Сбор и хранение данных банковских карт Покупателей не производятся ни Владельцем сайта, ни Продавцом.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>12.7. Скидки и бонусы</strong></p>
|
|
||||||
<p>Продавец вправе назначать скидки и вводить бонусные программы. Типы скидок, порядок и условия начисления устанавливаются Продавцом и публикуются на сайте.</p>
|
|
||||||
|
|
||||||
<p><strong>12.13. Ограничения по сумме оплаты наличными средствами</strong></p>
|
|
||||||
<p>Оплата товара наличными средствами не осуществляется.</p>
|
|
||||||
|
|
||||||
<p><strong>12.14. Сроки возврата денежных средств</strong></p>
|
|
||||||
<p>Требования Покупателя о возврате средств за товар удовлетворяются в течение 10 дней с момента представления соответствующего заявления.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>13. Возврат и обмен товара</h2>
|
|
||||||
|
|
||||||
<p><strong>13.1. Общие правила возврата</strong></p>
|
|
||||||
<p>Цифровые товары (предоставляемые в электронной форме) не подлежат возврату согласно российскому законодательству. Возврат физических товаров осуществляется в соответствии с разделом <a routerLink="/return-policy">«Политика возврата»</a> и действующими законами о правах потребителей.</p>
|
|
||||||
|
|
||||||
<p><strong>13.2. Возврат товара</strong></p>
|
|
||||||
<p>Возврат или замена товаров, представленных на сайте и подлежащих возврату, происходят в соответствии с данным соглашением и законодательством Российской Федерации.</p>
|
|
||||||
|
|
||||||
<p><strong>13.4. Акционные наборы</strong></p>
|
|
||||||
<p>При приобретении набора товаров по акции с предоставлением скидки возврат или обмен возможен только в комплексе. Отдельные товары из комплекта вернуть нельзя.</p>
|
|
||||||
|
|
||||||
<p><strong>13.5. Компенсация затрат на доставку</strong></p>
|
|
||||||
<p>При возврате качественного товара Продавец или Владелец сайта вправе взыскать с Покупателя затраты на доставку товара.</p>
|
|
||||||
|
|
||||||
<p><strong>13.6. Сроки удовлетворения требований</strong></p>
|
|
||||||
<p>Заявления о возврате средств удовлетворяются в течение 10 дней с момента подачи.</p>
|
|
||||||
|
|
||||||
<p><strong>13.6.7. Документация при возврате товара</strong></p>
|
|
||||||
<p>При возврате товара Покупатель обязан предоставить следующие документы:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Заявление на возврат.</li>
|
|
||||||
<li>Копию квитанции об оплате или чек.</li>
|
|
||||||
<li>Копию акта приема-передачи (опись вложения).</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>13.6.9. Механизм возврата средств</strong></p>
|
|
||||||
<p>Возврат средств осуществляется путем возвращения стоимости оплаченного товара. Способ возврата указывается в заявлении на возврат.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>14. Срок действия соглашения</h2>
|
|
||||||
|
|
||||||
<p><strong>14.1. Начало и прекращение действия</strong></p>
|
|
||||||
<p>Соглашение становится активным с момента его принятия (акцепта) Пользователем или Покупателем и действует вплоть до момента отзыва акцепта публичной оферты. Отзыв оферты влечет немедленное прекращение действия соглашения.</p>
|
|
||||||
|
|
||||||
<p><strong>14.2. Право отзыва оферты</strong></p>
|
|
||||||
<p>Владелец сайта имеет право отозвать данное предложение в порядке, предусмотренном статьей 436 Гражданского кодекса Российской Федерации.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>15. Порядок разрешения споров и урегулирования претензий</h2>
|
|
||||||
|
|
||||||
<p><strong>15.1. Добровольное урегулирование споров</strong></p>
|
|
||||||
<p>В случае возникновения разногласий между Пользователем и Владельцем сайта, связанных с исполнением условий соглашения, обе стороны обязаны попытаться урегулировать проблему путем взаимных консультаций и переговоров. Установлен обязательный досудебный порядок рассмотрения споров.</p>
|
|
||||||
|
|
||||||
<p><strong>15.2. Претензионный порядок</strong></p>
|
|
||||||
<p>Процедура рассмотрения претензий предусматривает следующие этапы:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Пользователь, полагающий, что его права были нарушены действием или бездействием Владельца сайта, направляет соответствующую претензию, содержащую описание сути претензии.</li>
|
|
||||||
<li>В течение 25 рабочих дней с момента получения претензии Владельцем сайта последний обязан подготовить ответ и разъяснить свою позицию.</li>
|
|
||||||
<li>Если достичь компромисса не удается, спор рассматривается в судебном порядке в соответствии с пунктом 15.5 соглашения.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>15.3. Рассмотрение претензий</strong></p>
|
|
||||||
<p>Владельцем сайта не принимаются к рассмотрению анонимные претензии или жалобы, содержащие недостаточно информации для идентификации заявителя.</p>
|
|
||||||
|
|
||||||
<p><strong>15.5. Судебное рассмотрение споров</strong></p>
|
|
||||||
<p>Если переговоры и претензионный порядок не приводят к соглашению, спор решается в судебном порядке по месту нахождения Владельца сайта.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>16. Прочие условия</h2>
|
|
||||||
|
|
||||||
<p><strong>16.1. Действие соглашения</strong></p>
|
|
||||||
<p>Данный документ представляет собой официальное соглашение между Пользователем и Владельцем сайта относительно использования ресурса и замещает собой все предшествующие договоренности.</p>
|
|
||||||
|
|
||||||
<p><strong>16.2. Согласие с условиями</strong></p>
|
|
||||||
<p>Пользователь, продолжая использовать сайт, выражает своё согласие с условиями соглашения. Несогласие предполагает удаление личного кабинета.</p>
|
|
||||||
|
|
||||||
<p><strong>16.3. Применимое законодательство</strong></p>
|
|
||||||
<p>Вопросы, не урегулированные соглашением, разрешаются в соответствии с законодательством Армении.</p>
|
|
||||||
|
|
||||||
<p><strong>16.4. Определение термина «Законодательство»</strong></p>
|
|
||||||
<p>Термин «законодательство» по всему тексту соглашения, если специально не указано иначе, подразумевает законы Армении.</p>
|
|
||||||
|
|
||||||
<p><strong>16.5. Бесплатные услуги</strong></p>
|
|
||||||
<p>Бесплатные услуги, предоставляемые в рамках соглашения, не предполагают применение норм законодательства о защите прав потребителей к взаимоотношениям между Пользователем и Продавцом.</p>
|
|
||||||
|
|
||||||
<p><strong>16.6. Отсутствующие отношения</strong></p>
|
|
||||||
<p>Никакие положения соглашения не могут трактоваться как установление агентских связей, партнёрства, совместного предприятия, трудовых отношений или иных видов взаимоотношений, прямо не предусмотренных соглашением.</p>
|
|
||||||
|
|
||||||
<p><strong>16.7. Признание недействительности пунктов</strong></p>
|
|
||||||
<p>Признание одного или нескольких положений соглашения недействительным не влияет на законную силу и применимость оставшихся пунктов.</p>
|
|
||||||
|
|
||||||
<p><strong>16.8. Реакция на нарушения</strong></p>
|
|
||||||
<p>Невмешательство Владельца сайта в случае нарушений соглашений Пользователями не препятствует последующим мерам защиты интересов Владельца позже.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<h1>Политика возврата товаров</h1>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>1. Общие положения</h2>
|
|
||||||
<p>1.1. Настоящий документ устанавливает правила возврата товаров и отказ от услуг, приобретенных через маркетплейс DexarMarket.</p>
|
|
||||||
<p>1.2. Маркетплейс выполняет лишь роль информационного посредника, и все вопросы возврата регулируются между Покупателем и Продавцом в соответствии с нормами российского законодательства.</p>
|
|
||||||
<p>1.3. Возврат товаров надлежащего качества регламентируется статьей 26.1 Закона РФ «О защите прав потребителей».</p>
|
|
||||||
<p>1.4. Возврат некачественных товаров подчиняется статьям 18–24 Закона РФ «О защите прав потребителей».</p>
|
|
||||||
<p>1.5. Особенности возврата цифровых товаров определены российским законодательством.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>2. Сроки возврата</h2>
|
|
||||||
<p><strong>2.1. Товары надлежащего качества:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Дистанционный способ приобретения:</strong> 7 дней с момента получения товара.</li>
|
|
||||||
<li><strong>До передачи товара:</strong> в любое время.</li>
|
|
||||||
<li><strong>Без письменного извещения о правилах возврата:</strong> 3 месяца с момента получения.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>2.2. Товары ненадлежащего качества:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li>Внутри установленного гарантийного срока.</li>
|
|
||||||
<li>Если гарантия не указана: 2 года с момента передачи товара.</li>
|
|
||||||
<li>Для существенных дефектов: весь срок службы товара.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>2.3. Цифровые товары:</strong></p>
|
|
||||||
<p>Возврат доступен только до начала скачивания или активации продукта. После начала использования возврат невозможен (за исключением случаев наличия значительных дефектов, мешающих нормальному применению товара).</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>3. Условия возврата товаров надлежащего качества</h2>
|
|
||||||
<p>Вернуть товар можно при выполнении следующих требований:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Сохранён внешний вид и потребительские характеристики товара.</li>
|
|
||||||
<li>Полностью сохранена оригинальная упаковка и вся комплектация.</li>
|
|
||||||
<li>Сохранены все заводские ярлыки, пломбы и наклейки.</li>
|
|
||||||
<li>Товар не использовался и не имеет следов износа.</li>
|
|
||||||
<li>Имеются доказательства покупки (чек, квитанция и т.д.).</li>
|
|
||||||
</ul>
|
|
||||||
<p>Покупатель оплачивает расходы на доставку при возврате качественного товара, если иное не оговорено отдельно с Продавцом.</p>
|
|
||||||
<p>Продавец обязан возместить деньги за качественный товар в течение 10 дней после подачи заявления о возврате.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>4. Категории товаров, не подлежащих возврату</h2>
|
|
||||||
<p>Согласно Постановлению Правительства РФ №2463 от 31 декабря 2020 г., нижеперечисленные группы товаров надлежащего качества не подлежат возврату:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Медицинские товары для домашнего пользования.</li>
|
|
||||||
<li>Предметы индивидуальной гигиены (расчёски, зубные щётки и т.д.).</li>
|
|
||||||
<li>Парфюмерия и косметика.</li>
|
|
||||||
<li>Метражные ткани, нити, ленты и аналогичные продукты.</li>
|
|
||||||
<li>Бельевые изделия, нижнее бельё, колготки.</li>
|
|
||||||
<li>Посуда и столовые принадлежности одноразового использования.</li>
|
|
||||||
<li>Бытовая химия, удобрения и химикаты.</li>
|
|
||||||
<li>Мебель.</li>
|
|
||||||
<li>Украшения и ювелирные изделия.</li>
|
|
||||||
<li>Автотранспортные средства и запчасти.</li>
|
|
||||||
<li>Сложнотехнические бытовые товары, имеющие гарантию.</li>
|
|
||||||
<li>Гражданское оружие и боеприпасы.</li>
|
|
||||||
<li>Животные и растения.</li>
|
|
||||||
<li>Периодически не выпускающиеся издания (книги, журналы, альбомы).</li>
|
|
||||||
</ul>
|
|
||||||
<p>Данные товары допускаются к возврату только при выявлении брака или дефекта.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>5. Процедура возврата</h2>
|
|
||||||
<p>Алгоритм возврата выглядит следующим образом:</p>
|
|
||||||
<ol>
|
|
||||||
<li>Связаться с Продавцом через сайт или предоставленный контакт.</li>
|
|
||||||
<li>Сообщить причину возврата и представить доказательства неисправности (фотографии, видеозаписи).</li>
|
|
||||||
<li>Получить инструкцию по отправке товара назад.</li>
|
|
||||||
<li>Упаковать товар в оригинальную коробку вместе с аксессуарами.</li>
|
|
||||||
<li>Отправить товар установленным методом.</li>
|
|
||||||
<li>Приложить почтовый трек-номер Продавцу.</li>
|
|
||||||
<li>Ждать проверки товара Продавцом (до 10 дней).</li>
|
|
||||||
<li>Деньги будут возвращены Покупателю.</li>
|
|
||||||
</ol>
|
|
||||||
<p>Маркетплейс готов поддержать разрешение конфликта, если такая необходимость возникнет.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>6. Возврат денежных средств</h2>
|
|
||||||
<p>Денежные средства возвращаются таким же способом, каким производилась оплата:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Через банковскую карту.</li>
|
|
||||||
<li>Электронный кошелёк.</li>
|
|
||||||
<li>По ссылке на оплату.</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Срок возврата:</strong></p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Качественный товар:</strong> 10 дней с момента принятия Продавцом заявления.</li>
|
|
||||||
<li><strong>Некачественный товар:</strong> 10 дней с момента обращения Покупателя.</li>
|
|
||||||
</ul>
|
|
||||||
<p>Покупатель вправе требовать возмещения стоимости дефектного товара и компенсации расходов на доставку.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>7. Обмен товара</h2>
|
|
||||||
<p>Если товар надлежащего качества не подходит по форме, размерам или характеристикам, Покупатель может обменять его на аналогичный продукт у Продавца.</p>
|
|
||||||
<p>Если подходящего товара нет в наличии, Покупатель вправе отказаться от покупки и вернуть потраченные средства.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>8. Гарантийное обслуживание</h2>
|
|
||||||
<p>Информация о гарантийных обязательствах доступна в соответствующем разделе каталога или у Продавца.</p>
|
|
||||||
<p>Гарантия выполняется самим Продавцом или официальным сервисом производителя.</p>
|
|
||||||
<p>Гарантийный случай не действует при механических повреждениях, неправильном использовании или самостоятельном ремонте.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>9. Ответственность сторон</h2>
|
|
||||||
<p>Продавец отвечает за качество товара и своевременность возврата средств.</p>
|
|
||||||
<p>Покупатель обязан сохранять целостность упаковки и комплектность товара до момента возврата.</p>
|
|
||||||
<p>Маркетплейс, выступая посредником, не несёт ответственности за невыполнение Продавцом обязательств, но помогает разрешать конфликты.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="legal-section">
|
|
||||||
<h2>10. Контактная информация</h2>
|
|
||||||
<p>Вопросы по возврату товаров можно задать Продавцу напрямую, контактные данные предоставляются в заказе.</p>
|
|
||||||
<p>Обращаясь к администрации Маркетплейса для разрешения разногласий:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Email Маркетплейса:</strong> <a href="mailto:info@dexarmarket.ru">info@dexarmarket.ru</a></li>
|
|
||||||
<li><strong>Тема письма:</strong> «Спор с Продавцом — №Заказа»</li>
|
|
||||||
<li><strong>Необходимые файлы:</strong> копии писем, фотографии товара, чек об оплате.</li>
|
|
||||||
</ul>
|
|
||||||
<p>Если конфликт невозможно разрешить мирно, Покупатель вправе подать жалобу в Роспотребнадзор или суд по месту расположения Продавца.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>О нас</h1>
|
|
||||||
<p class="subtitle">Современный маркетплейс для вашего удобства</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">🚀</div>
|
|
||||||
<h3>Кто мы</h3>
|
|
||||||
<p>Мы - динамично развивающийся маркетплейс, объединяющий продавцов и покупателей из разных стран. Наша платформа создает удобные условия для безопасной торговли различными товарами и услугами.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🎯</div>
|
|
||||||
<h3>Наша миссия</h3>
|
|
||||||
<p>Создавать простую и выгодную экосистему для бизнеса и покупателей, где каждый находит лучшие предложения.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🌍</div>
|
|
||||||
<h3>География</h3>
|
|
||||||
<p>Мы работаем в России, Армении, ОАЭ, Турции, Китае, Казахстане, Кыргызстане и других странах.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">💼</div>
|
|
||||||
<h3>Для бизнеса</h3>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Простое размещение товаров</li>
|
|
||||||
<li>Готовая аудитория</li>
|
|
||||||
<li>Удобные инструменты</li>
|
|
||||||
<li>Техническая поддержка</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🛍️</div>
|
|
||||||
<h3>Для покупателей</h3>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Широкий выбор товаров</li>
|
|
||||||
<li>Выгодные цены</li>
|
|
||||||
<li>Безопасные покупки</li>
|
|
||||||
<li>Быстрая доставка</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🔒</div>
|
|
||||||
<h3>Наши ценности</h3>
|
|
||||||
<div class="features-list">
|
|
||||||
<div class="feature">✓ Прозрачность</div>
|
|
||||||
<div class="feature">✓ Надежность</div>
|
|
||||||
<div class="feature">✓ Инновации</div>
|
|
||||||
<div class="feature">✓ Клиентский сервис</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">📈</div>
|
|
||||||
<h3>Наш путь</h3>
|
|
||||||
<div class="timeline">
|
|
||||||
<div class="timeline-item">
|
|
||||||
<strong>2024</strong>
|
|
||||||
<p>Запуск платформы в Армении</p>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-item">
|
|
||||||
<strong>2025</strong>
|
|
||||||
<p>Выход на российский рынок</p>
|
|
||||||
</div>
|
|
||||||
<div class="timeline-item">
|
|
||||||
<strong>Сегодня</strong>
|
|
||||||
<p>Международная экспансия</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">🏢</div>
|
|
||||||
<h3>Реквизиты компании</h3>
|
|
||||||
<p><strong>Компания:</strong> ООО «ЭЛЕКТРОМОТОРС»</p>
|
|
||||||
<p><strong>Директор:</strong> Тоноян Ваграм</p>
|
|
||||||
<p><strong>ИНН:</strong> 9909687443</p>
|
|
||||||
<p><strong>КПП:</strong> 770287001</p>
|
|
||||||
<p><strong>Адрес:</strong> АРМЕНИЯ, 0501, АРАГАЦОТНСКАЯ ОБЛАСТЬ, ТАЛИН, ул. ГАЯ, д. 12</p>
|
|
||||||
<p><strong>Банк:</strong> Уточняется</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📞</div>
|
|
||||||
<h3>Связаться с нами</h3>
|
|
||||||
<a href="mailto:info@novo.market" class="contact-email">info@novo.market</a>
|
|
||||||
<p><a href="tel:+37498731231">+374 98 731231</a></p>
|
|
||||||
<p class="support-note">Мы всегда на связи</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-about-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './about.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/info/about/about.component.scss']
|
|
||||||
})
|
|
||||||
export class AboutNovoComponent {}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>Контакты</h1>
|
|
||||||
<p class="subtitle">Свяжитесь с нами</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🏢</div>
|
|
||||||
<h3>Организация</h3>
|
|
||||||
<p class="org-name">ООО «ЭЛЕКТРОМОТОРС»</p>
|
|
||||||
<p><strong>ИНН:</strong> 9909687443</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📞</div>
|
|
||||||
<h3>Телефон</h3>
|
|
||||||
<p><a href="tel:+37498731231">+374 98 731231</a></p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">✉️</div>
|
|
||||||
<h3>Email</h3>
|
|
||||||
<p><a href="mailto:info@novo.market">info@novo.market</a></p>
|
|
||||||
<p class="note">Ответ в течение 24 часов</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📍</div>
|
|
||||||
<h3>Адрес</h3>
|
|
||||||
<p>Армения, 0501, Арагацотиская обл., г. Талин, ул. Гая, д. 12</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">⏰</div>
|
|
||||||
<h3>Часы работы</h3>
|
|
||||||
<p><strong>Поддержка:</strong> 9:00 - 21:00</p>
|
|
||||||
<p><strong>Выходные:</strong> Суббота - Воскресенье</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">💬</div>
|
|
||||||
<h3>Связаться с нами</h3>
|
|
||||||
<p>При возникновении технических проблем с работой сайта или вопросов по оформлению заказа обращайтесь на <a href="mailto:info@novo.market">info@novo.market</a> с подробным описанием проблемы.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-contacts-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './contacts.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/info/contacts/contacts.component.scss']
|
|
||||||
})
|
|
||||||
export class ContactsNovoComponent {}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>Доставка</h1>
|
|
||||||
<p class="subtitle">Быстро и удобно до вашей двери</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📧</div>
|
|
||||||
<h3>Цифровые товары</h3>
|
|
||||||
<div class="features-list">
|
|
||||||
<div class="feature">⚡ Мгновенная доставка</div>
|
|
||||||
<div class="feature">📨 На ваш email</div>
|
|
||||||
<div class="feature">💰 Бесплатно</div>
|
|
||||||
<div class="feature">🔒 Безопасно</div>
|
|
||||||
</div>
|
|
||||||
<p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за цифровые товары. За качество и работоспособность отвечает продавец.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📦</div>
|
|
||||||
<h3>Физические товары</h3>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>СДЭК (2-7 дней)</li>
|
|
||||||
<li>Почта России (5-14 дней)</li>
|
|
||||||
<li>Boxberry (2-5 дней)</li>
|
|
||||||
<li>DPD (1-3 дня)</li>
|
|
||||||
<li>Яндекс.Доставка (в день заказа*)</li>
|
|
||||||
</ul>
|
|
||||||
<p class="note">*При наличии в вашем городе</p>
|
|
||||||
<p class="note important" style="margin-top: 12px;">⚠️ Платформа не несет ответственности за действия транспортных компаний. За доставку отвечают СДЭК, Почта России, Boxberry, DPD и другие перевозчики.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">💰</div>
|
|
||||||
<h3>Стоимость доставки</h3>
|
|
||||||
<div class="delivery-cost">
|
|
||||||
<div class="cost-item">
|
|
||||||
<strong>Цифровые товары</strong>
|
|
||||||
<span class="free">Бесплатно</span>
|
|
||||||
</div>
|
|
||||||
<div class="cost-item">
|
|
||||||
<strong>Физические товары</strong>
|
|
||||||
<span>Зависит от веса и региона</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="note">Точная стоимость рассчитывается при оформлении</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🔍</div>
|
|
||||||
<h3>Отслеживание</h3>
|
|
||||||
<p>После отправки вы получите трек-номер на email. Отслеживайте посылку на сайте службы доставки или в личном кабинете.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">✓</div>
|
|
||||||
<h3>При получении проверьте</h3>
|
|
||||||
<div class="check-grid">
|
|
||||||
<div class="check-item">✓ Целостность упаковки</div>
|
|
||||||
<div class="check-item">✓ Соответствие товара</div>
|
|
||||||
<div class="check-item">✓ Комплектность</div>
|
|
||||||
<div class="check-item">✓ Отсутствие повреждений</div>
|
|
||||||
</div>
|
|
||||||
<p class="note important">Если есть проблемы - составьте акт с курьером</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📞</div>
|
|
||||||
<h3>Вопросы по доставке?</h3>
|
|
||||||
<p>Свяжитесь с продавцом или нами:</p>
|
|
||||||
<a href="mailto:info@novo.market" class="contact-email">info@novo.market</a>
|
|
||||||
<p><a href="tel:+37498731231">+374 98 731231</a></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-delivery-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './delivery.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/info/delivery/delivery.component.scss']
|
|
||||||
})
|
|
||||||
export class DeliveryNovoComponent {}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>Частые вопросы</h1>
|
|
||||||
<p class="subtitle">Быстрые ответы на популярные вопросы</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🛍️</div>
|
|
||||||
<h3>Как сделать заказ?</h3>
|
|
||||||
<div class="process-steps-compact">
|
|
||||||
<p>1. Добавьте товар в корзину</p>
|
|
||||||
<p>2. Укажите данные для доставки</p>
|
|
||||||
<p>3. Выберите способ оплаты</p>
|
|
||||||
<p>4. Подтвердите заказ</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">💳</div>
|
|
||||||
<h3>Способы оплаты</h3>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Банковские карты</li>
|
|
||||||
<li>СБП</li>
|
|
||||||
<li>Электронные кошельки</li>
|
|
||||||
<li>Наличные при получении*</li>
|
|
||||||
</ul>
|
|
||||||
<p class="note">*Зависит от продавца</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🚚</div>
|
|
||||||
<h3>Доставка</h3>
|
|
||||||
<div class="delivery-info">
|
|
||||||
<div class="delivery-item">
|
|
||||||
<strong>Цифровые товары</strong>
|
|
||||||
<p>Мгновенно на email</p>
|
|
||||||
</div>
|
|
||||||
<div class="delivery-item">
|
|
||||||
<strong>Физические товары</strong>
|
|
||||||
<p>СДЭК, Почта России, DPD</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">↩️</div>
|
|
||||||
<h3>Возврат товара</h3>
|
|
||||||
<p>Можно вернуть качественный товар в течение 7 дней, если он не использовался и сохранена упаковка.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🔒</div>
|
|
||||||
<h3>Безопасность</h3>
|
|
||||||
<div class="features-list">
|
|
||||||
<div class="feature">✓ Защищенные платежи</div>
|
|
||||||
<div class="feature">✓ Проверка продавцов</div>
|
|
||||||
<div class="feature">✓ Гарантия возврата</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">⏱️</div>
|
|
||||||
<h3>Обработка заказа</h3>
|
|
||||||
<p>Заказ обрабатывается сразу после оплаты. Продавец отправляет товар в течение 1-3 рабочих дней.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">💬</div>
|
|
||||||
<h3>Служба поддержки</h3>
|
|
||||||
<div class="contacts-grid">
|
|
||||||
<div class="contact-item">
|
|
||||||
<strong>Email</strong>
|
|
||||||
<a href="mailto:info@novo.market">info@novo.market</a>
|
|
||||||
</div>
|
|
||||||
<div class="contact-item">
|
|
||||||
<strong>Телефон</strong>
|
|
||||||
<a href="tel:+37498731231">+374 98 731231</a>
|
|
||||||
</div>
|
|
||||||
<div class="contact-item">
|
|
||||||
<strong>Время работы</strong>
|
|
||||||
<p>24/7 техподдержка</p>
|
|
||||||
</div>
|
|
||||||
<div class="contact-item">
|
|
||||||
<strong>Время ответа</strong>
|
|
||||||
<p>До 2 часов в рабочее время</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-faq-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './faq.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/info/faq/faq.component.scss']
|
|
||||||
})
|
|
||||||
export class FaqNovoComponent {}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>Гарантия</h1>
|
|
||||||
<p class="subtitle">Защита ваших покупок</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🏷️</div>
|
|
||||||
<h3>Сроки гарантии</h3>
|
|
||||||
<div class="warranty-periods">
|
|
||||||
<div class="warranty-item">
|
|
||||||
<strong>Электроника</strong>
|
|
||||||
<span>12-24 месяца</span>
|
|
||||||
</div>
|
|
||||||
<div class="warranty-item">
|
|
||||||
<strong>Компьютерная техника</strong>
|
|
||||||
<span>12-36 месяцев</span>
|
|
||||||
</div>
|
|
||||||
<div class="warranty-item">
|
|
||||||
<strong>Одежда и обувь</strong>
|
|
||||||
<span>30 дней - 6 месяцев</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">✓</div>
|
|
||||||
<h3>Условия гарантии</h3>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Использование по назначению</li>
|
|
||||||
<li>Без самостоятельного ремонта</li>
|
|
||||||
<li>Сохранены пломбы</li>
|
|
||||||
<li>Нет механических повреждений</li>
|
|
||||||
<li>Есть гарантийный талон</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🛠️</div>
|
|
||||||
<h3>Ваши права при браке</h3>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Бесплатный ремонт</li>
|
|
||||||
<li>Замена товара</li>
|
|
||||||
<li>Возврат денег</li>
|
|
||||||
<li>Снижение цены</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">⏱️</div>
|
|
||||||
<h3>Срок ремонта</h3>
|
|
||||||
<p>Максимум 45 дней по закону. Если срок нарушен - можно требовать замену или возврат денег.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">🚫</div>
|
|
||||||
<h3>Гарантия не действует</h3>
|
|
||||||
<div class="noguar-grid">
|
|
||||||
<div class="noguar-item">Механические повреждения</div>
|
|
||||||
<div class="noguar-item">Неправильная эксплуатация</div>
|
|
||||||
<div class="noguar-item">Попадание жидкости</div>
|
|
||||||
<div class="noguar-item">Самостоятельный ремонт</div>
|
|
||||||
<div class="noguar-item">Форс-мажор</div>
|
|
||||||
<div class="noguar-item">Естественный износ</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📝</div>
|
|
||||||
<h3>Как подать заявку</h3>
|
|
||||||
<div class="process-steps-compact">
|
|
||||||
<p>1. Свяжитесь с продавцом</p>
|
|
||||||
<p>2. Опишите проблему</p>
|
|
||||||
<p>3. Получите адрес сервиса</p>
|
|
||||||
<p>4. Отправьте товар</p>
|
|
||||||
<p>5. Получите акт приема</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📞</div>
|
|
||||||
<h3>Нужна помощь?</h3>
|
|
||||||
<p>При возникновении споров:</p>
|
|
||||||
<a href="mailto:info@novo.market" class="contact-email">info@novo.market</a>
|
|
||||||
<p><a href="tel:+37498731231">+374 98 731231</a></p>
|
|
||||||
<p class="note">Тема: "Гарантийный вопрос - Заказ №..."</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-guarantee-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './guarantee.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/info/guarantee/guarantee.component.scss']
|
|
||||||
})
|
|
||||||
export class GuaranteeNovoComponent {}
|
|
||||||
@@ -1,113 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>О компании</h1>
|
|
||||||
<p class="subtitle">Официальная информация и контакты</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">🏢</div>
|
|
||||||
<h3>Наименование</h3>
|
|
||||||
<p class="org-name">ОБЩЕСТВО С ОГРАНИЧЕННОЙ ОТВЕТСТВЕННОСТЬЮ «ЭЛЕКТРОМОТОРС»</p>
|
|
||||||
<p class="org-short">Сокращенно: ООО «ЭЛЕКТРОМОТОРС»</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📍</div>
|
|
||||||
<h3>Адрес регистрации</h3>
|
|
||||||
<p>АРМЕНИЯ, 0501, АРАГАЦОТИСКАЯ ОБЛАСТЬ, ТАЛИН, ул. ГАЯ, д. 12</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">🏛️</div>
|
|
||||||
<h3>Фактический адрес</h3>
|
|
||||||
<div class="offices-grid">
|
|
||||||
<div class="office">
|
|
||||||
<strong>Головной офис</strong>
|
|
||||||
<p>0501, Армения, Арагацотиская обл., г. Талин, ул. Гая, д. 12</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">📋</div>
|
|
||||||
<h3>Реквизиты</h3>
|
|
||||||
<div class="requisites">
|
|
||||||
<div class="req-item">
|
|
||||||
<span class="req-label">ИНН</span>
|
|
||||||
<span class="req-value">9909687443</span>
|
|
||||||
</div>
|
|
||||||
<div class="req-item">
|
|
||||||
<span class="req-label">КПП</span>
|
|
||||||
<span class="req-value">770287001</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">💳</div>
|
|
||||||
<h3>Банковские реквизиты</h3>
|
|
||||||
<div class="requisites">
|
|
||||||
<div class="req-item">
|
|
||||||
<span class="req-label">Банк</span>
|
|
||||||
<span class="req-value">Уточняется</span>
|
|
||||||
</div>
|
|
||||||
<div class="req-item">
|
|
||||||
<span class="req-label">Р/счёт</span>
|
|
||||||
<span class="req-value">Уточняется</span>
|
|
||||||
</div>
|
|
||||||
<div class="req-item">
|
|
||||||
<span class="req-label">К/счёт</span>
|
|
||||||
<span class="req-value">Уточняется</span>
|
|
||||||
</div>
|
|
||||||
<div class="req-item">
|
|
||||||
<span class="req-label">БИК</span>
|
|
||||||
<span class="req-value">Уточняется</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="note" style="margin-top: 10px; font-size: 12px; color: #666;">Банковские реквизиты будут добавлены после открытия счета</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card wide">
|
|
||||||
<div class="card-icon">📞</div>
|
|
||||||
<h3>Связаться с нами</h3>
|
|
||||||
<div class="contacts-grid">
|
|
||||||
<a href="tel:+37498731231" class="contact-link">
|
|
||||||
<span class="contact-icon">📱</span>
|
|
||||||
<div>
|
|
||||||
<div class="contact-label">Телефон</div>
|
|
||||||
<div class="contact-value">+374 98 731231</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="mailto:info@novo.market" class="contact-link">
|
|
||||||
<span class="contact-icon">✉️</span>
|
|
||||||
<div>
|
|
||||||
<div class="contact-label">Электронная почта</div>
|
|
||||||
<div class="contact-value">info@novo.market</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<a href="https://novo.market" target="_blank" class="contact-link">
|
|
||||||
<span class="contact-icon">🌐</span>
|
|
||||||
<div>
|
|
||||||
<div class="contact-label">Сайт</div>
|
|
||||||
<div class="contact-value">novo.market</div>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="work-hours">
|
|
||||||
<p><strong>Режим работы:</strong> 9:00 - 21:00</p>
|
|
||||||
<p><strong>Поддержка:</strong> В рабочее время</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="info-card">
|
|
||||||
<div class="card-icon">👤</div>
|
|
||||||
<h3>Руководство</h3>
|
|
||||||
<p><strong>Генеральный директор</strong></p>
|
|
||||||
<p>Тоноян Ваграм</p>
|
|
||||||
<p class="basis">Действует на основании Устава</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-company-details-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './company-details.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/legal/company-details/company-details.component.scss']
|
|
||||||
})
|
|
||||||
export class CompanyDetailsNovoComponent {}
|
|
||||||
@@ -1,162 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>Правила оплаты</h1>
|
|
||||||
<p class="subtitle">Все способы оплаты и условия проведения платежей</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📋</div>
|
|
||||||
<h2>1. Общие положения</h2>
|
|
||||||
<p>1.1. Настоящие Правила определяют порядок оплаты Товаров и Услуг, приобретаемых Покупателями через Маркетплейс Novo Market.</p>
|
|
||||||
<p>1.2. Оплата производится за Товары/Услуги, размещенные независимыми Продавцами. Маркетплейс выступает в качестве информационного посредника и обеспечивает техническую инфраструктуру для проведения платежей.</p>
|
|
||||||
<p>1.3. Оплата товаров и услуг на Маркетплейсе осуществляется в российских рублях (RUB).</p>
|
|
||||||
<p>1.4. Цены на Товары/Услуги устанавливаются Продавцами самостоятельно и указываются на странице соответствующего Товара/Услуги.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">💳</div>
|
|
||||||
<h2>2. Способы оплаты</h2>
|
|
||||||
<p>2.1. Маркетплейс поддерживает следующие способы оплаты:</p>
|
|
||||||
<div class="payment-methods-grid">
|
|
||||||
<div class="method-item">
|
|
||||||
<span class="method-icon">🏦</span>
|
|
||||||
<div>
|
|
||||||
<strong>Банковские карты</strong>
|
|
||||||
<p>Visa, Mastercard, МИР</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="method-item">
|
|
||||||
<span class="method-icon">⚡</span>
|
|
||||||
<div>
|
|
||||||
<strong>СБП</strong>
|
|
||||||
<p>Система быстрых платежей - мгновенный перевод через мобильное приложение банка</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="method-item">
|
|
||||||
<span class="method-icon">👛</span>
|
|
||||||
<div>
|
|
||||||
<strong>Электронные кошельки</strong>
|
|
||||||
<p>ЮMoney, QIWI (при наличии)</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="method-item">
|
|
||||||
<span class="method-icon">🔗</span>
|
|
||||||
<div>
|
|
||||||
<strong>Оплата по ссылке</strong>
|
|
||||||
<p>Генерация уникальной платежной ссылки для каждого заказа</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>2.2. Доступные способы оплаты могут различаться в зависимости от Продавца и типа Товара/Услуги.</p>
|
|
||||||
<p>2.3. Все платежи обрабатываются через сертифицированные платежные системы с соблюдением стандартов безопасности PCI DSS.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">⚙️</div>
|
|
||||||
<h2>3. Процесс оплаты</h2>
|
|
||||||
<p>3.1. Процедура оплаты заказа включает следующие этапы:</p>
|
|
||||||
<ol class="compact-list">
|
|
||||||
<li>Выбор Товаров/Услуг и добавление их в корзину</li>
|
|
||||||
<li>Оформление заказа с указанием контактных данных и способа доставки</li>
|
|
||||||
<li>Выбор способа оплаты из доступных вариантов</li>
|
|
||||||
<li>Перенаправление на защищенную страницу платежной системы</li>
|
|
||||||
<li>Ввод платежных данных и подтверждение оплаты</li>
|
|
||||||
<li>Получение уведомления об успешной оплате</li>
|
|
||||||
</ol>
|
|
||||||
<p>3.2. При оплате банковской картой Покупатель может быть перенаправлен на страницу банка-эмитента для прохождения дополнительной аутентификации (3D-Secure).</p>
|
|
||||||
<p>3.3. Обязательство Покупателя по оплате считается исполненным с момента поступления денежных средств на счет платежной системы.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">🛡️</div>
|
|
||||||
<h2>4. Безопасность платежей</h2>
|
|
||||||
<p>4.1. Все платежи обрабатываются через защищенное HTTPS-соединение с использованием протокола TLS 1.2 и выше.</p>
|
|
||||||
<p>4.2. Маркетплейс не хранит полные данные банковских карт Покупателей. Обработка платежных данных осуществляется сертифицированными платежными агрегаторами.</p>
|
|
||||||
<div class="features-list">
|
|
||||||
<div class="feature">✓ Шифрование TLS 1.2+</div>
|
|
||||||
<div class="feature">✓ Технология 3D-Secure</div>
|
|
||||||
<div class="feature">✓ Защита от мошенничества</div>
|
|
||||||
<div class="feature">✓ Конфиденциальность данных</div>
|
|
||||||
</div>
|
|
||||||
<p>4.3. Для защиты от мошенничества применяется технология 3D-Secure, требующая подтверждения платежа через SMS-код или push-уведомление от банка.</p>
|
|
||||||
<p>4.4. В случае подозрительной активности платежная система имеет право запросить дополнительную верификацию личности Покупателя.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">✅</div>
|
|
||||||
<h2>5. Подтверждение оплаты</h2>
|
|
||||||
<p>5.1. После успешной оплаты Покупатель получает подтверждение на указанный при оформлении заказа адрес электронной почты.</p>
|
|
||||||
<p>5.2. Подтверждение содержит следующую информацию:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Номер заказа</li>
|
|
||||||
<li>Дата и время оплаты</li>
|
|
||||||
<li>Сумма платежа</li>
|
|
||||||
<li>Состав заказа</li>
|
|
||||||
<li>Контактные данные Продавца</li>
|
|
||||||
</ul>
|
|
||||||
<p>5.3. Информация о заказе также отображается в личном кабинете Покупателя на Маркетплейсе (при наличии регистрации).</p>
|
|
||||||
<p>5.4. Фискальный чек направляется Продавцом в соответствии с требованиями законодательства РФ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">↩️</div>
|
|
||||||
<h2>6. Возврат средств</h2>
|
|
||||||
<p>6.1. Порядок возврата денежных средств регулируется <a routerLink="/return-policy">Политикой возврата</a> и зависит от типа приобретенного Товара/Услуги.</p>
|
|
||||||
<p>6.2. Возврат средств производится на тот же платежный инструмент, с которого была произведена оплата.</p>
|
|
||||||
<p>6.3. Срок возврата денежных средств составляет:</p>
|
|
||||||
<div class="refund-times">
|
|
||||||
<div class="refund-item">
|
|
||||||
<strong>Банковская карта</strong>
|
|
||||||
<span>3-30 дней</span>
|
|
||||||
</div>
|
|
||||||
<div class="refund-item">
|
|
||||||
<strong>Электронный кошелек</strong>
|
|
||||||
<span>1-5 дней</span>
|
|
||||||
</div>
|
|
||||||
<div class="refund-item">
|
|
||||||
<strong>СБП</strong>
|
|
||||||
<span>1-3 дня</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p class="note">Возврат производится на тот же платежный инструмент, который использовался при оплате</p>
|
|
||||||
<p>6.4. За обработку возврата средств Маркетплейс комиссию не взимает. Комиссии платежных систем и банков могут применяться в соответствии с их тарифами.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">❌</div>
|
|
||||||
<h2>7. Неуспешные платежи</h2>
|
|
||||||
<p>7.1. Платеж может быть отклонен по следующим причинам:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Недостаточно средств на счете</li>
|
|
||||||
<li>Неверно введены платежные данные</li>
|
|
||||||
<li>Карта заблокирована или просрочена</li>
|
|
||||||
<li>Превышены лимиты на операции, установленные банком</li>
|
|
||||||
<li>Отказ в проведении транзакции системой безопасности</li>
|
|
||||||
</ul>
|
|
||||||
<p>7.2. В случае неуспешной оплаты Покупатель получает уведомление с указанием причины отказа.</p>
|
|
||||||
<p>7.3. При возникновении проблем с оплатой рекомендуется:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Проверить правильность введенных данных</li>
|
|
||||||
<li>Связаться с банком-эмитентом карты для уточнения причины отказа</li>
|
|
||||||
<li>Попробовать альтернативный способ оплаты</li>
|
|
||||||
<li>Обратиться в службу поддержки: <a href="mailto:info@novo.market">info@novo.market</a></li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📧</div>
|
|
||||||
<h2>8. Контакты для вопросов по оплате</h2>
|
|
||||||
<p>По вопросам, связанным с оплатой заказов, вы можете обратиться:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li><strong>Email:</strong> <a href="mailto:info@novo.market" class="contact-email">info@novo.market</a></li>
|
|
||||||
<li><strong>Телефон:</strong> <a href="tel:+37498731231">+374 98 731231</a></li>
|
|
||||||
<li><strong>Время работы:</strong> Круглосуточно (техническая поддержка)</li>
|
|
||||||
<li><strong>Среднее время ответа:</strong> До 24 часов в рабочие дни</li>
|
|
||||||
</ul>
|
|
||||||
<p>При обращении указывайте номер заказа и краткое описание проблемы для более быстрого решения вопроса.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-payment-terms-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './payment-terms.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/legal/payment-terms/payment-terms.component.scss']
|
|
||||||
})
|
|
||||||
export class PaymentTermsNovoComponent {}
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>Политика конфиденциальности</h1>
|
|
||||||
<p class="subtitle">Защита ваших персональных данных</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📋</div>
|
|
||||||
<h2>1. ОБЩИЕ ПОЛОЖЕНИЯ</h2>
|
|
||||||
|
|
||||||
<p>1.1. Настоящая Политика конфиденциальности (далее — Политика) определяет обязательства Общества с ограниченной ответственностью «ЭЛЕКТРОМОТОРС» (далее - Оператор) по обработке, использованию, хранению и защите персональных данных (далее ПДн, Персональные данные) пользователей (далее - Пользователь).</p>
|
|
||||||
|
|
||||||
<p>1.2. Оператор берет на себя обязательство строго соблюдать права и свободы человека и гражданина при обработке его персональных данных, включая защиту прав на неприкосновенность частной жизни, личную и семейную тайну.</p>
|
|
||||||
|
|
||||||
<p>1.3. Политика обработки персональных данных (далее — Политика) разработана в соответствии с Федеральным законом от 27.07.2006. №152-ФЗ «О персональных данных» (далее — ФЗ-152).</p>
|
|
||||||
|
|
||||||
<p>1.4. Ознакомиться с Политикой возможно на сайте Оператора в информационно-телекоммуникационной сети «Интернет» по адресу: <a href="https://novo.market">https://novo.market</a>.</p>
|
|
||||||
|
|
||||||
<p>1.5. Действует в отношении всех действий, связанных с обработкой персональных данных на сайте <a href="https://novo.market">https://novo.market</a> и в информационных системах Оператора.</p>
|
|
||||||
|
|
||||||
<p>1.6. Пользователь, оформляющий заказ, открывающий личный кабинет или иным образом взаимодействующий с Оператором, выражает согласие на обработку своих персональных данных в соответствии с Политикой и законодательством РФ. Продолжительное использование сайта свидетельствует о согласии с Положениями Политики. Пользователь, не готовый согласиться с условиями, должен воздержаться от использования ресурса.</p>
|
|
||||||
|
|
||||||
<p><strong>Дополнительно:</strong> Настоящая Политика распространяется на персональные данные, собранные как до вступления документа в силу, так и после.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📖</div>
|
|
||||||
<h2>2. ТЕРМИНЫ И ОПРЕДЕЛЕНИЯ</h2>
|
|
||||||
|
|
||||||
<p>В данной Политике используются следующие основные термины и определения:</p>
|
|
||||||
|
|
||||||
<div class="compact-list">
|
|
||||||
<p><strong>Персональные данные (ПДн)</strong> — любая информация, прямо или косвенно относящаяся к определённому физическому лицу (субъекту персональных данных).</p>
|
|
||||||
|
|
||||||
<p><strong>Информационная система персональных данных (ИСПДн)</strong> — совокупность персональных данных, хранимых в базах данных, а также технологий и средств для их обработки.</p>
|
|
||||||
|
|
||||||
<p><strong>Автоматизированная обработка ПДн</strong> — обработка данных с использованием компьютерных средств.</p>
|
|
||||||
|
|
||||||
<p><strong>Блокировка ПДн</strong> — временная приостановка обработки данных (за исключением случаев уточнения данных).</p>
|
|
||||||
|
|
||||||
<p><strong>Обезличивание ПДн</strong> — действия, ведущие к невозможности установления принадлежности данных конкретному лицу без дополнительной информации.</p>
|
|
||||||
|
|
||||||
<p><strong>Интернет-сайт (Сайт)</strong> — автоматизированная информационная система, доступная в сети Интернет по адресу: <a href="https://novo.market">https://novo.market</a>.</p>
|
|
||||||
|
|
||||||
<p><strong>Обработка ПДн</strong> — любые действия с персональными данными, включая сбор, запись, хранение, обновление, использование, передачу, уничтожение и другие операции.</p>
|
|
||||||
|
|
||||||
<p><strong>Оператор</strong> — государственный или частный орган, самостоятельно или совместно организующий обработку персональных данных.</p>
|
|
||||||
|
|
||||||
<p><strong>Предоставление ПДн</strong> — передача данных определённому лицу или группе лиц.</p>
|
|
||||||
|
|
||||||
<p><strong>Распространение ПДн</strong> — открытие данных неопределённому кругу лиц, включая публикацию в СМИ или сети Интернет.</p>
|
|
||||||
|
|
||||||
<p><strong>Трансграничная передача ПДн</strong> — передача данных зарубежным властям, компаниям или физическим лицам.</p>
|
|
||||||
|
|
||||||
<p><strong>Уничтожение ПДн</strong> — действия, приводящие к утрате возможности восстановления данных или уничтожения материальных носителей.</p>
|
|
||||||
|
|
||||||
<p><strong>Субъект ПДн</strong> — физическое лицо, чья информация обрабатывается.</p>
|
|
||||||
|
|
||||||
<p><strong>Конфиденциальность ПДн</strong> — обязанность Оператора защищать данные от распространения без согласия субъекта или законного основания.</p>
|
|
||||||
|
|
||||||
<p><strong>Продавец (Исполнитель)</strong> — лицо, предлагающее товары или услуги на сайте <a href="https://novo.market">https://novo.market</a>.</p>
|
|
||||||
|
|
||||||
<p><strong>Пользователь</strong> — лицо, посещающее или использующее ресурсы, управляемые Оператором, включая сайт <a href="https://novo.market">https://novo.market</a>.</p>
|
|
||||||
|
|
||||||
<p><strong>Заказ</strong> — оформленный Пользователем заказ товаров или услуг на сайте.</p>
|
|
||||||
|
|
||||||
<p><strong>Файлы cookie</strong> — небольшие файлы, сохраняемые на устройстве пользователя для запоминания предпочтений и действий при последующих посещениях сайта.</p>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">⚖️</div>
|
|
||||||
<h2>3. ПРАВОВЫЕ ОСНОВАНИЯ ОБРАБОТКИ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
|
|
||||||
|
|
||||||
<p>3.1. Правовым основанием Обработки ПДн в зависимости от целей процесса, предусматривающего Обработку ПДн может являться:</p>
|
|
||||||
|
|
||||||
<h3>3.1.1. Конституция Российской Федерации, а также совокупность правовых актов:</h3>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Налоговый кодекс Российской Федерации;</li>
|
|
||||||
<li>Гражданский Кодекс Российской Федерации;</li>
|
|
||||||
<li>ст. 86 - 90 Трудового кодекса Российской Федерации;</li>
|
|
||||||
<li>Федеральный закон от 07.08.2001 № 115-ФЗ «О противодействии легализации (отмыванию) доходов, полученных преступным путем, и финансированию терроризма»;</li>
|
|
||||||
<li>Федеральный закон от 27.07.2006 г. № 152-ФЗ «О персональных данных»;</li>
|
|
||||||
<li>Федеральный закон от 22.04.1996 № 39-ФЗ «О рынке ценных бумаг»;</li>
|
|
||||||
<li>Федеральный закон от 26.12.1995 №208-ФЗ «Об акционерных обществах»;</li>
|
|
||||||
<li>Федеральный закон от 27.07.2006 № 149-ФЗ «Об информации, информационных технологиях и о защите информации»;</li>
|
|
||||||
<li>Федеральный закон от 01.04.1996 № 27-ФЗ «Об индивидуальном (персонифицированном) учете в системе обязательного пенсионного страхования»;</li>
|
|
||||||
<li>Федеральный закон от 06.04.2011 № 63-ФЗ «Об электронной подписи»;</li>
|
|
||||||
<li>Федеральный закон от 06.12.2011 № 402-ФЗ «О бухгалтерском учете»;</li>
|
|
||||||
<li>Федеральный закон от 27.06.2011 № 161-ФЗ «О национальной платежной системе»;</li>
|
|
||||||
<li>Постановление Правительства Российской Федерации от 15.09.2008 № 687 «Об утверждении положения об особенностях обработки персональных данных, осуществляемой без использования средств автоматизации»;</li>
|
|
||||||
<li>Постановлением Правительства РФ от 01.11.2012 № 1119 "Об утверждении требований к защите персональных данных при их обработке в информационных системах персональных данных"</li>
|
|
||||||
<li>иные нормативные правовые акты Российской Федерации и нормативные документы исполнительных органов государственной власти.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>3.1.2. Устав Оператора.</p>
|
|
||||||
|
|
||||||
<p>3.1.3. Договоры, заключаемые между Оператором и Субъектом Персональных данных, в том числе в случае реализации Оператором своего права на уступку прав (требований) по таким договорам, между Оператором и иным лицом, поручившим Оператору Обработку ПДн, а также для заключения договоров, стороной которых являются Субъекты Персональных данных.</p>
|
|
||||||
|
|
||||||
<p>3.1.4. Согласие на Обработку ПДн (в случаях, прямо не предусмотренных законодательством Российской Федерации, но соответствующих полномочиям Оператора), в т.ч. согласие соискателей на замещение вакантных должностей на Обработку ПДн, согласие практиканта на Обработку ПДн, согласие работников на Обработку ПДн; согласие клиентов на Обработку ПДн, согласие Пользователей соответствующего Сайта, согласие иных Субъектов Персональных данных.</p>
|
|
||||||
|
|
||||||
<p>3.1.5. Договор между оператором и третьим лицом, где последнее поручает Оператору обработку персональных данных Субъекта Персональных данных или передает на основании заключенного договора персональные данные Субъекта Персональных данных.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">👥</div>
|
|
||||||
<h2>4. КАТЕГОРИИ СУБЪЕКТОВ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
|
|
||||||
|
|
||||||
<p>4.1. Оператором осуществляется Обработка полученных в установленном законом порядке ПДн, принадлежащих:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Кандидатам на работу и работникам Оператора;</li>
|
|
||||||
<li>Уволенным работникам Оператора;</li>
|
|
||||||
<li>Близким родственникам/членам семьи работников Оператора, практикантам;</li>
|
|
||||||
<li>Потенциальным клиентам, клиентам - физическим лицам, клиентам - индивидуальным предпринимателям;</li>
|
|
||||||
<li>Физическим лицам, заключившим с Оператором гражданско-правовые договоры;</li>
|
|
||||||
<li>Пользователям Сайтов Оператора, получателям Заказов;</li>
|
|
||||||
<li>Клиентам других юридических лиц;</li>
|
|
||||||
<li>Собственникам Оператора;</li>
|
|
||||||
<li>Иным субъектам, вступившим или намеревающимися вступить в договорные отношения с Оператором.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">📝</div>
|
|
||||||
<h2>5. КАТЕГОРИИ ОБРАБАТЫВАЕМЫХ ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
|
|
||||||
|
|
||||||
<p>5.1. Оператор обрабатывает следующие категории ПДн Пользователей:</p>
|
|
||||||
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Сведения, полученные при регистрации и/или оформлении Заказа (фамилию, имя, фактический адрес, номер телефона, адрес электронной почты, cookie);</li>
|
|
||||||
<li>Сведения, полученные при взаимодействии с Пользователями (фамилия, имя, отчество, пол, место рождения, дата рождения, паспортные данные, адрес, контакты);</li>
|
|
||||||
<li>Сведения о способе доставки и оплаты Заказа;</li>
|
|
||||||
<li>Сведения о претензиях Пользователя;</li>
|
|
||||||
<li>Сведения о геолокации (местонахождении).</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>5.2. Персональные данные могут быть получены Оператором путем:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Предоставления Субъектами через формы на Сайте или по email;</li>
|
|
||||||
<li>Получения от третьих лиц (Продавцов, контрагентов) в рамках законодательства РФ.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">⚙️</div>
|
|
||||||
<h2>6. ПРИНЦИПЫ, ПОРЯДОК И УСЛОВИЯ ОБРАБОТКИ</h2>
|
|
||||||
|
|
||||||
<h3>6.1. Принципы обработки Персональных данных</h3>
|
|
||||||
<p>Обработка ПДн у Оператора осуществляется на основе следующих принципов:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>законности и справедливой основы;</li>
|
|
||||||
<li>ограничения Обработки ПДн достижением конкретных, заранее определенных и законных целей;</li>
|
|
||||||
<li>недопущения Обработки ПДн, несовместимой с целями сбора ПДн;</li>
|
|
||||||
<li>обеспечения точности, достаточности и актуальности ПДн;</li>
|
|
||||||
<li>хранение ПДн не дольше, чем этого требуют цели Обработки;</li>
|
|
||||||
<li>уничтожения либо обезличивания ПДн по достижении целей их Обработки.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>6.2. Обязанности работников Оператора</h3>
|
|
||||||
<p>Работники Оператора, допущенные к Обработке Персональных данных, обязаны:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Знать и выполнять положения законодательства РФ в области ПДн;</li>
|
|
||||||
<li>Знать и выполнять положения настоящей Политики;</li>
|
|
||||||
<li>Обрабатывать ПДн только в рамках должностных обязанностей;</li>
|
|
||||||
<li>Не разглашать ПДн, обрабатываемые Оператором;</li>
|
|
||||||
<li>Сообщать о действиях, которые могут привести к нарушению Политики.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3>6.3-6.24. Дополнительные условия</h3>
|
|
||||||
<p>Включают условия обработки, хранения, трансграничной передачи данных, работы с общедоступными источниками, специальными и биометрическими данными, а также порядок получения разъяснений согласно законодательству РФ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">✅</div>
|
|
||||||
<h2>7. ПРАВА СУБЪЕКТА ПЕРСОНАЛЬНЫХ ДАННЫХ</h2>
|
|
||||||
|
|
||||||
<h3>7.1. Согласие Субъекта</h3>
|
|
||||||
<p>Субъект ПДн дает согласие на Обработку свободно, своей волей и в своем интересе.</p>
|
|
||||||
|
|
||||||
<h3>7.2. Права Субъекта</h3>
|
|
||||||
<p>Субъект ПДн имеет право на получение информации о:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>подтверждение факта Обработки ПДн;</li>
|
|
||||||
<li>правовые основания и цели Обработки;</li>
|
|
||||||
<li>применяемые способы Обработки;</li>
|
|
||||||
<li>сроки Обработки и хранения данных;</li>
|
|
||||||
<li>уточнение, блокирование или уничтожение данных;</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>Субъект ПДн имеет право отозвать свое Согласие</strong> и потребовать удалить свои ПДн, направив сообщение на: <a href="mailto:info@novo.market" class="contact-email">info@novo.market</a></p>
|
|
||||||
|
|
||||||
<p>Субъект также имеет право на защиту своих прав, возмещение убытков и компенсацию морального вреда.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">📋</div>
|
|
||||||
<h2>8. ОБЯЗАННОСТИ ОПЕРАТОРА</h2>
|
|
||||||
|
|
||||||
<p>8.1. Оператор обязан предоставить Субъекту информацию о обработке его данных при обращении.</p>
|
|
||||||
|
|
||||||
<p>8.2. Оператор обеспечивает хранение данных граждан РФ на территории Российской Федерации.</p>
|
|
||||||
|
|
||||||
<p>8.3. Оператор несет иные обязанности, установленные ФЗ-152.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">🛡️</div>
|
|
||||||
<h2>9. ОБЕСПЕЧЕНИЕ БЕЗОПАСНОСТИ</h2>
|
|
||||||
|
|
||||||
<p>9.1-9.3. Безопасность обеспечивается комплексом организационных и технических мер:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>назначение ответственных лиц;</li>
|
|
||||||
<li>ограничение доступа к ПДн;</li>
|
|
||||||
<li>обучение сотрудников;</li>
|
|
||||||
<li>учет и хранение носителей с ПДн;</li>
|
|
||||||
<li>использование антивирусных средств;</li>
|
|
||||||
<li>средства шифрования и межсетевого экранирования;</li>
|
|
||||||
<li>физическая защита помещений;</li>
|
|
||||||
<li>контроль мер безопасности.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">⚠️</div>
|
|
||||||
<h2>10. ОТВЕТСТВЕННОСТЬ</h2>
|
|
||||||
|
|
||||||
<p>10.1. Лица, виновные в нарушении норм обработки и защиты Персональных данных, несут ответственность согласно законодательству РФ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">🎯</div>
|
|
||||||
<h2>11. ЦЕЛИ ОБРАБОТКИ</h2>
|
|
||||||
|
|
||||||
<p>11.1. Оператор обрабатывает ПДн для следующих целей:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Предоставления доступа к Сайту и личному кабинету;</li>
|
|
||||||
<li>Исполнения договоров купли-продажи и оказания услуг;</li>
|
|
||||||
<li>Доставки товаров;</li>
|
|
||||||
<li>Урегулирования претензий;</li>
|
|
||||||
<li>Обработки платежей;</li>
|
|
||||||
<li>Улучшения качества обслуживания;</li>
|
|
||||||
<li>Получения обратной связи;</li>
|
|
||||||
<li>Проведения маркетинговых исследований;</li>
|
|
||||||
<li>Направления рекламных сообщений;</li>
|
|
||||||
<li>В иных случаях, предусмотренных законодательством РФ.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">🍪</div>
|
|
||||||
<h2>12. АВТОМАТИЧЕСКИ СОБИРАЕМАЯ ИНФОРМАЦИЯ</h2>
|
|
||||||
|
|
||||||
<p>12.1. Оператор собирает и обрабатывает:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>информацию об интересах на основе поисковых запросов;</li>
|
|
||||||
<li>информацию для формирования рейтинга (отзывы, данные об исполнении Заказов);</li>
|
|
||||||
<li>статистику об использовании Сайта.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p>12.2. Оператор использует cookies, веб-отметки и другие технологии мониторинга. Эти технологии не дают возможность автоматически получать ПДн.</p>
|
|
||||||
|
|
||||||
<p>12.3. Если собранные сведения можно соотнести с личным кабинетом Пользователя, они могут обрабатываться совместно с ПДн.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<div class="info-card wide contact-card">
|
|
||||||
<div class="card-icon">📧</div>
|
|
||||||
<h3>Контакты для связи</h3>
|
|
||||||
<p>По всем вопросам обработки персональных данных обращайтесь:</p>
|
|
||||||
<a href="mailto:info@novo.market" class="contact-email">info@novo.market</a>
|
|
||||||
<p><a href="tel:+37498731231">+374 98 731231</a></p>
|
|
||||||
<p class="note">Мы ответим в течение 30 дней согласно законодательству РФ</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-privacy-policy-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './privacy-policy.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/legal/privacy-policy/privacy-policy.component.scss']
|
|
||||||
})
|
|
||||||
export class PrivacyPolicyNovoComponent {}
|
|
||||||
@@ -1,267 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>Пользовательское соглашение</h1>
|
|
||||||
<p class="subtitle">Правила работы на платформе</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📚</div>
|
|
||||||
<h2>Ключевые термины</h2>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li><strong>Платформа</strong> — программно-технический комплекс novo.market. Мы предоставляем инфраструктуру для торговли, не являясь продавцами.</li>
|
|
||||||
<li><strong>Клиент</strong> — физическое лицо, использующее сайт после регистрации и принятия данных условий.</li>
|
|
||||||
<li><strong>Аккаунт</strong> — индивидуальная зона клиента с авторизацией через email или телефон.</li>
|
|
||||||
<li><strong>Заказчик</strong> — клиент, совершивший покупку через платформу.</li>
|
|
||||||
<li><strong>Оператор</strong> — компания ООО «ЭЛЕКТРОМОТОРС», ИНН 9909687443.</li>
|
|
||||||
<li><strong>Поставщик</strong> — физ./юрлица и ИП, реализующие товары через платформу.</li>
|
|
||||||
<li><strong>Продукция</strong> — физические, электронные товары и сервисы в каталоге.</li>
|
|
||||||
<li><strong>Электронная продукция</strong> — софт, курсы, подписки, медиаконтент в цифровом виде.</li>
|
|
||||||
<li><strong>Покупка</strong> — оформленная заявка клиента на приобретение.</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📋</div>
|
|
||||||
<h2>1. Базовые правила</h2>
|
|
||||||
<p>1.1. Этот текст определяет правила взаимодействия с нашими ресурсами.</p>
|
|
||||||
<p>1.2. Соглашение обязательно для клиента и оператора платформы.</p>
|
|
||||||
<p>1.3. Документ квалифицируется как публичная оферта по ст. 435 и п. 2 ст. 437 ГК РФ.</p>
|
|
||||||
<p>1.4. Акцепт происходит автоматически при любом действии: визите, регистрации, оформлении покупки.</p>
|
|
||||||
<p>1.5. Подписание бумажного договора не требуется — электронная форма юридически действительна.</p>
|
|
||||||
<p>1.6. Несогласие с условиями означает обязанность покинуть сайт.</p>
|
|
||||||
<p>1.7. Также применяется <a routerLink="/privacy-policy">Политика конфиденциальности</a>.</p>
|
|
||||||
<p>1.8. Мы можем обновлять условия в одностороннем порядке.</p>
|
|
||||||
<p>1.9. Промо-кампании могут иметь специальные правила.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">🎯</div>
|
|
||||||
<h2>2. Предмет соглашения</h2>
|
|
||||||
<p>2.1. Целью является предоставление пользователям возможности покупать товары и услуги, представленные на ресурсе.</p>
|
|
||||||
<p>2.2. Соглашение регулирует порядок использования сайта и предоставляемых функций.</p>
|
|
||||||
<p>2.3. Действие распространяется на все типы товаров, услуг и продуктов на сайте.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">🛒</div>
|
|
||||||
<h2>3. Условия продажи товаров и услуг</h2>
|
|
||||||
<p><strong>3.1. Принятие условий:</strong> Оформляя заказ, пользователь соглашается с условиями.</p>
|
|
||||||
<p><strong>3.2. Заключение договоров:</strong> Договор заключается между продавцом и покупателем. Маркетплейс — информационный посредник, не участник сделки.</p>
|
|
||||||
<p><strong>3.3. Обработка контактов:</strong> Пользователь даёт согласие на использование контактных данных для выполнения обязательств.</p>
|
|
||||||
<p><strong>3.4. Третьи лица:</strong> Продавец может привлекать третьих лиц для исполнения договора.</p>
|
|
||||||
<p><strong>3.5. Права и обязанности:</strong> Права и обязанности возникают у продавца. Владелец сайта — оператор маркетплейса, не отвечает за действия продавцов.</p>
|
|
||||||
<p><strong>3.6. Функции владельца:</strong> Информационное и техническое сопровождение, координация взаимодействия, помощь в урегулировании споров.</p>
|
|
||||||
<p><strong>3.7. Переуступка прав:</strong> Продавец может уступать права третьим сторонам.</p>
|
|
||||||
<p><strong>3.8. Применение законов:</strong> Регулируются ФЗ "О защите прав потребителей" №2300-1 и ГК РФ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">📱</div>
|
|
||||||
<h2>4. Регистрация и личный кабинет</h2>
|
|
||||||
<p><strong>4.1. Процедура:</strong> Регистрация необязательна для заказа, но открывает доступ к личному кабинету.</p>
|
|
||||||
<p><strong>4.2. Обязанности:</strong> Предоставлять точную информацию и поддерживать её актуальность.</p>
|
|
||||||
<p><strong>4.3. Идентификация:</strong> Действия с использованием логина и пароля считаются действиями пользователя.</p>
|
|
||||||
<p><strong>4.4. Конфиденциальность:</strong> Хранить логин и пароль в тайне. При подозрении на нарушение — сообщить на <a href="mailto:info@novo.market">info@novo.market</a>.</p>
|
|
||||||
<p><strong>4.5. Подтверждение данных:</strong> Владелец может запросить подтверждение информации.</p>
|
|
||||||
<p><strong>4.6. Завершение сеанса:</strong> Самостоятельно завершать работу в личном кабинете («Выход»).</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">⚙️</div>
|
|
||||||
<h2>5. Права и обязанности владельца сайта</h2>
|
|
||||||
<p><strong>Права владельца:</strong></p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Устанавливать ограничения в использовании ресурса</li>
|
|
||||||
<li>Направлять информацию о нововведениях</li>
|
|
||||||
<li>Передавать полномочия третьим лицам</li>
|
|
||||||
<li>Изменять условия акций в одностороннем порядке</li>
|
|
||||||
<li>Ограничивать действия пользователей, создающих риски</li>
|
|
||||||
<li>Проводить технические работы без предупреждения</li>
|
|
||||||
<li>Использовать статистику в собственных интересах</li>
|
|
||||||
<li>Менять список товаров, цены и условия</li>
|
|
||||||
<li>Отказывать в обслуживании при подозрении в нарушениях</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Обязанности:</strong> Оказание услуг, предусмотренных п. 2.1 соглашения.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">👤</div>
|
|
||||||
<h2>6. Права и обязанности пользователя</h2>
|
|
||||||
<p><strong>Обязанности пользователя:</strong></p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Ознакомиться с соглашением до заключения договора</li>
|
|
||||||
<li>Этично вести себя при написании отзывов</li>
|
|
||||||
<li>Общаться вежливо и уважительно</li>
|
|
||||||
<li>Не распространять ненормативную лексику</li>
|
|
||||||
<li>Не создавать помех работе сайта</li>
|
|
||||||
<li>Не загружать вирусы и вредоносные программы</li>
|
|
||||||
<li>Не использовать автоматизированные программы без разрешения</li>
|
|
||||||
<li>Не пытаться получить доступ к чужим данным</li>
|
|
||||||
<li>Законно использовать контент сайта</li>
|
|
||||||
<li>Регистрироваться только от собственного имени</li>
|
|
||||||
<li>Своевременно оплачивать заказы</li>
|
|
||||||
<li>Использовать товары для личных нужд</li>
|
|
||||||
<li>Беречь информационную безопасность ресурса</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Запрещено:</strong></p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Загрузка контента, противоречащего закону, с вирусами или ложной информацией</li>
|
|
||||||
<li>Нарушение прав других пользователей</li>
|
|
||||||
<li>Поведение, нарушающее российское законодательство</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>Права:</strong> Отказаться от рекламных сообщений через сайт или письмом на <a href="mailto:info@novo.market">info@novo.market</a>.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">©</div>
|
|
||||||
<h2>7. Исключительные права на контент</h2>
|
|
||||||
<p><strong>7.1. Интеллектуальная собственность:</strong> Весь контент (дизайн, тексты, графика, видео, программы, базы данных) является объектом авторских прав владельца и правообладателей.</p>
|
|
||||||
<p><strong>7.2. Использование:</strong> Разрешено только в рамках функциональности сайта. Любое другое использование без разрешения недопустимо.</p>
|
|
||||||
<p><strong>7.3. Личное использование:</strong> Для личного некоммерческого использования при сохранении обозначений авторских прав.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">🔗</div>
|
|
||||||
<h2>8. Сайты и контент третьих лиц</h2>
|
|
||||||
<p><strong>8.1. Внешние ссылки:</strong> Сайт может содержать ссылки на сторонние ресурсы. Ответственность за их содержание не относится к владельцу сайта.</p>
|
|
||||||
<p><strong>8.2. Характер ссылок:</strong> Ссылки не означают одобрения или рекомендаций.</p>
|
|
||||||
<p><strong>8.3. Цитирование:</strong> При воспроизведении материалов необходима активная ссылка на источник.</p>
|
|
||||||
<p><strong>8.4. Брендинг:</strong> Логотип, название и оформление принадлежат владельцу. Использование без разрешения запрещено.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">⚖️</div>
|
|
||||||
<h2>9. Ответственность сторон</h2>
|
|
||||||
<p><strong>9.1. Общее:</strong> Пользователь использует сайт на свой риск. Ресурсы предоставляются "как есть".</p>
|
|
||||||
<p><strong>9.2. Ограничения:</strong> Владелец не гарантирует постоянную доступность и отсутствие ошибок.</p>
|
|
||||||
<p><strong>9.3. Информация:</strong> Пользователь несёт ответственность за последствия использования информации.</p>
|
|
||||||
<p><strong>9.4. Недостоверные данные:</strong> Владелец не отвечает за неверные данные пользователя.</p>
|
|
||||||
<p><strong>9.5. Безопасность аккаунта:</strong> Пользователь отвечает за сохранность логина и пароля.</p>
|
|
||||||
<p><strong>9.6. Ответственность продавца:</strong> Продавец отвечает за качество, безопасность и соответствие товаров.</p>
|
|
||||||
<p><strong>9.7. Освобождение владельца:</strong> Владелец не отвечает за:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Качество, безопасность и правомерность товаров продавцов</li>
|
|
||||||
<li>Выполнение продавцами обязательств</li>
|
|
||||||
<li>Истинность информации о товарах</li>
|
|
||||||
<li>Нарушение прав третьих лиц</li>
|
|
||||||
<li>Доставку, комплектацию и состояние товаров</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>9.8. Доставка:</strong> Ответственность за доставку несут транспортные компании.</p>
|
|
||||||
<p><strong>9.9. Третьи лица:</strong> Пользователь отвечает перед третьими лицами за свои действия.</p>
|
|
||||||
<p><strong>9.10. Форс-мажор:</strong> Работа может быть приостановлена при непредвиденных обстоятельствах.</p>
|
|
||||||
<p><strong>9.11. Реклама:</strong> Ответственность за рекламу несут продавцы.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">🛍️</div>
|
|
||||||
<h2>10. Товар и порядок покупки</h2>
|
|
||||||
<p><strong>10.1. Оформление:</strong> Заказ оформляется по процедуре на сайте.</p>
|
|
||||||
<p><strong>10.2. Некорректные данные:</strong> Покупатель принимает риски неполучения заказа при ошибочной информации.</p>
|
|
||||||
<p><strong>10.3. Уведомление о доставке:</strong> После формирования заказа покупатель получает информацию о доставке.</p>
|
|
||||||
<p><strong>10.4. Уточнение времени:</strong> Перед доставкой сотрудник свяжется для уточнения времени.</p>
|
|
||||||
<p><strong>10.5. Наличие товара:</strong> Продавец не гарантирует постоянное наличие товара на складе.</p>
|
|
||||||
<p><strong>10.6. Законодательство:</strong> Поставка осуществляется в соответствии с законами РФ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">🚚</div>
|
|
||||||
<h2>11. Доставка заказа</h2>
|
|
||||||
<p><strong>11.1. Способы и сроки:</strong> Указаны на сайте. Фактические сроки уточняются при подтверждении заказа.</p>
|
|
||||||
<p><strong>11.2. Цифровые товары:</strong> Подтверждаются отправкой письма с файлом или кодом. Физический товар — реальной передачей.</p>
|
|
||||||
<p><strong>11.3. Территория:</strong> Границы доставки опубликованы на сайте.</p>
|
|
||||||
<p><strong>11.4. Приём товара:</strong> Товар передается покупателю или указанному лицу.</p>
|
|
||||||
<p><strong>11.5. Проверка личности:</strong> При предоплате может потребоваться удостоверение личности.</p>
|
|
||||||
<p><strong>11.6. Переход риска:</strong> Риск переходит к покупателю при передаче товара.</p>
|
|
||||||
<p><strong>11.7. Стоимость доставки:</strong> Рассчитывается индивидуально.</p>
|
|
||||||
<p><strong>11.8. Выполнение обязательств:</strong> Продавец выполнил обязательства при передаче товара.</p>
|
|
||||||
<p><strong>11.9. Осмотр товара:</strong> Покупатель может осмотреть товар при получении.</p>
|
|
||||||
<p><strong>11.10. Проверка:</strong> Можно проверить количество, ассортимент и комплектацию.</p>
|
|
||||||
<p><strong>11.11. Время курьера:</strong> Максимум 20 минут по адресу.</p>
|
|
||||||
<p><strong>11.12. Сертификация:</strong> Товары соответствуют ГОСТ и ТУ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">💳</div>
|
|
||||||
<h2>12. Оплата товара</h2>
|
|
||||||
<p><strong>12.1. Валюта и цена:</strong> В рублях РФ, включая налоги и сборы.</p>
|
|
||||||
<p><strong>12.2. Ошибки в ценах:</strong> При обнаружении продавец уведомляет покупателя. Покупатель может подтвердить или отменить заказ.</p>
|
|
||||||
<p><strong>12.3. Изменение цен:</strong> Продавец может менять цены, но не на оплаченные товары.</p>
|
|
||||||
<p><strong>12.4. Способы оплаты:</strong> Указаны на сайте при оформлении заказа.</p>
|
|
||||||
<p><strong>12.5. Банковские карты:</strong></p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Платежи держателем карты или лицом по доверенности</li>
|
|
||||||
<li>Авторизация через банк</li>
|
|
||||||
<li>Может требоваться документ личности</li>
|
|
||||||
<li>Электронный чек на email</li>
|
|
||||||
<li>Проверка заказов для предотвращения мошенничества</li>
|
|
||||||
<li>Данные карт не хранятся</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>12.6. Скидки и бонусы:</strong> Устанавливаются продавцом и публикуются на сайте.</p>
|
|
||||||
<p><strong>12.7. Наличные:</strong> Оплата наличными не осуществляется.</p>
|
|
||||||
<p><strong>12.8. Возврат средств:</strong> В течение 10 дней с момента заявления.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">↩️</div>
|
|
||||||
<h2>13. Возврат и обмен товара</h2>
|
|
||||||
<p><strong>13.1. Общие правила:</strong> Цифровые товары не подлежат возврату. Физические товары — согласно <a routerLink="/return-policy">Политике возврата</a> и законам о правах потребителей.</p>
|
|
||||||
<p><strong>13.2. Процедура возврата:</strong> В соответствии с соглашением и законодательством РФ.</p>
|
|
||||||
<p><strong>13.3. Акционные наборы:</strong> Возврат только в комплексе, отдельные товары вернуть нельзя.</p>
|
|
||||||
<p><strong>13.4. Затраты на доставку:</strong> При возврате качественного товара продавец может взыскать затраты на доставку.</p>
|
|
||||||
<p><strong>13.5. Сроки:</strong> Заявления удовлетворяются в течение 10 дней.</p>
|
|
||||||
<p><strong>13.6. Документы при возврате:</strong></p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Заявление на возврат</li>
|
|
||||||
<li>Копия квитанции или чека</li>
|
|
||||||
<li>Копия акта приема-передачи</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>13.7. Механизм возврата:</strong> Способ возврата указывается в заявлении.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">⏳</div>
|
|
||||||
<h2>14. Срок действия соглашения</h2>
|
|
||||||
<p><strong>14.1. Начало и прекращение:</strong> Соглашение активно с момента принятия пользователем до отзыва акцепта.</p>
|
|
||||||
<p><strong>14.2. Право отзыва:</strong> Владелец может отозвать оферту согласно ст. 436 ГК РФ.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">⚡</div>
|
|
||||||
<h2>15. Порядок разрешения споров</h2>
|
|
||||||
<p><strong>15.1. Добровольное урегулирование:</strong> Обязательный досудебный порядок рассмотрения споров.</p>
|
|
||||||
<p><strong>15.2. Претензионный порядок:</strong></p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Пользователь направляет претензию</li>
|
|
||||||
<li>25 рабочих дней на ответ владельца</li>
|
|
||||||
<li>Если компромисс не достигнут — судебное рассмотрение</li>
|
|
||||||
</ul>
|
|
||||||
<p><strong>15.3. Анонимные претензии:</strong> Не принимаются к рассмотрению.</p>
|
|
||||||
<p><strong>15.4. Судебное рассмотрение:</strong> По месту нахождения владельца сайта.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📝</div>
|
|
||||||
<h2>16. Прочие условия</h2>
|
|
||||||
<p><strong>16.1. Действие:</strong> Документ заменяет все предшествующие договоренности.</p>
|
|
||||||
<p><strong>16.2. Согласие:</strong> Продолжая использовать сайт, пользователь соглашается с условиями.</p>
|
|
||||||
<p><strong>16.3. Законодательство:</strong> Вопросы разрешаются по законодательству Армении.</p>
|
|
||||||
<p><strong>16.4. Термин "Законодательство":</strong> Подразумевает законы Армении.</p>
|
|
||||||
<p><strong>16.5. Бесплатные услуги:</strong> Не предполагают применение норм о защите прав потребителей.</p>
|
|
||||||
<p><strong>16.6. Отсутствие отношений:</strong> Не устанавливают агентских связей, партнёрства или трудовых отношений.</p>
|
|
||||||
<p><strong>16.7. Недействительность пунктов:</strong> Не влияет на законную силу остальных положений.</p>
|
|
||||||
<p><strong>16.8. Реакция на нарушения:</strong> Невмешательство не препятствует последующим мерам защиты.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">📞</div>
|
|
||||||
<h2>Контакты</h2>
|
|
||||||
<p>Вопросы по соглашению:</p>
|
|
||||||
<a href="mailto:info@novo.market" class="contact-email">info@novo.market</a>
|
|
||||||
<p><a href="tel:+37498731231">+374 98 731231</a></p>
|
|
||||||
<p class="support-note">Мы всегда готовы помочь</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-public-offer-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './public-offer.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/legal/public-offer/public-offer.component.scss']
|
|
||||||
})
|
|
||||||
export class PublicOfferNovoComponent {}
|
|
||||||
@@ -1,175 +0,0 @@
|
|||||||
<div class="legal-page">
|
|
||||||
<div class="legal-container">
|
|
||||||
<div class="novo-header">
|
|
||||||
<h1>Политика возврата товаров</h1>
|
|
||||||
<p class="subtitle">Ваши права и условия возврата</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-cards">
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📋</div>
|
|
||||||
<h2>1. Общие положения</h2>
|
|
||||||
<p>1.1. Данная политика описывает порядок возврата продукции и отказа от сервисов, купленных на платформе Novo Market.</p>
|
|
||||||
<p>1.2. Наша площадка действует как информационный посредник, поэтому все споры о возврате решаются напрямую между клиентом и поставщиком согласно законам РФ.</p>
|
|
||||||
<p>1.3. Для качественной продукции применяется ст. 26.1 ФЗ «О защите прав потребителей».</p>
|
|
||||||
<p>1.4. Для товаров с недостатками действуют ст. 18–24 того же закона.</p>
|
|
||||||
<p>1.5. Электронные товары возвращаются по специальным правилам, прописанным в законодательстве.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">📅</div>
|
|
||||||
<h2>2. Временные рамки</h2>
|
|
||||||
<p><strong>2.1. Качественная продукция:</strong></p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li><strong>Покупки онлайн:</strong> неделя после доставки</li>
|
|
||||||
<li><strong>До отправки:</strong> без ограничений</li>
|
|
||||||
<li><strong>При отсутствии уведомления:</strong> 90 дней от получения</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>2.2. Бракованные изделия:</strong></p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Весь период действия гарантии</li>
|
|
||||||
<li>При отсутствии гарантийного талона: 24 месяца от покупки</li>
|
|
||||||
<li>Серьезные поломки: на протяжении всего срока эксплуатации</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<p><strong>2.3. Электронная продукция:</strong></p>
|
|
||||||
<p>Вернуть можно лишь до момента загрузки файла или ввода лицензионного ключа.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">✓</div>
|
|
||||||
<h2>3. Требования для возврата исправных товаров</h2>
|
|
||||||
<p>Чтобы вернуть покупку без дефектов, необходимо соблюсти условия:</p>
|
|
||||||
<div class="features-list">
|
|
||||||
<div class="feature">✓ Презентабельный внешний вид и функциональность не нарушены</div>
|
|
||||||
<div class="feature">✓ Заводская упаковка и все компоненты на месте</div>
|
|
||||||
<div class="feature">✓ Этикетки, защитные пленки и стикеры не повреждены</div>
|
|
||||||
<div class="feature">✓ Продукт в первозданном состоянии, без следов эксплуатации</div>
|
|
||||||
<div class="feature">✓ Есть подтверждение оплаты (кассовый или товарный чек)</div>
|
|
||||||
</div>
|
|
||||||
<p>Обратная пересылка исправного товара оплачивается клиентом, если с продавцом не достигнуто иное соглашение.</p>
|
|
||||||
<p>Поставщик возвращает средства за неиспользованную продукцию за 10 суток с момента получения запроса.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">❌</div>
|
|
||||||
<h2>4. Что нельзя вернуть</h2>
|
|
||||||
<p>По Постановлению №2463 Правительства РФ, следующие исправные категории продукции возврату не подлежат:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Медицинская техника для личного применения</li>
|
|
||||||
<li>Гигиенические принадлежности (щетки для зубов, расчески и т.п.)</li>
|
|
||||||
<li>Косметические средства и ароматы</li>
|
|
||||||
<li>Ткани, пряжа и фурнитура на метраж</li>
|
|
||||||
<li>Нательное белье, чулочные изделия</li>
|
|
||||||
<li>Разовая посуда и приборы</li>
|
|
||||||
<li>Чистящие средства, препараты для сада</li>
|
|
||||||
<li>Мебельные гарнитуры</li>
|
|
||||||
<li>Драгоценности и бижутерия</li>
|
|
||||||
<li>Машины и комплектующие к ним</li>
|
|
||||||
<li>Сложная бытовая техника на гарантийном обслуживании</li>
|
|
||||||
<li>Оружие и патроны</li>
|
|
||||||
<li>Живые существа и флора</li>
|
|
||||||
<li>Печатная продукция (издания, каталоги)</li>
|
|
||||||
</ul>
|
|
||||||
<p>Эти позиции принимаются обратно исключительно с производственным браком.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">⏱️</div>
|
|
||||||
<h2>5. Как оформить возврат</h2>
|
|
||||||
<p>Пошаговая инструкция по возврату покупки:</p>
|
|
||||||
<div class="process-steps">
|
|
||||||
<div class="step">
|
|
||||||
<span class="step-number">1</span>
|
|
||||||
<p>Свяжитесь с поставщиком через платформу или указанные реквизиты</p>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<span class="step-number">2</span>
|
|
||||||
<p>Объясните основание для возврата, приложите снимки или видеоматериалы</p>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<span class="step-number">3</span>
|
|
||||||
<p>Дождитесь рекомендаций по обратной отправке</p>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<span class="step-number">4</span>
|
|
||||||
<p>Поместите изделие в заводскую тару со всеми дополнениями</p>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<span class="step-number">5</span>
|
|
||||||
<p>Направьте посылку и передайте код отслеживания продавцу</p>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<span class="step-number">6</span>
|
|
||||||
<p>Подождите экспертизу (занимает до 10 суток)</p>
|
|
||||||
</div>
|
|
||||||
<div class="step">
|
|
||||||
<span class="step-number">7</span>
|
|
||||||
<p>Получите возмещение на счет</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>Наша площадка всегда готова помочь в урегулировании спорных ситуаций.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">💰</div>
|
|
||||||
<h2>6. Компенсация платежа</h2>
|
|
||||||
<p>Деньги возмещаются на тот же способ оплаты:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li>Карта Visa/MC/Мир</li>
|
|
||||||
<li>Виртуальные кошельки</li>
|
|
||||||
<li>Платежная ссылка</li>
|
|
||||||
</ul>
|
|
||||||
<div class="refund-times">
|
|
||||||
<div class="refund-item">
|
|
||||||
<strong>Исправный товар</strong>
|
|
||||||
<span>До 10 дней</span>
|
|
||||||
</div>
|
|
||||||
<div class="refund-item">
|
|
||||||
<strong>Бракованный</strong>
|
|
||||||
<span>До 10 дней</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p>Клиент имеет право потребовать компенсацию за некачественную продукцию и расходы по пересылке.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">🔄</div>
|
|
||||||
<h2>7. Обмен продукции</h2>
|
|
||||||
<p>При несоответствии исправного изделия по габаритам, размерам или параметрам, клиент может заменить его у поставщика на подходящий вариант.</p>
|
|
||||||
<p>При отсутствии нужной модели клиент вправе отказаться от заказа и получить компенсацию.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">🛡️</div>
|
|
||||||
<h2>8. Сервисное обслуживание</h2>
|
|
||||||
<p>Информация о гарантийных условиях размещена в карточке товара или доступна у продавца.</p>
|
|
||||||
<p>Гарантия реализуется поставщиком или авторизованным сервисом производителя.</p>
|
|
||||||
<p>Гарантийное обслуживание не распространяется на физические ущербы, неправильное применение или самостоятельное вмешательство.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card">
|
|
||||||
<div class="card-icon">⚖️</div>
|
|
||||||
<h2>9. Обязанности участников</h2>
|
|
||||||
<p>Поставщик несет ответственность за качество продукции и своевременное возмещение платежей.</p>
|
|
||||||
<p>Клиент обязан сохранять упаковку и комплектность до момента обратной отправки.</p>
|
|
||||||
<p>Наш маркетплейс, выступая посредником, не отвечает за невыполнение продавцами своих обязательств, но содействует в урегулировании конфликтов.</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="info-card wide">
|
|
||||||
<div class="card-icon">📧</div>
|
|
||||||
<h2>10. Как с нами связаться</h2>
|
|
||||||
<p>Вопросы по возврату можно задавать напрямую продавцу — контакты указаны в вашем заказе.</p>
|
|
||||||
<p>Для разрешения споров через администрацию платформы:</p>
|
|
||||||
<ul class="compact-list">
|
|
||||||
<li><strong>E-mail:</strong> <a href="mailto:info@novo.market" class="contact-email">info@novo.market</a></li>
|
|
||||||
<li><strong>Телефон:</strong> <a href="tel:+37498731231">+374 98 731231</a></li>
|
|
||||||
<li><strong>Тема:</strong> «Разногласия с продавцом — №Заказа»</li>
|
|
||||||
<li><strong>Приложите:</strong> переписку, снимки, чек оплаты</li>
|
|
||||||
</ul>
|
|
||||||
<p>В случае невозможности достичь соглашения, клиент может обратиться в Роспотребнадзор или судебные инстанции.</p>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-return-policy-novo',
|
|
||||||
standalone: true,
|
|
||||||
imports: [CommonModule],
|
|
||||||
templateUrl: './return-policy.component.html',
|
|
||||||
styleUrls: ['../../../../../pages/legal/return-policy/return-policy.component.scss']
|
|
||||||
})
|
|
||||||
export class ReturnPolicyNovoComponent {}
|
|
||||||
69
src/app/components/back-button/back-button.component.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Component, ChangeDetectionStrategy, inject } from '@angular/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { TranslateService } from '../../i18n/translate.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-back-button',
|
||||||
|
template: `
|
||||||
|
<button class="dexar-back-btn" (click)="goBack()" [attr.aria-label]="i18n.t('itemDetail.back')">
|
||||||
|
<svg width="37" height="24" viewBox="0 0 37 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M4.73 11.46c-.97-.52-.97-1.4 0-1.92L20.39 1.21c1.48-.79 3.89-.19 3.89.95v3.74h6.94c1.28 0 2.31.59 2.31 1.31v6.56c0 .73-1.03 1.31-2.31 1.31h-6.94v3.74c0 1.15-2.42 1.74-3.89.96L4.73 11.46Z"
|
||||||
|
fill="#497671" fill-opacity="0.75" stroke="white" stroke-opacity="0.6" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
`,
|
||||||
|
styles: [`
|
||||||
|
.dexar-back-btn {
|
||||||
|
position: sticky;
|
||||||
|
top: 72px;
|
||||||
|
left: 20px;
|
||||||
|
z-index: 100;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 4px;
|
||||||
|
margin-bottom: -40px;
|
||||||
|
width: fit-content;
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
svg path {
|
||||||
|
transition: fill 0.2s ease, fill-opacity 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.08);
|
||||||
|
|
||||||
|
svg path {
|
||||||
|
fill: #A1B4B5;
|
||||||
|
fill-opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.dexar-back-btn {
|
||||||
|
top: 64px;
|
||||||
|
left: 12px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
width: 30px;
|
||||||
|
height: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class BackButtonComponent {
|
||||||
|
i18n = inject(TranslateService);
|
||||||
|
|
||||||
|
constructor(private location: Location) {}
|
||||||
|
|
||||||
|
goBack(): void {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<div class="delivery-selector">
|
||||||
|
@if (isDigital) {
|
||||||
|
<div class="delivery-chip delivery-chip--digital">{{ 'cart.digitalDelivery' | translate }}</div>
|
||||||
|
} @else if (options.length > 0) {
|
||||||
|
<label class="delivery-label" [for]="selectId">{{ 'cart.deliveryMethod' | translate }}</label>
|
||||||
|
|
||||||
|
<div class="delivery-control">
|
||||||
|
<select [id]="selectId" [ngModel]="selectedKey" (ngModelChange)="onSelectionChange($event)" [attr.aria-required]="required ? 'true' : null">
|
||||||
|
@if (required) {
|
||||||
|
<option value="">{{ 'cart.selectDelivery' | translate }}</option>
|
||||||
|
}
|
||||||
|
|
||||||
|
@for (option of options; track trackByOption($index, option)) {
|
||||||
|
<option [value]="optionKey(option)">{{ optionLabel(option) }}</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (selectedDelivery) {
|
||||||
|
<div class="delivery-meta">
|
||||||
|
@if (selectedDelivery.deliveryPlace) {
|
||||||
|
<span>{{ 'cart.deliveryPlace' | translate }}: {{ selectedDelivery.deliveryPlace }}</span>
|
||||||
|
}
|
||||||
|
@if (selectedDelivery.deliveryTime) {
|
||||||
|
<span>{{ 'cart.deliveryTime' | translate }}: {{ selectedDelivery.deliveryTime }}</span>
|
||||||
|
}
|
||||||
|
<span>
|
||||||
|
{{ 'cart.deliveryLabel' | translate }}:
|
||||||
|
{{ selectedDeliveryTotal | number:'1.2-2' }} {{ currency }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-selector {
|
||||||
|
margin-top: 12px;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: var(--radius-md, 12px);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
|
font-weight: var(--font-weight-bold, 700);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-control select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: var(--radius-sm, 10px);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-size: var(--font-size-base, 0.875rem);
|
||||||
|
line-height: 1.4;
|
||||||
|
transition: border-color var(--transition-normal), box-shadow var(--transition-normal);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-control select:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 3px rgba(73, 118, 113, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-meta {
|
||||||
|
display: grid;
|
||||||
|
gap: 4px;
|
||||||
|
margin-top: 10px;
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
line-height: 1.45;
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: var(--radius-full, 999px);
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
|
font-weight: var(--font-weight-bold, 700);
|
||||||
|
}
|
||||||
|
|
||||||
|
.delivery-chip--digital {
|
||||||
|
color: #2563eb;
|
||||||
|
background: rgba(37, 99, 235, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, inject } from '@angular/core';
|
||||||
|
import { DecimalPipe } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { CartItem, DeliveryOption } from '../../models';
|
||||||
|
import { TranslatePipe } from '../../i18n/translate.pipe';
|
||||||
|
import { LanguageService } from '../../services/language.service';
|
||||||
|
import { CurrencyRatesService } from '../../services/currency-rates.service';
|
||||||
|
|
||||||
|
let nextDeliverySelectorId = 0;
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'app-delivery-selector',
|
||||||
|
standalone: true,
|
||||||
|
imports: [FormsModule, DecimalPipe, TranslatePipe],
|
||||||
|
templateUrl: './delivery-selector.component.html',
|
||||||
|
styleUrls: ['./delivery-selector.component.scss'],
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush
|
||||||
|
})
|
||||||
|
export class DeliverySelectorComponent {
|
||||||
|
private readonly languageService = inject(LanguageService);
|
||||||
|
private readonly currencyRates = inject(CurrencyRatesService);
|
||||||
|
|
||||||
|
@Input({ required: true }) item: CartItem | null = null;
|
||||||
|
|
||||||
|
@Output() selectedDeliveryChange = new EventEmitter<DeliveryOption | null>();
|
||||||
|
|
||||||
|
readonly selectId = `delivery-select-${nextDeliverySelectorId++}`;
|
||||||
|
|
||||||
|
get options(): DeliveryOption[] {
|
||||||
|
return this.item?.deliveryOptions ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedDelivery(): DeliveryOption | null {
|
||||||
|
return this.item?.selectedDelivery ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Source currency the item's prices are stored in. */
|
||||||
|
get sourceCurrency(): string {
|
||||||
|
return this.item?.currency || 'RUB';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Currency the shopper has selected for display. */
|
||||||
|
get currency(): string {
|
||||||
|
return this.languageService.currentCurrency();
|
||||||
|
}
|
||||||
|
|
||||||
|
private convert(amount: number): number {
|
||||||
|
return this.currencyRates.convert(amount, this.sourceCurrency, this.currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
get required(): boolean {
|
||||||
|
return this.item?.deliveryMode !== 'digital'
|
||||||
|
&& this.options.length > 0
|
||||||
|
&& this.item?.deliverySelectionRequired !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get isDigital(): boolean {
|
||||||
|
return this.item?.deliveryMode === 'digital';
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedKey(): string {
|
||||||
|
return this.selectedDelivery ? this.optionKey(this.selectedDelivery) : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectedDeliveryTotal(): number {
|
||||||
|
const base = (this.selectedDelivery?.deliveryPrice ?? 0) * (this.item?.quantity ?? 1);
|
||||||
|
return this.convert(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
optionKey(option: DeliveryOption): string {
|
||||||
|
return `${option.deliveryPlace}__${option.deliveryTime}__${option.deliveryPrice}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
optionLabel(option: DeliveryOption): string {
|
||||||
|
const details = [option.deliveryPlace, option.deliveryTime].filter(Boolean);
|
||||||
|
details.push(`${this.convert(option.deliveryPrice).toFixed(2)} ${this.currency}`);
|
||||||
|
return details.join(' • ');
|
||||||
|
}
|
||||||
|
|
||||||
|
trackByOption(index: number, option: DeliveryOption): string {
|
||||||
|
return `${index}-${this.optionKey(option)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectionChange(nextKey: string): void {
|
||||||
|
if (!nextKey) {
|
||||||
|
this.selectedDeliveryChange.emit(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedDeliveryChange.emit(
|
||||||
|
this.options.find(option => this.optionKey(option) === nextKey) ?? null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<path d="M3 4h2l2.5 10.5a2 2 0 0 0 2 1.5H17a2 2 0 0 0 2-1.5L21 7H6" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
|
||||||
<circle cx="10" cy="19" r="1.5" stroke="currentColor" stroke-width="2"/>
|
|
||||||
<circle cx="17" cy="19" r="1.5" stroke="currentColor" stroke-width="2"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 411 B |
@@ -1,7 +0,0 @@
|
|||||||
:host {
|
|
||||||
display: block;
|
|
||||||
|
|
||||||
svg {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'app-empty-cart-icon',
|
|
||||||
standalone: true,
|
|
||||||
templateUrl: './empty-cart-icon.component.html',
|
|
||||||
styleUrls: ['./empty-cart-icon.component.scss']
|
|
||||||
})
|
|
||||||
export class EmptyCartIconComponent {}
|
|
||||||
@@ -1,97 +1,57 @@
|
|||||||
<!-- novo VERSION - Modern Clean Footer -->
|
<footer class="app-footer">
|
||||||
@if (isnovo) {
|
<div class="app-footer__container page-container">
|
||||||
<footer class="novo-footer">
|
<div class="app-footer__top">
|
||||||
<div class="novo-footer-container">
|
<div class="app-footer__brand">
|
||||||
<div class="novo-footer-top">
|
<app-logo [srcOverride]="footerLogoUrl()" />
|
||||||
<div class="novo-footer-col">
|
<p>{{ 'footer.description' | translate }}</p>
|
||||||
<h4>{{ brandName }}</h4>
|
</div>
|
||||||
<p class="novo-footer-desc">Современный маркетплейс для комфортных покупок</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-footer-col">
|
@for (group of footerGroups(); track group.id) {
|
||||||
<h4>Компания</h4>
|
<div class="app-footer__group">
|
||||||
<ul class="novo-footer-links">
|
@if (group.title) {
|
||||||
<li><a routerLink="/about">О нас</a></li>
|
<h4>{{ group.title }}</h4>
|
||||||
<li><a routerLink="/contacts">Контакты</a></li>
|
}
|
||||||
<li><a routerLink="/company-details">Реквизиты</a></li>
|
<ul>
|
||||||
|
@for (item of group.items; track item.id) {
|
||||||
|
<li>
|
||||||
|
@if (item.external) {
|
||||||
|
<a [href]="item.route" target="_blank" rel="noopener">{{ item.label | translate }}</a>
|
||||||
|
} @else {
|
||||||
|
<a [routerLink]="item.route | langRoute">{{ item.label | translate }}</a>
|
||||||
|
}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="novo-footer-col">
|
<div class="app-footer__group">
|
||||||
<h4>Поддержка</h4>
|
<h4>{{ 'footer.payment' | translate }}</h4>
|
||||||
<ul class="novo-footer-links">
|
<div class="app-footer__payments">
|
||||||
<li><a routerLink="/faq">FAQ</a></li>
|
@for (icon of paymentIcons(); track $index) {
|
||||||
<li><a routerLink="/delivery">Доставка</a></li>
|
<img [src]="icon.src" [alt]="icon.alt" loading="lazy" [width]="icon.width" [height]="icon.height" (error)="onImageError($event)" />
|
||||||
<li><a routerLink="/guarantee">Гарантия</a></li>
|
}
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-footer-col">
|
|
||||||
<h4>Правовая информация</h4>
|
|
||||||
<ul class="novo-footer-links">
|
|
||||||
<li><a routerLink="/public-offer">Оферта</a></li>
|
|
||||||
<li><a routerLink="/privacy-policy">Конфиденциальность</a></li>
|
|
||||||
<li><a routerLink="/return-policy">Возврат</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="novo-footer-bottom">
|
|
||||||
<div class="novo-copyright">
|
|
||||||
<p>© {{ currentYear }} {{ brandName }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="novo-payment-icons">
|
|
||||||
<img src="/assets/images/mir-logo.svg" alt="МИР" loading="lazy" width="40" height="28" />
|
|
||||||
<img src="/assets/images/visa-logo.svg" alt="Visa" loading="lazy" width="40" height="28" />
|
|
||||||
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" loading="lazy" width="40" height="28" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
} @else {
|
|
||||||
<!-- DEXAR VERSION - Original -->
|
|
||||||
<footer class="footer">
|
|
||||||
<div class="footer-container">
|
|
||||||
<div class="footer-section">
|
|
||||||
<h3>Информация</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a routerLink="/about">О компании</a></li>
|
|
||||||
<li><a routerLink="/contacts">Контакты</a></li>
|
|
||||||
<li><a routerLink="/company-details">Реквизиты организации</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-section">
|
|
||||||
<h3>Документы</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a routerLink="/payment-terms">Правила оплаты</a></li>
|
|
||||||
<li><a routerLink="/return-policy">Политика возврата</a></li>
|
|
||||||
<li><a routerLink="/public-offer">Публичная оферта</a></li>
|
|
||||||
<li><a routerLink="/privacy-policy">Политика конфиденциальности</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-section">
|
|
||||||
<h3>Помощь</h3>
|
|
||||||
<ul>
|
|
||||||
<li><a routerLink="/faq">Часто задаваемые вопросы</a></li>
|
|
||||||
<li><a routerLink="/delivery">Доставка</a></li>
|
|
||||||
<li><a routerLink="/guarantee">Гарантия</a></li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="footer-section payment-systems">
|
|
||||||
<h3>Способы оплаты</h3>
|
|
||||||
<div class="payment-logos">
|
|
||||||
<img src="/assets/images/mir-logo.svg" alt="МИР" class="payment-logo" loading="lazy" decoding="async" width="60" height="40" />
|
|
||||||
<img src="/assets/images/visa-logo.svg" alt="Visa" class="payment-logo" loading="lazy" decoding="async" width="60" height="40" />
|
|
||||||
<img src="/assets/images/mastercard-logo.svg" alt="Mastercard" class="payment-logo" loading="lazy" decoding="async" width="60" height="40" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-bottom">
|
<div class="app-footer__bottom">
|
||||||
<p>© {{ currentYear }} {{ brandName }}. Все права защищены.</p>
|
<p>
|
||||||
|
@if (copyrightText()) {
|
||||||
|
{{ copyrightText() }}
|
||||||
|
} @else {
|
||||||
|
© {{ currentYear }} {{ brandName }}. {{ 'footer.allRightsReserved' | translate }}
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
@if (contactEmail) {
|
||||||
|
<a [href]="'mailto:' + contactEmail">{{ contactEmail }}</a>
|
||||||
|
}
|
||||||
|
@if (contactPhone) {
|
||||||
|
<a [href]="'tel:' + contactPhone">{{ contactPhone }}</a>
|
||||||
|
}
|
||||||
|
@if (companyAddress) {
|
||||||
|
<span class="app-footer__address">{{ companyAddress }}</span>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</div>
|
||||||
}
|
</footer>
|
||||||
|
|||||||
@@ -1,219 +1,114 @@
|
|||||||
.footer {
|
:host {
|
||||||
background-color: #1a1a1a;
|
display: block;
|
||||||
color: #ffffff;
|
width: 100%;
|
||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
padding: 3rem 0 1rem;
|
|
||||||
border-top: 1px solid #333;
|
|
||||||
|
|
||||||
.footer-container {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section {
|
|
||||||
h3 {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #b0b0b0;
|
|
||||||
text-decoration: none;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: #ffffff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.payment-systems {
|
|
||||||
.payment-logos {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
.payment-logo {
|
|
||||||
height: 40px;
|
|
||||||
width: auto;
|
|
||||||
background: white;
|
|
||||||
padding: 0.5rem;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-bottom {
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 2rem 2rem 0;
|
|
||||||
margin-top: 2rem;
|
|
||||||
border-top: 1px solid #333;
|
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
p {
|
|
||||||
color: #808080;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
padding: 2rem 0 1rem;
|
|
||||||
|
|
||||||
.footer-container {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
gap: 1.5rem;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer-section {
|
|
||||||
h3 {
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ========== novo FOOTER STYLES ==========
|
.app-footer {
|
||||||
.novo-footer {
|
background: var(--text-primary, #1e3c38);
|
||||||
background: var(--text-primary);
|
color: #fff;
|
||||||
color: white;
|
|
||||||
margin-top: auto;
|
|
||||||
padding: 3rem 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.novo-footer-container {
|
.app-footer__container {
|
||||||
max-width: 1400px;
|
width: 100%;
|
||||||
margin: 0 auto;
|
|
||||||
padding: 0 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.novo-footer-top {
|
.app-footer__top {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 2fr 1fr 1fr 1fr;
|
grid-template-columns: 2fr repeat(3, minmax(160px, 1fr));
|
||||||
gap: 3rem;
|
gap: 1.5rem;
|
||||||
padding-bottom: 3rem;
|
padding-bottom: 1.5rem;
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
border-bottom: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
.novo-footer-col {
|
.app-footer__brand {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer__group {
|
||||||
h4 {
|
h4 {
|
||||||
font-size: 1.1rem;
|
margin: 0 0 0.75rem;
|
||||||
font-weight: 600;
|
font-size: var(--font-size-md, 0.9375rem);
|
||||||
margin-bottom: 1.25rem;
|
color: #fff;
|
||||||
color: white;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.novo-footer-desc {
|
|
||||||
color: rgba(255, 255, 255, 0.7);
|
|
||||||
line-height: 1.6;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.novo-footer-links {
|
ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 0;
|
margin: 0;
|
||||||
margin: 0;
|
padding: 0;
|
||||||
|
display: grid;
|
||||||
li {
|
gap: 0.5rem;
|
||||||
margin-bottom: 0.75rem;
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: rgba(255, 255, 255, 0.7);
|
color: rgba(255, 255, 255, 0.8);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.95rem;
|
|
||||||
transition: all 0.3s;
|
&:hover {
|
||||||
display: inline-block;
|
color: #fff;
|
||||||
|
}
|
||||||
&:hover {
|
|
||||||
color: white;
|
&:focus-visible {
|
||||||
transform: translateX(4px);
|
outline: 2px solid #fff;
|
||||||
}
|
outline-offset: 2px;
|
||||||
|
border-radius: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.novo-footer-bottom {
|
.app-footer__payments {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5rem;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
img {
|
||||||
|
border-radius: var(--radius-xs, 4px);
|
||||||
|
background: #fff;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-footer__bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 1.5rem 0;
|
gap: 1rem;
|
||||||
}
|
padding-top: 1rem;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
|
||||||
.novo-copyright {
|
|
||||||
p {
|
p {
|
||||||
color: rgba(255, 255, 255, 0.6);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
color: rgba(255, 255, 255, 0.7);
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: rgba(255, 255, 255, 0.85);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: var(--font-size-sm, 0.8125rem);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.novo-payment-icons {
|
@media (max-width: 960px) {
|
||||||
display: flex;
|
.app-footer__top {
|
||||||
gap: 0.75rem;
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
img {
|
|
||||||
height: 28px;
|
|
||||||
width: auto;
|
|
||||||
background: white;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
border-radius: 6px;
|
|
||||||
opacity: 0.9;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 968px) {
|
@media (max-width: 640px) {
|
||||||
.novo-footer-top {
|
.app-footer__top {
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
gap: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
.novo-footer {
|
|
||||||
padding: 2rem 0 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.novo-footer-container {
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.novo-footer-top {
|
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
gap: 2rem;
|
|
||||||
padding-bottom: 2rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.novo-footer-bottom {
|
.app-footer__bottom {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 1rem;
|
align-items: flex-start;
|
||||||
padding: 1rem 0;
|
|
||||||
text-align: center;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||