This commit is contained in:
vladislav 2026-08-27 08:39:48 -04:00
parent 092989646b
commit 28cf129e11
Signed by: Vladislav
GPG Key ID: D97C9E5E4860EE45
5 changed files with 531 additions and 279 deletions

View File

@ -17,56 +17,69 @@ async def send_delivery(
current_user: dict = Depends(get_current_user)
):
"""
Создаёт новый документ отправки с обновлёнными статусом и полями.
Массовая отправка: создаёт новый документ для каждого выбранного item.
"""
# Проверяем, что текущий пользователь — отправитель
if str(current_user["_id"]) != payload.sender:
raise HTTPException(status_code=403, detail="Не авторизован как отправитель")
results = []
errors = []
# Валидация delivery_id
try:
delivery_id = ObjectId(payload.item)
except Exception:
raise HTTPException(status_code=400, detail="Неверный delivery ID")
for entry in payload.items:
# Проверяем, что текущий пользователь — отправитель
if str(current_user["_id"]) != entry.sender:
errors.append({"item": entry.item, "error": "Не авторизован как отправитель"})
continue
update_data = {
"status": "Ожидает получения",
"delivery_type": payload.delivery_type,
"sent_at": datetime.utcnow(),
"updated_at": datetime.utcnow()
# Валидация delivery_id
try:
delivery_id = ObjectId(entry.item)
except Exception:
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(),
}
# Добавляем трек только для СДЭК
if payload.delivery_type == "СДЭК" and payload.track:
update_data["track"] = payload.track
# Ищем существующую запись
existing = await delivery_collection.find_one(
{"_id": delivery_id, "sender._id": entry.sender},
{"_id": 0}
)
if not existing:
errors.append({"item": entry.item, "error": "Доставка не найдена или не для вас"})
continue
# Уведомление получателю
notify = await notify_in_profile(NotifyItem(
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}
result = await delivery_collection.insert_one(new_doc)
if result.inserted_id:
# TODO: send email
results.append({
"item": entry.item,
"delivery_id": str(result.inserted_id)
})
return {
"status": "success" if not errors else "partial",
"processed": results,
"errors": errors
}
# Добавляем трек только для СДЭК (если передан)
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": 0}
)
if not existing:
raise HTTPException(status_code=404, detail="Доставка не найдена или не для вас")
# Собираем новый документ для вставки
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'])} — прислал вам картины.'
))
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
return {"status": "success", "delivery_id": str(iserted_id)}
@router.post("/accept")
async def accept_delivery(
@ -74,37 +87,59 @@ 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 = []
# Берём исходный документ, чтобы вставить новый
existing = await delivery_collection.find_one(
{"_id": delivery_id, "receiver._id": current_user["_id"]},
{"_id": 0}
)
for item_id in payload.items:
try:
delivery_id = ObjectId(item_id)
except Exception:
errors.append({"item": item_id, "error": "Неверный delivery ID"})
continue
if not existing:
raise HTTPException(status_code=404, detail="Доставка не найдена или не для вас")
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'])}.'
))
new_doc = {
**existing,
"status": "Получено",
"notify": notify.upserted_id,
"received_at": datetime.utcnow(),
"updated_at": datetime.utcnow(),
}
existing = await delivery_collection.find_one(
{"_id": delivery_id, "receiver._id": current_user["_id"]},
{"_id": 0}
)
if not existing:
errors.append({"item": item_id, "error": "Доставка не найдена или не для вас"})
continue
result = await delivery_collection.insert_one(new_doc)
# Уведомление отправителю
notify = await notify_in_profile(NotifyItem(
user_id=str(existing["sender"]["_id"]),
type_item="delivery",
title="Доставка картин",
link="/profile/delivery",
text=f"Картина «{existing['title']}» получена — {str(existing['receiver']['username'])}."
))
await notify_collection.update_one({'_id': existing['notify']}, {'$set': {'read': False}})
return {"status": "success", "delivery_id": str(result.inserted_id)}
new_doc = {
**existing,
"status": "Получено",
"notify": notify.upserted_id,
"received_at": datetime.utcnow(),
"updated_at": datetime.utcnow(),
}
result = await delivery_collection.insert_one(new_doc)
# Помечаем предыдущее уведомление как непрочитанное (если нужно)
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
}

View File

@ -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):

View File

@ -154,9 +154,10 @@
<span class="b_bvl0d">Получить выбранные</span>
</button>
</div>
<script>
// Collect selected IDs
// ─────────────────────────────────────────────
// Helpers: collect selected IDs
// ─────────────────────────────────────────────
function getSelectedSendIds() {
return Array.from(document.querySelectorAll('.send-checkbox:checked')).map(cb => ({
id: cb.dataset.id,
@ -169,102 +170,66 @@
return Array.from(document.querySelectorAll('.accept-checkbox:checked')).map(cb => cb.dataset.id);
}
// Show / hide sticky buttons depending on selection
// ─────────────────────────────────────────────
// Sticky buttons visibility
// ─────────────────────────────────────────────
function updateStickyButtons() {
const sendCount = document.querySelectorAll('.send-checkbox:checked').length;
const sendCount = document.querySelectorAll('.send-checkbox:checked').length;
const acceptCount = document.querySelectorAll('.accept-checkbox:checked').length;
const sendBtn = document.getElementById('btnSendSelected');
const sendBtn = document.getElementById('btnSendSelected');
const acceptBtn = document.getElementById('btnAcceptSelected');
if (sendBtn) sendBtn.style.display = sendCount > 0 ? 'inline-flex' : 'none';
if (sendBtn) sendBtn.style.display = sendCount > 0 ? 'inline-flex' : 'none';
if (acceptBtn) acceptBtn.style.display = acceptCount > 0 ? 'inline-flex' : 'none';
}
// Listen to checkbox changes
document.addEventListener('change', (e) => {
if (e.target.classList.contains('delivery-checkbox')) {
updateStickyButtons();
// ─────────────────────────────────────────────
// 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', '');
}
});
// Bulk Accept
async function btnAcceptSelected() {
const ids = getSelectedAcceptIds();
if (!ids.length) return;
for (const itemId of ids) {
const checkbox = document.querySelector(`.accept-checkbox[data-id="${itemId}"]`);
const figcaption = checkbox.closest('figcaption');
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 });
continue;
}
statusDiv.className = 'b_thlt1';
statusSpan.className = 'b_5dr47';
statusSpan.textContent = 'Получено';
checkbox.closest('label').style.display = 'none';
} catch (error) {
console.error('Network error:', error);
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
}
}
updateStickyButtons();
}
// Bulk Send
async function btnSendSelected() {
const items = getSelectedSendIds();
if (!items.length) return;
const delivery_type = document.getElementById('delivery_type')?.value || '';
const trackValue = document.getElementById('track');
let track = '';
if (delivery_type === 'СДЭК' && trackValue) {
track = trackValue.value;
}
// Validate required fields
const requiredInputs = document.querySelectorAll('input[required]');
// ─────────────────────────────────────────────
// Validation helper (used by Send)
// ─────────────────────────────────────────────
function validateRequiredFields() {
const requiredInputs = document.querySelectorAll('input[required]');
const requiredTextareas = document.querySelectorAll('textarea[required]');
const requiredFields = [...requiredInputs, ...requiredTextareas];
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 });
showToast({ message: field.dataset.toast || 'Заполните обязательное поле', duration: 2500 });
if (!firstErrorField) firstErrorField = field;
} else {
group.classList.remove('error');
@ -274,54 +239,162 @@
if (hasError && firstErrorField) {
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => firstErrorField.focus(), 600);
return;
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 });
}
for (const { id: itemId, sender, receiver: too } of items) {
const checkbox = document.querySelector(`.send-checkbox[data-id="${itemId}"]`);
const figcaption = checkbox.closest('figcaption');
const statusDiv = figcaption.querySelectorAll('div')[0];
const statusSpan = statusDiv.querySelector('span');
updateStickyButtons();
}
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
})
});
// ─────────────────────────────────────────────
// Bulk Send
// ─────────────────────────────────────────────
async function btnSendSelected() {
const items = getSelectedSendIds();
if (!items.length) return;
if (!res.ok) {
let errorMessage = 'Ошибка сохранения';
// 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 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 });
continue;
const errorText = await res.text();
errorMessage = errorText || errorMessage;
} catch (e) {}
}
showToast({ message: errorMessage, duration: 2500 });
return;
}
statusDiv.className = 'b_thlt1_2';
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';
} catch (error) {
console.error('Network error:', error);
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
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() {

View File

@ -71,16 +71,26 @@
</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>
{% 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>
{% endif %}
{# 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 %}
<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 ''}}
@ -205,6 +215,15 @@
{% for doc in data if doc.status not in statuses %}
{{ 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 defer>
const query = window.location.href.split('?')[1] || '';
let exhibition_id = null;
@ -250,67 +269,69 @@
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) {
}
}
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 });
// ─────────────────────────────────────────────
// 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', '');
}
};
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]');
// ─────────────────────────────────────────────
// Validation helper (used by Send)
// ─────────────────────────────────────────────
function validateRequiredFields() {
const requiredInputs = document.querySelectorAll('input[required]');
const requiredTextareas = document.querySelectorAll('textarea[required]');
const requiredFields = [...requiredInputs, ...requiredTextareas];
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
if (!group) return;
let isEmpty = false;
if (field.type === 'file') {
@ -322,7 +343,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,48 +353,162 @@ async function btnSend(btn, itemId, sender, too){
if (hasError && firstErrorField) {
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
setTimeout(() => firstErrorField.focus(), 600);
return;
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,
delivery_type,
track: track
})
});
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) {
// 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;
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) {}
}
statusDiv.className = 'b_thlt1_2';
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 });
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>

View File

@ -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);