diff --git a/.DS_Store b/.DS_Store index b207f46..a1fe879 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/Dockerfile.backend b/Dockerfile.backend index 4586976..964bd13 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -13,7 +13,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY backend/ . # Копирование .env.docker в .env для использования в контейнере -COPY backend/.env.docker ./app/.env +COPY backend/.env.docker ./.env # Создание директории для загрузок если её нет RUN mkdir -p /app/uploads/products @@ -25,4 +25,4 @@ RUN chmod -R 777 /app/uploads EXPOSE 8000 # Запуск приложения с Uvicorn -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] \ No newline at end of file diff --git a/Dockerfile.frontend b/Dockerfile.frontend index 5984ed1..cbf2766 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -8,12 +8,12 @@ COPY frontend/package*.json ./ # Установка зависимостей с флагом --legacy-peer-deps RUN npm ci --legacy-peer-deps -# Копирование исходного кода -COPY frontend/ ./ - # Копирование .env.docker в .env.local для использования в контейнере COPY frontend/.env.docker ./.env.local +# Копирование исходного кода +COPY frontend/ ./ + # Сборка приложения RUN npm run build @@ -21,4 +21,4 @@ RUN npm run build EXPOSE 3000 # Запуск приложения -CMD ["npm", "start"] \ No newline at end of file +CMD ["sh", "-c", "npm run build && npm start"] \ No newline at end of file diff --git a/Logo DRESSED FOR SUCCESS/DFS_chik&active.svg b/Logo DRESSED FOR SUCCESS/DFS_chik&active.svg new file mode 100644 index 0000000..5a9e546 --- /dev/null +++ b/Logo DRESSED FOR SUCCESS/DFS_chik&active.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo DRESSED FOR SUCCESS/DFS_home&sleep.svg b/Logo DRESSED FOR SUCCESS/DFS_home&sleep.svg new file mode 100644 index 0000000..561a369 --- /dev/null +++ b/Logo DRESSED FOR SUCCESS/DFS_home&sleep.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Logo DRESSED FOR SUCCESS/Dressed for Success.cdr b/Logo DRESSED FOR SUCCESS/Dressed for Success.cdr index 813806a..8d19762 100644 Binary files a/Logo DRESSED FOR SUCCESS/Dressed for Success.cdr and b/Logo DRESSED FOR SUCCESS/Dressed for Success.cdr differ diff --git a/Logo DRESSED FOR SUCCESS/Резервная_копия_Dressed for Success.cdr b/Logo DRESSED FOR SUCCESS/Резервная_копия_Dressed for Success.cdr new file mode 100644 index 0000000..813806a Binary files /dev/null and b/Logo DRESSED FOR SUCCESS/Резервная_копия_Dressed for Success.cdr differ diff --git a/backend/.DS_Store b/backend/.DS_Store index c5f9229..14adb56 100644 Binary files a/backend/.DS_Store and b/backend/.DS_Store differ diff --git a/backend/.env.docker b/backend/.env.docker index f7aea9f..e8c8b23 100644 --- a/backend/.env.docker +++ b/backend/.env.docker @@ -10,5 +10,5 @@ SECRET_KEY=supersecretkey FRONTEND_URL=http://frontend:3000 # Настройки Meilisearch -MEILISEARCH_URL=http://localhost:7700 +MEILISEARCH_URL=http://meilisearch:7700 MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM diff --git a/backend/Dockerfile b/backend/Dockerfile index c0a05eb..aab9eca 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,10 +1,26 @@ -FROM python:3.10-slim +# Используем официальный образ с Uvicorn+Gunicorn, оптимизированный для FastAPI +FROM tiangolo/uvicorn-gunicorn-fastapi:python3.11 WORKDIR /app -COPY requirements.txt . +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt -RUN pip install --no-cache-dir -r requirements.txt +COPY ./app ./app -# Для разработки код монтируется через volumes, а в продакшн-билде можно добавить COPY . /app -CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] # hot-reload \ No newline at end of file +EXPOSE 8000 + +# Точка входа: стандартный CMD из базового образа запустит Gunicorn+Uvicorn + + + +# FROM python:3.10-slim + +# WORKDIR /app + +# COPY requirements.txt . + +# RUN pip install --no-cache-dir -r requirements.txt + +# # Для разработки код монтируется через volumes, а в продакшн-билде можно добавить COPY . /app +# CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"] # hot-reload \ No newline at end of file diff --git a/backend/alembic/.DS_Store b/backend/alembic/.DS_Store index 87e1d7d..cb22d8d 100644 Binary files a/backend/alembic/.DS_Store and b/backend/alembic/.DS_Store differ diff --git a/backend/app/.DS_Store b/backend/app/.DS_Store index 8e8e357..c55c872 100644 Binary files a/backend/app/.DS_Store and b/backend/app/.DS_Store differ diff --git a/backend/docs/README.md b/backend/docs/README.md index 1374486..7773f84 100644 --- a/backend/docs/README.md +++ b/backend/docs/README.md @@ -3,7 +3,8 @@ ## Содержание 1. [API документация](api_documentation.md) - Подробное описание всех API эндпоинтов -2. [Структура базы данных](database_structure.md) - Описание таблиц и связей в базе данных +2. [API заказов](orders_api.md) - Подробное описание API заказов +3. [Структура базы данных](database_structure.md) - Описание таблиц и связей в базе данных ## Технологический стек @@ -114,4 +115,4 @@ alembic upgrade head После запуска приложения, документация API доступна по адресу: - Swagger UI: http://localhost:8000/docs -- ReDoc: http://localhost:8000/redoc \ No newline at end of file +- ReDoc: http://localhost:8000/redoc \ No newline at end of file diff --git a/backend/docs/api_documentation.md b/backend/docs/api_documentation.md index e841fd2..bf4542a 100644 --- a/backend/docs/api_documentation.md +++ b/backend/docs/api_documentation.md @@ -113,11 +113,14 @@ Базовый URL: `/orders` +Подробная документация по API заказов доступна в отдельном файле: [Документация API заказов](orders_api.md) + | Метод | Эндпоинт | Описание | Параметры запроса | Требуется авторизация | Формат ответа | |-------|----------|----------|-------------------|------------------------|------------------| | GET | `/` | Получение списка заказов | `skip`, `limit`, `status` | Да | `[{"id": number, "status": string, "total_amount": number, ...}]` | | GET | `/{order_id}` | Получение информации о заказе | - | Да | `{"success": true, "order": {...}}` | -| POST | `/` | Создание нового заказа | `OrderCreate` (shipping_address_id, payment_method, notes, cart_items, items) | Да | `{"success": true, "order": {...}}` | +| POST | `/new` | Создание нового заказа (новый формат) | `OrderCreateNew` (user_info, delivery, items, payment_method, comment) | Нет | `{"success": true, "message": "...", "order": {...}}` | +| POST | `/` | Создание нового заказа (старый формат) | `OrderCreate` (shipping_address_id, payment_method, notes, cart_items, items) | Нет | `{"success": true, "order": {...}}` | | PUT | `/{order_id}` | Обновление заказа | `OrderUpdate` (status, shipping_address_id, payment_method, payment_details, tracking_number, notes) | Да (админ) | `{"success": true, "order": {...}}` | | POST | `/{order_id}/cancel` | Отмена заказа | - | Да | `{"success": true, "message": "..."}` | @@ -191,8 +194,9 @@ ### Корзина и заказы - `CartItemCreate`: variant_id, quantity - `CartItemUpdate`: quantity +- `OrderCreateNew`: user_info (first_name, last_name, email, phone), delivery (method, address, cdek_info), items (product_id, variant_id, quantity, price), payment_method, comment - `OrderCreate`: shipping_address_id, payment_method, notes, cart_items, items -- `OrderUpdate`: status, shipping_address_id, payment_method, payment_details, tracking_number, notes +- `OrderUpdate`: status, shipping_address_id, payment_method, payment_details, tracking_number, notes, delivery_method, city, delivery_address, cdek_info, courier_info, user_info_json ### Отзывы - `ReviewCreate`: product_id, rating, text diff --git a/backend/docs/database_structure.md b/backend/docs/database_structure.md index d6146a3..85e93fe 100644 --- a/backend/docs/database_structure.md +++ b/backend/docs/database_structure.md @@ -128,11 +128,20 @@ |------|-----|----------| | id | Integer | Первичный ключ | | user_id | Integer | Внешний ключ на таблицу users | -| status | String | Статус заказа (новый, оплачен, отправлен, доставлен, отменен) | +| status | String | Статус заказа (pending, processing, shipped, delivered, cancelled, refunded) | | total_amount | Float | Общая сумма заказа | -| shipping_address_id | Integer | Внешний ключ на таблицу addresses | -| payment_method | String | Способ оплаты | -| tracking_number | String | Номер отслеживания (опционально) | +| user_info_json | JSON | JSON с информацией о пользователе | +| delivery_method | String | Способ доставки (cdek, courier) | +| city | String | Город доставки | +| delivery_address | String | Адрес доставки (форматированный) | +| cdek_info | JSON | Информация о доставке CDEK | +| courier_info | JSON | Информация о курьерской доставке | +| shipping_address_id | Integer | Внешний ключ на таблицу addresses (для обратной совместимости) | +| payment_method | String | Способ оплаты (card, sbp) | +| payment_details | String | Детали оплаты | +| items_json | JSON | JSON со списком заказанных товаров | +| tracking_number | String | Номер отслеживания | +| notes | String | Примечания к заказу | | created_at | DateTime | Дата и время создания | | updated_at | DateTime | Дата и время обновления | @@ -207,4 +216,4 @@ 13. `product_variants` 1:N `order_items` (вариант продукта может быть в нескольких заказах) 14. `orders` 1:N `order_items` (заказ может содержать несколько товаров) -15. `addresses` 1:N `orders` (адрес может быть использован в нескольких заказах) \ No newline at end of file +15. `addresses` 1:N `orders` (адрес может быть использован в нескольких заказах) \ No newline at end of file diff --git a/backend/docs/orders_api.md b/backend/docs/orders_api.md new file mode 100644 index 0000000..b8bdd81 --- /dev/null +++ b/backend/docs/orders_api.md @@ -0,0 +1,594 @@ +# Документация API заказов + +## Содержание +1. [Общая информация](#общая-информация) +2. [Эндпоинты](#эндпоинты) + - [Получение списка заказов](#получение-списка-заказов) + - [Получение информации о заказе](#получение-информации-о-заказе) + - [Создание заказа (новый формат)](#создание-заказа-новый-формат) + - [Создание заказа (старый формат)](#создание-заказа-старый-формат) + - [Обновление заказа](#обновление-заказа) + - [Отмена заказа](#отмена-заказа) +3. [Модели данных](#модели-данных) + - [Структура заказа](#структура-заказа) + - [Структура элемента заказа](#структура-элемента-заказа) + - [Структура создания заказа (новый формат)](#структура-создания-заказа-новый-формат) + - [Структура создания заказа (старый формат)](#структура-создания-заказа-старый-формат) + - [Структура обновления заказа](#структура-обновления-заказа) + +## Общая информация + +Базовый URL: `/orders` + +API заказов позволяет: +- Получать список заказов пользователя +- Получать детальную информацию о заказе +- Создавать новые заказы +- Обновлять существующие заказы +- Отменять заказы + +## Эндпоинты + +### Получение списка заказов + +**Запрос:** +``` +GET /orders +``` + +**Параметры запроса:** +- `skip` (опционально): Количество пропускаемых записей (по умолчанию: 0) +- `limit` (опционально): Максимальное количество возвращаемых записей (по умолчанию: 100) +- `status` (опционально): Фильтр по статусу заказа (pending, processing, shipped, delivered, cancelled, refunded) + +**Требуется авторизация:** Да + +**Формат ответа:** +```json +[ + { + "id": 1, + "user_id": 123, + "status": "pending", + "total_amount": 5990.0, + "user_info_json": { + "first_name": "Иван", + "last_name": "Иванов", + "email": "ivan@example.com", + "phone": "+7 (999) 123-45-67" + }, + "delivery_method": "cdek", + "city": "Москва", + "delivery_address": "Москва, ул. Примерная, д. 1", + "cdek_info": { + "pvz": { + "code": "MSK123", + "address": "ул. Примерная, д. 1" + } + }, + "payment_method": "card", + "created_at": "2023-06-01T12:00:00", + "updated_at": "2023-06-01T12:00:00", + "items": [ + { + "id": 1, + "order_id": 1, + "variant_id": 123, + "quantity": 1, + "price": 5990.0, + "product_id": 456, + "product_name": "Платье летнее", + "product_image": "dress.jpg", + "variant_name": "M", + "size": "M", + "total_price": 5990.0 + } + ] + } +] +``` + +### Получение информации о заказе + +**Запрос:** +``` +GET /orders/{order_id} +``` + +**Параметры запроса:** +- `order_id`: ID заказа + +**Требуется авторизация:** Да + +**Формат ответа:** +```json +{ + "success": true, + "order": { + "id": 1, + "user_id": 123, + "status": "pending", + "total_amount": 5990.0, + "user_info_json": { + "first_name": "Иван", + "last_name": "Иванов", + "email": "ivan@example.com", + "phone": "+7 (999) 123-45-67" + }, + "delivery_method": "cdek", + "city": "Москва", + "delivery_address": "Москва, ул. Примерная, д. 1", + "cdek_info": { + "pvz": { + "code": "MSK123", + "address": "ул. Примерная, д. 1" + } + }, + "payment_method": "card", + "created_at": "2023-06-01T12:00:00", + "updated_at": "2023-06-01T12:00:00", + "items": [ + { + "id": 1, + "order_id": 1, + "variant_id": 123, + "quantity": 1, + "price": 5990.0, + "product_id": 456, + "product_name": "Платье летнее", + "product_image": "dress.jpg", + "variant_name": "M", + "size": "M", + "total_price": 5990.0 + } + ] + } +} +``` + +### Создание заказа (новый формат) + +**Запрос:** +``` +POST /orders/new +``` + +**Тело запроса:** +```json +{ + "user_info": { + "first_name": "Иван", + "last_name": "Иванов", + "email": "ivan@example.com", + "phone": "+7 (999) 123-45-67" + }, + "delivery": { + "method": "cdek", + "address": { + "city": "Москва", + "street": "Примерная", + "house": "1", + "apartment": "123", + "postal_code": "123456", + "formatted_address": "Москва, ул. Примерная, д. 1, кв. 123" + }, + "cdek_info": { + "pvz": { + "code": "MSK123", + "city_code": 44, + "city": "Москва", + "address": "ул. Примерная, д. 1", + "work_time": "Пн-Вс 10:00-20:00", + "location": [37.123, 55.456] + }, + "tariff": { + "tariff_code": 136, + "tariff_name": "Посылка склад-склад", + "delivery_sum": 300, + "period_min": 1, + "period_max": 2 + }, + "delivery_type": "office" + } + }, + "items": [ + { + "product_id": 456, + "variant_id": 123, + "quantity": 1, + "price": 5990.0 + } + ], + "payment_method": "card", + "comment": "Позвоните перед доставкой" +} +``` + +**Требуется авторизация:** Нет (опционально) + +**Формат ответа:** +```json +{ + "success": true, + "message": "Заказ успешно создан", + "order_id": 1, + "order": { + "id": 1, + "user_id": 123, + "status": "pending", + "total_amount": 5990.0, + "user_info_json": { + "first_name": "Иван", + "last_name": "Иванов", + "email": "ivan@example.com", + "phone": "+7 (999) 123-45-67" + }, + "delivery_method": "cdek", + "city": "Москва", + "delivery_address": "Москва, ул. Примерная, д. 1, кв. 123", + "cdek_info": { + "pvz": { + "code": "MSK123", + "city_code": 44, + "city": "Москва", + "address": "ул. Примерная, д. 1", + "work_time": "Пн-Вс 10:00-20:00", + "location": [37.123, 55.456] + }, + "tariff": { + "tariff_code": 136, + "tariff_name": "Посылка склад-склад", + "delivery_sum": 300, + "period_min": 1, + "period_max": 2 + }, + "delivery_type": "office" + }, + "payment_method": "card", + "notes": "Позвоните перед доставкой", + "created_at": "2023-06-01T12:00:00", + "updated_at": "2023-06-01T12:00:00", + "items": [ + { + "id": 1, + "order_id": 1, + "variant_id": 123, + "quantity": 1, + "price": 5990.0, + "product_id": 456, + "product_name": "Платье летнее", + "product_image": "dress.jpg", + "variant_name": "M", + "size": "M", + "total_price": 5990.0 + } + ] + }, + "user_message": "Для вас был создан аккаунт с email: ivan@example.com" +} +``` + +### Создание заказа (старый формат) + +**Запрос:** +``` +POST /orders +``` + +**Тело запроса:** +```json +{ + "shipping_address_id": 1, + "payment_method": "card", + "notes": "Позвоните перед доставкой", + "cart_items": [1, 2, 3], + "items": [ + { + "variant_id": 123, + "quantity": 1 + } + ] +} +``` + +**Требуется авторизация:** Нет (опционально) + +**Формат ответа:** +```json +{ + "success": true, + "message": "Заказ успешно создан", + "order": { + "id": 1, + "user_id": 123, + "status": "pending", + "total_amount": 5990.0, + "shipping_address_id": 1, + "shipping_address": { + "id": 1, + "address_line1": "ул. Примерная, д. 1", + "address_line2": "кв. 123", + "city": "Москва", + "state": "", + "postal_code": "123456", + "country": "Россия", + "is_default": true + }, + "payment_method": "card", + "notes": "Позвоните перед доставкой", + "created_at": "2023-06-01T12:00:00", + "updated_at": "2023-06-01T12:00:00", + "items": [ + { + "id": 1, + "order_id": 1, + "variant_id": 123, + "quantity": 1, + "price": 5990.0, + "product_id": 456, + "product_name": "Платье летнее", + "product_image": "dress.jpg", + "variant_name": "M", + "size": "M", + "total_price": 5990.0 + } + ] + } +} +``` + +### Обновление заказа + +**Запрос:** +``` +PUT /orders/{order_id} +``` + +**Тело запроса:** +```json +{ + "status": "cancelled", + "shipping_address_id": 2, + "payment_method": "card", + "payment_details": "Оплата картой при получении", + "tracking_number": "TRACK123456", + "notes": "Новый комментарий к заказу" +} +``` + +**Требуется авторизация:** Да (обычные пользователи могут только отменять заказы) + +**Формат ответа:** +```json +{ + "success": true, + "message": "Заказ успешно обновлен", + "order": { + "id": 1, + "user_id": 123, + "status": "cancelled", + "total_amount": 5990.0, + "shipping_address_id": 2, + "shipping_address": { + "id": 2, + "address_line1": "ул. Новая, д. 2", + "address_line2": "кв. 456", + "city": "Москва", + "state": "", + "postal_code": "654321", + "country": "Россия", + "is_default": false + }, + "payment_method": "card", + "payment_details": "Оплата картой при получении", + "tracking_number": "TRACK123456", + "notes": "Новый комментарий к заказу", + "created_at": "2023-06-01T12:00:00", + "updated_at": "2023-06-01T13:00:00", + "items": [ + { + "id": 1, + "order_id": 1, + "variant_id": 123, + "quantity": 1, + "price": 5990.0, + "product_id": 456, + "product_name": "Платье летнее", + "product_image": "dress.jpg", + "variant_name": "M", + "size": "M", + "total_price": 5990.0 + } + ] + } +} +``` + +### Отмена заказа + +**Запрос:** +``` +POST /orders/{order_id}/cancel +``` + +**Требуется авторизация:** Да + +**Формат ответа:** +```json +{ + "success": true, + "message": "Заказ успешно отменен", + "order": { + "id": 1, + "user_id": 123, + "status": "cancelled", + "total_amount": 5990.0, + "shipping_address_id": 1, + "shipping_address": { + "id": 1, + "address_line1": "ул. Примерная, д. 1", + "address_line2": "кв. 123", + "city": "Москва", + "state": "", + "postal_code": "123456", + "country": "Россия", + "is_default": true + }, + "payment_method": "card", + "notes": "Позвоните перед доставкой", + "created_at": "2023-06-01T12:00:00", + "updated_at": "2023-06-01T13:00:00", + "items": [ + { + "id": 1, + "order_id": 1, + "variant_id": 123, + "quantity": 1, + "price": 5990.0, + "product_id": 456, + "product_name": "Платье летнее", + "product_image": "dress.jpg", + "variant_name": "M", + "size": "M", + "total_price": 5990.0 + } + ] + } +} +``` + +## Модели данных + +### Структура заказа + +| Поле | Тип | Описание | +|------|-----|----------| +| id | Integer | Первичный ключ | +| user_id | Integer | Внешний ключ на таблицу users | +| status | String | Статус заказа (pending, processing, shipped, delivered, cancelled, refunded) | +| total_amount | Float | Общая сумма заказа | +| user_info_json | JSON | JSON с информацией о пользователе | +| delivery_method | String | Способ доставки (cdek, courier) | +| city | String | Город доставки | +| delivery_address | String | Адрес доставки (форматированный) | +| cdek_info | JSON | Информация о доставке CDEK | +| courier_info | JSON | Информация о курьерской доставке | +| shipping_address_id | Integer | Внешний ключ на таблицу addresses (для обратной совместимости) | +| payment_method | String | Способ оплаты (card, sbp) | +| payment_details | String | Детали оплаты | +| items_json | JSON | JSON со списком заказанных товаров | +| tracking_number | String | Номер отслеживания | +| notes | String | Примечания к заказу | +| created_at | DateTime | Дата и время создания | +| updated_at | DateTime | Дата и время обновления | + +### Структура элемента заказа + +| Поле | Тип | Описание | +|------|-----|----------| +| id | Integer | Первичный ключ | +| order_id | Integer | Внешний ключ на таблицу orders | +| variant_id | Integer | Внешний ключ на таблицу product_variants | +| quantity | Integer | Количество товара | +| price | Float | Цена товара на момент заказа | +| created_at | DateTime | Дата и время создания | + +### Структура создания заказа (новый формат) + +```typescript +interface UserInfo { + first_name: string; + last_name: string; + email: string; + phone: string; +} + +interface Address { + city: string; + street: string; + house: string; + apartment?: string; + postal_code?: string; + formatted_address?: string; +} + +interface CdekPvz { + code: string; + city_code: number; + city: string; + address: string; + work_time?: string; + location?: [number, number]; +} + +interface CdekTariff { + tariff_code: number; + tariff_name: string; + delivery_sum: number; + period_min: number; + period_max: number; +} + +interface CdekInfo { + pvz: CdekPvz; + tariff: CdekTariff; + delivery_type: string; +} + +interface DeliveryInfo { + method: string; + address: Address; + cdek_info?: CdekInfo; +} + +interface OrderItem { + product_id: number; + variant_id: number; + quantity: number; + price: number; +} + +interface OrderCreateNew { + user_info: UserInfo; + delivery: DeliveryInfo; + items: OrderItem[]; + payment_method: string; + comment?: string; +} +``` + +### Структура создания заказа (старый формат) + +```typescript +interface OrderItemCreate { + variant_id: number; + quantity: number; +} + +interface OrderCreate { + shipping_address_id?: number; + payment_method?: string; + notes?: string; + cart_items?: number[]; + items?: OrderItemCreate[]; +} +``` + +### Структура обновления заказа + +```typescript +interface OrderUpdate { + status?: string; + shipping_address_id?: number; + payment_method?: string; + payment_details?: string; + tracking_number?: string; + notes?: string; + delivery_method?: string; + city?: string; + delivery_address?: string; + cdek_info?: any; + courier_info?: any; + user_info_json?: any; +} +``` diff --git a/backend/requirements-new.txt b/backend/requirements_new.txt similarity index 100% rename from backend/requirements-new.txt rename to backend/requirements_new.txt diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index c4fa331..d7189cd 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -9,19 +9,24 @@ services: expose: - "8000" environment: - # - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db + - DATABASE_URL=postgresql://postgres:postgres@postgres:5432/shop_db - DEBUG=0 - SECRET_KEY=${SECRET_KEY:-supersecretkey} - UPLOAD_DIRECTORY=/app/uploads + - MEILISEARCH_URL=http://meilisearch:7700 + - MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM depends_on: postgres: condition: service_healthy + meilisearch: + condition: service_started volumes: - ./backend/uploads:/app/uploads networks: app_network: aliases: - backend + - fastapi restart: always healthcheck: test: ["CMD", "curl", "--fail", "http://localhost:8000/" ] @@ -42,6 +47,9 @@ services: - NEXT_PUBLIC_API_URL=https://${DOMAIN_NAME}/api - NEXT_PUBLIC_BASE_URL=https://${DOMAIN_NAME} - NODE_ENV=production + volumes: + - frontend_next_prod:/app/.next # сохраняем сборку в отдельном томе + command: sh -c "npm run build && npm start" # явно запускаем сборку перед стартом depends_on: backend: condition: service_healthy @@ -117,10 +125,38 @@ services: aliases: - redis + meilisearch: + image: getmeili/meilisearch:latest + container_name: dressed-for-success-meilisearch + hostname: meilisearch + expose: + - "7700" + environment: + - MEILI_MASTER_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM + - MEILI_NO_ANALYTICS=true + - MEILI_ENV=production + volumes: + - meilisearch_data:/data.ms + healthcheck: + test: ["CMD", "wget", "--spider", "http://localhost:7700/health"] + interval: 10s + timeout: 5s + retries: 3 + start_period: 15s + restart: always + networks: + app_network: + aliases: + - meilisearch + networks: app_network: driver: bridge volumes: postgres_data: + driver: local + meilisearch_data: + driver: local + frontend_next_prod: driver: local \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 0310f35..9c59af0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,50 +1,116 @@ -version: '3.8' # Compose Specification v3.8 +version: '3.8' services: + # --- Backend Service --- fastapi: build: - context: ./backend # директория с Dockerfile FastAPI - volumes: - - ./backend:/app # монтируем код для live-reload - command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # hot-reload - ports: - - "8000:8000" + context: ./backend + dockerfile: Dockerfile + # Не монтируем код в продакшене, он уже скопирован в образ + # volumes: + # - ./backend:/app + # Используем CMD из базового образа tiangolo/uvicorn-gunicorn-fastapi (Gunicorn + Uvicorn workers) + # command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # Это для разработки + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 + expose: + - "8000" # Внутренний порт, доступный другим сервисам в сети networks: - app-network + environment: + # Переменная для подключения к MeiliSearch внутри Docker-сети + - MEILISEARCH_URL=http://meilisearch:7700 + - MEILISEARCH_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM + # Добавьте сюда другие переменные окружения для бэкенда (ключи API, настройки БД и т.д.) + # - DATABASE_URL=... + # - SECRET_KEY=... + depends_on: + - meilisearch # Запускать после MeiliSearch + restart: unless-stopped + # --- Frontend Service --- + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + # Не монтируем исходный код в продакшене + # volumes: + # - ./frontend:/app + # Сохраняем .next для возможного ускорения перезапусков (опционально) + # volumes: + # - frontend_next:/app/.next + # Команда запускает уже собранное приложение + command: npm start + expose: + - "3000" # Внутренний порт для Next.js сервера + networks: + - app-network + environment: + # URL для API-запросов из БРАУЗЕРА (через Nginx) + # Браузер будет обращаться к /api/... на том же домене/хосте + - NEXT_PUBLIC_API_URL=/api + # URL для API-запросов со стороны СЕРВЕРА Next.js (SSR, API Routes) + # Используем внутреннее имя сервиса Docker для серверных запросов + - NEXT_SERVER_API_URL=http://fastapi/api + # URL для доступа к MinIO (для изображений) + - NEXT_PUBLIC_MINIO_URL=http://45.129.128.113:9000 + # Включаем режим отладки для логирования URL изображений + - NEXT_PUBLIC_DEBUG=true + # Убедитесь, что ваш Next.js код использует INTERNAL_API_URL для серверных запросов + # и NEXT_PUBLIC_API_URL (или просто относительный путь /api) для клиентских + depends_on: + - fastapi # Желательно запускать после бэкенда + restart: unless-stopped + + # --- PHP Service --- php: - image: php:8.2-apache # официальный PHP Apache образ + image: php:8.2-apache volumes: - - ./php:/var/www/html - ports: - - "8081:80" + - ./php:/var/www/html # Монтируем PHP скрипты + expose: + - "80" # Apache внутри контейнера слушает порт 80 networks: - app-network + restart: unless-stopped + # --- Nginx Reverse Proxy --- nginx: - image: nginx:stable + image: nginx:alpine volumes: - - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # монтируем конфиг NGINX + # Монтируем наш конфиг Nginx (только для чтения) + - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro + # Опционально: монтируем статику, если она не раздается через Next.js/FastAPI + # - ./static:/usr/share/nginx/html/static:ro ports: + # Единственная точка входа: порт 80 хоста -> порт 80 Nginx - "80:80" + # Если нужен HTTPS (рекомендуется), добавьте: + # - "443:443" + # И настройте SSL в nginx.conf, монтируя сертификаты depends_on: - fastapi + - frontend - php networks: - app-network + restart: always + # --- MeiliSearch Service --- meilisearch: image: getmeili/meilisearch:latest - container_name: dressed-for-success-meilisearch + container_name: meilisearch hostname: meilisearch - ports: - - "7700:7700" + # Не публикуем порт наружу, доступ только через FastAPI/Nginx + # ports: + # - "7700:7700" + expose: + - "7700" # Внутренний порт environment: + # ВАЖНО: Используйте СВОЙ надежный мастер-ключ! - MEILI_MASTER_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM - MEILI_NO_ANALYTICS=true - MEILI_ENV=production volumes: - - meilisearch_data:/meili_data + - meili_data:/data.ms # Сохранение данных MeiliSearch healthcheck: test: ["CMD", "wget", "--no-verbose", "--spider", "http://localhost:7700/health"] interval: 10s @@ -53,14 +119,88 @@ services: start_period: 15s restart: always networks: - app-network: - aliases: - - meilisearch - dns_search: . - -volumes: - meilisearch_data: + - app-network + # Алиас не обязателен, если имя сервиса совпадает с hostname + # aliases: + # - meilisearch +# --- Сеть для взаимодействия контейнеров --- networks: - app-network: # общая сеть для контейнеров + app-network: driver: bridge + +# --- Тома для сохранения данных --- +volumes: + meili_data: # Для данных MeiliSearch + driver: local + frontend_next: # Для кэша сборки Next.js (опционально) + driver: local + + + +# version: '3.8' # Compose Specification v3.8 + +# services: +# fastapi: +# build: +# context: ./backend # директория с Dockerfile FastAPI +# volumes: +# - ./backend:/app # монтируем код для live-reload +# command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload # hot-reload +# ports: +# - "8000:8000" +# networks: +# - app-network + +# php: +# image: php:8.2-apache # официальный PHP Apache образ +# volumes: +# - ./php:/var/www/html +# ports: +# - "8081:80" +# networks: +# - app-network + +# nginx: +# image: nginx:stable +# volumes: +# - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro # монтируем конфиг NGINX +# ports: +# - "80:80" +# depends_on: +# - fastapi +# - php +# networks: +# - app-network + +# meilisearch: +# image: getmeili/meilisearch:latest +# container_name: dressed-for-success-meilisearch +# hostname: meilisearch +# ports: +# - "7700:7700" +# environment: +# - MEILI_MASTER_KEY=dNmMmxymWpqTFyWSSFyg3xaGlEY6Pn7Ld-dyrnKRMzM +# - MEILI_NO_ANALYTICS=true +# - MEILI_ENV=production +# volumes: +# - meilisearch_data:/meili_data +# healthcheck: +# test: ["CMD", "wget", "--no-verbose", "--spider", "http://localhost:7700/health"] +# interval: 10s +# timeout: 5s +# retries: 3 +# start_period: 15s +# restart: always +# networks: +# app-network: +# aliases: +# - meilisearch +# dns_search: . + +# volumes: +# meilisearch_data: + +# networks: +# app-network: # общая сеть для контейнеров +# driver: bridge diff --git a/frontend/.DS_Store b/frontend/.DS_Store index e424019..972a85b 100644 Binary files a/frontend/.DS_Store and b/frontend/.DS_Store differ diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..e7dadb3 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,21 @@ +# Используем Node.js Alpine для сборки и запуска (один этап, без builder) +FROM node:20-alpine +WORKDIR /app + +# Копируем package.json и package-lock.json +COPY package*.json ./ + +# Устанавливаем зависимости +RUN npm ci --legacy-peer-deps + +# Копируем весь исходный код приложения +COPY . . + +# Собираем проект +RUN npm run build + +# Открываем порт для SSR сервера +EXPOSE 3000 + +# Запуск в режиме production +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/frontend/app/(main)/catalog/[slug]/page.tsx b/frontend/app/(main)/catalog/[slug]/page.tsx index 0203c24..5d2a2eb 100644 --- a/frontend/app/(main)/catalog/[slug]/page.tsx +++ b/frontend/app/(main)/catalog/[slug]/page.tsx @@ -1,6 +1,6 @@ "use client" -import { Suspense, useState, useEffect } from "react" +import React, { Suspense, useState, useEffect } from "react" import Link from "next/link" import { notFound } from "next/navigation" import { ArrowLeft, ChevronRight, Truck, RotateCcw, Heart } from "lucide-react" @@ -26,6 +26,10 @@ interface ProductPageProps { } export default function ProductPage({ params }: ProductPageProps) { + // Используем React.use() для доступа к свойствам params + const resolvedParams = React.use(params); + const slug = resolvedParams.slug; + const [product, setProduct] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -42,7 +46,7 @@ export default function ProductPage({ params }: ProductPageProps) { const fetchProduct = async () => { try { setLoading(true) - const productData = await catalogService.getProductBySlug(params.slug) + const productData = await catalogService.getProductBySlug(slug) if (!productData) { return notFound() } @@ -55,28 +59,28 @@ export default function ProductPage({ params }: ProductPageProps) { } } fetchProduct() - }, [params.slug]) + }, [slug]) return (
{/* Навигация и хлебные крошки */} -
- Вернуться в каталог {/* Хлебные крошки */} - - {product.category_name} @@ -198,7 +202,7 @@ export default function ProductPage({ params }: ProductPageProps) { {/* Описание товара */} - - Описание - Уход - - Описание отсутствует

)} - + {product.care_instructions ? ( typeof product.care_instructions === 'string' ? ( @@ -253,7 +257,7 @@ export default function ProductPage({ params }: ProductPageProps) {
{/* Информация о доставке и возврате */} -
- +
@@ -303,12 +307,12 @@ function ProductSkeleton() {
- +
- +
@@ -316,9 +320,9 @@ function ProductSkeleton() {
- + - +
@@ -329,9 +333,9 @@ function ProductSkeleton() {
- + - +
@@ -339,7 +343,7 @@ function ProductSkeleton() {
- +
diff --git a/frontend/app/(main)/page.tsx b/frontend/app/(main)/page.tsx index 4ab7e4e..d082c0d 100644 --- a/frontend/app/(main)/page.tsx +++ b/frontend/app/(main)/page.tsx @@ -49,7 +49,7 @@ export default function HomePage() { id: 2, name: "Chik & Active", description: "Комфортные комплекты для активного отдыха, спорта и расслабленных выходных.", - image: "/placeholder.svg?height=800&width=600", + image: "/images/home/drops/DFS_chik&active.svg", status: "Скоро", isAvailable: false, }, @@ -57,7 +57,7 @@ export default function HomePage() { id: 3, name: "Home & Sleep", description: "Уютные вещи для дома, отдыха и приятных сновидений.", - image: "/placeholder.svg?height=800&width=600", + image: "/images/home/drops/DFS_home&sleep.svg", status: "Скоро", isAvailable: false, }, @@ -92,7 +92,7 @@ export default function HomePage() { }} > Dressed for Success Collection {/* Изображение коллекции */} -
+
{collection.name}
@@ -430,9 +431,9 @@ export default function HomePage() {
-

+ {/*

Кристина, создательница бренда -

+

*/}
@@ -487,9 +488,9 @@ export default function HomePage() {
{/* Левая колонка - Фото */}
-
+
Платья AIR -
+
Блузы SHIK {/* Левая колонка - Фото */}
-
+
Шорты IBIZA s.value === status) || + return ORDER_STATUSES.find(s => s.value === status) || { value: status, label: status, color: 'bg-gray-100 text-gray-800' }; } @@ -171,6 +171,10 @@ function formatDate(dateString?: string): string { export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { const router = useRouter(); + // Используем React.use() для доступа к свойствам params + const resolvedParams = React.use(params); + const orderId = resolvedParams.id; + const [order, setOrder] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); @@ -195,16 +199,16 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { setError(null); try { - const orderId = parseInt(params.id); - if (isNaN(orderId)) { + const id = parseInt(orderId); + if (isNaN(id)) { throw new Error('Неверный ID заказа'); } - const response = await api.get(`/orders/${orderId}`); - + const response = await api.get(`/orders/${id}`); + // Обработка ответа API let orderData: Order | null = null; - + if (response && typeof response === 'object') { if ('success' in response && response.success && 'order' in response) { // Если API вернул объект вида { success: true, order: {...} } @@ -220,7 +224,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { orderData = response as Order; } } - + if (orderData) { // Адаптация полей варианта if (orderData.items) { @@ -232,7 +236,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { variant_color: item.color // Добавим обработку цвета, если он есть })); } - + setOrder(orderData); setUpdatedStatus(orderData.status || ''); setUpdatedTrackingNumber(orderData.tracking_number || ''); @@ -251,9 +255,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { // Обновление заказа const updateOrder = async () => { if (!order) return; - + setIsActionLoading(true); - + try { // В реальности здесь был бы запрос к API // const response = await api.patch(`/orders/${order.id}`, { @@ -261,7 +265,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { // tracking_number: updatedTrackingNumber, // notes: updatedNotes // }); - + // Имитация успешного обновления const updatedOrder = { ...order, @@ -270,9 +274,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { notes: updatedNotes, updated_at: new Date().toISOString() }; - + setOrder(updatedOrder); - + // Добавляем запись в историю заказа об обновлении статуса if (updatedStatus !== order.status) { const newHistoryItem: OrderHistoryItem = { @@ -283,29 +287,29 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { created_at: new Date().toISOString(), created_by: 'Администратор' }; - + setOrderHistory([...orderHistory, newHistoryItem]); } - + // При обновлении отслеживания также добавляем запись в историю if (updatedTrackingNumber && updatedTrackingNumber !== order.tracking_number) { const trackingHistoryItem: OrderHistoryItem = { id: Date.now() + 1, // уникальный id order_id: order.id, status: updatedStatus, - comment: order.tracking_number - ? `Номер отслеживания изменен на "${updatedTrackingNumber}"` + comment: order.tracking_number + ? `Номер отслеживания изменен на "${updatedTrackingNumber}"` : `Добавлен номер отслеживания "${updatedTrackingNumber}"`, created_at: new Date().toISOString(), created_by: 'Администратор' }; - + setOrderHistory([...orderHistory, trackingHistoryItem]); } - + setIsEditing(false); toast.success('Заказ успешно обновлен'); - + // Если статус изменился на "Отправлен", предложить отправить письмо клиенту if (updatedStatus === 'shipped' && order.status !== 'shipped') { // Разместите эту функциональность в следующем релизе @@ -329,46 +333,46 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { // Обработка объекта адреса в разных форматах let addressLines = []; - + if (address.address_line1) { addressLines.push(address.address_line1); } - + if (address.address_line2) { addressLines.push(address.address_line2); } - + if (address.city || address.state || address.postal_code) { let cityLine = [address.city, address.state, address.postal_code].filter(Boolean).join(', '); if (cityLine) addressLines.push(cityLine); } - + if (address.country) { addressLines.push(address.country); } - + // Если нет данных адреса в стандартных полях, попробуем другие форматы if (addressLines.length === 0) { if (address.full_address) { return address.full_address; } - + if (address.street) { addressLines.push(`${address.street} ${address.house || ''} ${address.apartment ? `, кв. ${address.apartment}` : ''}`); } - + if (address.city || address.postal_code) { addressLines.push(`${address.city || ''} ${address.postal_code || ''}`); } } - + return addressLines.join(', '); }; // Отправка письма клиенту со статусом заказа const handleSendEmail = async (type: 'status' | 'invoice' | 'tracking') => { if (!order) return; - + setIsActionLoading(true); try { // Подготовка данных для отправки в зависимости от типа письма @@ -376,13 +380,13 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { order_id: order.id, email_type: type }; - + // В реальном приложении вызов API для отправки письма // const response = await api.post(`/orders/${order.id}/send-email`, emailData); - + // Имитация запроса к API await new Promise(resolve => setTimeout(resolve, 1000)); - + toast.success(`Письмо успешно отправлено на ${order.user_email || 'email клиента'}`); } catch (error) { console.error('Ошибка при отправке письма:', error); @@ -391,11 +395,11 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { setIsActionLoading(false); } }; - + // Копирование информации о заказе в буфер обмена const handleCopyOrderInfo = () => { if (!order) return; - + const orderInfo = ` Заказ #${order.id} Клиент: ${order.user_name || `Пользователь #${order.user_id}`} @@ -404,16 +408,16 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { Дата создания: ${formatDate(order.created_at)} Общая сумма: ${formatPrice(order.total_amount)} `; - + navigator.clipboard.writeText(orderInfo) .then(() => toast.success('Информация о заказе скопирована в буфер обмена')) .catch(() => toast.error('Не удалось скопировать информацию')); }; - + // Обновление данных заказа const handleRefreshOrder = async () => { if (!order) return; - + setIsActionLoading(true); try { await loadOrder(); // Используем существующую функцию загрузки заказа @@ -430,7 +434,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { const getOrderProgress = () => { const statuses = ['pending', 'processing', 'shipped', 'delivered']; const currentIndex = statuses.indexOf(order?.status || ''); - + if (currentIndex === -1) return 0; return (currentIndex / (statuses.length - 1)) * 100; }; @@ -438,11 +442,11 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { // Загрузка истории заказа const loadOrderHistory = async () => { if (!order) return; - + try { // В реальном приложении здесь будет запрос к API // const response = await api.get(`/orders/${order.id}/history`); - + // Имитация данных истории заказа для примера const mockHistory: OrderHistoryItem[] = [ { @@ -454,7 +458,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { created_by: 'Система' } ]; - + // Добавляем запись об обновлении статуса, если заказ был обновлен if (order.updated_at && order.updated_at !== order.created_at) { mockHistory.push({ @@ -466,14 +470,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { created_by: 'Администратор' }); } - + setOrderHistory(mockHistory); } catch (error) { console.error('Ошибка при загрузке истории заказа:', error); // Не показываем ошибку пользователю, так как это не критичная функция } }; - + // Загружаем историю при изменении заказа useEffect(() => { if (order) { @@ -484,20 +488,20 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { // Использование функции в useEffect useEffect(() => { loadOrder(); - }, [params.id]); + }, [orderId]); // Добавление комментария к истории заказа const addComment = async () => { if (!order || !newComment.trim()) return; - + setIsCommentLoading(true); - + try { // В реальном приложении здесь будет запрос к API // const response = await api.post(`/orders/${order.id}/history`, { // comment: newComment, // }); - + // Имитация успешного ответа const newHistoryItem: OrderHistoryItem = { id: Date.now(), // временный id @@ -507,14 +511,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { created_at: new Date().toISOString(), created_by: 'Администратор' }; - + // Добавляем новый комментарий в историю setOrderHistory([...orderHistory, newHistoryItem]); - + // Сбрасываем форму и закрываем диалог setNewComment(''); setCommentDialogOpen(false); - + toast.success('Комментарий добавлен к истории заказа'); } catch (error) { console.error('Ошибка при добавлении комментария:', error); @@ -527,16 +531,16 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { // Экспорт истории заказа в PDF const exportOrderHistory = async () => { if (!order || orderHistory.length === 0) return; - + setIsExporting(true); - + try { // В реальном приложении здесь был бы код для генерации PDF // Например, с использованием библиотеки jsPDF или запрос к бэкенду - + // Имитация экспорта для демонстрации await new Promise(resolve => setTimeout(resolve, 1500)); - + toast.success('История заказа экспортирована'); } catch (error) { console.error('Ошибка при экспорте истории заказа:', error); @@ -549,14 +553,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { // Печать информации о заказе const printOrder = () => { if (!order) return; - + // Открываем новое окно для печати const printWindow = window.open('', '_blank'); if (!printWindow) { toast.error('Не удалось открыть окно печати. Проверьте настройки блокировки всплывающих окон в браузере.'); return; } - + // Формируем HTML для печати const printContent = ` @@ -587,7 +591,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {

Заказ #${order.id}

${new Date().toLocaleDateString('ru-RU')}
- +
Дата создания:
@@ -613,7 +617,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
${order.user_email}
` : ''}
- +

Товары

@@ -660,7 +664,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
- + ${order.shipping_address ? `

Адрес доставки

@@ -673,11 +677,11 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { ].filter(Boolean).join(', ')}
${order.shipping_address.country || ''}
` : ''} - + ${order.notes ? `

Примечания

${order.notes.replace(/\n/g, '
')}
` : ''} - + - + `; - + // Записываем HTML в новое окно и запускаем печать printWindow.document.write(printContent); printWindow.document.close(); - + // Даем время для загрузки содержимого перед печатью setTimeout(() => { printWindow.focus(); @@ -709,19 +713,19 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { // Функция для отправки уведомления клиенту const sendNotification = async () => { if (!order || !notifyEmail.trim() || !notifyMessage.trim()) return; - + setIsNotifyLoading(true); - + try { // В реальности здесь был бы запрос к API // await api.post(`/orders/${order.id}/notify`, { // email: notifyEmail, // message: notifyMessage // }); - + // Имитация успешной отправки await new Promise(resolve => setTimeout(resolve, 1000)); - + // Добавляем запись в историю заказа const notifyHistoryItem: OrderHistoryItem = { id: Date.now(), @@ -731,13 +735,13 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { created_at: new Date().toISOString(), created_by: 'Администратор' }; - + setOrderHistory([...orderHistory, notifyHistoryItem]); - + // Закрываем диалог и сбрасываем форму setNotifyDialogOpen(false); setNotifyMessage(''); - + toast.success('Уведомление отправлено клиенту'); } catch (error) { console.error('Ошибка при отправке уведомления:', error); @@ -755,14 +759,14 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { delivered: `Уважаемый клиент!\n\nВаш заказ №${order?.id} доставлен. Спасибо за покупку!\nБудем рады видеть вас снова.\n\nС уважением,\nКоманда магазина`, cancelled: `Уважаемый клиент!\n\nК сожалению, ваш заказ №${order?.id} был отменен. Если у вас есть вопросы, пожалуйста, свяжитесь с нами.\n\nС уважением,\nКоманда магазина` }; - + return templates[status] || `Уважаемый клиент!\n\nВаш заказ №${order?.id} обновлен. Статус заказа: ${getStatusInfo(status).label}.\n\nС уважением,\nКоманда магазина`; }; // Открытие диалога уведомления с предзаполненными данными const openNotifyDialog = () => { if (!order) return; - + setNotifyEmail(order.user_email || ''); setNotifyMessage(prepareNotificationTemplate(order.status)); setNotifyDialogOpen(true); @@ -791,7 +795,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { Вернуться к списку заказов
- + Ошибка @@ -820,10 +824,10 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { Вернуться к списку заказов - +
- - +
- + {/* Действия с заказом */}
- - - + - + {/* ...другие кнопки действий... */}
- + {/* Заголовок заказа */} @@ -916,7 +920,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
{order?.status && } - + {order?.status && !['cancelled', 'refunded'].includes(order.status) && (
@@ -925,8 +929,8 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { {Math.round(getOrderProgress())}%
-
@@ -940,7 +944,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
)} - + {/* Предупреждение для отмененных заказов */} {order?.status === 'cancelled' && (
@@ -954,7 +958,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
)} - + {/* Уведомление для доставленных заказов */} {order?.status === 'delivered' && (
@@ -967,7 +971,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
)} - + {/* Клиент и адрес доставки */} - +
- - -
- + {/* Детали заказа */}
{/* Общая информация о заказе */} @@ -1176,7 +1180,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
- + {/* Информация об оплате */} @@ -1212,7 +1216,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
- + {/* Информация о доставке */} @@ -1233,9 +1237,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { Трекинг-номер: {order.tracking_number} -
-
- + {/* Товары в заказе */} @@ -1323,9 +1327,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
{item.product_image ? (
- {item.product_name
@@ -1364,7 +1368,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
- + {/* Мобильная версия списка товаров */}
{order.items.map((item) => ( @@ -1372,9 +1376,9 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
{item.product_image ? (
- {item.product_name
@@ -1412,7 +1416,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
))} - +
Итого: {formatPrice(order.total_amount)} @@ -1426,7 +1430,7 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) { )} - + {/* История заказа */} @@ -1436,8 +1440,8 @@ export default function OrderDetailsPage({ params }: OrderDetailsPageProps) {
{orderHistory.length > 0 && ( -