заказы

This commit is contained in:
2025-12-21 03:45:02 +03:00
parent 83ecf65abf
commit 546c70cbcd
15 changed files with 862 additions and 38 deletions
+48 -26
View File
@@ -1,15 +1,17 @@
from datetime import date, datetime, timedelta
from datetime import datetime, timedelta
from fastapi import APIRouter, Depends, Request
from fastapi.responses import RedirectResponse
from db.handlers.access import AccessLevelHandler
from db.handlers.categories import CategoryHandler
from db.handlers.orders import OrdersHandler
from utils import render, requestDict, logger
from .user import router as user
from .stocks import router as stocks
from .toolbox import router as toolbox
from .toolkit import router as toolkit
from .records import router as records
from .orders import router as orders
router = APIRouter()
@@ -19,6 +21,7 @@ router.include_router(stocks, prefix="/stocks", tags=["stocks"])
router.include_router(toolbox, prefix="/toolbox", tags=["toolbox"])
router.include_router(toolkit, prefix="/toolkit", tags=["toolkit"])
router.include_router(records, prefix="/records", tags=["records"])
router.include_router(orders, prefix="/orders", tags=["orders"])
@router.get("/")
@@ -52,6 +55,21 @@ async def post_requests(
from db.handlers.toolkit import ToolkitHandler
from db.handlers.user import UserHandler
def getDates(data: dict):
startDate = data.get("startDate")
if isinstance(startDate, str):
startDate = datetime.strptime(startDate, "%Y-%m-%d").date()
if startDate is None:
startDate = datetime.now().date() - timedelta(days=30)
endDate = data.get("endDate")
if isinstance(endDate, str):
endDate = datetime.strptime(endDate, "%Y-%m-%d").date()
if endDate is None:
endDate = datetime.now().date()
if startDate > endDate:
startDate, endDate = endDate, startDate
return startDate, endDate
reqData = {
"tab": request_data.get("body").get("tabId"),
}
@@ -105,18 +123,7 @@ async def post_requests(
"categories": categories,
}
case "jurnal_toolkits":
startDate = request_data.get("body").get(
"startDate", date.today() - timedelta(days=7)
)
if isinstance(startDate, str):
startDate = datetime.strptime(startDate, "%Y-%m-%d").date()
endDate = request_data.get("body").get("endDate", date.today())
if isinstance(endDate, str):
endDate = datetime.strptime(endDate, "%Y-%m-%d").date()
if startDate > endDate:
startDate, endDate = endDate, startDate
startDate, endDate = getDates(request_data.get("body"))
jurnal_toolkits = await StocksRecordsHandler.getLogs(startDate, endDate)
if isinstance(jurnal_toolkits, list):
@@ -147,18 +154,7 @@ async def post_requests(
"endDate": endDate.strftime("%Y-%m-%d"),
}
case "jurnal_service":
startDate = request_data.get("body").get(
"startDate", date.today() - timedelta(days=7)
)
if isinstance(startDate, str):
startDate = datetime.strptime(startDate, "%Y-%m-%d").date()
endDate = request_data.get("body").get("endDate", date.today())
if isinstance(endDate, str):
endDate = datetime.strptime(endDate, "%Y-%m-%d").date()
if startDate > endDate:
startDate, endDate = endDate, startDate
startDate, endDate = getDates(request_data.get("body"))
jurnal_service = await ServiceRecordsHandler.getLogs(startDate, endDate)
if isinstance(jurnal_service, list):
@@ -199,7 +195,33 @@ async def post_requests(
"users": users,
"accessLevels": accessLevels,
}
# logger.info(resultData)
case "orders":
fullAccess = reqData["accessData"].get("view_all_toolboxes", False)
startDate, endDate = getDates(request_data.get("body"))
if fullAccess:
result = await OrdersHandler.get_all(startDate, endDate)
if "errorMessage" in result.keys():
resultData["message"] = result["errorMessage"]
else:
resultData["status"] = "ok"
if "orders" in result.keys():
resultData["data"]["orders"] = result["orders"]
else:
userId = reqData.get("userData").get("id")
result = await OrdersHandler.get_all_by_consumer(
userId, startDate, endDate
)
if "errorMessage" in result.keys():
resultData["message"] = result["errorMessage"]
else:
resultData["status"] = "ok"
if "orders" in result.keys():
resultData["data"]["orders"] = result["orders"]
if resultData["status"] == "ok":
resultData["data"]["startDate"] = startDate.strftime("%Y-%m-%d")
resultData["data"]["endDate"] = endDate.strftime("%Y-%m-%d")
resultData["data"]["users"] = await UserHandler.getAll()
resultData["data"]["fullAccess"] = fullAccess
case _:
pass
return resultData
Binary file not shown.
Binary file not shown.
+42
View File
@@ -0,0 +1,42 @@
from fastapi import APIRouter, Depends
from db.handlers.orders import OrdersHandler
from utils import requestDict, logger
router = APIRouter()
@router.get("/")
async def get_requests():
return await OrdersHandler.countNew()
@router.post("/")
async def post_requests(
reqData: dict = Depends(requestDict),
):
logger.info("Обновление заказа")
response = {"status": "error"}
orderId = reqData.get("body").get("orderId")
userId = reqData.get("body").get("userId")
status = reqData.get("body").get("status")
comment = reqData.get("body").get("comment")
if (orderId is not None and userId is not None) and (
status is not None or comment is not None
):
result = await OrdersHandler.update(int(orderId), userId, status, comment)
if "errorMessage" in result.keys():
response["message"] = result["errorMessage"]
else:
response["status"] = "ok"
if "orders" in result.keys():
response["data"] = result["orders"]
return response
@router.post("/new", summary="Добавление нового заказа")
async def add_order(reqData: dict = Depends(requestDict)):
logger.info(f"Добавление нового заказа")
userId = reqData.get("body").get("userId")
customer_comment = reqData.get("body").get("customer_comment")
return await OrdersHandler.new(userId, customer_comment)
+1
View File
@@ -2,6 +2,7 @@ body {
background-image: url("../images/background.svg");
background-repeat: repeat;
background-size: 512px auto;
background-attachment: fixed;
}
.loader-bg {
+617 -3
View File
@@ -89,7 +89,11 @@ async function checkActiveUser() {
}
}
async function openTab(event, tabId) {
async function openTab(event, tabId, autoLoad = false) {
const activeTab = loadFromStorage('tab');
if (activeTab && activeTab.tabId === tabId && !autoLoad) {
return;
}
// Убираем активный класс со всех вкладок и кнопок
document.querySelectorAll('.tab-nav-btn').forEach(btn => {
btn.classList.remove('active');
@@ -136,6 +140,12 @@ function prepareTabs() {
};
}
tabsData['orders'] = {
title: 'Заказы',
icon: 'bi-basket',
description: 'Управление заказами'
};
if (accessData.view_requests) {
tabsData['jurnal_toolkits'] = {
title: 'Журнал перемещений',
@@ -170,7 +180,7 @@ function prepareTabs() {
<div id="mainTabsNavWrapper">
<nav class="nav nav-pills gap-2" id="mainTabsNav" role="tablist">
${Object.entries(tabsData).map(([tabId, tabData], index) => `
<button class="nav-link tab-nav-btn d-flex flex-column align-items-center justify-content-center py-3 px-2"
<button class="nav-link tab-nav-btn d-flex flex-column align-items-center justify-content-center py-3 px-2 position-relative"
id="${tabId}-tab"
role="tab"
onclick="openTab(event, '${tabId}')"
@@ -240,7 +250,7 @@ function prepareTabs() {
const activeTabId = activeTabData.tabId;
const tabBtn = document.getElementById(`${activeTabId}-tab`);
if (tabBtn) {
openTab({ currentTarget: tabBtn }, activeTabId);
openTab({ currentTarget: tabBtn }, activeTabId, true);
}
}
}
@@ -280,6 +290,9 @@ function fillTab(tabId, tabData) {
case 'toolkits':
renderToolkitsTab(tabId, tabData.toolkits, tabData.categories);
break;
case 'orders':
renderOrdersTab(tabId, tabData);
break;
case 'jurnal_toolkits':
renderJurnalToolkitsTab(tabId, tabData);
break;
@@ -5986,6 +5999,578 @@ function renderRequestsTab(tabId, tabData) {
renderRequestsTable();
}
function renderOrdersTab(tabId, tabData) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
const { orders, users, startDate, endDate, fullAccess } = tabData;
// Создаем мапу пользователей
const userMap = {};
users.forEach(user => {
userMap[user.id] = user.username;
});
// const ordersStatuses = {
// }
// Сохраненные фильтры
const savedFilters = loadFromStorage(tabId);
let currentFilters = {
customer: savedFilters?.customer || 'all',
status: savedFilters?.status || 'all',
search: savedFilters?.search || ''
};
// Хранилище измененных данных
const changedOrders = {};
const statusesList = []
const statusesMap = {
'new': 'Новый',
'working': 'В работе',
'complete': 'Выполнен',
'cancelled': 'Отменен',
}
// Оригинальные данные заказов (для сравнения)
const originalOrders = {};
orders.forEach(o => {
originalOrders[o.id] = {
status: o.status,
executor_comment: o.executor_comment || ''
};
if (!statusesList.includes(o.status)) {
statusesList.push(o.status)
}
});
// Рендерим дополнительный контейнер с фильтрами и кнопками
tabOptionalContent.innerHTML = `
<div class="row align-items-center mb-1">
<!-- Левая часть с фильтрами -->
<div class="col-12 col-md-6 mb-2 mb-md-0">
<div class="row align-items-center mb-2">
<!-- Поиск -->
<div class="col-12 col-md-9">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-search"></i>
</span>
<input type="text" class="form-control"
id="${tabId}-search-filter"
placeholder="Поиск по таблице..."
value="${currentFilters.search}">
</div>
</div>
<!-- Кнопка сброса -->
<div class="col-12 col-md-3">
<button class="btn btn-outline-secondary" id="${tabId}-filter-reset-btn">
<i class="bi bi-x-circle me-1"></i>Сброс
</button>
</div>
</div>
<!-- Фильтр по статусу -->
<div class="row g-2">
<div class="col-12 col-md-5">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-list-check"></i>
</span>
<select class="form-select" id="${tabId}-status-filter">
<option value="all">Все статусы</option>
${statusesList.map(status => `<option value="${status}">${statusesMap[status] || status}</option>`).join('')}
</select>
</div>
</div>
${!fullAccess ? `
<div class="col-12 col-md-7">
<button class="btn btn-primary" id="${tabId}-new-order-btn">
<i class="bi bi-plus-circle me-1"></i>Оформить заказ
</button>
</div>
` : `
<div class="col-12 col-md-7">
<div class="input-group">
<span class="input-group-text">
<i class="bi bi-person"></i>
</span>
<select class="form-select" id="${tabId}-customer-filter">
<option value="all">Все заказчики</option>
${[...new Set(orders.map(o => o.customer_id))].map(customerId => `
<option value="${customerId}">${userMap[customerId] || `Пользователь ${customerId}`}</option>
`).join('')}
</select>
</div>
</div>
` }
</div>
</div>
<div class="col-12 col-md-2">
<button class="btn btn-outline-primary" id="${tabId}-date-update-btn">
Обновить список заказов
</button>
</div>
<!-- Правая часть с датами и кнопками -->
<div class="col-12 col-md-4">
<div class="d-flex flex-wrap gap-2 justify-content-md-end">
<div class="input-group date">
<span class="input-group-text" style="width: 170px;">
<i class="bi bi-calendar me-1"></i>Дата начала:
</span>
<input type="date" class="form-control" id="${tabId}-date-from">
</div>
<div class="input-group date">
<span class="input-group-text" style="width: 170px;">
<i class="bi bi-calendar me-1"></i>Дата окончания:
</span>
<input type="date" class="form-control" id="${tabId}-date-to">
</div>
</div>
</div>
</div>
`;
// Модальное окно для нового заказа
if (!fullAccess) {
document.body.insertAdjacentHTML('beforeend', `
<div class="modal fade" id="${tabId}-new-order-modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Оформление нового заказа</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<div class="mb-3">
<label for="${tabId}-order-description" class="form-label">Описание заказа</label>
<textarea class="form-control" id="${tabId}-order-description"
rows="4" placeholder="Опишите, что вам нужно..."></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
<button type="button" class="btn btn-primary" id="${tabId}-order-submit-btn">Заказать</button>
</div>
</div>
</div>
</div>
`);
}
// Инициализация фильтров
if (fullAccess) {
document.getElementById(`${tabId}-customer-filter`).value = currentFilters.customer;
}
document.getElementById(`${tabId}-status-filter`).value = currentFilters.status;
document.getElementById(`${tabId}-search-filter`).value = currentFilters.search;
// Обработчики событий
const filterResetBtn = document.getElementById(`${tabId}-filter-reset-btn`);
filterResetBtn.addEventListener('click', () => {
currentFilters = {
customer: 'all',
status: 'all',
search: ''
};
if (fullAccess) {
document.getElementById(`${tabId}-customer-filter`).value = currentFilters.customer;
}
document.getElementById(`${tabId}-status-filter`).value = currentFilters.status;
document.getElementById(`${tabId}-search-filter`).value = currentFilters.search;
saveToStorage(tabId, currentFilters);
renderOrdersTable();
});
// Дата фильтры
const startDateInput = document.getElementById(`${tabId}-date-from`);
const endDateInput = document.getElementById(`${tabId}-date-to`);
startDateInput.value = startDate;
endDateInput.value = endDate;
const refreshDateBtn = document.getElementById(`${tabId}-date-update-btn`);
refreshDateBtn.addEventListener('click', async () => {
const newStartDate = startDateInput.value;
const newEndDate = endDateInput.value;
if (newStartDate && newEndDate) {
tabContent.innerHTML = `
<div class="alert alert-info m-4" role="alert">
<i class="bi bi-info-circle me-2"></i>
Загрузка данных...
</div>
`;
const cookiesData = { userData, accessData };
const newPeriodData = await apiRequest('/', {
tabId: tabId,
startDate: newStartDate,
endDate: newEndDate,
cookiesData
});
if (newPeriodData.status === 'ok') {
renderOrdersTab(tabId, {
...tabData,
...newPeriodData.data,
startDate: newStartDate,
endDate: newEndDate,
fullAccess
});
await checkNewOrders();
}
}
});
// Кнопка нового заказа
if (!fullAccess) {
const newOrderBtn = document.getElementById(`${tabId}-new-order-btn`);
newOrderBtn.addEventListener('click', () => {
const modal = new bootstrap.Modal(document.getElementById(`${tabId}-new-order-modal`));
modal.show();
});
// Отправка нового заказа
const orderSubmitBtn = document.getElementById(`${tabId}-order-submit-btn`);
orderSubmitBtn.onclick = async () => {
if (orderSubmitBtn.disabled) return;
orderSubmitBtn.disabled = true;
const description = document.getElementById(`${tabId}-order-description`).value;
if (!description.trim()) {
showInfo('Пожалуйста, введите описание заказа', 'warning');
orderSubmitBtn.disabled = false;
return;
}
try {
const result = await apiRequest('/orders/new', {
userId: userData.id,
customer_comment: description
});
if (result.status === 'ok') {
const modal = bootstrap.Modal.getInstance(
document.getElementById(`${tabId}-new-order-modal`)
);
modal.hide();
document.getElementById(`${tabId}-order-description`).value = '';
refreshDateBtn.click();
} else {
showInfo(result.message || 'Ошибка при создании заказа', 'danger');
}
} finally {
orderSubmitBtn.disabled = false;
}
};
}
if (orders.length === 0) {
tabContent.innerHTML = `
<div class="alert alert-info m-4" role="alert">
<i class="bi bi-info-circle me-2"></i>
Нет заказов за период ${startDate} - ${endDate}
</div>
`;
return;
}
// Рендерим таблицу
tabContent.innerHTML = `
<div class="card border-0 shadow-sm">
<div class="card-body p-0">
<div class="table-responsive">
<table class="table table-hover mb-0" id="${tabId}-orders-table">
<thead class="table-light">
<tr>
<th width="150">Заказчик</th>
<th>Описание заказа</th>
<th width="120">Статус</th>
<th width="150">Исполнитель</th>
<th>Комментарий исполнителя</th>
${fullAccess ? '<th width="30">⚙️</th>' : ''}
</tr>
</thead>
<tbody id="${tabId}-orders-body">
<!-- Заказы будут вставлены здесь -->
</tbody>
</table>
</div>
<div class="text-center p-3 border-top" id="${tabId}-no-orders" style="display: none;">
<i class="bi bi-inbox fs-1 text-muted"></i>
<p class="text-muted mt-2">Нет заказов по выбранным фильтрам</p>
</div>
</div>
</div>
`;
// Функция фильтрации
function filterOrders() {
let filtered = orders;
// Фильтр по заказчику
if (fullAccess && currentFilters.customer !== 'all') {
filtered = filtered.filter(o => o.customer_id == currentFilters.customer);
}
// Фильтр по статусу
if (currentFilters.status !== 'all') {
filtered = filtered.filter(o => o.status === currentFilters.status);
}
// Поиск
if (currentFilters.search.trim()) {
const searchTerm = currentFilters.search.toLowerCase();
filtered = filtered.filter(o => {
return (
(userMap[o.customer_id] || '').toLowerCase().includes(searchTerm) ||
o.customer_comment.toLowerCase().includes(searchTerm) ||
(userMap[o.executor_id] || '').toLowerCase().includes(searchTerm) ||
(o.executor_comment || '').toLowerCase().includes(searchTerm) ||
o.status.toLowerCase().includes(searchTerm)
);
});
}
return filtered;
}
// Функция рендеринга таблицы
function renderOrdersTable() {
const tbody = document.getElementById(`${tabId}-orders-body`);
const noOrdersDiv = document.getElementById(`${tabId}-no-orders`);
const filteredOrders = filterOrders();
if (filteredOrders.length === 0) {
tbody.innerHTML = '';
noOrdersDiv.style.display = 'block';
return;
}
noOrdersDiv.style.display = 'none';
tbody.innerHTML = filteredOrders.map(order => {
// Определяем класс для статуса
const statusClass = {
new: 'warning',
working: 'primary',
complete: 'success',
cancelled: 'danger'
}[order.status] || 'secondary';
// Определяем русское название статуса
const statusText = {
new: 'Новый',
working: 'В работе',
complete: 'Выполнен',
cancelled: 'Отменен'
}[order.status] || order.status;
// Рендерим статус
let statusCell;
if (fullAccess && (order.status === 'new' || order.status === 'working')) {
statusCell = `
<select class="form-select form-select-sm order-status"
data-order-id="${order.id}"
style="min-width: 120px;">
<option value="new" ${order.status === 'new' ? 'selected' : ''} disabled>Новый</option>
<option value="working" ${order.status === 'working' ? 'selected' : ''}>В работе</option>
<option value="complete" ${order.status === 'complete' ? 'selected' : ''}>Выполнен</option>
<option value="cancelled" ${order.status === 'cancelled' ? 'selected' : ''}>Отменен</option>
</select>
`;
} else {
statusCell = `<span class="badge bg-${statusClass}">${statusText}</span>`;
}
// Рендерим комментарий исполнителя
let commentCell;
if (fullAccess && (order.status === 'new' || order.status === 'working')) {
commentCell = `
<textarea class="form-control form-control-sm executor-comment"
data-order-id="${order.id}"
rows="2"
placeholder="Введите комментарий...">${order.executor_comment || ''}</textarea>
`;
} else {
commentCell = `<small class="text-muted">${order.executor_comment || 'Нет комментария'}</small>`;
}
return `
<tr data-order-id="${order.id}">
<td class="align-middle">
${userMap[order.customer_id] || `Пользователь ${order.customer_id}`}<br>
<small class="text-muted">${order.created_at}</small>
</td>
<td class="align-middle"><small>${order.customer_comment}</small></td>
<td class="align-middle">
${statusCell}<br>
<small class="text-muted">${order.updated_at}</small>
</td>
<td class="align-middle">${order.executor_id ? (userMap[order.executor_id] || `Пользователь ${order.executor_id}`) : '-'}</td>
<td class="align-middle">${commentCell}</td>
${fullAccess ? `
<td class="align-middle text-center">
${fullAccess && (order.status === 'new' || order.status === 'working') ? `
<button class="btn btn-sm btn-success save-row-btn"
data-order-id="${order.id}"
disabled>
<i class="bi bi-save"></i>
</button>
` : '☑️'}
</td>
`: ''}
</tr>
`;
}).join('');
function updateRowState(orderId) {
const original = originalOrders[orderId];
const current = changedOrders[orderId];
const hasChanges =
current &&
(
(current.status !== undefined && current.status !== original.status) ||
(current.executor_comment !== undefined && current.executor_comment !== original.executor_comment)
);
const saveBtn = document.querySelector(
`.save-row-btn[data-order-id="${orderId}"]`
);
if (saveBtn) {
saveBtn.disabled = !hasChanges;
}
}
// Добавляем обработчики изменений
if (fullAccess) {
document.querySelectorAll(`.order-status`).forEach(select => {
select.addEventListener('change', function () {
const orderId = this.dataset.orderId;
changedOrders[orderId] = {
...changedOrders[orderId],
status: this.value
};
updateRowState(orderId);
});
});
document.querySelectorAll(`.executor-comment`).forEach(textarea => {
textarea.addEventListener('input', function () {
const orderId = this.dataset.orderId;
changedOrders[orderId] = {
...changedOrders[orderId],
executor_comment: this.value
};
updateRowState(orderId);
});
});
document.querySelectorAll('.save-row-btn').forEach(btn => {
btn.addEventListener('click', async function () {
const orderId = this.dataset.orderId;
const original = originalOrders[orderId];
const current = changedOrders[orderId];
if (!current) return;
// Формируем diff
const payload = { orderId, userId: userData.id };
if (current.status !== undefined && current.status !== original.status) {
payload.status = current.status;
}
if (
current.executor_comment !== undefined &&
current.executor_comment !== original.executor_comment
) {
payload.comment = current.executor_comment;
}
if (Object.keys(payload).length === 2) return;
this.disabled = true;
const result = await apiRequest('/orders/', payload);
if (result.status === 'ok') {
// Обновляем оригинал
originalOrders[orderId] = {
status: payload.status ?? original.status,
executor_comment: payload.comment ?? original.executor_comment
};
delete changedOrders[orderId];
if (payload.status && original.status === 'new') {
await checkNewOrders();
}
this.innerHTML = '<i class="bi bi-check-circle"></i>';
setTimeout(() => {
this.innerHTML = '<i class="bi bi-save"></i>';
}, 1500);
if (originalOrders[orderId].status === 'complete' || originalOrders[orderId].status === 'cancelled') {
const foundOrder = orders.find(order => order.id === Number(orderId));
if (foundOrder) {
foundOrder.executor_id = userData.id;
foundOrder.executor_comment = originalOrders[orderId].executor_comment;
foundOrder.status = current.status;
}
renderOrdersTable();
}
} else {
showInfo('Ошибка сохранения', 'danger');
this.disabled = false;
}
});
});
}
}
// Обработчики фильтров
if (fullAccess) {
document.getElementById(`${tabId}-customer-filter`).addEventListener('change', function () {
currentFilters.customer = this.value;
saveToStorage(tabId, currentFilters);
renderOrdersTable();
});
}
document.getElementById(`${tabId}-status-filter`).addEventListener('change', function () {
currentFilters.status = this.value;
saveToStorage(tabId, currentFilters);
renderOrdersTable();
});
document.getElementById(`${tabId}-search-filter`).addEventListener('input', function () {
currentFilters.search = this.value;
saveToStorage(tabId, currentFilters);
renderOrdersTable();
});
// Первоначальный рендеринг
renderOrdersTable();
}
function renderJurnalToolkitsTab(tabId, tabData) {
const tabContent = document.getElementById(`${tabId}-tab-content`);
const tabOptionalContent = document.getElementById(`${tabId}-tab-optional-content`);
@@ -7446,6 +8031,34 @@ function renderUsersTab(tabId, tabData) {
renderUsersCards();
}
async function checkNewOrders() {
const result = await apiRequest('/orders/', {}, 'GET');
if (result && result.orders > 0) {
const ordersTabBtn = document.getElementById('orders-tab');
// Удаляем старый бейдж, если есть
const oldBadge = ordersTabBtn.querySelector('.orders-badge');
if (oldBadge) oldBadge.remove();
const newOrdersBadge = document.createElement('span');
newOrdersBadge.className =
'badge rounded-pill bg-danger position-absolute orders-badge';
newOrdersBadge.textContent = result.orders;
// Позиция: правый верх
newOrdersBadge.style.top = '6px';
newOrdersBadge.style.right = '8px';
ordersTabBtn.appendChild(newOrdersBadge);
}
if (result && result.orders == 0) {
const ordersTabBtn = document.getElementById('orders-tab');
const oldBadge = ordersTabBtn.querySelector('.orders-badge');
if (oldBadge) oldBadge.remove();
}
}
document.addEventListener('DOMContentLoaded', async () => {
await getCookieData();
@@ -7456,6 +8069,7 @@ document.addEventListener('DOMContentLoaded', async () => {
}
prepareTabs();
await checkNewOrders();
});
window.openTab = openTab;
Binary file not shown.
Binary file not shown.
Binary file not shown.
+98
View File
@@ -0,0 +1,98 @@
from datetime import date, datetime, time
from sqlalchemy import func, select
from db import CRUD
from db.schemas.orders import Orders
from utils.loggers import logger
class OrdersHandler:
@staticmethod
async def new(user_id: int, order: str):
try:
await Orders(customer_id=user_id, customer_comment=order).save()
except Exception as e:
logger.error(f"Ошибка создания заказа: {str(e)}")
return {"errorMessage": f"Ошибка создания заказа: {str(e)}"}
return {"status": "ok"}
@staticmethod
async def get_all_by_consumer(
user_id: int, startDate: date, endDate: date, toDict: bool = True
):
try:
startDate = datetime.combine(startDate, time.min)
endDate = datetime.combine(endDate, time.max)
query = (
select(Orders)
.where(
Orders.customer_id == user_id,
Orders.created_at.between(startDate, endDate),
)
.order_by(Orders.created_at.asc())
)
orders = await CRUD.read(query, True)
ordresList = [order.toDict() for order in orders] if toDict else orders
return {"orders": ordresList}
except Exception as e:
logger.error(f"Ошибка получения заказов: {str(e)}")
return {"errorMessage": f"Ошибка получения заказов: {str(e)}"}
@staticmethod
async def get_all(startDate: date, endDate: date, toDict: bool = True):
try:
startDate = datetime.combine(startDate, time.min)
endDate = datetime.combine(endDate, time.max)
query = (
select(Orders)
.where(
Orders.created_at.between(startDate, endDate),
)
.order_by(Orders.created_at.asc())
)
orders = await CRUD.read(query, True)
ordresList = [order.toDict() for order in orders] if toDict else orders
return {"orders": ordresList}
except Exception as e:
logger.error(f"Ошибка получения заказов: {str(e)}")
return {"errorMessage": f"Ошибка получения заказов: {str(e)}"}
@staticmethod
async def countNew():
try:
query = select(func.count(Orders.id)).where(Orders.status == "new")
return {"orders": await CRUD.read(query)}
except Exception as e:
logger.error(f"Ошибка получения количества заказов: {str(e)}")
return {"errorMessage": f"Ошибка получения количества заказов: {str(e)}"}
@staticmethod
async def get(order_id: int, toDict: bool = False):
try:
order = await CRUD.read(select(Orders).where(Orders.id == order_id))
return order.toDict() if toDict else order
except Exception as e:
logger.error(f"Ошибка получения заказа: {str(e)}")
return {"errorMessage": f"Ошибка получения заказа: {str(e)}"}
@staticmethod
async def update(
order_id: int, user_id: int, status: str = None, comment: str = None
):
try:
if status is None and comment is None:
logger.error("Не указан статус и комментарий")
return {"errorMessage": "Не указан статус и комментарий"}
order = await OrdersHandler.get(order_id)
if not order or isinstance(order, dict):
logger.error("Заказ не найден")
return {"errorMessage": "Заказ не найден"}
changeData = {"executor_id": user_id}
if status:
changeData["status"] = status
if comment:
changeData["executor_comment"] = comment
await order.edit(**changeData)
except Exception as e:
logger.error(f"Ошибка обновления заказа: {str(e)}")
return {"errorMessage": f"Ошибка обновления заказа: {str(e)}"}
return {"status": "ok"}
+13
View File
@@ -25,6 +25,8 @@ def handleToolkitImage(imageData, title: str):
class ToolkitHandler:
@staticmethod
async def add(toolkitData: dict, user_id: int = None):
title = toolkitData.get("title", None)
if not title:
@@ -71,6 +73,7 @@ class ToolkitHandler:
await ServiceRecordsHandler.add(user_id, {"Добавлен инструмент": toolkitData})
return newToolkit.toDict()
@staticmethod
async def updateMovindDate(toolkitId: int):
toolkit = await CRUD.read(select(Toolkit).where(Toolkit.id == toolkitId))
if not toolkit:
@@ -82,6 +85,7 @@ class ToolkitHandler:
return False
return True
@staticmethod
async def updateRefillDate(toolkitId: int):
logger.info(f"Обновление даты пополнения инструмента {toolkitId}...")
toolkit = await CRUD.read(select(Toolkit).where(Toolkit.id == toolkitId))
@@ -94,12 +98,14 @@ class ToolkitHandler:
return False
return True
@staticmethod
async def hideToolkit(userId: int, toolkitId: int, hidden: bool = True):
logger.info(
f"{'Скрытие' if hidden else 'Отображение'} инструмента {toolkitId}..."
)
return await ToolkitHandler.edit(userId, id=toolkitId, hidden=hidden)
@staticmethod
async def edit(user_id: int, **kwargs):
title = kwargs.get("title", None)
toolkitId = kwargs.pop("id")
@@ -165,11 +171,13 @@ class ToolkitHandler:
)
return editedToolkit.toDict()
@staticmethod
async def getAll():
query = select(Toolkit)
toolkits = await CRUD.read(query, True)
return [toolkit.toDict() for toolkit in toolkits] if toolkits else []
@staticmethod
async def get(toolkitId: int):
query = select(Toolkit).where(Toolkit.id == toolkitId)
toolkit = await CRUD.read(query)
@@ -183,16 +191,19 @@ class ToolkitHandler:
logger.info(data)
return data
@staticmethod
async def getSeveral(toolkitIds: list[int]) -> list[dict]:
query = select(Toolkit).where(Toolkit.id.in_(toolkitIds))
toolkits = await CRUD.read(query, True)
return [toolkit.toDict() for toolkit in toolkits] if toolkits else []
@staticmethod
async def checkCatogoryUse(category_id: int):
query = select(Toolkit).where(Toolkit.category_id == category_id)
toolkit = await CRUD.read(query)
return True if toolkit else False
@staticmethod
async def delete(toolkitId: int, user_id: int = None):
movements = await StockHandler.checkToolkitExists(toolkitId)
if movements:
@@ -217,6 +228,7 @@ class ToolkitHandler:
)
return {"status": "ok"} if result else {"errorMessage": "Инструмент не удален"}
@staticmethod
async def addComment(toolkitId: int, user_id: int, comment: str):
logger.info(f"Добавление комментария к инструменту {toolkitId}...")
logger.info(f"Комментарий: {comment}")
@@ -236,6 +248,7 @@ class ToolkitHandler:
logger.info(f"Комментарий к инструменту {toolkit.title} успешно добавлен")
return {"status": "ok"}
@staticmethod
async def initialize():
from .categories import CategoryHandler
+9 -5
View File
@@ -4,13 +4,17 @@ from .toolkit import *
from .categories import *
from .toolbox import *
from .stock import *
from .records import *
from .orders import *
__all__ = [
"User",
"Access",
"Toolbox",
"Category",
"Stock",
"AccessLevel",
"Toolkit",
"Category",
"Toolbox",
"Stock",
"StocksRecords",
"ServicesRecords",
"Orders",
]
Binary file not shown.
Binary file not shown.
+30
View File
@@ -0,0 +1,30 @@
from datetime import datetime
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, Text
from db import CRUD, Base
import utils
class Orders(Base):
__tablename__ = "orders"
id = Column(Integer, primary_key=True, autoincrement=True)
customer_id = Column(Integer, ForeignKey("users.id"))
executor_id = Column(Integer, ForeignKey("users.id"), nullable=True)
customer_comment = Column(Text, nullable=False)
executor_comment = Column(String, nullable=True)
status = Column(String, default="new")
created_at = Column(DateTime, default=datetime.now)
updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now)
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def toDict(self):
return utils.toDict(self)
async def save(self):
return await CRUD.create(self, refresh=True)
async def edit(self, **kwargs):
return await CRUD.update(Orders, self.id, **kwargs)