kafka add docker and codestyle and smtp many comp
This commit is contained in:
parent
f3aeaad948
commit
98e5091742
@ -31,6 +31,9 @@ services:
|
|||||||
DB_USER: authuser
|
DB_USER: authuser
|
||||||
DB_PASSWORD: authpassword
|
DB_PASSWORD: authpassword
|
||||||
JWT_SECRET: supersecretkey
|
JWT_SECRET: supersecretkey
|
||||||
|
KAFKA_BROKER: kafka:9092
|
||||||
|
KAFKA_CLIENT_ID: auth-service
|
||||||
|
KAFKA_GROUP_ID: auth-service-group
|
||||||
volumes:
|
volumes:
|
||||||
- ./auth-service:/usr/src/app
|
- ./auth-service:/usr/src/app
|
||||||
working_dir: /usr/src/app
|
working_dir: /usr/src/app
|
||||||
@ -53,6 +56,9 @@ services:
|
|||||||
DB_NAME: authdb
|
DB_NAME: authdb
|
||||||
DB_USER: authuser
|
DB_USER: authuser
|
||||||
DB_PASSWORD: authpassword
|
DB_PASSWORD: authpassword
|
||||||
|
KAFKA_BROKER: kafka:9092
|
||||||
|
KAFKA_CLIENT_ID: mail-service
|
||||||
|
KAFKA_GROUP_ID: mail-service-group
|
||||||
|
|
||||||
nginx-proxy-manager:
|
nginx-proxy-manager:
|
||||||
image: 'jc21/nginx-proxy-manager:latest'
|
image: 'jc21/nginx-proxy-manager:latest'
|
||||||
@ -76,5 +82,39 @@ services:
|
|||||||
auth-service:
|
auth-service:
|
||||||
condition: service_started
|
condition: service_started
|
||||||
|
|
||||||
|
zookeeper:
|
||||||
|
image: confluentinc/cp-zookeeper:7.5.0
|
||||||
|
container_name: zookeeper
|
||||||
|
environment:
|
||||||
|
ZOOKEEPER_CLIENT_PORT: 2181
|
||||||
|
ZOOKEEPER_TICK_TIME: 2000
|
||||||
|
ports:
|
||||||
|
- '2181:2181'
|
||||||
|
volumes:
|
||||||
|
- zookeeper_data:/var/lib/zookeeper/data
|
||||||
|
- zookeeper_log:/var/lib/zookeeper/log
|
||||||
|
|
||||||
|
kafka:
|
||||||
|
image: confluentinc/cp-kafka:7.5.0
|
||||||
|
container_name: kafka
|
||||||
|
depends_on:
|
||||||
|
- zookeeper
|
||||||
|
ports:
|
||||||
|
- '9092:9092'
|
||||||
|
environment:
|
||||||
|
KAFKA_BROKER_ID: 1
|
||||||
|
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
|
||||||
|
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092,PLAINTEXT_HOST://localhost:29092
|
||||||
|
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
|
||||||
|
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
|
||||||
|
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||||
|
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
||||||
|
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
||||||
|
volumes:
|
||||||
|
- kafka_data:/var/lib/kafka/data
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mysql_data:
|
mysql_data:
|
||||||
|
kafka_data:
|
||||||
|
zookeeper_data:
|
||||||
|
zookeeper_log:
|
||||||
|
|||||||
248
frontend/package-lock.json
generated
248
frontend/package-lock.json
generated
@ -12,7 +12,8 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.30.1",
|
"react-router-dom": "^6.30.1",
|
||||||
"react-scripts": "5.0.1"
|
"react-scripts": "5.0.1",
|
||||||
|
"react-select": "^5.10.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@alloc/quick-lru": {
|
"node_modules/@alloc/quick-lru": {
|
||||||
@ -2218,6 +2219,122 @@
|
|||||||
"postcss-selector-parser": "^6.0.10"
|
"postcss-selector-parser": "^6.0.10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@emotion/babel-plugin": {
|
||||||
|
"version": "11.13.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz",
|
||||||
|
"integrity": "sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-module-imports": "^7.16.7",
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"@emotion/hash": "^0.9.2",
|
||||||
|
"@emotion/memoize": "^0.9.0",
|
||||||
|
"@emotion/serialize": "^1.3.3",
|
||||||
|
"babel-plugin-macros": "^3.1.0",
|
||||||
|
"convert-source-map": "^1.5.0",
|
||||||
|
"escape-string-regexp": "^4.0.0",
|
||||||
|
"find-root": "^1.1.0",
|
||||||
|
"source-map": "^0.5.7",
|
||||||
|
"stylis": "4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/babel-plugin/node_modules/convert-source-map": {
|
||||||
|
"version": "1.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz",
|
||||||
|
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A=="
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/babel-plugin/node_modules/source-map": {
|
||||||
|
"version": "0.5.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||||
|
"integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=0.10.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/cache": {
|
||||||
|
"version": "11.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.14.0.tgz",
|
||||||
|
"integrity": "sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/memoize": "^0.9.0",
|
||||||
|
"@emotion/sheet": "^1.4.0",
|
||||||
|
"@emotion/utils": "^1.4.2",
|
||||||
|
"@emotion/weak-memoize": "^0.4.0",
|
||||||
|
"stylis": "4.2.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/hash": {
|
||||||
|
"version": "0.9.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.2.tgz",
|
||||||
|
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g=="
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/memoize": {
|
||||||
|
"version": "0.9.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.9.0.tgz",
|
||||||
|
"integrity": "sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ=="
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/react": {
|
||||||
|
"version": "11.14.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
|
||||||
|
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.18.3",
|
||||||
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
|
"@emotion/cache": "^11.14.0",
|
||||||
|
"@emotion/serialize": "^1.3.3",
|
||||||
|
"@emotion/use-insertion-effect-with-fallbacks": "^1.2.0",
|
||||||
|
"@emotion/utils": "^1.4.2",
|
||||||
|
"@emotion/weak-memoize": "^0.4.0",
|
||||||
|
"hoist-non-react-statics": "^3.3.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/serialize": {
|
||||||
|
"version": "1.3.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.3.3.tgz",
|
||||||
|
"integrity": "sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@emotion/hash": "^0.9.2",
|
||||||
|
"@emotion/memoize": "^0.9.0",
|
||||||
|
"@emotion/unitless": "^0.10.0",
|
||||||
|
"@emotion/utils": "^1.4.2",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/sheet": {
|
||||||
|
"version": "1.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.4.0.tgz",
|
||||||
|
"integrity": "sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg=="
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/unitless": {
|
||||||
|
"version": "0.10.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.10.0.tgz",
|
||||||
|
"integrity": "sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg=="
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/use-insertion-effect-with-fallbacks": {
|
||||||
|
"version": "1.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.2.0.tgz",
|
||||||
|
"integrity": "sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/utils": {
|
||||||
|
"version": "1.4.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.4.2.tgz",
|
||||||
|
"integrity": "sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA=="
|
||||||
|
},
|
||||||
|
"node_modules/@emotion/weak-memoize": {
|
||||||
|
"version": "0.4.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.4.0.tgz",
|
||||||
|
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg=="
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
||||||
@ -2289,6 +2406,28 @@
|
|||||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/core": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/dom": {
|
||||||
|
"version": "1.7.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.2.tgz",
|
||||||
|
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/core": "^1.7.2",
|
||||||
|
"@floating-ui/utils": "^0.2.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/utils": {
|
||||||
|
"version": "0.2.10",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz",
|
||||||
|
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="
|
||||||
|
},
|
||||||
"node_modules/@humanwhocodes/config-array": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
@ -3443,6 +3582,23 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
"resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz",
|
||||||
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
|
"integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/react": {
|
||||||
|
"version": "19.1.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.8.tgz",
|
||||||
|
"integrity": "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g==",
|
||||||
|
"peer": true,
|
||||||
|
"dependencies": {
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@types/react-transition-group": {
|
||||||
|
"version": "4.4.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.12.tgz",
|
||||||
|
"integrity": "sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/resolve": {
|
"node_modules/@types/resolve": {
|
||||||
"version": "1.17.1",
|
"version": "1.17.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.17.1.tgz",
|
||||||
@ -5795,6 +5951,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
|
"resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz",
|
||||||
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
|
"integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg=="
|
||||||
},
|
},
|
||||||
|
"node_modules/csstype": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="
|
||||||
|
},
|
||||||
"node_modules/damerau-levenshtein": {
|
"node_modules/damerau-levenshtein": {
|
||||||
"version": "1.0.8",
|
"version": "1.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
"resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
|
||||||
@ -6077,6 +6238,15 @@
|
|||||||
"utila": "~0.4"
|
"utila": "~0.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dom-helpers": {
|
||||||
|
"version": "5.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz",
|
||||||
|
"integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.8.7",
|
||||||
|
"csstype": "^3.0.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/dom-serializer": {
|
"node_modules/dom-serializer": {
|
||||||
"version": "1.4.1",
|
"version": "1.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz",
|
||||||
@ -7417,6 +7587,11 @@
|
|||||||
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
|
"url": "https://github.com/avajs/find-cache-dir?sponsor=1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/find-root": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng=="
|
||||||
|
},
|
||||||
"node_modules/find-up": {
|
"node_modules/find-up": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||||
@ -8055,6 +8230,19 @@
|
|||||||
"he": "bin/he"
|
"he": "bin/he"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hoist-non-react-statics": {
|
||||||
|
"version": "3.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
|
||||||
|
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
|
||||||
|
"dependencies": {
|
||||||
|
"react-is": "^16.7.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/hoist-non-react-statics/node_modules/react-is": {
|
||||||
|
"version": "16.13.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
|
},
|
||||||
"node_modules/hoopy": {
|
"node_modules/hoopy": {
|
||||||
"version": "0.1.4",
|
"version": "0.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz",
|
||||||
@ -10326,6 +10514,11 @@
|
|||||||
"node": ">= 4.0.0"
|
"node": ">= 4.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/memoize-one": {
|
||||||
|
"version": "6.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
|
||||||
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw=="
|
||||||
|
},
|
||||||
"node_modules/merge-descriptors": {
|
"node_modules/merge-descriptors": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||||
@ -12835,6 +13028,41 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-select": {
|
||||||
|
"version": "5.10.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-select/-/react-select-5.10.2.tgz",
|
||||||
|
"integrity": "sha512-Z33nHdEFWq9tfnfVXaiM12rbJmk+QjFEztWLtmXqQhz6Al4UZZ9xc0wiatmGtUOCCnHN0WizL3tCMYRENX4rVQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.12.0",
|
||||||
|
"@emotion/cache": "^11.4.0",
|
||||||
|
"@emotion/react": "^11.8.1",
|
||||||
|
"@floating-ui/dom": "^1.0.1",
|
||||||
|
"@types/react-transition-group": "^4.4.0",
|
||||||
|
"memoize-one": "^6.0.0",
|
||||||
|
"prop-types": "^15.6.0",
|
||||||
|
"react-transition-group": "^4.3.0",
|
||||||
|
"use-isomorphic-layout-effect": "^1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||||
|
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-transition-group": {
|
||||||
|
"version": "4.4.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
|
"integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/runtime": "^7.5.5",
|
||||||
|
"dom-helpers": "^5.0.1",
|
||||||
|
"loose-envify": "^1.4.0",
|
||||||
|
"prop-types": "^15.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.6.0",
|
||||||
|
"react-dom": ">=16.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/read-cache": {
|
"node_modules/read-cache": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||||
@ -14266,6 +14494,11 @@
|
|||||||
"postcss": "^8.2.15"
|
"postcss": "^8.2.15"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/stylis": {
|
||||||
|
"version": "4.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz",
|
||||||
|
"integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="
|
||||||
|
},
|
||||||
"node_modules/sucrase": {
|
"node_modules/sucrase": {
|
||||||
"version": "3.35.0",
|
"version": "3.35.0",
|
||||||
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
"resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz",
|
||||||
@ -15136,6 +15369,19 @@
|
|||||||
"requires-port": "^1.0.0"
|
"requires-port": "^1.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/use-isomorphic-layout-effect": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-tpZZ+EX0gaghDAiFR37hj5MgY6ZN55kLiPkJsKxBMZ6GZdOSPJXiOzPM984oPYZ5AnehYx5WQp1+ME8I/P/pRA==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/util-deprecate": {
|
"node_modules/util-deprecate": {
|
||||||
"version": "1.0.2",
|
"version": "1.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||||
|
|||||||
@ -7,7 +7,8 @@
|
|||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
"react-router-dom": "^6.30.1",
|
"react-router-dom": "^6.30.1",
|
||||||
"react-scripts": "5.0.1"
|
"react-scripts": "5.0.1",
|
||||||
|
"react-select": "^5.10.2"
|
||||||
},
|
},
|
||||||
"proxy": "http://project.ru",
|
"proxy": "http://project.ru",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import React from 'react';
|
|||||||
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import Login from './pages/Login';
|
import Login from './pages/Login';
|
||||||
import Dashboard from './pages/Dashboard';
|
import Dashboard from './pages/Dashboard';
|
||||||
import logo from './logo.svg';
|
|
||||||
import './App.css';
|
import './App.css';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|||||||
46
frontend/src/components/MultiSelect.js
Normal file
46
frontend/src/components/MultiSelect.js
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React, { useRef, useState } from 'react';
|
||||||
|
import styles from '../styles/MultiSelect.module.css';
|
||||||
|
|
||||||
|
export default function MultiSelect({ options, value, onChange, placeholder = 'Выберите...', disabled }) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const ref = useRef();
|
||||||
|
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (!disabled) setOpen((o) => !o);
|
||||||
|
};
|
||||||
|
const handleSelect = (id) => {
|
||||||
|
if (value.includes(id)) {
|
||||||
|
onChange(value.filter((v) => v !== id));
|
||||||
|
} else {
|
||||||
|
onChange([...value, id]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const handleBlur = (e) => {
|
||||||
|
if (!ref.current.contains(e.relatedTarget)) setOpen(false);
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper} tabIndex={0} ref={ref} onBlur={handleBlur}>
|
||||||
|
<div className={styles.control + (disabled ? ' ' + styles.disabled : '')} onClick={handleToggle}>
|
||||||
|
<span className={styles.value}>
|
||||||
|
{value.length === 0 ? <span className={styles.placeholder}>{placeholder}</span> :
|
||||||
|
options.filter(o => value.includes(o.value)).map(o => o.label).join(', ')}
|
||||||
|
</span>
|
||||||
|
<span className={styles.arrow}>{open ? '▲' : '▼'}</span>
|
||||||
|
</div>
|
||||||
|
{open && !disabled && (
|
||||||
|
<div className={styles.dropdown}>
|
||||||
|
{options.map(opt => (
|
||||||
|
<div
|
||||||
|
key={opt.value}
|
||||||
|
className={styles.option + (value.includes(opt.value) ? ' ' + styles.selected : '')}
|
||||||
|
onClick={() => handleSelect(opt.value)}
|
||||||
|
tabIndex={-1}
|
||||||
|
>
|
||||||
|
<input type="checkbox" checked={value.includes(opt.value)} readOnly /> {opt.label}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import styles from '../styles/CampaignModal.module.css';
|
import styles from '../styles/CampaignModal.module.css';
|
||||||
|
import MultiSelect from '../components/MultiSelect';
|
||||||
|
|
||||||
export default function CreateCampaignModal({ isOpen, onClose, campaign, groups, versions, loading, onChange, onSave, getVersionName }) {
|
export default function CreateCampaignModal({ isOpen, onClose, campaign, groups, versions, smtpServers, loading, onChange, onSave, getVersionName }) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
|
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
|
||||||
<h3 className={styles.title}>Добавить кампанию</h3>
|
<h3 className={styles.title}>Добавить кампанию</h3>
|
||||||
@ -24,6 +25,15 @@ export default function CreateCampaignModal({ isOpen, onClose, campaign, groups,
|
|||||||
<label className={styles.label}>Тема
|
<label className={styles.label}>Тема
|
||||||
<input type="text" value={campaign.subject_override || ''} onChange={e => onChange({ ...campaign, subject_override: e.target.value })} className={styles.input} />
|
<input type="text" value={campaign.subject_override || ''} onChange={e => onChange({ ...campaign, subject_override: e.target.value })} className={styles.input} />
|
||||||
</label>
|
</label>
|
||||||
|
<label className={styles.label}>SMTP-серверы
|
||||||
|
<MultiSelect
|
||||||
|
options={smtpServers.map(s => ({ value: s.id, label: `${s.name} (${s.host})` }))}
|
||||||
|
value={campaign.smtp_server_ids || []}
|
||||||
|
onChange={ids => onChange({ ...campaign, smtp_server_ids: ids })}
|
||||||
|
placeholder="Выберите SMTP-серверы"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<label className={styles.label}>Статус
|
<label className={styles.label}>Статус
|
||||||
<select value={campaign.status} onChange={e => onChange({ ...campaign, status: e.target.value })} required className={styles.input}>
|
<select value={campaign.status} onChange={e => onChange({ ...campaign, status: e.target.value })} required className={styles.input}>
|
||||||
<option value="draft">Черновик</option>
|
<option value="draft">Черновик</option>
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Modal from './Modal';
|
import Modal from './Modal';
|
||||||
import styles from '../styles/CampaignModal.module.css';
|
import styles from '../styles/CampaignModal.module.css';
|
||||||
|
import MultiSelect from '../components/MultiSelect';
|
||||||
|
|
||||||
export default function EditCampaignModal({ isOpen, onClose, campaign, groups, versions, loading, onChange, onSave, getVersionName }) {
|
export default function EditCampaignModal({ isOpen, onClose, campaign, groups, versions, smtpServers, loading, onChange, onSave, getVersionName }) {
|
||||||
return (
|
return (
|
||||||
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
|
<Modal isOpen={isOpen} onClose={onClose} disabled={loading}>
|
||||||
<h3 className={styles.title}>Редактировать кампанию</h3>
|
<h3 className={styles.title}>Редактировать кампанию</h3>
|
||||||
@ -24,6 +25,15 @@ export default function EditCampaignModal({ isOpen, onClose, campaign, groups, v
|
|||||||
<label className={styles.label}>Тема
|
<label className={styles.label}>Тема
|
||||||
<input type="text" value={campaign.subject_override || ''} onChange={e => onChange({ ...campaign, subject_override: e.target.value })} className={styles.input} />
|
<input type="text" value={campaign.subject_override || ''} onChange={e => onChange({ ...campaign, subject_override: e.target.value })} className={styles.input} />
|
||||||
</label>
|
</label>
|
||||||
|
<label className={styles.label}>SMTP-серверы
|
||||||
|
<MultiSelect
|
||||||
|
options={smtpServers.map(s => ({ value: s.id, label: `${s.name} (${s.host})` }))}
|
||||||
|
value={campaign.smtp_server_ids || []}
|
||||||
|
onChange={ids => onChange({ ...campaign, smtp_server_ids: ids })}
|
||||||
|
placeholder="Выберите SMTP-серверы"
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
<label className={styles.label}>Статус
|
<label className={styles.label}>Статус
|
||||||
<select value={campaign.status} onChange={e => onChange({ ...campaign, status: e.target.value })} required className={styles.input}>
|
<select value={campaign.status} onChange={e => onChange({ ...campaign, status: e.target.value })} required className={styles.input}>
|
||||||
<option value="draft">Черновик</option>
|
<option value="draft">Черновик</option>
|
||||||
|
|||||||
@ -20,6 +20,7 @@ function CampaignPage() {
|
|||||||
const [createLoading, setCreateLoading] = useState(false);
|
const [createLoading, setCreateLoading] = useState(false);
|
||||||
const [groups, setGroups] = useState([]);
|
const [groups, setGroups] = useState([]);
|
||||||
const [versions, setVersions] = useState([]);
|
const [versions, setVersions] = useState([]);
|
||||||
|
const [smtpServers, setSmtpServers] = useState([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchCampaigns(page);
|
fetchCampaigns(page);
|
||||||
@ -29,6 +30,7 @@ function CampaignPage() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchGroups();
|
fetchGroups();
|
||||||
fetchVersions();
|
fetchVersions();
|
||||||
|
fetchSmtpServers();
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@ -94,6 +96,24 @@ function CampaignPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const fetchSmtpServers = async () => {
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/mail/smtp-servers?limit=1000', {
|
||||||
|
headers: token ? { Authorization: `Bearer ${token}` } : {}
|
||||||
|
});
|
||||||
|
const data = await res.json();
|
||||||
|
if (res.ok && Array.isArray(data)) {
|
||||||
|
setSmtpServers(data);
|
||||||
|
} else if (res.ok && Array.isArray(data.rows)) {
|
||||||
|
setSmtpServers(data.rows);
|
||||||
|
} else {
|
||||||
|
setSmtpServers([]);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
setSmtpServers([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
if (!window.confirm('Удалить кампанию?')) return;
|
if (!window.confirm('Удалить кампанию?')) return;
|
||||||
setDeleteLoading(id);
|
setDeleteLoading(id);
|
||||||
@ -116,7 +136,10 @@ function CampaignPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleEdit = (c) => {
|
const handleEdit = (c) => {
|
||||||
setEditCampaign(c);
|
setEditCampaign({
|
||||||
|
...c,
|
||||||
|
smtp_server_ids: c.SmtpServers ? c.SmtpServers.map(s => s.id) : [],
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditSave = async (e) => {
|
const handleEditSave = async (e) => {
|
||||||
@ -129,7 +152,7 @@ function CampaignPage() {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ ...editCampaign, user_id: user?.id })
|
body: JSON.stringify({ ...editCampaign, user_id: user?.id, smtp_server_ids: editCampaign.smtp_server_ids })
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@ -152,6 +175,7 @@ function CampaignPage() {
|
|||||||
subject_override: '',
|
subject_override: '',
|
||||||
scheduled_at: '',
|
scheduled_at: '',
|
||||||
status: 'draft',
|
status: 'draft',
|
||||||
|
smtp_server_ids: [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -165,7 +189,7 @@ function CampaignPage() {
|
|||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {})
|
...(token ? { Authorization: `Bearer ${token}` } : {})
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ ...createCampaign, user_id: user?.id })
|
body: JSON.stringify({ ...createCampaign, user_id: user?.id, smtp_server_ids: createCampaign.smtp_server_ids })
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
@ -246,6 +270,7 @@ function CampaignPage() {
|
|||||||
campaign={editCampaign}
|
campaign={editCampaign}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
versions={versions}
|
versions={versions}
|
||||||
|
smtpServers={smtpServers}
|
||||||
loading={editLoading}
|
loading={editLoading}
|
||||||
onChange={setEditCampaign}
|
onChange={setEditCampaign}
|
||||||
onSave={handleEditSave}
|
onSave={handleEditSave}
|
||||||
@ -259,6 +284,7 @@ function CampaignPage() {
|
|||||||
campaign={createCampaign}
|
campaign={createCampaign}
|
||||||
groups={groups}
|
groups={groups}
|
||||||
versions={versions}
|
versions={versions}
|
||||||
|
smtpServers={smtpServers}
|
||||||
loading={createLoading}
|
loading={createLoading}
|
||||||
onChange={setCreateCampaign}
|
onChange={setCreateCampaign}
|
||||||
onSave={handleCreateSave}
|
onSave={handleCreateSave}
|
||||||
@ -272,13 +298,6 @@ function CampaignPage() {
|
|||||||
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
||||||
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
||||||
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
||||||
const modalOverlayStyle = { position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 };
|
|
||||||
const modalStyle = { background: '#fff', borderRadius: 14, padding: '32px 28px 24px 28px', minWidth: 340, boxShadow: '0 12px 48px 0 rgba(31,38,135,0.22)', position: 'relative', animation: 'modalIn 0.18s cubic-bezier(.4,1.3,.6,1)'};
|
|
||||||
const closeBtnStyle = { position: 'absolute', top: 12, right: 16, background: 'none', border: 'none', fontSize: 26, color: '#6366f1', cursor: 'pointer', fontWeight: 700, lineHeight: 1, padding: 0, zIndex: 2, opacity: 0.8, transition: 'opacity 0.2s' };
|
|
||||||
const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: 'flex', flexDirection: 'column', gap: 4 };
|
|
||||||
const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' };
|
|
||||||
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
|
||||||
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
|
|
||||||
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
||||||
|
|
||||||
export default CampaignPage;
|
export default CampaignPage;
|
||||||
@ -202,13 +202,6 @@ function EmailTemplatesPage() {
|
|||||||
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
||||||
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
||||||
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
||||||
const modalOverlayStyle = { position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 };
|
|
||||||
const modalStyle = { background: '#fff', borderRadius: 14, padding: '32px 28px 24px 28px', minWidth: 340, boxShadow: '0 12px 48px 0 rgba(31,38,135,0.22)', position: 'relative', animation: 'modalIn 0.18s cubic-bezier(.4,1.3,.6,1)'};
|
|
||||||
const closeBtnStyle = { position: 'absolute', top: 12, right: 16, background: 'none', border: 'none', fontSize: 26, color: '#6366f1', cursor: 'pointer', fontWeight: 700, lineHeight: 1, padding: 0, zIndex: 2, opacity: 0.8, transition: 'opacity 0.2s' };
|
|
||||||
const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: 'flex', flexDirection: 'column', gap: 4 };
|
|
||||||
const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' };
|
|
||||||
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
|
||||||
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
|
|
||||||
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
||||||
|
|
||||||
export default EmailTemplatesPage;
|
export default EmailTemplatesPage;
|
||||||
@ -424,13 +424,6 @@ function SubscribersInGroupPage({ group, onBack, token }) {
|
|||||||
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
||||||
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
||||||
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
||||||
const modalOverlayStyle = { position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 };
|
|
||||||
const modalStyle = { background: '#fff', borderRadius: 14, padding: '32px 28px 24px 28px', minWidth: 340, boxShadow: '0 12px 48px 0 rgba(31,38,135,0.22)', position: 'relative', animation: 'modalIn 0.18s cubic-bezier(.4,1.3,.6,1)'};
|
|
||||||
const closeBtnStyle = { position: 'absolute', top: 12, right: 16, background: 'none', border: 'none', fontSize: 26, color: '#6366f1', cursor: 'pointer', fontWeight: 700, lineHeight: 1, padding: 0, zIndex: 2, opacity: 0.8, transition: 'opacity 0.2s' };
|
|
||||||
const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: 'flex', flexDirection: 'column', gap: 4 };
|
|
||||||
const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' };
|
|
||||||
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
|
||||||
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
|
|
||||||
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
||||||
|
|
||||||
export default GroupsPage;
|
export default GroupsPage;
|
||||||
@ -216,13 +216,6 @@ function SmtpServersPage() {
|
|||||||
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
||||||
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
||||||
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
||||||
const modalOverlayStyle = { position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 };
|
|
||||||
const modalStyle = { background: '#fff', borderRadius: 14, padding: '32px 28px 24px 28px', minWidth: 340, boxShadow: '0 12px 48px 0 rgba(31,38,135,0.22)', position: 'relative', animation: 'modalIn 0.18s cubic-bezier(.4,1.3,.6,1)'};
|
|
||||||
const closeBtnStyle = { position: 'absolute', top: 12, right: 16, background: 'none', border: 'none', fontSize: 26, color: '#6366f1', cursor: 'pointer', fontWeight: 700, lineHeight: 1, padding: 0, zIndex: 2, opacity: 0.8, transition: 'opacity 0.2s' };
|
|
||||||
const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: 'flex', flexDirection: 'column', gap: 4 };
|
|
||||||
const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' };
|
|
||||||
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
|
||||||
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
|
|
||||||
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
||||||
|
|
||||||
export default SmtpServersPage;
|
export default SmtpServersPage;
|
||||||
@ -206,13 +206,6 @@ function UnsubscribedPage() {
|
|||||||
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
||||||
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
||||||
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
||||||
const modalOverlayStyle = { position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 };
|
|
||||||
const modalStyle = { background: '#fff', borderRadius: 14, padding: '32px 28px 24px 28px', minWidth: 340, boxShadow: '0 12px 48px 0 rgba(31,38,135,0.22)', position: 'relative', animation: 'modalIn 0.18s cubic-bezier(.4,1.3,.6,1)'};
|
|
||||||
const closeBtnStyle = { position: 'absolute', top: 12, right: 16, background: 'none', border: 'none', fontSize: 26, color: '#6366f1', cursor: 'pointer', fontWeight: 700, lineHeight: 1, padding: 0, zIndex: 2, opacity: 0.8, transition: 'opacity 0.2s' };
|
|
||||||
const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: 'flex', flexDirection: 'column', gap: 4 };
|
|
||||||
const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' };
|
|
||||||
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
|
||||||
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
|
|
||||||
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
||||||
|
|
||||||
export default UnsubscribedPage;
|
export default UnsubscribedPage;
|
||||||
@ -239,13 +239,6 @@ function UsersPage() {
|
|||||||
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
const thStyle = { padding: '10px 16px', textAlign: 'left', fontWeight: 600, borderBottom: '2px solid #e5e7eb', background: '#f3f4f6' };
|
||||||
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
const tdStyle = { padding: '10px 16px', background: '#fff' };
|
||||||
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
const btnStyle = { marginRight: 8, padding: '6px 12px', borderRadius: 6, border: 'none', background: '#6366f1', color: '#fff', cursor: 'pointer', fontWeight: 500 };
|
||||||
const modalOverlayStyle = { position: 'fixed', top: 0, left: 0, width: '100vw', height: '100vh', background: 'rgba(0,0,0,0.18)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 };
|
|
||||||
const modalStyle = { background: '#fff', borderRadius: 14, padding: '32px 28px 24px 28px', minWidth: 340, boxShadow: '0 12px 48px 0 rgba(31,38,135,0.22)', position: 'relative', animation: 'modalIn 0.18s cubic-bezier(.4,1.3,.6,1)'};
|
|
||||||
const closeBtnStyle = { position: 'absolute', top: 12, right: 16, background: 'none', border: 'none', fontSize: 26, color: '#6366f1', cursor: 'pointer', fontWeight: 700, lineHeight: 1, padding: 0, zIndex: 2, opacity: 0.8, transition: 'opacity 0.2s' };
|
|
||||||
const labelStyle = { fontWeight: 500, color: '#374151', fontSize: 15, display: 'flex', flexDirection: 'column', gap: 4 };
|
|
||||||
const inputStyle = { marginTop: 4, padding: '10px 12px', borderRadius: 8, border: '1.5px solid #c7d2fe', fontSize: 16, outline: 'none', background: '#f8fafc', transition: 'border 0.2s' };
|
|
||||||
const saveBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
|
||||||
const cancelBtnStyle = { background: '#f3f4f6', color: '#6366f1', border: 'none', borderRadius: 8, padding: '10px 18px', fontSize: 16, fontWeight: 600, cursor: 'pointer', transition: 'background 0.2s' };
|
|
||||||
|
|
||||||
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
const addBtnStyle = { background: 'linear-gradient(90deg, #6366f1 0%, #06b6d4 100%)', color: '#fff', border: 'none', borderRadius: 8, padding: '10px 22px', fontSize: 16, fontWeight: 600, cursor: 'pointer', boxShadow: '0 2px 8px 0 rgba(99,102,241,0.10)', transition: 'background 0.2s' };
|
||||||
|
|
||||||
|
|||||||
70
frontend/src/styles/MultiSelect.module.css
Normal file
70
frontend/src/styles/MultiSelect.module.css
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
.control {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
background: #f8fafc;
|
||||||
|
border: 1.5px solid #c7d2fe;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border 0.2s;
|
||||||
|
min-height: 40px;
|
||||||
|
}
|
||||||
|
.control:focus, .control:active {
|
||||||
|
border-color: #6366f1;
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
background: #f3f4f6;
|
||||||
|
color: #b3b3b3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
.value {
|
||||||
|
flex: 1;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.placeholder {
|
||||||
|
color: #9ca3af;
|
||||||
|
}
|
||||||
|
.arrow {
|
||||||
|
margin-left: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #6366f1;
|
||||||
|
}
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: calc(100% + 4px);
|
||||||
|
background: #fff;
|
||||||
|
border: 1.5px solid #c7d2fe;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 24px 0 rgba(99,102,241,0.10);
|
||||||
|
z-index: 10;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 6px 0;
|
||||||
|
}
|
||||||
|
.option {
|
||||||
|
padding: 8px 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 15px;
|
||||||
|
color: #374151;
|
||||||
|
transition: background 0.15s;
|
||||||
|
}
|
||||||
|
.option:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
.selected {
|
||||||
|
background: #e0e7ff;
|
||||||
|
color: #3730a3;
|
||||||
|
}
|
||||||
@ -1,10 +1,15 @@
|
|||||||
import { Campaign, EmailTemplateVersion, MailingGroup } from '../models/index.js';
|
import { Campaign, EmailTemplateVersion, MailingGroup, SmtpServer } from '../models/index.js';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
async create(req, res) {
|
async create(req, res) {
|
||||||
try {
|
try {
|
||||||
const campaign = await Campaign.create(req.body);
|
const { smtp_server_ids, ...campaignData } = req.body;
|
||||||
res.status(201).json(campaign);
|
const campaign = await Campaign.create(campaignData);
|
||||||
|
if (Array.isArray(smtp_server_ids)) {
|
||||||
|
await campaign.setSmtpServers(smtp_server_ids);
|
||||||
|
}
|
||||||
|
const campaignWithSmtps = await Campaign.findByPk(campaign.id, { include: [SmtpServer] });
|
||||||
|
res.status(201).json(campaignWithSmtps);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).json({ error: err.message });
|
res.status(400).json({ error: err.message });
|
||||||
}
|
}
|
||||||
@ -14,7 +19,7 @@ export default {
|
|||||||
const limit = parseInt(req.query.limit) || 20;
|
const limit = parseInt(req.query.limit) || 20;
|
||||||
const offset = parseInt(req.query.offset) || 0;
|
const offset = parseInt(req.query.offset) || 0;
|
||||||
const result = await Campaign.findAndCountAll({
|
const result = await Campaign.findAndCountAll({
|
||||||
include: [EmailTemplateVersion, MailingGroup],
|
include: [EmailTemplateVersion, MailingGroup, SmtpServer],
|
||||||
limit,
|
limit,
|
||||||
offset,
|
offset,
|
||||||
order: [['id', 'ASC']]
|
order: [['id', 'ASC']]
|
||||||
@ -26,7 +31,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async getById(req, res) {
|
async getById(req, res) {
|
||||||
try {
|
try {
|
||||||
const campaign = await Campaign.findByPk(req.params.id, { include: [EmailTemplateVersion, MailingGroup] });
|
const campaign = await Campaign.findByPk(req.params.id, { include: [EmailTemplateVersion, MailingGroup, SmtpServer] });
|
||||||
if (!campaign) return res.status(404).json({ error: 'Campaign not found' });
|
if (!campaign) return res.status(404).json({ error: 'Campaign not found' });
|
||||||
res.json(campaign);
|
res.json(campaign);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -35,10 +40,15 @@ export default {
|
|||||||
},
|
},
|
||||||
async update(req, res) {
|
async update(req, res) {
|
||||||
try {
|
try {
|
||||||
|
const { smtp_server_ids, ...campaignData } = req.body;
|
||||||
const campaign = await Campaign.findByPk(req.params.id);
|
const campaign = await Campaign.findByPk(req.params.id);
|
||||||
if (!campaign) return res.status(404).json({ error: 'Campaign not found' });
|
if (!campaign) return res.status(404).json({ error: 'Campaign not found' });
|
||||||
await campaign.update(req.body);
|
await campaign.update(campaignData);
|
||||||
res.json(campaign);
|
if (Array.isArray(smtp_server_ids)) {
|
||||||
|
await campaign.setSmtpServers(smtp_server_ids);
|
||||||
|
}
|
||||||
|
const campaignWithSmtps = await Campaign.findByPk(campaign.id, { include: [SmtpServer] });
|
||||||
|
res.json(campaignWithSmtps);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(400).json({ error: err.message });
|
res.status(400).json({ error: err.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import EmailTemplateVersionModel from './emailTemplateVersion.js';
|
|||||||
import CampaignModel from './campaign.js';
|
import CampaignModel from './campaign.js';
|
||||||
import DeliveryLogModel from './deliveryLog.js';
|
import DeliveryLogModel from './deliveryLog.js';
|
||||||
import SmtpServerModel from './smtpServer.js';
|
import SmtpServerModel from './smtpServer.js';
|
||||||
|
import { DataTypes } from 'sequelize';
|
||||||
|
|
||||||
const sequelize = new Sequelize(
|
const sequelize = new Sequelize(
|
||||||
process.env.DB_NAME,
|
process.env.DB_NAME,
|
||||||
@ -29,6 +30,9 @@ const Campaign = CampaignModel(sequelize);
|
|||||||
const DeliveryLog = DeliveryLogModel(sequelize);
|
const DeliveryLog = DeliveryLogModel(sequelize);
|
||||||
const SmtpServer = SmtpServerModel(sequelize);
|
const SmtpServer = SmtpServerModel(sequelize);
|
||||||
|
|
||||||
|
// Промежуточная таблица для связи many-to-many
|
||||||
|
const CampaignSmtpServer = sequelize.define('CampaignSmtpServer', {}, { tableName: 'campaign_smtp_servers', timestamps: false });
|
||||||
|
|
||||||
// Связи
|
// Связи
|
||||||
MailingGroup.belongsToMany(Subscriber, { through: GroupSubscriber, foreignKey: 'group_id', otherKey: 'subscriber_id' });
|
MailingGroup.belongsToMany(Subscriber, { through: GroupSubscriber, foreignKey: 'group_id', otherKey: 'subscriber_id' });
|
||||||
Subscriber.belongsToMany(MailingGroup, { through: GroupSubscriber, foreignKey: 'subscriber_id', otherKey: 'group_id' });
|
Subscriber.belongsToMany(MailingGroup, { through: GroupSubscriber, foreignKey: 'subscriber_id', otherKey: 'group_id' });
|
||||||
@ -44,6 +48,9 @@ Campaign.belongsTo(MailingGroup, { foreignKey: 'group_id' });
|
|||||||
DeliveryLog.belongsTo(Campaign, { foreignKey: 'campaign_id' });
|
DeliveryLog.belongsTo(Campaign, { foreignKey: 'campaign_id' });
|
||||||
DeliveryLog.belongsTo(Subscriber, { foreignKey: 'subscriber_id' });
|
DeliveryLog.belongsTo(Subscriber, { foreignKey: 'subscriber_id' });
|
||||||
|
|
||||||
|
Campaign.belongsToMany(SmtpServer, { through: CampaignSmtpServer, foreignKey: 'campaign_id', otherKey: 'smtp_server_id' });
|
||||||
|
SmtpServer.belongsToMany(Campaign, { through: CampaignSmtpServer, foreignKey: 'smtp_server_id', otherKey: 'campaign_id' });
|
||||||
|
|
||||||
// Удалены связи SmtpServer <-> MailingGroup по group_id
|
// Удалены связи SmtpServer <-> MailingGroup по group_id
|
||||||
|
|
||||||
// (связи с user_id только по полю, без внешнего ключа на User)
|
// (связи с user_id только по полю, без внешнего ключа на User)
|
||||||
@ -58,4 +65,5 @@ export {
|
|||||||
Campaign,
|
Campaign,
|
||||||
DeliveryLog,
|
DeliveryLog,
|
||||||
SmtpServer,
|
SmtpServer,
|
||||||
|
CampaignSmtpServer,
|
||||||
};
|
};
|
||||||
Loading…
Reference in New Issue
Block a user