feat
This commit is contained in:
parent
092989646b
commit
28cf129e11
@ -17,55 +17,68 @@ async def send_delivery(
|
|||||||
current_user: dict = Depends(get_current_user)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Создаёт новый документ отправки с обновлёнными статусом и полями.
|
Массовая отправка: создаёт новый документ для каждого выбранного item.
|
||||||
"""
|
"""
|
||||||
|
results = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for entry in payload.items:
|
||||||
# Проверяем, что текущий пользователь — отправитель
|
# Проверяем, что текущий пользователь — отправитель
|
||||||
if str(current_user["_id"]) != payload.sender:
|
if str(current_user["_id"]) != entry.sender:
|
||||||
raise HTTPException(status_code=403, detail="Не авторизован как отправитель")
|
errors.append({"item": entry.item, "error": "Не авторизован как отправитель"})
|
||||||
|
continue
|
||||||
|
|
||||||
# Валидация delivery_id
|
# Валидация delivery_id
|
||||||
try:
|
try:
|
||||||
delivery_id = ObjectId(payload.item)
|
delivery_id = ObjectId(entry.item)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise HTTPException(status_code=400, detail="Неверный delivery ID")
|
errors.append({"item": entry.item, "error": "Неверный delivery ID"})
|
||||||
|
continue
|
||||||
|
|
||||||
update_data = {
|
update_data = {
|
||||||
"status": "Ожидает получения",
|
"status": "Ожидает получения",
|
||||||
"delivery_type": payload.delivery_type,
|
"delivery_type": payload.delivery_type,
|
||||||
"sent_at": datetime.utcnow(),
|
"sent_at": datetime.utcnow(),
|
||||||
"updated_at": datetime.utcnow()
|
"updated_at": datetime.utcnow(),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Добавляем трек только для СДЭК (если передан)
|
# Добавляем трек только для СДЭК
|
||||||
if payload.delivery_type == "СДЭК" and payload.track:
|
if payload.delivery_type == "СДЭК" and payload.track:
|
||||||
update_data["track"] = payload.track
|
update_data["track"] = payload.track
|
||||||
|
|
||||||
# Ищем существующую запись, чтобы создать новую на её основе
|
# Ищем существующую запись
|
||||||
existing = await delivery_collection.find_one(
|
existing = await delivery_collection.find_one(
|
||||||
{"_id": delivery_id, "sender._id": payload.sender},
|
{"_id": delivery_id, "sender._id": entry.sender},
|
||||||
{"_id": 0}
|
{"_id": 0}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail="Доставка не найдена или не для вас")
|
errors.append({"item": entry.item, "error": "Доставка не найдена или не для вас"})
|
||||||
|
continue
|
||||||
|
|
||||||
# Собираем новый документ для вставки
|
# Уведомление получателю
|
||||||
notify = await notify_in_profile(NotifyItem(
|
notify = await notify_in_profile(NotifyItem(
|
||||||
user_id=str(existing['receiver']['_id']),
|
user_id=str(existing["receiver"]["_id"]),
|
||||||
type_item='delivery',
|
type_item="delivery",
|
||||||
title='Доставка картин',
|
title="Доставка картин",
|
||||||
link=f'/profile/delivery',
|
link="/profile/delivery",
|
||||||
text=f'{str(existing['sender']['username'])} — прислал вам картины.'
|
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)
|
result = await delivery_collection.insert_one(new_doc)
|
||||||
if (iserted_id:=result.inserted_id):
|
if result.inserted_id:
|
||||||
pass
|
# TODO: send email
|
||||||
# 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")
|
@router.post("/accept")
|
||||||
@ -74,28 +87,35 @@ async def accept_delivery(
|
|||||||
current_user: dict = Depends(get_current_user)
|
current_user: dict = Depends(get_current_user)
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Создаёт новый документ отправки со статусом 'Получено'.
|
Массовое получение: создаёт новый документ со статусом 'Получено' для каждого item.
|
||||||
"""
|
"""
|
||||||
try:
|
results = []
|
||||||
delivery_id = ObjectId(payload.item)
|
errors = []
|
||||||
except Exception:
|
|
||||||
raise HTTPException(status_code=400, detail="Неверный delivery ID")
|
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(
|
existing = await delivery_collection.find_one(
|
||||||
{"_id": delivery_id, "receiver._id": current_user["_id"]},
|
{"_id": delivery_id, "receiver._id": current_user["_id"]},
|
||||||
{"_id": 0}
|
{"_id": 0}
|
||||||
)
|
)
|
||||||
|
|
||||||
if not existing:
|
if not existing:
|
||||||
raise HTTPException(status_code=404, detail="Доставка не найдена или не для вас")
|
errors.append({"item": item_id, "error": "Доставка не найдена или не для вас"})
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Уведомление отправителю
|
||||||
notify = await notify_in_profile(NotifyItem(
|
notify = await notify_in_profile(NotifyItem(
|
||||||
user_id=str(existing['sender']['_id']),
|
user_id=str(existing["sender"]["_id"]),
|
||||||
type_item='delivery',
|
type_item="delivery",
|
||||||
title='Доставка картин',
|
title="Доставка картин",
|
||||||
link=f'/profile/delivery',
|
link="/profile/delivery",
|
||||||
text=f'Картина «{existing['title']}» получена — {str(existing['receiver']['username'])}.'
|
text=f"Картина «{existing['title']}» получена — {str(existing['receiver']['username'])}."
|
||||||
))
|
))
|
||||||
|
|
||||||
new_doc = {
|
new_doc = {
|
||||||
**existing,
|
**existing,
|
||||||
"status": "Получено",
|
"status": "Получено",
|
||||||
@ -106,5 +126,20 @@ async def accept_delivery(
|
|||||||
|
|
||||||
result = await delivery_collection.insert_one(new_doc)
|
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
|
||||||
|
}
|
||||||
10
models.py
10
models.py
@ -14,16 +14,20 @@ class NotifyItem(BaseModel):
|
|||||||
type_item: str = "c"
|
type_item: str = "c"
|
||||||
|
|
||||||
|
|
||||||
class DeliverySendRequest(BaseModel):
|
class DeliverySendItem(BaseModel):
|
||||||
|
item: str
|
||||||
sender: str
|
sender: str
|
||||||
too: str
|
too: str
|
||||||
item: str
|
|
||||||
|
|
||||||
|
class DeliverySendRequest(BaseModel):
|
||||||
|
items: List[DeliverySendItem]
|
||||||
delivery_type: Literal["СДЭК", "Самостоятельно"]
|
delivery_type: Literal["СДЭК", "Самостоятельно"]
|
||||||
track: Optional[str] = Field(None, description="Трэк-номер для СДЭК")
|
track: Optional[str] = Field(None, description="Трэк-номер для СДЭК")
|
||||||
|
|
||||||
|
|
||||||
class DeliveryAcceptRequest(BaseModel):
|
class DeliveryAcceptRequest(BaseModel):
|
||||||
item: str
|
items: List[str]
|
||||||
|
|
||||||
|
|
||||||
class UserRegister(BaseModel):
|
class UserRegister(BaseModel):
|
||||||
|
|||||||
@ -154,9 +154,10 @@
|
|||||||
<span class="b_bvl0d">Получить выбранные</span>
|
<span class="b_bvl0d">Получить выбранные</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Collect selected IDs
|
// ─────────────────────────────────────────────
|
||||||
|
// Helpers: collect selected IDs
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
function getSelectedSendIds() {
|
function getSelectedSendIds() {
|
||||||
return Array.from(document.querySelectorAll('.send-checkbox:checked')).map(cb => ({
|
return Array.from(document.querySelectorAll('.send-checkbox:checked')).map(cb => ({
|
||||||
id: cb.dataset.id,
|
id: cb.dataset.id,
|
||||||
@ -169,7 +170,9 @@
|
|||||||
return Array.from(document.querySelectorAll('.accept-checkbox:checked')).map(cb => cb.dataset.id);
|
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() {
|
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 acceptCount = document.querySelectorAll('.accept-checkbox:checked').length;
|
||||||
@ -181,29 +184,78 @@
|
|||||||
if (acceptBtn) acceptBtn.style.display = acceptCount > 0 ? 'inline-flex' : 'none';
|
if (acceptBtn) acceptBtn.style.display = acceptCount > 0 ? 'inline-flex' : 'none';
|
||||||
}
|
}
|
||||||
|
|
||||||
// Listen to checkbox changes
|
// ─────────────────────────────────────────────
|
||||||
document.addEventListener('change', (e) => {
|
// Show / hide track field depending on delivery_type
|
||||||
if (e.target.classList.contains('delivery-checkbox')) {
|
// ─────────────────────────────────────────────
|
||||||
updateStickyButtons();
|
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
|
// Bulk Accept
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
async function btnAcceptSelected() {
|
async function btnAcceptSelected() {
|
||||||
const ids = getSelectedAcceptIds();
|
const ids = getSelectedAcceptIds();
|
||||||
if (!ids.length) return;
|
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 {
|
try {
|
||||||
const res = await fetch('/api/delivery/accept', {
|
const res = await fetch('/api/delivery/accept', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ item: itemId })
|
body: JSON.stringify({ items: ids })
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
@ -218,81 +270,67 @@
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
showToast({ message: errorMessage, duration: 2500 });
|
showToast({ message: errorMessage, duration: 2500 });
|
||||||
continue;
|
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';
|
statusDiv.className = 'b_thlt1';
|
||||||
statusSpan.className = 'b_5dr47';
|
statusSpan.className = 'b_5dr47';
|
||||||
statusSpan.textContent = 'Получено';
|
statusSpan.textContent = 'Получено';
|
||||||
checkbox.closest('label').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) {
|
} catch (error) {
|
||||||
console.error('Network error:', error);
|
console.error('Network error:', error);
|
||||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
updateStickyButtons();
|
updateStickyButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
// Bulk Send
|
// Bulk Send
|
||||||
|
// ─────────────────────────────────────────────
|
||||||
async function btnSendSelected() {
|
async function btnSendSelected() {
|
||||||
const items = getSelectedSendIds();
|
const items = getSelectedSendIds();
|
||||||
if (!items.length) return;
|
if (!items.length) return;
|
||||||
|
|
||||||
|
// Validate required fields first
|
||||||
|
if (!validateRequiredFields()) return;
|
||||||
|
|
||||||
const delivery_type = document.getElementById('delivery_type')?.value || '';
|
const delivery_type = document.getElementById('delivery_type')?.value || '';
|
||||||
const trackValue = document.getElementById('track');
|
const trackInput = document.getElementById('track');
|
||||||
let track = '';
|
let track = '';
|
||||||
if (delivery_type === 'СДЭК' && trackValue) {
|
if (delivery_type === 'СДЭК' && trackInput) {
|
||||||
track = trackValue.value;
|
track = trackInput.value.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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');
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch('/api/delivery/send', {
|
const res = await fetch('/api/delivery/send', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
sender,
|
items: items.map(i => ({
|
||||||
too,
|
item: i.id,
|
||||||
item: itemId,
|
sender: i.sender,
|
||||||
|
too: i.receiver
|
||||||
|
})),
|
||||||
delivery_type,
|
delivery_type,
|
||||||
track
|
track: track || null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -308,20 +346,55 @@
|
|||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
showToast({ message: errorMessage, duration: 2500 });
|
showToast({ message: errorMessage, duration: 2500 });
|
||||||
continue;
|
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';
|
statusDiv.className = 'b_thlt1_2';
|
||||||
statusSpan.className = 'b_5dr47_2';
|
statusSpan.className = 'b_5dr47_2';
|
||||||
statusSpan.textContent = 'Ожидает получения';
|
statusSpan.textContent = 'Ожидает получения';
|
||||||
checkbox.closest('label').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) {
|
} catch (error) {
|
||||||
console.error('Network error:', error);
|
console.error('Network error:', error);
|
||||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
}
|
}
|
||||||
}
|
|
||||||
updateStickyButtons();
|
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>
|
||||||
<script defer>
|
<script defer>
|
||||||
function toggleTrackField() {
|
function toggleTrackField() {
|
||||||
|
|||||||
@ -71,15 +71,25 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id%}
|
{# Checkbox instead of per-item button #}
|
||||||
<button class="b_wy3rt ba" onclick="btnSend(this, '{{doc._id}}', '{{doc.sender._id}}', '{{doc.receiver._id}}')">
|
{% if doc.status in ['Ожидает отправки', 'Возврат художнику'] and user._id == doc.sender._id %}
|
||||||
<span class="b_bvl0d">Отправить</span>
|
<label class="offer-label delivery-checkbox-label">
|
||||||
</button>
|
<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 %}
|
{% endif %}
|
||||||
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id%}
|
|
||||||
<button class="b_wy3rt ba" onclick="btnAccept(this, '{{doc._id}}')">
|
{% if doc.status == 'Ожидает получения' and user._id == doc.receiver._id %}
|
||||||
<span class="b_bvl0d">Получить</span>
|
<label class="offer-label delivery-checkbox-label">
|
||||||
</button>
|
<input type="checkbox"
|
||||||
|
class="offer-checkbox delivery-checkbox accept-checkbox"
|
||||||
|
data-id="{{doc._id}}">
|
||||||
|
<span>Выбрать для получения</span>
|
||||||
|
</label>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<p class="b_6spaf">
|
<p class="b_6spaf">
|
||||||
@ -205,6 +215,15 @@
|
|||||||
{% for doc in data if doc.status not in statuses %}
|
{% for doc in data if doc.status not in statuses %}
|
||||||
{{ render_doc(doc) }}
|
{{ render_doc(doc) }}
|
||||||
{% endfor %}
|
{% 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>
|
<script defer>
|
||||||
const query = window.location.href.split('?')[1] || '';
|
const query = window.location.href.split('?')[1] || '';
|
||||||
let exhibition_id = null;
|
let exhibition_id = null;
|
||||||
@ -250,57 +269,59 @@
|
|||||||
toggleTrack();
|
toggleTrack();
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
async function btnAccept(btn, itemId){
|
// ─────────────────────────────────────────────
|
||||||
const figcaption = btn.parentElement;
|
// Helpers: collect selected IDs
|
||||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
// ─────────────────────────────────────────────
|
||||||
const statusSpan = statusDiv.querySelector('span')
|
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 {
|
function getSelectedAcceptIds() {
|
||||||
const res = await fetch('/api/delivery/accept', {
|
return Array.from(document.querySelectorAll('.accept-checkbox:checked')).map(cb => cb.dataset.id);
|
||||||
method: 'POST',
|
}
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
item: itemId,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
// ─────────────────────────────────────────────
|
||||||
let errorMessage = 'Ошибка сохранения';
|
// Sticky buttons visibility
|
||||||
try {
|
// ─────────────────────────────────────────────
|
||||||
const errorData = await res.json();
|
function updateStickyButtons() {
|
||||||
errorMessage = errorData.detail || errorMessage;
|
const sendCount = document.querySelectorAll('.send-checkbox:checked').length;
|
||||||
} catch (e) {
|
const acceptCount = document.querySelectorAll('.accept-checkbox:checked').length;
|
||||||
try {
|
|
||||||
const errorText = await res.text();
|
const sendBtn = document.getElementById('btnSendSelected');
|
||||||
errorMessage = errorText || errorMessage;
|
const acceptBtn = document.getElementById('btnAcceptSelected');
|
||||||
} catch (e) {
|
|
||||||
|
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;
|
// Validation helper (used by Send)
|
||||||
const trackValue = document.getElementById('track');
|
// ─────────────────────────────────────────────
|
||||||
|
function validateRequiredFields() {
|
||||||
let track = ''
|
|
||||||
if (delivery_type === 'СДЭК' && trackValue) {
|
|
||||||
track = trackValue.value
|
|
||||||
}
|
|
||||||
const requiredInputs = document.querySelectorAll('input[required]');
|
const requiredInputs = document.querySelectorAll('input[required]');
|
||||||
const requiredTextareas = document.querySelectorAll('textarea[required]');
|
const requiredTextareas = document.querySelectorAll('textarea[required]');
|
||||||
const requiredFields = [...requiredInputs, ...requiredTextareas];
|
const requiredFields = [...requiredInputs, ...requiredTextareas];
|
||||||
@ -310,7 +331,7 @@ async function btnSend(btn, itemId, sender, too){
|
|||||||
|
|
||||||
requiredFields.forEach(field => {
|
requiredFields.forEach(field => {
|
||||||
const group = field.closest('.floating-group');
|
const group = field.closest('.floating-group');
|
||||||
if (!group) return; // skip if no group wrapper
|
if (!group) return;
|
||||||
|
|
||||||
let isEmpty = false;
|
let isEmpty = false;
|
||||||
if (field.type === 'file') {
|
if (field.type === 'file') {
|
||||||
@ -322,7 +343,7 @@ async function btnSend(btn, itemId, sender, too){
|
|||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
group.classList.add('error');
|
group.classList.add('error');
|
||||||
hasError = true;
|
hasError = true;
|
||||||
showToast({message: field.dataset.toast, duration:2500});
|
showToast({ message: field.dataset.toast || 'Заполните обязательное поле', duration: 2500 });
|
||||||
if (!firstErrorField) firstErrorField = field;
|
if (!firstErrorField) firstErrorField = field;
|
||||||
} else {
|
} else {
|
||||||
group.classList.remove('error');
|
group.classList.remove('error');
|
||||||
@ -332,19 +353,98 @@ async function btnSend(btn, itemId, sender, too){
|
|||||||
if (hasError && firstErrorField) {
|
if (hasError && firstErrorField) {
|
||||||
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
firstErrorField.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||||
setTimeout(() => firstErrorField.focus(), 600);
|
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;
|
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 {
|
try {
|
||||||
const res = await fetch('/api/delivery/send', {
|
const res = await fetch('/api/delivery/send', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
sender,
|
items: items.map(i => ({
|
||||||
too,
|
item: i.id,
|
||||||
item: itemId,
|
sender: i.sender,
|
||||||
|
too: i.receiver
|
||||||
|
})),
|
||||||
delivery_type,
|
delivery_type,
|
||||||
track: track
|
track: track || null
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -354,26 +454,61 @@ async function btnSend(btn, itemId, sender, too){
|
|||||||
const errorData = await res.json();
|
const errorData = await res.json();
|
||||||
errorMessage = errorData.detail || errorMessage;
|
errorMessage = errorData.detail || errorMessage;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// If response isn't JSON, try to get text
|
|
||||||
try {
|
try {
|
||||||
const errorText = await res.text();
|
const errorText = await res.text();
|
||||||
errorMessage = errorText || errorMessage;
|
errorMessage = errorText || errorMessage;
|
||||||
} catch (e) {
|
} catch (e) {}
|
||||||
// Fallback to generic error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
showToast({ message: errorMessage, duration: 2500 });
|
showToast({ message: errorMessage, duration: 2500 });
|
||||||
return;
|
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';
|
statusDiv.className = 'b_thlt1_2';
|
||||||
statusSpan.className = 'b_5dr47_2';
|
statusSpan.className = 'b_5dr47_2';
|
||||||
statusSpan.textContent = 'Ожидает получения';
|
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) {
|
} catch (error) {
|
||||||
console.error('Network error:', error);
|
console.error('Network error:', error);
|
||||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
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>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -98,9 +98,10 @@
|
|||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if post.status == 'Ожидает получения' %}
|
{% 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>
|
<span class="b_al8e7">Получить</span>
|
||||||
</button>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if post.status in ['Возврат художнику']%}
|
{% if post.status in ['Возврат художнику']%}
|
||||||
<a class="b_289i1" href="/profile/delivery/exhibition/{{exhibition._id}}#main">
|
<a class="b_289i1" href="/profile/delivery/exhibition/{{exhibition._id}}#main">
|
||||||
@ -114,8 +115,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
{#
|
||||||
<script>
|
<script>
|
||||||
async function btnAccept(btn, itemId){
|
async function btnAccept(btn, itemId){
|
||||||
const figcaption = btn.parentElement;
|
const figcaption = btn.parentElement;
|
||||||
const statusDiv = figcaption.querySelectorAll('div')[0];
|
const statusDiv = figcaption.querySelectorAll('div')[0];
|
||||||
const statusSpan = statusDiv.querySelector('span')
|
const statusSpan = statusDiv.querySelector('span')
|
||||||
@ -153,6 +155,9 @@ async function btnAccept(btn, itemId){
|
|||||||
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
showToast({ message: 'Не удалось подключиться к серверу', duration: 2500 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
</script>
|
||||||
|
#}
|
||||||
|
<script>
|
||||||
async function sendInvitations(exhibition_id) {
|
async function sendInvitations(exhibition_id) {
|
||||||
const data = new FormData();
|
const data = new FormData();
|
||||||
data.set('exhibition_id', exhibition_id);
|
data.set('exhibition_id', exhibition_id);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user