Merge pull request 'production' (#2) from production into main
Reviewed-on: http://127.0.0.1:30300/kultmost/avtopost.kultmost.ru/pulls/2
This commit is contained in:
commit
b023fd00d2
177
build.ipynb
177
build.ipynb
@ -12,7 +12,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!pip install -r requirements.txt"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -72,7 +73,8 @@
|
||||
"EOF\n",
|
||||
"\n",
|
||||
"chmod +x .git/hooks/pre-commit\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -120,7 +122,8 @@
|
||||
" load_env('env')\n",
|
||||
"except:\n",
|
||||
" load_env('env_example')"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -132,7 +135,7 @@
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": 1,
|
||||
"execution_count": null,
|
||||
"id": "2809c42c",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
@ -161,7 +164,8 @@
|
||||
" f.write('\\n\\n'.join(app))\n",
|
||||
"\n",
|
||||
"build()"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -195,7 +199,8 @@
|
||||
"kubectl -n container-registry create secret generic registry-basic-auth \\\n",
|
||||
" --from-file=auth=/tmp/registry.htpasswd \\\n",
|
||||
" --dry-run=client -o yaml | kubectl apply -f -\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -209,7 +214,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!kubectl create secret docker-registry regcred --docker-server=$REGISTRY_HOST --docker-username='user' --docker-password=$REGISTRY_PASS --docker-email=\"$(git config --global --get user.email)\""
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -224,7 +230,8 @@
|
||||
"source": [
|
||||
"!kubectl create namespace kultmost\n",
|
||||
"!kubectl create secret docker-registry regcred -n kultmost --docker-server=$REGISTRY_HOST --docker-username='user' --docker-password=$REGISTRY_PASS --docker-email=\"$(git config --global --get user.email)\""
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -238,7 +245,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker login $REGISTRY_HOST -u 'user' -p $REGISTRY_PASS"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -260,7 +268,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker pull $REGISTRY_HOST/python:3.13-alpine"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -274,7 +283,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker pull $REGISTRY_HOST/python:3.13-slim"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -296,7 +306,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker build -t $REGISTRY_HOST/api-tg:latest tg/."
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -310,7 +321,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker push $REGISTRY_HOST/api-tg:latest"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -332,7 +344,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker build -t $REGISTRY_HOST/avtopost-api:latest ."
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -346,7 +359,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker image push $REGISTRY_HOST/avtopost-api:latest"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -360,7 +374,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!kubectl rollout restart deployments/kultmost-api -n kultmost"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -382,7 +397,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!kubectl rollout restart deployments/vk-celery -n kultmost"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -396,7 +412,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!kubectl rollout restart deployments/email-celery -n kultmost"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -421,7 +438,8 @@
|
||||
"docker pull gitea/gitea:1.22\n",
|
||||
"docker tag gitea/gitea:1.22 $REGISTRY_HOST/gitea/gitea:1.22\n",
|
||||
"docker push $REGISTRY_HOST/gitea/gitea:1.22"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -445,7 +463,8 @@
|
||||
"docker tag busybox:1.36 $REGISTRY_HOST/busybox:1.36\n",
|
||||
"docker tag rabbitmq:3.11-management $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
||||
"docker tag alpine:3.14 $REGISTRY_HOST/alpine:3.14"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -464,7 +483,8 @@
|
||||
"docker push $REGISTRY_HOST/busybox:1.36\n",
|
||||
"docker push $REGISTRY_HOST/rabbitmq:3.11-management\n",
|
||||
"docker push $REGISTRY_HOST/alpine:3.14"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -486,7 +506,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker build -t $REGISTRY_HOST/ssh-tunnel:2 tunnel/"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -500,7 +521,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!docker push $REGISTRY_HOST/ssh-tunnel:2"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -514,7 +536,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!kubectl create secret generic ssh-id-rsa --from-file=id_rsa=/home/user/.ssh/id_rsa -n kultmost"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -528,7 +551,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!kubectl apply -n kultmost -f k8s/stunnel.yaml"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -550,7 +574,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!envsubst < k8s/prod.yaml | kubectl apply -n kultmost -f -"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -564,7 +589,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!envsubst < k8s/kultmost.yaml | kubectl apply -n kultmost -f -"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -578,7 +604,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!envsubst < k8s/mq.yaml | kubectl apply -n kultmost -f -"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -592,7 +619,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!envsubst < k8s/gitea.yaml | kubectl apply -n gitea -f -"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -606,7 +634,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!kubectl get pod -n kultmost"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -631,7 +660,8 @@
|
||||
"echo \"$INIT_SSH'bash -s' <<'SCRIPT'\"\n",
|
||||
"echo \"kubectl create secret docker-registry regcred --docker-server=$REGISTRY_HOST --docker-username='user' --docker-password=$REGISTRY_PASS --docker-email=\\\"$(git config --global --get user.email)\\\" && clear && history -c && history -w\"\n",
|
||||
"echo \"SCRIPT\""
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -654,7 +684,8 @@
|
||||
"echo \"clear\"\n",
|
||||
"echo \"history -c\"\n",
|
||||
"echo \"history -w\""
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -684,7 +715,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!rsync -avz -ogp --chown=www-data:www-data ./static/ kultmost:/var/www/static/"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -698,7 +730,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!echo $(git branch --show-current)"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -712,7 +745,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!rsync -avh --exclude-from=.rsyncignore --exclude=.venv/ . kultmost:/app/$(git branch --show-current)/"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -750,7 +784,8 @@
|
||||
"EOF\n",
|
||||
"\n",
|
||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now production.service && systemctl status --no-pager -l production.service'\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -780,7 +815,8 @@
|
||||
"EOF\n",
|
||||
"\n",
|
||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now fastapi.service && systemctl status --no-pager -l fastapi.service'\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -818,7 +854,8 @@
|
||||
"EOF\n",
|
||||
"\n",
|
||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -854,7 +891,8 @@
|
||||
"[Install]\n",
|
||||
"WantedBy=default.target\n",
|
||||
"EOF"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -884,7 +922,8 @@
|
||||
"EOF\n",
|
||||
"\n",
|
||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now staging.service && systemctl status --no-pager -l staging.service'\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -906,7 +945,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'systemctl restart production.service'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -920,7 +960,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'systemctl status production.service'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -934,7 +975,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'systemctl restart fastapi.service'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -948,7 +990,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'systemctl status fastapi.service'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -986,7 +1029,8 @@
|
||||
"EOF\n",
|
||||
"\n",
|
||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now vk-celery.service && systemctl status --no-pager -l vk-celery.service'\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -1000,7 +1044,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'systemctl restart vk-celery.service'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -1014,7 +1059,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'systemctl status vk-celery.service'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -1044,7 +1090,8 @@
|
||||
"EOF\n",
|
||||
"\n",
|
||||
"ssh kultmost 'systemctl daemon-reload && systemctl enable --now email-celery.service && systemctl status --no-pager -l email-celery.service'\n"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -1058,7 +1105,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'systemctl restart email-celery.service'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -1072,7 +1120,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'systemctl status email-celery.service'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -1094,7 +1143,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# !scp kultmost:/etc/nginx/sites-enabled/default nginx_conf "
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -1110,7 +1160,8 @@
|
||||
"!scp nginx_conf kultmost:/tmp/default\n",
|
||||
"!ssh kultmost 'sudo mv /tmp/default /etc/nginx/sites-enabled/default'\n",
|
||||
"!ssh kultmost 'nginx -t'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -1124,7 +1175,8 @@
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"!ssh kultmost 'nginx -s reload'"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -1149,7 +1201,8 @@
|
||||
"sudo usermod -a -G microk8s $USER\n",
|
||||
"newgrp microk8s\n",
|
||||
"microk8s status"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
@ -1165,7 +1218,8 @@
|
||||
"curl -LO https://cert-manager.io/public-keys/cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg\n",
|
||||
"helm install cert-manager oci://quay.io/jetstack/charts/cert-manager --version v1.20.2 --namespace cert-manager --create-namespace --verify --keyring ./cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg --set crds.enabled=true\n",
|
||||
"microk8s helm install cert-manager oci://quay.io/jetstack/charts/cert-manager --version v1.20.2 --namespace cert-manager --create-namespace --verify --keyring ./cert-manager-keyring-2021-09-20-1020CF3C033D4F35BAE1C19E1226061C665DF13E.gpg --set crds.enabled=true"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
@ -1186,6 +1240,16 @@
|
||||
},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"// MIT License\n",
|
||||
"\n",
|
||||
"// Copyright (c) 2026 vladislav\n",
|
||||
"\n",
|
||||
"// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:\n",
|
||||
"\n",
|
||||
"// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n",
|
||||
"\n",
|
||||
"// THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n",
|
||||
"\n",
|
||||
"m1 = figma.currentPage.children[0]\n",
|
||||
"HTML_TAGS = new Set(['a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kbd', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'picture', 'pre', 'progress', 'q', 'rp', 'rt', 'ruby', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'title', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr']);\n",
|
||||
"\n",
|
||||
@ -1386,7 +1450,8 @@
|
||||
"\n",
|
||||
"sss = await element_create(pc, tablet, mob)\n",
|
||||
"sss.innerHTML"
|
||||
]
|
||||
],
|
||||
"display_name": ".venv"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
|
||||
@ -17,55 +17,68 @@ async def send_delivery(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Создаёт новый документ отправки с обновлёнными статусом и полями.
|
||||
Массовая отправка: создаёт новый документ для каждого выбранного item.
|
||||
"""
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
for entry in payload.items:
|
||||
# Проверяем, что текущий пользователь — отправитель
|
||||
if str(current_user["_id"]) != payload.sender:
|
||||
raise HTTPException(status_code=403, detail="Не авторизован как отправитель")
|
||||
if str(current_user["_id"]) != entry.sender:
|
||||
errors.append({"item": entry.item, "error": "Не авторизован как отправитель"})
|
||||
continue
|
||||
|
||||
# Валидация delivery_id
|
||||
try:
|
||||
delivery_id = ObjectId(payload.item)
|
||||
delivery_id = ObjectId(entry.item)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Неверный delivery ID")
|
||||
errors.append({"item": entry.item, "error": "Неверный delivery ID"})
|
||||
continue
|
||||
|
||||
update_data = {
|
||||
"status": "Ожидает получения",
|
||||
"delivery_type": payload.delivery_type,
|
||||
"sent_at": datetime.utcnow(),
|
||||
"updated_at": datetime.utcnow()
|
||||
"updated_at": datetime.utcnow(),
|
||||
}
|
||||
|
||||
# Добавляем трек только для СДЭК (если передан)
|
||||
# Добавляем трек только для СДЭК
|
||||
if payload.delivery_type == "СДЭК" and payload.track:
|
||||
update_data["track"] = payload.track
|
||||
|
||||
# Ищем существующую запись, чтобы создать новую на её основе
|
||||
# Ищем существующую запись
|
||||
existing = await delivery_collection.find_one(
|
||||
{"_id": delivery_id, "sender._id": payload.sender},
|
||||
{"_id": delivery_id, "sender._id": entry.sender},
|
||||
{"_id": 0}
|
||||
)
|
||||
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail="Доставка не найдена или не для вас")
|
||||
errors.append({"item": entry.item, "error": "Доставка не найдена или не для вас"})
|
||||
continue
|
||||
|
||||
# Собираем новый документ для вставки
|
||||
# Уведомление получателю
|
||||
notify = await notify_in_profile(NotifyItem(
|
||||
user_id=str(existing['receiver']['_id']),
|
||||
type_item='delivery',
|
||||
title='Доставка картин',
|
||||
link=f'/profile/delivery',
|
||||
text=f'{str(existing['sender']['username'])} — прислал вам картины.'
|
||||
user_id=str(existing["receiver"]["_id"]),
|
||||
type_item="delivery",
|
||||
title="Доставка картин",
|
||||
link="/profile/delivery",
|
||||
text=f"{str(existing['sender']['username'])} — прислал вам картины."
|
||||
))
|
||||
new_doc = {**existing, **update_data, 'notify':notify.upserted_id}
|
||||
|
||||
# Вставляем новый документ
|
||||
new_doc = {**existing, **update_data, "notify": notify.upserted_id}
|
||||
|
||||
result = await delivery_collection.insert_one(new_doc)
|
||||
if (iserted_id:=result.inserted_id):
|
||||
pass
|
||||
# send email
|
||||
if result.inserted_id:
|
||||
# TODO: send email
|
||||
results.append({
|
||||
"item": entry.item,
|
||||
"delivery_id": str(result.inserted_id)
|
||||
})
|
||||
|
||||
return {"status": "success", "delivery_id": str(iserted_id)}
|
||||
return {
|
||||
"status": "success" if not errors else "partial",
|
||||
"processed": results,
|
||||
"errors": errors
|
||||
}
|
||||
|
||||
|
||||
@router.post("/accept")
|
||||
@ -74,28 +87,35 @@ async def accept_delivery(
|
||||
current_user: dict = Depends(get_current_user)
|
||||
):
|
||||
"""
|
||||
Создаёт новый документ отправки со статусом 'Получено'.
|
||||
Массовое получение: создаёт новый документ со статусом 'Получено' для каждого item.
|
||||
"""
|
||||
try:
|
||||
delivery_id = ObjectId(payload.item)
|
||||
except Exception:
|
||||
raise HTTPException(status_code=400, detail="Неверный delivery ID")
|
||||
results = []
|
||||
errors = []
|
||||
|
||||
for item_id in payload.items:
|
||||
try:
|
||||
delivery_id = ObjectId(item_id)
|
||||
except Exception:
|
||||
errors.append({"item": item_id, "error": "Неверный delivery ID"})
|
||||
continue
|
||||
|
||||
# Берём исходный документ, чтобы вставить новый
|
||||
existing = await delivery_collection.find_one(
|
||||
{"_id": delivery_id, "receiver._id": current_user["_id"]},
|
||||
{"_id": 0}
|
||||
)
|
||||
|
||||
if not existing:
|
||||
raise HTTPException(status_code=404, detail="Доставка не найдена или не для вас")
|
||||
errors.append({"item": item_id, "error": "Доставка не найдена или не для вас"})
|
||||
continue
|
||||
|
||||
# Уведомление отправителю
|
||||
notify = await notify_in_profile(NotifyItem(
|
||||
user_id=str(existing['sender']['_id']),
|
||||
type_item='delivery',
|
||||
title='Доставка картин',
|
||||
link=f'/profile/delivery',
|
||||
text=f'Картина «{existing['title']}» получена — {str(existing['receiver']['username'])}.'
|
||||
user_id=str(existing["sender"]["_id"]),
|
||||
type_item="delivery",
|
||||
title="Доставка картин",
|
||||
link="/profile/delivery",
|
||||
text=f"Картина «{existing['title']}» получена — {str(existing['receiver']['username'])}."
|
||||
))
|
||||
|
||||
new_doc = {
|
||||
**existing,
|
||||
"status": "Получено",
|
||||
@ -106,5 +126,20 @@ async def accept_delivery(
|
||||
|
||||
result = await delivery_collection.insert_one(new_doc)
|
||||
|
||||
await notify_collection.update_one({'_id': existing['notify']}, {'$set': {'read': False}})
|
||||
return {"status": "success", "delivery_id": str(result.inserted_id)}
|
||||
# Помечаем предыдущее уведомление как непрочитанное (если нужно)
|
||||
if existing.get("notify"):
|
||||
await notify_collection.update_one(
|
||||
{"_id": existing["notify"]},
|
||||
{"$set": {"read": False}}
|
||||
)
|
||||
|
||||
results.append({
|
||||
"item": item_id,
|
||||
"delivery_id": str(result.inserted_id)
|
||||
})
|
||||
|
||||
return {
|
||||
"status": "success" if not errors else "partial",
|
||||
"processed": results,
|
||||
"errors": errors
|
||||
}
|
||||
55
main.ipynb
55
main.ipynb
@ -557,7 +557,7 @@
|
||||
"\n",
|
||||
" # 1) If there's an exhibition that is going to be finished soon -> move deliveries to \"Возврат художнику\"\n",
|
||||
" exhibition_finished = await exhibition_collection.find_one_and_update(\n",
|
||||
" {**prod_filter, \"date_finish\": {\"$lt\": limit_dt}},\n",
|
||||
" {**prod_filter, \"status\": 1, \"date_finish\": {\"$lt\": limit_dt}},\n",
|
||||
" {\"$set\": {\"status\": 2}},\n",
|
||||
" return_document=ReturnDocument.AFTER,\n",
|
||||
" )\n",
|
||||
@ -2903,7 +2903,7 @@
|
||||
" competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})\n",
|
||||
" data = json.dumps(competition, cls=MongoJSONEncoder, ensure_ascii=False)\n",
|
||||
" return templates.TemplateResponse(\n",
|
||||
" request=request, name=\"form/competition_edit.html\", context={\"competition\": str(data), \"user\": user}\n",
|
||||
" request=request, name=\"form/competition_edit.html\", context={\"competition\": str(data), \"competition_id\": competition_id, \"user\": user}\n",
|
||||
" )\n"
|
||||
],
|
||||
"display_name": ".venv"
|
||||
@ -4554,6 +4554,48 @@
|
||||
"id": "6157e497",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# @app.get('/profile/delivery/{type_d}/{item_id}')\n",
|
||||
"# async def profile_delivery_by_old(request: Request, type_d:str, item_id:str, user: Dict = Security(get_current_user)):\n",
|
||||
"# cursor = delivery_collection.aggregate([\n",
|
||||
"# {\"$match\": {\n",
|
||||
"# \"$or\": [\n",
|
||||
"# {\"sender._id\": user.get('_id')},\n",
|
||||
"# {\"user._id\": user.get('_id')},\n",
|
||||
"# {\"receiver._id\": user.get('_id')}\n",
|
||||
"# ],\n",
|
||||
"# f'{type_d}_id': ObjectId(item_id),\n",
|
||||
"# 'prod': PROD,\n",
|
||||
"# }},\n",
|
||||
"# {\"$sort\": {\"_id\": -1}},\n",
|
||||
"# {\"$group\": {\n",
|
||||
"# \"_id\": \"$post_id\",\n",
|
||||
"# \"doc\": {\"$first\": \"$$ROOT\"}\n",
|
||||
"# }},\n",
|
||||
"# {\"$replaceRoot\": {\"newRoot\": \"$doc\"}},\n",
|
||||
"# {\"$sort\": {\"_id\": -1}},\n",
|
||||
"# ])\n",
|
||||
"\n",
|
||||
"# data = [doc async for doc in cursor]\n",
|
||||
"# data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
|
||||
"\n",
|
||||
"# return templates.TemplateResponse(\n",
|
||||
"# request=request,\n",
|
||||
"# name=\"profile/delivery_old.html\",\n",
|
||||
"# context={\n",
|
||||
"# \"user\": user,\n",
|
||||
"# \"data\": data,\n",
|
||||
"# }\n",
|
||||
"# )\n"
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "089ba597",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"@app.get('/profile/delivery/competition/{competition_id}')\n",
|
||||
"async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)):\n",
|
||||
@ -4564,7 +4606,6 @@
|
||||
" {\"user._id\": user.get('_id')},\n",
|
||||
" {\"receiver._id\": user.get('_id')}\n",
|
||||
" ],\n",
|
||||
" 'competition_id': ObjectId(competition_id),\n",
|
||||
" 'prod': PROD,\n",
|
||||
" }},\n",
|
||||
" {\"$sort\": {\"_id\": -1}},\n",
|
||||
@ -4577,6 +4618,8 @@
|
||||
" ])\n",
|
||||
"\n",
|
||||
" data = [doc async for doc in cursor]\n",
|
||||
" data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
|
||||
"\n",
|
||||
" return templates.TemplateResponse(\n",
|
||||
" request=request,\n",
|
||||
" name=\"profile/delivery_old.html\",\n",
|
||||
@ -4591,7 +4634,7 @@
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"id": "d68edb20",
|
||||
"id": "2148ef21",
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
@ -4617,6 +4660,8 @@
|
||||
" ])\n",
|
||||
"\n",
|
||||
" data = [doc async for doc in cursor]\n",
|
||||
" data.sort(key=lambda doc: doc[\"_id\"].generation_time, reverse=True)\n",
|
||||
"\n",
|
||||
" return templates.TemplateResponse(\n",
|
||||
" request=request,\n",
|
||||
" name=\"profile/delivery_old.html\",\n",
|
||||
@ -4624,7 +4669,7 @@
|
||||
" \"user\": user,\n",
|
||||
" \"data\": data,\n",
|
||||
" }\n",
|
||||
" )\n"
|
||||
" )"
|
||||
],
|
||||
"display_name": ".venv"
|
||||
},
|
||||
|
||||
44
main.py
44
main.py
@ -345,7 +345,7 @@ async def cron_every_five_min(token: str = Security(get_token)):
|
||||
|
||||
# 1) If there's an exhibition that is going to be finished soon -> move deliveries to "Возврат художнику"
|
||||
exhibition_finished = await exhibition_collection.find_one_and_update(
|
||||
{**prod_filter, "date_finish": {"$lt": limit_dt}},
|
||||
{**prod_filter, "status": 1, "date_finish": {"$lt": limit_dt}},
|
||||
{"$set": {"status": 2}},
|
||||
return_document=ReturnDocument.AFTER,
|
||||
)
|
||||
@ -2162,7 +2162,7 @@ async def form_(
|
||||
competition = await competitions_col.find_one({'_id': ObjectId(competition_id)})
|
||||
data = json.dumps(competition, cls=MongoJSONEncoder, ensure_ascii=False)
|
||||
return templates.TemplateResponse(
|
||||
request=request, name="form/competition_edit.html", context={"competition": str(data), "user": user}
|
||||
request=request, name="form/competition_edit.html", context={"competition": str(data), "competition_id": competition_id, "user": user}
|
||||
)
|
||||
|
||||
|
||||
@ -3459,6 +3459,40 @@ async def profile_delivery_history_post(request: Request, post_id:str, user: Dic
|
||||
)
|
||||
|
||||
|
||||
# @app.get('/profile/delivery/{type_d}/{item_id}')
|
||||
# async def profile_delivery_by_old(request: Request, type_d:str, item_id:str, user: Dict = Security(get_current_user)):
|
||||
# cursor = delivery_collection.aggregate([
|
||||
# {"$match": {
|
||||
# "$or": [
|
||||
# {"sender._id": user.get('_id')},
|
||||
# {"user._id": user.get('_id')},
|
||||
# {"receiver._id": user.get('_id')}
|
||||
# ],
|
||||
# f'{type_d}_id': ObjectId(item_id),
|
||||
# 'prod': PROD,
|
||||
# }},
|
||||
# {"$sort": {"_id": -1}},
|
||||
# {"$group": {
|
||||
# "_id": "$post_id",
|
||||
# "doc": {"$first": "$$ROOT"}
|
||||
# }},
|
||||
# {"$replaceRoot": {"newRoot": "$doc"}},
|
||||
# {"$sort": {"_id": -1}},
|
||||
# ])
|
||||
|
||||
# data = [doc async for doc in cursor]
|
||||
# data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True)
|
||||
|
||||
# return templates.TemplateResponse(
|
||||
# request=request,
|
||||
# name="profile/delivery_old.html",
|
||||
# context={
|
||||
# "user": user,
|
||||
# "data": data,
|
||||
# }
|
||||
# )
|
||||
|
||||
|
||||
@app.get('/profile/delivery/competition/{competition_id}')
|
||||
async def profile_delivery_by_competition(request: Request, competition_id:str, user: Dict = Security(get_current_user)):
|
||||
cursor = delivery_collection.aggregate([
|
||||
@ -3468,7 +3502,6 @@ async def profile_delivery_by_competition(request: Request, competition_id:str,
|
||||
{"user._id": user.get('_id')},
|
||||
{"receiver._id": user.get('_id')}
|
||||
],
|
||||
'competition_id': ObjectId(competition_id),
|
||||
'prod': PROD,
|
||||
}},
|
||||
{"$sort": {"_id": -1}},
|
||||
@ -3481,6 +3514,8 @@ async def profile_delivery_by_competition(request: Request, competition_id:str,
|
||||
])
|
||||
|
||||
data = [doc async for doc in cursor]
|
||||
data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="profile/delivery_old.html",
|
||||
@ -3513,6 +3548,8 @@ async def profile_delivery_by_exhibition(request: Request, exhibition_id:str, us
|
||||
])
|
||||
|
||||
data = [doc async for doc in cursor]
|
||||
data.sort(key=lambda doc: doc["_id"].generation_time, reverse=True)
|
||||
|
||||
return templates.TemplateResponse(
|
||||
request=request,
|
||||
name="profile/delivery_old.html",
|
||||
@ -3522,7 +3559,6 @@ async def profile_delivery_by_exhibition(request: Request, exhibition_id:str, us
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@app.get(
|
||||
"/profile/delivery",
|
||||
response_class=HTMLResponse,
|
||||
|
||||
10
models.py
10
models.py
@ -14,16 +14,20 @@ class NotifyItem(BaseModel):
|
||||
type_item: str = "c"
|
||||
|
||||
|
||||
class DeliverySendRequest(BaseModel):
|
||||
class DeliverySendItem(BaseModel):
|
||||
item: str
|
||||
sender: str
|
||||
too: str
|
||||
item: str
|
||||
|
||||
|
||||
class DeliverySendRequest(BaseModel):
|
||||
items: List[DeliverySendItem]
|
||||
delivery_type: Literal["СДЭК", "Самостоятельно"]
|
||||
track: Optional[str] = Field(None, description="Трэк-номер для СДЭК")
|
||||
|
||||
|
||||
class DeliveryAcceptRequest(BaseModel):
|
||||
item: str
|
||||
items: List[str]
|
||||
|
||||
|
||||
class UserRegister(BaseModel):
|
||||
|
||||
@ -712,6 +712,7 @@
|
||||
" },{\n",
|
||||
" \"$set\":{\n",
|
||||
" \"sender\": user,\n",
|
||||
" \"user\": user,\n",
|
||||
" \"receiver\": display_user(exhibition_owner),\n",
|
||||
" \"exhibition_id\": str(exhibition.get('_id')),\n",
|
||||
" \"address\": exhibition.get('address'),\n",
|
||||
|
||||
1
tasks.py
1
tasks.py
@ -541,6 +541,7 @@ async def exhibition_send_invitations(exhibition_id):
|
||||
},{
|
||||
"$set":{
|
||||
"sender": user,
|
||||
"user": user,
|
||||
"receiver": display_user(exhibition_owner),
|
||||
"exhibition_id": str(exhibition.get('_id')),
|
||||
"address": exhibition.get('address'),
|
||||
|
||||
@ -31,6 +31,7 @@
|
||||
<span class="b_0jst1">{{ {-1:'Формируется',0:'Прием заявок',1:'Идет отбор',2:'Закончился'}[competition.get('status',-1)] }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<span class="b_sdf">До {{ (competition.date_finish.strftime('%d.%m.%Y')) if competition.date_finish else ''}}</span>
|
||||
<header class="c_m876y b_9xtz9">
|
||||
<h3 class="c_hysjg b_8f3uh">{{ competition.title }}</h3>
|
||||
</header>
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{% import "competition/list.html" as competition %}
|
||||
{% import "components/checkbox.html" as checkbox %}
|
||||
|
||||
{% macro section(user, competition) -%}
|
||||
{% macro section(user, competition, competition_id) -%}
|
||||
<form class="c_sbrxk">
|
||||
<div class="c_dtjoh">
|
||||
<h3>Выбор</h3>
|
||||
@ -346,7 +346,7 @@ function bindLinkValidation({
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const competition = JSON.parse({{competition|tojson}})
|
||||
const competitionId = competition._id
|
||||
const competitionId = `{{competition_id}}`
|
||||
|
||||
document.getElementById('title').value = competition.title;
|
||||
document.getElementById('preview_title').textContent = competition.title;
|
||||
@ -595,7 +595,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
console.log('Отправка формы...');
|
||||
try {
|
||||
const res = await fetch(`/api/competition/${competitionId}`, {
|
||||
const res = await fetch(`/api/competition/{{competition_id}}`, {
|
||||
method: 'PUT',
|
||||
body: formData
|
||||
});
|
||||
@ -895,5 +895,4 @@ input[type="number"]:focus::-webkit-inner-spin-button {display: none;}
|
||||
.c_sbrxk{z-index: 1000;}
|
||||
#preview_poster{width: 335px;}
|
||||
</style>
|
||||
|
||||
{%- endmacro %}
|
||||
@ -14,6 +14,6 @@
|
||||
|
||||
{% block content %}
|
||||
{{ header.section(user) }}
|
||||
{{ form.section(user, competition) }}
|
||||
{{ form.section(user, competition, competition_id) }}
|
||||
{{ footer.section() }}
|
||||
{% endblock %}
|
||||
|
||||
@ -7,7 +7,6 @@
|
||||
|
||||
{% block title %}Где картины{% endblock %}
|
||||
|
||||
|
||||
{% macro render_doc(doc) %}
|
||||
<figure class="c_4b1hy b_uu2ub" data-exhibition_id="{{doc.get('exhibition_id')}}" data-delivery-id="{{doc._id}}" data-sender="{{doc.sender._id}}" data-receiver="{{doc.receiver._id}}">
|
||||
<img class="b_1fvkt" src="/b/posts/{{doc.img}}">
|
||||
@ -29,8 +28,10 @@
|
||||
<span class="b_5dr47_2">{{doc.status}}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<a class="b_6spaf" href="/profile/delivery/{{doc.post_id}}#main">Посмотреть историю</a>
|
||||
<h2 class="c_9hvtd b_f5fb2">{{doc.title}}</h2>
|
||||
|
||||
{% if doc.type == 'exhibition' and doc.get('exhibition') %}
|
||||
<p class="b_6spaf">{{doc.get('exhibition').title}}</p>
|
||||
<div class="b_xz6em">
|
||||
@ -53,6 +54,7 @@
|
||||
{% if doc.track %}
|
||||
<p class="b_6spaf">Трэк-номер: {{doc.track}}</p>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.sender %}
|
||||
<div class="b_xz6em">
|
||||
<img src="/a/{{doc.sender._id}}" class="b_ob59y">
|
||||
@ -62,6 +64,7 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if doc.receiver %}
|
||||
<div class="b_xz6em">
|
||||
<img src="/a/{{doc.receiver._id}}" class="b_ob59y">
|
||||
@ -72,17 +75,29 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id%}
|
||||
<button class="b_wy3rt ba" onclick="btnSend(this, '{{doc._id}}', '{{doc.sender._id}}', '{{doc.receiver._id}}')">
|
||||
<span class="b_bvl0d">Отправить</span>
|
||||
</button>
|
||||
|
||||
{# Checkbox instead of per-item button #}
|
||||
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id %}
|
||||
<label class="offer-label delivery-checkbox-label">
|
||||
<input type="checkbox"
|
||||
class="offer-checkbox delivery-checkbox send-checkbox"
|
||||
data-id="{{doc._id}}"
|
||||
data-sender="{{doc.sender._id}}"
|
||||
data-receiver="{{doc.receiver._id}}">
|
||||
<span>Выбрать для отправки</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id%}
|
||||
<button class="b_wy3rt ba" onclick="btnAccept(this, '{{doc._id}}')">
|
||||
<span class="b_bvl0d">Получить</span>
|
||||
</button>
|
||||
|
||||
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id %}
|
||||
<label class="offer-label delivery-checkbox-label">
|
||||
<input type="checkbox"
|
||||
class="offer-checkbox delivery-checkbox accept-checkbox"
|
||||
data-id="{{doc._id}}">
|
||||
<span>Выбрать для получения</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<p class="b_6spaf">
|
||||
{{ (doc._id.generation_time.strftime('%d.%m.%Y')) if doc._id else ''}}
|
||||
<b>{{ '%02d' % ((doc._id.generation_time.hour + 3) % 24) if doc._id else '' }}:{{ '%02d' % doc._id.generation_time.minute if doc._id else '' }}</b>
|
||||
@ -91,18 +106,18 @@
|
||||
</figure>
|
||||
{% endmacro %}
|
||||
|
||||
|
||||
{% block content %}
|
||||
{{ header.section(user) }}
|
||||
<section class="c_dg3k4">
|
||||
<div class="c_gtoe9">
|
||||
{{ profile_card.profile_card(user) }}
|
||||
<div class="c_ru432">
|
||||
{{ profile_nav.profile_nav(user, 'delivery') }}
|
||||
<div class="c_tihtk" id="main">
|
||||
{% set statuses = ['Ожидает отправки', 'Возврат художнику'] %}
|
||||
{% if data | selectattr('status', 'in', statuses) | list | length > 0 %}
|
||||
<article class="c_9fgdh b_ax8vz">
|
||||
<div class="c_gtoe9">
|
||||
{{ profile_card.profile_card(user) }}
|
||||
<div class="c_ru432">
|
||||
{{ profile_nav.profile_nav(user, 'delivery') }}
|
||||
<div class="c_tihtk" id="main">
|
||||
|
||||
{% set statuses = ['Ожидает отправки', 'Возврат художнику'] %}
|
||||
{% if data | selectattr('status', 'in', statuses) | list | length > 0 %}
|
||||
<article class="c_9fgdh b_ax8vz">
|
||||
<div class="c_z3dpx b_73p10">
|
||||
<header class="c_m876y b_9xtz9">
|
||||
<h3 class="b_8f3uh">Настройки отправки</h3>
|
||||
@ -110,28 +125,300 @@
|
||||
<p class="b_tfnz2">Можно менять перед каждой отправкой картины</p>
|
||||
<div class="b_73p10">
|
||||
<div class="c_aihm6 floating-group">
|
||||
<select class="c_xhd5k" type="text" id="delivery_type" name="delivery_type" required>
|
||||
<select class="c_xhd5k" id="delivery_type" name="delivery_type" required>
|
||||
<option>СДЭК</option>
|
||||
<option>Самостоятельно</option>
|
||||
</select>
|
||||
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
|
||||
</div>
|
||||
<div class="c_aihm6 floating-group">
|
||||
<div class="c_aihm6 floating-group" id="track-group">
|
||||
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
|
||||
data-toast="Поле Трэк-номер должно быть заполнено">
|
||||
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endif %}
|
||||
{% for doc in data if doc.status in statuses %}
|
||||
{{ render_doc(doc) }}
|
||||
{% endfor %}
|
||||
{% for doc in data if doc.status not in statuses %}
|
||||
{{ render_doc(doc) }}
|
||||
{% endfor %}
|
||||
</article>
|
||||
{% endif %}
|
||||
|
||||
{% for doc in data %}
|
||||
{{ render_doc(doc) }}
|
||||
{% endfor %}
|
||||
|
||||
{# Sticky bottom action bar #}
|
||||
<div class="st" style="z-index: 2000;min-height: 101px;padding: 25px;gap: 5px;">
|
||||
<button class="b_wy3rt ba" id="btnSendSelected" style="display: none;" onclick="btnSendSelected()">
|
||||
<span class="b_bvl0d">Отправить выбранные</span>
|
||||
</button>
|
||||
<button class="b_wy3rt ba" id="btnAcceptSelected" style="display: none;" onclick="btnAcceptSelected()">
|
||||
<span class="b_bvl0d">Получить выбранные</span>
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
// ─────────────────────────────────────────────
|
||||
// Helpers: collect selected IDs
|
||||
// ─────────────────────────────────────────────
|
||||
function getSelectedSendIds() {
|
||||
return Array.from(document.querySelectorAll('.send-checkbox:checked')).map(cb => ({
|
||||
id: cb.dataset.id,
|
||||
sender: cb.dataset.sender,
|
||||
receiver: cb.dataset.receiver
|
||||
}));
|
||||
}
|
||||
|
||||
function getSelectedAcceptIds() {
|
||||
return Array.from(document.querySelectorAll('.accept-checkbox:checked')).map(cb => cb.dataset.id);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Sticky buttons visibility
|
||||
// ─────────────────────────────────────────────
|
||||
function updateStickyButtons() {
|
||||
const sendCount = document.querySelectorAll('.send-checkbox:checked').length;
|
||||
const acceptCount = document.querySelectorAll('.accept-checkbox:checked').length;
|
||||
|
||||
const sendBtn = document.getElementById('btnSendSelected');
|
||||
const acceptBtn = document.getElementById('btnAcceptSelected');
|
||||
|
||||
if (sendBtn) sendBtn.style.display = sendCount > 0 ? 'inline-flex' : 'none';
|
||||
if (acceptBtn) acceptBtn.style.display = acceptCount > 0 ? 'inline-flex' : 'none';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Show / hide track field depending on delivery_type
|
||||
// ─────────────────────────────────────────────
|
||||
function toggleTrackField() {
|
||||
const deliveryType = document.getElementById('delivery_type');
|
||||
const trackGroup = document.getElementById('track-group');
|
||||
const trackInput = document.getElementById('track');
|
||||
|
||||
if (!deliveryType || !trackGroup || !trackInput) return;
|
||||
|
||||
if (deliveryType.value === 'Самостоятельно') {
|
||||
trackGroup.style.display = 'none';
|
||||
trackInput.removeAttribute('required');
|
||||
trackInput.value = '';
|
||||
} else {
|
||||
trackGroup.style.display = '';
|
||||
trackInput.setAttribute('required', '');
|
||||
}
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Validation helper (used by Send)
|
||||
// ─────────────────────────────────────────────
|
||||
function validateRequiredFields() {
|
||||
const requiredInputs = document.querySelectorAll('input[required]');
|
||||
const requiredTextareas = document.querySelectorAll('textarea[required]');
|
||||
const requiredFields = [...requiredInputs, ...requiredTextareas];
|
||||
|
||||
let firstErrorField = null;
|
||||
let hasError = false;
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
const group = field.closest('.floating-group');
|
||||
if (!group) return;
|
||||
|
||||
let isEmpty = false;
|
||||
if (field.type === 'file') {
|
||||
isEmpty = !field.files || field.files.length === 0;
|
||||
} else {
|
||||
isEmpty = !field.value || !field.value.trim();
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
group.classList.add('error');
|
||||
hasError = true;
|
||||
showToast({ message: field.dataset.toast || 'Заполните обязательное поле', duration: 2500 });
|
||||
if (!firstErrorField) firstErrorField = field;
|
||||
} else {
|
||||
group.classList.remove('error');
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError && firstErrorField) {
|
||||
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
setTimeout(() => firstErrorField.focus(), 600);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Bulk Accept
|
||||
// ─────────────────────────────────────────────
|
||||
async function btnAcceptSelected() {
|
||||
const ids = getSelectedAcceptIds();
|
||||
if (!ids.length) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/delivery/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: ids })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMessage = 'Ошибка сохранения';
|
||||
try {
|
||||
const errorData = await res.json();
|
||||
errorMessage = errorData.detail || errorMessage;
|
||||
} catch (e) {
|
||||
try {
|
||||
const errorText = await res.text();
|
||||
errorMessage = errorText || errorMessage;
|
||||
} catch (e) {}
|
||||
}
|
||||
showToast({ message: errorMessage, duration: 2500 });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Update UI for successfully processed items
|
||||
(data.processed || []).forEach(({ item }) => {
|
||||
const checkbox = document.querySelector(`.accept-checkbox[data-id="${item}"]`);
|
||||
if (!checkbox) return;
|
||||
|
||||
const figcaption = checkbox.closest('figcaption');
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span');
|
||||
|
||||
statusDiv.className = 'b_thlt1';
|
||||
statusSpan.className = 'b_5dr47';
|
||||
statusSpan.textContent = 'Получено';
|
||||
checkbox.closest('label').style.display = 'none';
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
if (data.errors?.length) {
|
||||
showToast({ message: `Ошибки: ${data.errors.length}`, duration: 3000 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||
}
|
||||
|
||||
updateStickyButtons();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Bulk Send
|
||||
// ─────────────────────────────────────────────
|
||||
async function btnSendSelected() {
|
||||
const items = getSelectedSendIds();
|
||||
if (!items.length) return;
|
||||
|
||||
// Validate required fields first
|
||||
if (!validateRequiredFields()) return;
|
||||
|
||||
const delivery_type = document.getElementById('delivery_type')?.value || '';
|
||||
const trackInput = document.getElementById('track');
|
||||
let track = '';
|
||||
if (delivery_type === 'СДЭК' && trackInput) {
|
||||
track = trackInput.value.trim();
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/delivery/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
items: items.map(i => ({
|
||||
item: i.id,
|
||||
sender: i.sender,
|
||||
too: i.receiver
|
||||
})),
|
||||
delivery_type,
|
||||
track: track || null
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMessage = 'Ошибка сохранения';
|
||||
try {
|
||||
const errorData = await res.json();
|
||||
errorMessage = errorData.detail || errorMessage;
|
||||
} catch (e) {
|
||||
try {
|
||||
const errorText = await res.text();
|
||||
errorMessage = errorText || errorMessage;
|
||||
} catch (e) {}
|
||||
}
|
||||
showToast({ message: errorMessage, duration: 2500 });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Update UI for successfully processed items
|
||||
(data.processed || []).forEach(({ item }) => {
|
||||
const checkbox = document.querySelector(`.send-checkbox[data-id="${item}"]`);
|
||||
if (!checkbox) return;
|
||||
|
||||
const figcaption = checkbox.closest('figcaption');
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span');
|
||||
|
||||
statusDiv.className = 'b_thlt1_2';
|
||||
statusSpan.className = 'b_5dr47_2';
|
||||
statusSpan.textContent = 'Ожидает получения';
|
||||
checkbox.closest('label').style.display = 'none';
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
if (data.errors?.length) {
|
||||
showToast({ message: `Ошибки: ${data.errors.length}`, duration: 3000 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||
}
|
||||
|
||||
updateStickyButtons();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Event listeners
|
||||
// ─────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initial state of track field
|
||||
toggleTrackField();
|
||||
|
||||
// Change of delivery type
|
||||
document.getElementById('delivery_type')?.addEventListener('change', toggleTrackField);
|
||||
|
||||
// Checkbox changes → update sticky buttons
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.classList.contains('delivery-checkbox')) {
|
||||
updateStickyButtons();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<script defer>
|
||||
function toggleTrackField() {
|
||||
const deliveryType = document.getElementById('delivery_type');
|
||||
const trackGroup = document.getElementById('track-group');
|
||||
const trackInput = document.getElementById('track');
|
||||
|
||||
if (!deliveryType || !trackGroup || !trackInput) return;
|
||||
|
||||
if (deliveryType.value === 'Самостоятельно') {
|
||||
trackGroup.style.display = 'none';
|
||||
trackInput.removeAttribute('required');
|
||||
trackInput.value = ''; // clear value when hidden
|
||||
} else {
|
||||
trackGroup.style.display = '';
|
||||
trackInput.setAttribute('required', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Run on page load
|
||||
document.addEventListener('DOMContentLoaded', toggleTrackField);
|
||||
|
||||
// Run every time the select changes
|
||||
document.getElementById('delivery_type')?.addEventListener('change', toggleTrackField);
|
||||
const query = window.location.href.split('?')[1] || '';
|
||||
let exhibition_id = null;
|
||||
if (query) {
|
||||
@ -175,132 +462,7 @@
|
||||
deliveryType.addEventListener('change', toggleTrack);
|
||||
toggleTrack();
|
||||
</script>
|
||||
<script>
|
||||
async function btnAccept(btn, itemId){
|
||||
const figcaption = btn.parentElement;
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span')
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/delivery/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
item: itemId,
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMessage = 'Ошибка сохранения';
|
||||
try {
|
||||
const errorData = await res.json();
|
||||
errorMessage = errorData.detail || errorMessage;
|
||||
} catch (e) {
|
||||
try {
|
||||
const errorText = await res.text();
|
||||
errorMessage = errorText || errorMessage;
|
||||
} catch (e) {
|
||||
}
|
||||
}
|
||||
showToast({ message: errorMessage, duration: 2500 });
|
||||
return;
|
||||
}
|
||||
statusDiv.className = 'b_thlt1';
|
||||
statusSpan.className = 'b_5dr47';
|
||||
statusSpan.textContent = 'Получено';
|
||||
btn.style.display = 'none'
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||
}
|
||||
};
|
||||
async function btnSend(btn, itemId, sender, too){
|
||||
const figcaption = btn.parentElement;
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span')
|
||||
|
||||
|
||||
const delivery_type = document.getElementById('delivery_type').value;
|
||||
const trackValue = document.getElementById('track');
|
||||
|
||||
let track = ''
|
||||
if (delivery_type === 'СДЭК' && trackValue) {
|
||||
track = trackValue.value
|
||||
}
|
||||
const requiredInputs = document.querySelectorAll('input[required]');
|
||||
const requiredTextareas = document.querySelectorAll('textarea[required]');
|
||||
const requiredFields = [...requiredInputs, ...requiredTextareas];
|
||||
|
||||
let firstErrorField = null;
|
||||
let hasError = false;
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
const group = field.closest('.floating-group');
|
||||
if (!group) return; // skip if no group wrapper
|
||||
|
||||
let isEmpty = false;
|
||||
if (field.type === 'file') {
|
||||
isEmpty = !field.files || field.files.length === 0;
|
||||
} else {
|
||||
isEmpty = !field.value || !field.value.trim();
|
||||
}
|
||||
|
||||
if (isEmpty) {
|
||||
group.classList.add('error');
|
||||
hasError = true;
|
||||
showToast({message: field.dataset.toast, duration:2500});
|
||||
if (!firstErrorField) firstErrorField = field;
|
||||
} else {
|
||||
group.classList.remove('error');
|
||||
}
|
||||
});
|
||||
|
||||
if (hasError && firstErrorField) {
|
||||
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
setTimeout(() => firstErrorField.focus(), 600);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/delivery/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sender,
|
||||
too,
|
||||
item: itemId,
|
||||
delivery_type,
|
||||
track: track
|
||||
})
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMessage = 'Ошибка сохранения';
|
||||
try {
|
||||
const errorData = await res.json();
|
||||
errorMessage = errorData.detail || errorMessage;
|
||||
} catch (e) {
|
||||
// If response isn't JSON, try to get text
|
||||
try {
|
||||
const errorText = await res.text();
|
||||
errorMessage = errorText || errorMessage;
|
||||
} catch (e) {
|
||||
// Fallback to generic error
|
||||
}
|
||||
}
|
||||
showToast({ message: errorMessage, duration: 2500 });
|
||||
return;
|
||||
}
|
||||
statusDiv.className = 'b_thlt1_2';
|
||||
statusSpan.className = 'b_5dr47_2';
|
||||
statusSpan.textContent = 'Ожидает получения';
|
||||
btn.style.display = 'none'
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -71,15 +71,25 @@
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id%}
|
||||
<button class="b_wy3rt ba" onclick="btnSend(this, '{{doc._id}}', '{{doc.sender._id}}', '{{doc.receiver._id}}')">
|
||||
<span class="b_bvl0d">Отправить</span>
|
||||
</button>
|
||||
{# Checkbox instead of per-item button #}
|
||||
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id %}
|
||||
<label class="offer-label delivery-checkbox-label">
|
||||
<input type="checkbox"
|
||||
class="offer-checkbox delivery-checkbox send-checkbox"
|
||||
data-id="{{doc._id}}"
|
||||
data-sender="{{doc.sender._id}}"
|
||||
data-receiver="{{doc.receiver._id}}">
|
||||
<span>Выбрать для отправки</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id%}
|
||||
<button class="b_wy3rt ba" onclick="btnAccept(this, '{{doc._id}}')">
|
||||
<span class="b_bvl0d">Получить</span>
|
||||
</button>
|
||||
|
||||
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id %}
|
||||
<label class="offer-label delivery-checkbox-label">
|
||||
<input type="checkbox"
|
||||
class="offer-checkbox delivery-checkbox accept-checkbox"
|
||||
data-id="{{doc._id}}">
|
||||
<span>Выбрать для получения</span>
|
||||
</label>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<p class="b_6spaf">
|
||||
@ -189,7 +199,7 @@
|
||||
</select>
|
||||
<label for="delivery_type" class="c_z3ybu floating-label">Способ доставки</label>
|
||||
</div>
|
||||
<div class="c_aihm6 floating-group">
|
||||
<div class="c_aihm6 floating-group" id="track-group">
|
||||
<input placeholder="..." type="text" id="track" name="track" class="c_xhd5k" required
|
||||
data-toast="Поле Трэк-номер должно быть заполнено">
|
||||
<label for="track" class="c_z3ybu floating-label">Трэк-номер</label>
|
||||
@ -205,7 +215,40 @@
|
||||
{% for doc in data if doc.status not in statuses %}
|
||||
{{ render_doc(doc) }}
|
||||
{% endfor %}
|
||||
<script defer>
|
||||
{# Sticky bottom action bar #}
|
||||
<div class="st" style="z-index: 2000;min-height: 101px;padding: 25px;gap: 5px;">
|
||||
<button class="b_wy3rt ba" id="btnSendSelected" style="display: none;" onclick="btnSendSelected()">
|
||||
<span class="b_bvl0d">Отправить выбранные</span>
|
||||
</button>
|
||||
<button class="b_wy3rt ba" id="btnAcceptSelected" style="display: none;" onclick="btnAcceptSelected()">
|
||||
<span class="b_bvl0d">Получить выбранные</span>
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
function toggleTrackField() {
|
||||
const deliveryType = document.getElementById('delivery_type');
|
||||
const trackGroup = document.getElementById('track-group');
|
||||
const trackInput = document.getElementById('track');
|
||||
|
||||
if (!deliveryType || !trackGroup || !trackInput) return;
|
||||
|
||||
if (deliveryType.value === 'Самостоятельно') {
|
||||
trackGroup.style.display = 'none';
|
||||
trackInput.removeAttribute('required');
|
||||
trackInput.value = ''; // clear value when hidden
|
||||
} else {
|
||||
trackGroup.style.display = '';
|
||||
trackInput.setAttribute('required', '');
|
||||
}
|
||||
}
|
||||
|
||||
// Run on page load
|
||||
document.addEventListener('DOMContentLoaded', toggleTrackField);
|
||||
|
||||
// Run every time the select changes
|
||||
document.getElementById('delivery_type')?.addEventListener('change', toggleTrackField);
|
||||
</script>
|
||||
<script>
|
||||
const query = window.location.href.split('?')[1] || '';
|
||||
let exhibition_id = null;
|
||||
if (query) {
|
||||
@ -250,57 +293,59 @@
|
||||
toggleTrack();
|
||||
</script>
|
||||
<script>
|
||||
async function btnAccept(btn, itemId){
|
||||
const figcaption = btn.parentElement;
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span')
|
||||
// ─────────────────────────────────────────────
|
||||
// Helpers: collect selected IDs
|
||||
// ─────────────────────────────────────────────
|
||||
function getSelectedSendIds() {
|
||||
return Array.from(document.querySelectorAll('.send-checkbox:checked')).map(cb => ({
|
||||
id: cb.dataset.id,
|
||||
sender: cb.dataset.sender,
|
||||
receiver: cb.dataset.receiver
|
||||
}));
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/delivery/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
item: itemId,
|
||||
})
|
||||
});
|
||||
function getSelectedAcceptIds() {
|
||||
return Array.from(document.querySelectorAll('.accept-checkbox:checked')).map(cb => cb.dataset.id);
|
||||
}
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMessage = 'Ошибка сохранения';
|
||||
try {
|
||||
const errorData = await res.json();
|
||||
errorMessage = errorData.detail || errorMessage;
|
||||
} catch (e) {
|
||||
try {
|
||||
const errorText = await res.text();
|
||||
errorMessage = errorText || errorMessage;
|
||||
} catch (e) {
|
||||
// ─────────────────────────────────────────────
|
||||
// Sticky buttons visibility
|
||||
// ─────────────────────────────────────────────
|
||||
function updateStickyButtons() {
|
||||
const sendCount = document.querySelectorAll('.send-checkbox:checked').length;
|
||||
const acceptCount = document.querySelectorAll('.accept-checkbox:checked').length;
|
||||
|
||||
const sendBtn = document.getElementById('btnSendSelected');
|
||||
const acceptBtn = document.getElementById('btnAcceptSelected');
|
||||
|
||||
if (sendBtn) sendBtn.style.display = sendCount > 0 ? 'inline-flex' : 'none';
|
||||
if (acceptBtn) acceptBtn.style.display = acceptCount > 0 ? 'inline-flex' : 'none';
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Show / hide track field depending on delivery_type
|
||||
// ─────────────────────────────────────────────
|
||||
function toggleTrackField() {
|
||||
const deliveryType = document.getElementById('delivery_type');
|
||||
const trackGroup = document.getElementById('track-group');
|
||||
const trackInput = document.getElementById('track');
|
||||
|
||||
if (!deliveryType || !trackGroup || !trackInput) return;
|
||||
|
||||
if (deliveryType.value === 'Самостоятельно') {
|
||||
trackGroup.style.display = 'none';
|
||||
trackInput.removeAttribute('required');
|
||||
trackInput.value = '';
|
||||
} else {
|
||||
trackGroup.style.display = '';
|
||||
trackInput.setAttribute('required', '');
|
||||
}
|
||||
}
|
||||
showToast({ message: errorMessage, duration: 2500 });
|
||||
return;
|
||||
}
|
||||
statusDiv.className = 'b_thlt1';
|
||||
statusSpan.className = 'b_5dr47';
|
||||
statusSpan.textContent = 'Получено';
|
||||
btn.style.display = 'none'
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||
}
|
||||
};
|
||||
async function btnSend(btn, itemId, sender, too){
|
||||
const figcaption = btn.parentElement;
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span')
|
||||
|
||||
|
||||
const delivery_type = document.getElementById('delivery_type').value;
|
||||
const trackValue = document.getElementById('track');
|
||||
|
||||
let track = ''
|
||||
if (delivery_type === 'СДЭК' && trackValue) {
|
||||
track = trackValue.value
|
||||
}
|
||||
// ─────────────────────────────────────────────
|
||||
// Validation helper (used by Send)
|
||||
// ─────────────────────────────────────────────
|
||||
function validateRequiredFields() {
|
||||
const requiredInputs = document.querySelectorAll('input[required]');
|
||||
const requiredTextareas = document.querySelectorAll('textarea[required]');
|
||||
const requiredFields = [...requiredInputs, ...requiredTextareas];
|
||||
@ -310,7 +355,7 @@ async function btnSend(btn, itemId, sender, too){
|
||||
|
||||
requiredFields.forEach(field => {
|
||||
const group = field.closest('.floating-group');
|
||||
if (!group) return; // skip if no group wrapper
|
||||
if (!group) return;
|
||||
|
||||
let isEmpty = false;
|
||||
if (field.type === 'file') {
|
||||
@ -322,7 +367,7 @@ async function btnSend(btn, itemId, sender, too){
|
||||
if (isEmpty) {
|
||||
group.classList.add('error');
|
||||
hasError = true;
|
||||
showToast({message: field.dataset.toast, duration:2500});
|
||||
showToast({ message: field.dataset.toast || 'Заполните обязательное поле', duration: 2500 });
|
||||
if (!firstErrorField) firstErrorField = field;
|
||||
} else {
|
||||
group.classList.remove('error');
|
||||
@ -332,19 +377,98 @@ async function btnSend(btn, itemId, sender, too){
|
||||
if (hasError && firstErrorField) {
|
||||
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
setTimeout(() => firstErrorField.focus(), 600);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Bulk Accept
|
||||
// ─────────────────────────────────────────────
|
||||
async function btnAcceptSelected() {
|
||||
const ids = getSelectedAcceptIds();
|
||||
if (!ids.length) return;
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/delivery/accept', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ items: ids })
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
let errorMessage = 'Ошибка сохранения';
|
||||
try {
|
||||
const errorData = await res.json();
|
||||
errorMessage = errorData.detail || errorMessage;
|
||||
} catch (e) {
|
||||
try {
|
||||
const errorText = await res.text();
|
||||
errorMessage = errorText || errorMessage;
|
||||
} catch (e) {}
|
||||
}
|
||||
showToast({ message: errorMessage, duration: 2500 });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Update UI for successfully processed items
|
||||
(data.processed || []).forEach(({ item }) => {
|
||||
const checkbox = document.querySelector(`.accept-checkbox[data-id="${item}"]`);
|
||||
if (!checkbox) return;
|
||||
|
||||
const figcaption = checkbox.closest('figcaption');
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span');
|
||||
|
||||
statusDiv.className = 'b_thlt1';
|
||||
statusSpan.className = 'b_5dr47';
|
||||
statusSpan.textContent = 'Получено';
|
||||
checkbox.closest('label').style.display = 'none';
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
if (data.errors?.length) {
|
||||
showToast({ message: `Ошибки: ${data.errors.length}`, duration: 3000 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||
}
|
||||
|
||||
updateStickyButtons();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Bulk Send
|
||||
// ─────────────────────────────────────────────
|
||||
async function btnSendSelected() {
|
||||
const items = getSelectedSendIds();
|
||||
if (!items.length) return;
|
||||
|
||||
// Validate required fields first
|
||||
if (!validateRequiredFields()) return;
|
||||
|
||||
const delivery_type = document.getElementById('delivery_type')?.value || '';
|
||||
const trackInput = document.getElementById('track');
|
||||
let track = '';
|
||||
if (delivery_type === 'СДЭК' && trackInput) {
|
||||
track = trackInput.value.trim();
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch('/api/delivery/send', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
sender,
|
||||
too,
|
||||
item: itemId,
|
||||
items: items.map(i => ({
|
||||
item: i.id,
|
||||
sender: i.sender,
|
||||
too: i.receiver
|
||||
})),
|
||||
delivery_type,
|
||||
track: track
|
||||
track: track || null
|
||||
})
|
||||
});
|
||||
|
||||
@ -354,26 +478,61 @@ async function btnSend(btn, itemId, sender, too){
|
||||
const errorData = await res.json();
|
||||
errorMessage = errorData.detail || errorMessage;
|
||||
} catch (e) {
|
||||
// If response isn't JSON, try to get text
|
||||
try {
|
||||
const errorText = await res.text();
|
||||
errorMessage = errorText || errorMessage;
|
||||
} catch (e) {
|
||||
// Fallback to generic error
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
showToast({ message: errorMessage, duration: 2500 });
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
|
||||
// Update UI for successfully processed items
|
||||
(data.processed || []).forEach(({ item }) => {
|
||||
const checkbox = document.querySelector(`.send-checkbox[data-id="${item}"]`);
|
||||
if (!checkbox) return;
|
||||
|
||||
const figcaption = checkbox.closest('figcaption');
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span');
|
||||
|
||||
statusDiv.className = 'b_thlt1_2';
|
||||
statusSpan.className = 'b_5dr47_2';
|
||||
statusSpan.textContent = 'Ожидает получения';
|
||||
btn.style.display = 'none'
|
||||
checkbox.closest('label').style.display = 'none';
|
||||
checkbox.checked = false;
|
||||
});
|
||||
|
||||
if (data.errors?.length) {
|
||||
showToast({ message: `Ошибки: ${data.errors.length}`, duration: 3000 });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Network error:', error);
|
||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||
}
|
||||
};
|
||||
|
||||
updateStickyButtons();
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────
|
||||
// Event listeners
|
||||
// ─────────────────────────────────────────────
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Initial state of track field
|
||||
toggleTrackField();
|
||||
|
||||
// Change of delivery type
|
||||
document.getElementById('delivery_type')?.addEventListener('change', toggleTrackField);
|
||||
|
||||
// Checkbox changes → update sticky buttons
|
||||
document.addEventListener('change', (e) => {
|
||||
if (e.target.classList.contains('delivery-checkbox')) {
|
||||
updateStickyButtons();
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@ -16,7 +16,7 @@
|
||||
<div class="c_ru432">
|
||||
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
||||
<div class="c_3ihvi" id="main">
|
||||
<a class="c_ctfod b_289i1" href="/profile/partner/exhibition/{{exhibition._id}}#main">
|
||||
<a class="c_ctfod b_289i1 ba" href="/profile/partner/exhibition/{{exhibition._id}}#main">
|
||||
<span class="c_d2l0b b_al8e7">Отобранные картины</span>
|
||||
</a>
|
||||
</div>
|
||||
@ -49,7 +49,7 @@
|
||||
<input type="text" id="filter-author" name="filter-author" class="c_xhd5k">
|
||||
<label for="filter-author" class="c_z3ybu floating-label">Автор (ФИО)</label>
|
||||
</div>
|
||||
<button id="apply-filters" class="btn b">Применить фильтры</button>
|
||||
<button id="apply-filters" class="btn b ba">Применить фильтры</button>
|
||||
</div>
|
||||
<article class="c_9fgdh b_ax8vz">
|
||||
<!-- Poster -->
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
{{ profile_nav.profile_nav(user, 'exhibitions') }}
|
||||
<div class="c_3ihvi" id="main">
|
||||
{% if now < exhibition.date_finish %}
|
||||
<a class="b_289i1" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
||||
<a class="b_289i1 ba" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
||||
<span class="c_d2l0b b_al8e7">Каталог картин</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
@ -29,7 +29,7 @@
|
||||
%}
|
||||
{% set uniq_count = ids | unique | list | length %}
|
||||
{% if uniq_count > 0 %}
|
||||
<button class="b_289i1" onclick="sendInvitations('{{exhibition._id}}')" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
||||
<button class="b_289i1 ba" onclick="sendInvitations('{{exhibition._id}}')" href="/profile/partner/exhibition/{{exhibition._id}}/catalog#main">
|
||||
<span class="c_d2l0b b_al8e7">Отправить приглашени{% if uniq_count > 1 %}я{% else %}е{% endif %}</span>
|
||||
</button>
|
||||
{% endif %}
|
||||
@ -98,9 +98,10 @@
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if post.status == 'Ожидает получения' %}
|
||||
<button class="b_289i1" onclick="btnAccept(this, '{{post.d_id}}')">
|
||||
{# <button class="b_289i1" onclick="btnAccept(this, '{{post.d_id}}')"><span class="b_al8e7">Получить</span></button> #}
|
||||
<a class="b_289i1" href="/profile/delivery/exhibition/{{exhibition._id}}#main">
|
||||
<span class="b_al8e7">Получить</span>
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if post.status in ['Возврат художнику']%}
|
||||
<a class="b_289i1" href="/profile/delivery/exhibition/{{exhibition._id}}#main">
|
||||
@ -114,8 +115,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{#
|
||||
<script>
|
||||
async function btnAccept(btn, itemId){
|
||||
async function btnAccept(btn, itemId){
|
||||
const figcaption = btn.parentElement;
|
||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||
const statusSpan = statusDiv.querySelector('span')
|
||||
@ -153,6 +155,9 @@ async function btnAccept(btn, itemId){
|
||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||
}
|
||||
};
|
||||
</script>
|
||||
#}
|
||||
<script>
|
||||
async function sendInvitations(exhibition_id) {
|
||||
const data = new FormData();
|
||||
data.set('exhibition_id', exhibition_id);
|
||||
|
||||
@ -1,5 +0,0 @@
|
||||
FROM registry.spiritofweb.ru/alpine:3.14
|
||||
|
||||
RUN apk --no-cache add openssh-client bash autossh sshpass
|
||||
|
||||
CMD []
|
||||
Loading…
Reference in New Issue
Block a user