From 8b36ea16eff3186ea90b668965318ca854853918 Mon Sep 17 00:00:00 2001 From: Deploy Bot Date: Fri, 16 Jan 2026 11:36:00 +0200 Subject: [PATCH] =?UTF-8?q?v4.1.24:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D1=8B=20=D0=BF=D0=BE=D0=BB=D1=8F=20arve=5Fchecked?= =?UTF-8?q?=20=D0=B8=20arve=5Fmakstud?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FEAT: Добавлены поля arve_checked (int) и arve_makstud (string) в production_records - POST /api/records: INSERT с arve_checked, arve_makstud - PUT /api/records/:id: UPDATE с arve_checked, arve_makstud - Docker: docker-compose.prod.yml, ecosystem.config.cjs (PM2) - wrangler.toml → wrangler.jsonc - seed.sql: полные тестовые данные - test_browser.js: E2E тесты - Удалены старые HOTFIX-файлы (v4.1.11-v4.1.23) - Удалены data/*.sqlite из репозитория --- ACCESS_RIGHTS.md | 217 ---------- CHANGELOG.md | 232 ----------- DATE_FIELDS_LOGIC.md | 381 ------------------ Dockerfile | 68 ++-- FILES_TO_COPY.txt | 147 +++++++ HOTFIX_v4.1.11.md | 181 --------- HOTFIX_v4.1.12.md | 211 ---------- HOTFIX_v4.1.15.md | 196 --------- HOTFIX_v4.1.16.md | 172 -------- HOTFIX_v4.1.17.md | 204 ---------- HOTFIX_v4.1.18.md | 203 ---------- HOTFIX_v4.1.19.md | 138 ------- HOTFIX_v4.1.20.md | 240 ----------- HOTFIX_v4.1.21.md | 91 ----- HOTFIX_v4.1.22.md | 124 ------ HOTFIX_v4.1.23.md | 191 --------- HOTFIX_v4.1.24.md | 194 +++++++++ backup.sh | 80 ++++ ...470c66e69f6b33a31e3f5a0095cc6d18656.sqlite | Bin 200704 -> 0 bytes dist/_worker.js | 14 +- dist/original.html | 2 +- docker-compose-simple.yml | 20 - docker-compose.prod.yml | 42 ++ docker-compose.yml | 60 ++- docker-entrypoint.sh | 61 --- ecosystem.config.cjs | 19 + fix-docker.sh | 69 ---- public/original.html | 2 +- seed.sql | 57 +++ src/index.tsx | 14 +- test_browser.js | 35 ++ wrangler.jsonc | 16 + wrangler.toml | 9 - 33 files changed, 705 insertions(+), 2985 deletions(-) delete mode 100644 ACCESS_RIGHTS.md delete mode 100644 CHANGELOG.md delete mode 100644 DATE_FIELDS_LOGIC.md mode change 100755 => 100644 Dockerfile create mode 100644 FILES_TO_COPY.txt delete mode 100644 HOTFIX_v4.1.11.md delete mode 100644 HOTFIX_v4.1.12.md delete mode 100644 HOTFIX_v4.1.15.md delete mode 100644 HOTFIX_v4.1.16.md delete mode 100644 HOTFIX_v4.1.17.md delete mode 100644 HOTFIX_v4.1.18.md delete mode 100644 HOTFIX_v4.1.19.md delete mode 100644 HOTFIX_v4.1.20.md delete mode 100644 HOTFIX_v4.1.21.md delete mode 100644 HOTFIX_v4.1.22.md delete mode 100644 HOTFIX_v4.1.23.md create mode 100644 HOTFIX_v4.1.24.md create mode 100755 backup.sh delete mode 100755 data/v3/d1/miniflare-D1DatabaseObject/2b35d4d42e3c9f6b5ad5b5579a7b1470c66e69f6b33a31e3f5a0095cc6d18656.sqlite delete mode 100644 docker-compose-simple.yml create mode 100644 docker-compose.prod.yml mode change 100755 => 100644 docker-compose.yml delete mode 100755 docker-entrypoint.sh create mode 100644 ecosystem.config.cjs delete mode 100755 fix-docker.sh create mode 100644 seed.sql create mode 100644 test_browser.js create mode 100644 wrangler.jsonc delete mode 100644 wrangler.toml diff --git a/ACCESS_RIGHTS.md b/ACCESS_RIGHTS.md deleted file mode 100644 index dd994ff..0000000 --- a/ACCESS_RIGHTS.md +++ /dev/null @@ -1,217 +0,0 @@ -# 🔐 ПРАВА ДОСТУПА - МАТРИЦЫ РАЗРЕШЕНИЙ - -**Версия**: v4.1.17 -**Дата**: 2026-01-14 -**Обновление**: Уточнены права доступа User и Guest - ---- - -## 🔐 МАТРИЦА ПРАВ ДОСТУПА - РЕДАКТИРОВАНИЕ - -| Поле | Admin | User | Guest | -|------|-------|------|-------| -| **MAT-1** календарь | ✅ | ❌ | ❌ | -| **MAT-1** подтверждение | ✅ | ✅ | ❌ | -| **MAT-2** календарь | ✅ | ❌ | ❌ | -| **MAT-2** подтверждение | ✅ | ✅ | ❌ | -| **PAKETT** календарь | ✅ | ❌ | ❌ | -| **Töölehti** toggle | ✅ | ✅ | ❌ | -| **LÕIKUS** toggle | ✅ | ✅ | ❌ | -| **KLAAS** toggle | ✅ | ✅ | ❌ | -| **VALMIS** toggle | ✅* | ✅* | ❌ | -| **VÄLJAS** toggle | ✅* | ✅* | ❌ | - -*Блокируется при наличии проблем/ошибок - ---- - -## 👁️ МАТРИЦА ПРАВ ДОСТУПА - ПРОСМОТР - -| Поле | Admin | User | Guest | -|------|-------|------|-------| -| **MAT-1** дата | ✅ | ✅ | ✅ | -| **MAT-1** кнопка ✓ | ✅ | ✅ | ❌ | -| **MAT-2** дата | ✅ | ✅ | ✅ | -| **MAT-2** кнопка ✓ | ✅ | ✅ | ❌ | -| **PAKETT** дата | ✅ | ✅ | ✅ | -| **Töölehti** дата+статус | ✅ | ✅ | ✅ | -| **LÕIKUS** дата | ✅ | ✅ | ✅ | -| **KLAAS** дата | ✅ | ✅ | ✅ | -| **VALMIS** дата | ✅ | ✅ | ✅ | -| **VÄLJAS** дата | ✅ | ✅ | ✅ | - ---- - -## 👤 РОЛИ ПОЛЬЗОВАТЕЛЕЙ - -### **Admin** (admin, aknaproff) - -#### Редактирование: -- ✅ **MAT-1**: выбор даты через календарь -- ✅ **MAT-1**: подтверждение (кнопка ✓) -- ✅ **MAT-2**: выбор даты через календарь -- ✅ **MAT-2**: подтверждение (кнопка ✓) -- ✅ **PAKETT**: выбор даты через календарь -- ✅ **Töölehti**: полный контроль 3-шагового цикла -- ✅ **LÕIKUS**: toggle даты -- ✅ **KLAAS**: toggle даты -- ✅ **VALMIS**: toggle даты (если нет блокировки) -- ✅ **VÄLJAS**: toggle даты (если нет блокировки) -- ✅ Добавление записей -- ✅ Редактирование записей -- ✅ Удаление записей -- ✅ Изменение проблем и ошибок - -#### Просмотр: -- ✅ Все поля видны полностью - ---- - -### **User** (kasutaja) - -#### Редактирование: -- ❌ **MAT-1**: НЕТ доступа к календарю -- ✅ **MAT-1**: ЕСТЬ доступ к подтверждению (кнопка ✓) -- ❌ **MAT-2**: НЕТ доступа к календарю -- ✅ **MAT-2**: ЕСТЬ доступ к подтверждению (кнопка ✓) -- ❌ **PAKETT**: нет доступа к календарю -- ✅ **Töölehti**: полный контроль 3-шагового цикла -- ✅ **LÕIKUS**: toggle даты -- ✅ **KLAAS**: toggle даты -- ✅ **VALMIS**: toggle даты (если нет блокировки) -- ✅ **VÄLJAS**: toggle даты (если нет блокировки) -- ❌ Добавление/редактирование/удаление записей - -#### Просмотр: -- ✅ **MAT-1, MAT-2**: видит дату + кнопку подтверждения -- ✅ **PAKETT**: видит дату -- ✅ **Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS**: видит дату - ---- - -### **Guest** (неавторизованный пользователь) - -#### Редактирование: -- ❌ **Все поля**: нет доступа к редактированию - -#### Просмотр: -- ✅ **MAT-1, MAT-2**: видит только дату (БЕЗ кнопки подтверждения) -- ✅ **PAKETT**: видит дату -- ✅ **Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS**: видит дату - ---- - -## 📊 ДЕТАЛЬНОЕ ОПИСАНИЕ - -### **MAT-1 (Material)** -- **Admin**: открывает календарь + подтверждает -- **User**: только подтверждает (кнопка ✓) -- **Guest**: только просмотр даты - -### **MAT-2 (Material2)** -- **Admin**: открывает календарь + подтверждает -- **User**: только подтверждает (кнопка ✓) -- **Guest**: только просмотр даты -- **Блокировка**: если MAT-1 пустая - -### **PAKETT (Package)** -- **Admin**: открывает календарь -- **User**: только просмотр -- **Guest**: только просмотр - -### **Töölehti (Worksheets)** -- **Admin**: полный контроль 3-цикла -- **User**: полный контроль 3-цикла -- **Guest**: только просмотр - -### **LÕIKUS (Cutting)** -- **Admin**: toggle (добавить/удалить дату) -- **User**: toggle (добавить/удалить дату) -- **Guest**: только просмотр - -### **KLAAS (Glazing)** -- **Admin**: toggle (добавить/удалить дату) -- **User**: toggle (добавить/удалить дату) -- **Guest**: только просмотр - -### **VALMIS (Ready)** -- **Admin**: toggle (если нет проблем/ошибок) -- **User**: toggle (если нет проблем/ошибок) -- **Guest**: только просмотр -- **Блокировка**: при наличии проблем или ошибок - -### **VÄLJAS (Issued)** -- **Admin**: toggle (если нет проблем/ошибок) -- **User**: toggle (если нет проблем/ошибок) -- **Guest**: только просмотр -- **Блокировка**: при наличии проблем или ошибок - ---- - -## 🔑 ТЕХНИЧЕСКИЕ ДЕТАЛИ - -### **Backend проверка прав:** -```typescript -// optionalAuthMiddleware - разрешает Guest просмотр -// authMiddleware - требует авторизацию - -// Календари (MAT-1, MAT-2, PAKETT) -if (userRole !== 'admin') { - return c.json({ error: 'Only admin can change dates' }, 403) -} - -// Подтверждение (MAT-1, MAT-2) -// Разрешено всем авторизованным (Admin + User) - -// Toggle (Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS) -// Разрешено всем авторизованным (Admin + User) -``` - -### **Frontend проверка прав:** -```javascript -// canEditRecords() - проверяет наличие токена и роль -// Для Admin + User = true -// Для Guest = false - -// MAT-1/MAT-2 календарь -if (currentUser.role !== 'admin') { - // Скрыть календарь -} - -// MAT-1/MAT-2 подтверждение -if (!token) { - // Скрыть кнопку ✓ -} - -// Toggle поля -if (!canEditRecords()) { - // Отключить клик -} -``` - ---- - -## ✅ СВОДКА - -### **Что может Admin:** -- 📅 Выбирать даты через календарь (MAT-1, MAT-2, PAKETT) -- ✓ Подтверждать материалы -- 🔄 Управлять всеми toggle-полями -- ➕ Добавлять/редактировать/удалять записи - -### **Что может User:** -- ❌ НЕ может выбирать даты через календарь -- ✓ МОЖЕТ подтверждать материалы (MAT-1, MAT-2) -- 🔄 МОЖЕТ управлять toggle-полями (Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS) -- ❌ НЕ может добавлять/редактировать/удалять записи - -### **Что может Guest:** -- 👁️ Только просмотр всех дат -- ❌ НЕ видит кнопки подтверждения -- ❌ НЕ может редактировать ничего - ---- - -**Статус**: ✅ Права доступа корректны -**Версия**: v4.1.17 -**Дата**: 2026-01-14 diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 2e6eb63..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,232 +0,0 @@ -# AKNAPROFF Tootmine - CHANGELOG - -## v4.1.10 - 2025-12-31 (HOTFIX - КРИТИЧЕСКИЕ ИСПРАВЛЕНИЯ) - -### 🔥 Критические исправления - -#### 1. Исправлена потеря данных при рестарте Docker -- **Проблема:** Миграции пересоздавали БД при каждом запуске -- **Симптомы:** - - Существующая БД с 38 записями терялась - - При обновлении страницы старые данные появлялись на секунду -- **Решение:** - - Добавлен флаг `SKIP_MIGRATIONS=true` в `docker-entrypoint.sh` - - Добавлена переменная `SKIP_MIGRATIONS: "true"` в `docker-compose.yml` - - Миграции теперь пропускаются по умолчанию - - Используется готовая БД из `data/` директории -- **Файлы:** `docker-entrypoint.sh`, `docker-compose.yml` - -#### 2. Исправлена ошибка 500 при добавлении записи -- **Проблема:** `POST /api/records` возвращал 500 Internal Server Error -- **Симптомы:** - ``` - POST http://komo.aknaproff.ee:8180/api/records - [HTTP/1.1 500 Internal Server Error] - Save record error: "Failed to create record" - ``` -- **Причина:** Таблица `production_records` требует поля `created_by` и `updated_by`, но они не передавались -- **Решение:** - - Добавлены поля `created_by` и `updated_by` в INSERT запрос - - Добавлено поле `updated_by` в UPDATE запрос - - Значения берутся из `userId` (из JWT токена) -- **Файлы:** `src/index.tsx`, `dist/_worker.js` - -#### 3. Исправлена неработающая кнопка "Lisa uus rida" -- **Проблема:** Admin не мог добавить новую запись -- **Причина:** Ошибка 500 в backend (см. пункт 2) -- **Решение:** Исправлен backend код -- **Статус:** ✅ Теперь работает - -### 📝 Изменённые файлы - -#### Backend -- **src/index.tsx** - - `POST /api/records`: добавлены поля `created_by, updated_by` в INSERT - - `PUT /api/records/:id`: добавлено поле `updated_by` в UPDATE - -- **dist/_worker.js** - - Пересобран с исправлениями - -#### Docker -- **docker-entrypoint.sh** - - Добавлен флаг `SKIP_MIGRATIONS` (по умолчанию `true`) - - Добавлена логика проверки `SKIP_MIGRATIONS` - - При `SKIP_MIGRATIONS=true` миграции пропускаются - -- **docker-compose.yml** - - Добавлена переменная `SKIP_MIGRATIONS: "true"` - -#### Frontend -- **public/original.html** - - Cache version обновлена: `app.js?v=4.1.10` - -### ⚠️ Breaking Changes -Нет breaking changes. Полная обратная совместимость с v4.1.9. - -### 🔄 Миграция с v4.1.9 -1. Остановить контейнер: `docker-compose down` -2. **КРИТИЧНО:** Сделать бэкап: `cp -r data data.backup.$(date +%Y%m%d_%H%M%S)` -3. Заменить файлы (НЕ трогать `data/`) -4. Проверить `SKIP_MIGRATIONS: "true"` в `docker-compose.yml` -5. Запустить: `docker-compose up -d --build` -6. Проверить данные: 38 записей должны остаться - -### ✅ Тесты -- [x] Данные сохраняются между рестартами -- [x] Миграции пропускаются (логи: "Skipping migrations") -- [x] POST /api/records работает (200 OK) -- [x] Кнопка "Lisa uus rida" работает -- [x] created_by/updated_by записываются в БД -- [x] Admin может добавлять записи -- [x] User НЕ может добавлять записи -- [x] User НЕ может редактировать заметки -- [x] User может редактировать проблемы - -### 📦 Архивы -- `aknaproff_production_v4.1.10_arm.tar.gz` (278 KB) -- `aknaproff_production_v4.1.10_arm.zip` (318 KB) - ---- - -## v4.1.9 - 2025-12-30 - -### 🔒 Исправлены права доступа - -#### 1. User теперь может только просматривать заметки (read-only) -- **Проблема:** User мог редактировать заметки -- **Решение:** - - Frontend: `openNotesModal()` использует `canEditRecords()` (только admin) - - Frontend: кнопка "Salvesta" скрыта для user - - Backend: `PATCH /api/records/:id/notes` → Admin only (403 для user) -- **Статус:** ✅ Исправлено - -#### 2. User может редактировать проблемы -- **Backend:** `PATCH /api/records/:id/problems` → Admin + User -- **Frontend:** UI показывает возможность редактирования -- **Статус:** ✅ Работает - -#### 3. Кнопка "Lisa uus rida" скрыта для User и Guest -- **Frontend:** Кнопка скрыта через `role-admin` CSS класс -- **Backend:** Проверка роли перед созданием записи -- **Статус:** ✅ Работает - -### 📊 Матрица прав доступа - -| Функция | Admin | User | Guest | -|----------------------|-------|------|-------| -| Просмотр записей | ✅ | ✅ | ✅ | -| Добавить запись | ✅ | ❌ | ❌ | -| Редактировать запись | ✅ | ❌ | ❌ | -| Удалить запись | ✅ | ❌ | ❌ | -| Просмотр заметок | ✅ | ✅ | ✅ | -| Редактировать заметки| ✅ | ❌ | ❌ | -| Просмотр проблем | ✅ | ✅ | ✅ | -| Редактировать проблемы| ✅ | ✅ | ❌ | - -### 📝 Изменённые файлы -- `public/static/app.js`: изменены `openNotesModal()`, `saveNotes()` -- `src/index.tsx`: добавлены проверки роли в API -- `public/original.html`: cache v4.1.9 - ---- - -## v4.1.8 - 2025-12-30 - -### 🐛 Исправления - -#### 1. Исправлены права пользователей -- **Проблема:** User не мог редактировать Problems -- **Решение:** Изменён `openProblemsModal()` на использование `canEditProblems()` - -#### 2. Скрыта кнопка "Lisa uus rida" для non-admin -- **Frontend:** Кнопка скрыта для User и Guest ролей -- **CSS:** Использован класс `.role-admin` - -#### 3. Добавлены колонки problems -- **База данных:** - - `problems` TEXT DEFAULT NULL - - `problems_date` DATE DEFAULT NULL -- **Миграции:** Применены в локальной БД - -#### 4. Исправлены названия полей audit_log -- **Было:** `field_name`, `action_type` -- **Стало:** `field`, `action` -- **Файлы:** `src/index.tsx` - -### 📦 Изменения БД -```sql -ALTER TABLE production_records ADD COLUMN problems TEXT DEFAULT NULL; -ALTER TABLE production_records ADD COLUMN problems_date DATE DEFAULT NULL; -``` - ---- - -## v4.1.7 - 2025-12-30 - -### 🔐 Исправлена аутентификация - -#### 1. Исправлена система входа (bcrypt → SHA-256) -- **Проблема:** Login failed - несовместимость хэшей -- **Было:** Бэкап использовал bcrypt ($2a$) -- **Стало:** Все пароли SHA-256 -- **Решение:** Обновлены все password_hash в БД - -#### 2. Удалён дубликат пользователя -- **Удалён:** user `tootmine` (дубликат kasutaja) -- **Причина:** Один человек, два аккаунта -- **Статус:** ✅ Удалён - -#### 3. Добавлена колонка deleted_at -- **Таблица:** `users` -- **Тип:** `DATETIME DEFAULT NULL` -- **Причина:** Код использовал `WHERE deleted_at IS NULL`, но колонки не было -- **Статус:** ✅ Добавлена - -### 📊 База данных -- **Записей:** 38 production records -- **Пользователей:** 3 active - - `admin` / `demo123` (admin) - - `aknaproff` / `demo123` (admin) - - `kasutaja` / `tootmine` (user) - -### 🔒 Учётные данные -Все пароли теперь SHA-256: -``` -admin: d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791 -aknaproff: d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791 -kasutaja: a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f -``` - ---- - -## v4.1.6 и ранее - -См. файлы: -- `CHANGES_v4.1.6.md` -- `CHANGES_v4.1.5.md` -- `CHANGES_v4.1.4.md` -- `CHANGES_v4.1.3.md` -- `CHANGES_v4.1.2.md` -- `CHANGES_v4.1.0.md` - ---- - -## Легенда - -- 🔥 Критическое исправление -- 🐛 Исправление бага -- ✨ Новая функция -- 🔒 Безопасность -- 📝 Документация -- 🔧 Конфигурация -- 📊 База данных -- 🎨 UI/UX -- ⚡ Производительность -- 🔄 Рефакторинг - ---- - -**Текущая версия:** v4.1.10 -**Дата:** 2025-12-31 -**Статус:** Production Ready ✅ -**Архитектура:** ARM64/AMD64 (Synology Compatible) diff --git a/DATE_FIELDS_LOGIC.md b/DATE_FIELDS_LOGIC.md deleted file mode 100644 index 31ff002..0000000 --- a/DATE_FIELDS_LOGIC.md +++ /dev/null @@ -1,381 +0,0 @@ -# 📋 ЛОГИКА ДАТ И ЧЕКБОКСОВ - ПОЛНОЕ РУКОВОДСТВО - -**Версия**: v4.1.17 -**Дата**: 2026-01-14 -**Обновление**: Исправлены права доступа (см. ACCESS_RIGHTS.md) - ---- - -## ⚠️ **ВАЖНО: ПРАВА ДОСТУПА** - -**Детальная информация о правах доступа находится в файле:** -📄 **[ACCESS_RIGHTS.md](./ACCESS_RIGHTS.md)** - -**Краткая сводка:** -- **Admin**: полный доступ (календари + toggle) -- **User**: подтверждение MAT-1/MAT-2 + toggle всех полей -- **Guest**: только просмотр - ---- - -## 🔄 **TÖÖLEHTI (WORKSHEETS) - 3-ШАГОВЫЙ ЦИКЛ** - -### **Как работает:** - -#### **ШАГ 1: Пусто → Серый фон с датой** -- **Состояние**: Пустая ячейка `-` -- **Действие**: Клик по ячейке -- **Результат**: - - Появляется **текущая дата** - - Фон **СЕРЫЙ** (не подтверждено) - - `worksheets_date` = TODAY - - `worksheets_confirmed` = 0 - -#### **ШАГ 2: Серый → Зеленый фон (подтверждение)** -- **Состояние**: Дата с серым фоном -- **Действие**: Клик по ячейке -- **Результат**: - - Дата **остается** - - Фон становится **ЗЕЛЕНЫЙ** (подтверждено) - - `worksheets_date` = **сохраняется** - - `worksheets_confirmed` = 1 - -#### **ШАГ 3: Зеленый → Пусто (сброс)** -- **Состояние**: Дата с зеленым фоном -- **Действие**: Клик по ячейке -- **Результат**: - - Дата **удаляется** - - Ячейка становится **пустой** `-` - - `worksheets_date` = NULL - - `worksheets_confirmed` = 0 - -### **Визуальная схема:** -``` -┌─────────────┐ ┌──────────────────┐ ┌──────────────────┐ ┌─────────────┐ -│ ПУСТО │ Клик 1 │ СЕРЫЙ ФОН │ Клик 2 │ ЗЕЛЕНЫЙ ФОН │ Клик 3 │ ПУСТО │ -│ - │ ──────> │ 14.01.2026 │ ──────> │ 14.01.2026 │ ──────> │ - │ -│ │ │ (не подтверждено)│ │ (подтверждено) │ │ │ -└─────────────┘ └──────────────────┘ └──────────────────┘ └─────────────┘ - ^ │ - │ │ - └─────────────────────────────────────────────────────────────────────────────────────┘ - Цикл повторяется -``` - ---- - -## 🟢 **MAT-1 (MATERIAL) - КАЛЕНДАРЬ + ПОДТВЕРЖДЕНИЕ** - -### **Функционал:** -- **Календарь**: Клик по ячейке → открывается календарь -- **Выбор даты**: Выбрать дату → сохраняется `material_date` -- **Кнопка подтверждения** ✓: Клик → зеленая рамка - - `material_confirmed` = 1 - - Ячейка с зеленой рамкой - -### **Кто может изменять:** -- ✅ Admin -- ✅ Public User (все) - -### **База данных:** -- `status_checkboxes.material_date` (TEXT) -- `status_checkboxes.material_confirmed` (INTEGER, 0 или 1) - ---- - -## 🔒 **MAT-2 (MATERIAL2) - ЗАВИСИТ ОТ MAT-1** - -### **Блокировка:** -- **Заблокировано**: Если MAT-1 пустая -- **Показывается**: 🔒 с подсказкой "MAT-1 peab olema täidetud" -- **Разблокировано**: Когда MAT-1 заполнена - -### **Функционал (когда разблокирована):** -- **Календарь**: Клик → открывается календарь -- **Выбор даты**: Выбрать дату → сохраняется `material2_date` -- **Кнопка подтверждения** ✓: Клик → зеленая рамка - - `material2_confirmed` = 1 - -### **Кто может изменять:** -- ✅ Admin -- ✅ Public User (если MAT-1 заполнена) - -### **База данных:** -- `status_checkboxes.material2_date` (TEXT) -- `status_checkboxes.material2_confirmed` (INTEGER, 0 или 1) - ---- - -## 📦 **PAKETT (PACKAGE) - КАЛЕНДАРЬ** - -### **Функционал:** -- **Календарь**: Клик → открывается календарь -- **Выбор даты**: Выбрать дату → сохраняется `package_date` -- **Нет подтверждения**: Только дата (без кнопки ✓) - -### **Кто может изменять:** -- ✅ Admin -- ✅ Public User (все) - -### **База данных:** -- `status_checkboxes.package_date` (TEXT) - ---- - -## 🔴 **LÕIKUS (CUTTING) - ТОЛЬКО ADMIN** - -### **Функционал:** -- **Toggle**: Клик → дата появляется/исчезает -- **Автоматическая дата**: Устанавливается на **сегодня** -- **Ошибка-флаг** 🚫: `cutting_error` - - Если установлена → красная метка - - Блокирует **VALMIS** и **VÄLJAS** - -### **Кто может изменять:** -- 🔴 **ТОЛЬКО Admin** - -### **База данных:** -- `status_checkboxes.cutting_date` (TEXT) -- `status_checkboxes.cutting_error` (INTEGER, 0 или 1) - ---- - -## 🔴 **KLAAS (GLAZING) - ТОЛЬКО ADMIN** - -### **Функционал:** -- **Toggle**: Клик → дата появляется/исчезает -- **Автоматическая дата**: Устанавливается на **сегодня** -- **Ошибка-флаг** 🚫: `glazing_error` - - Если установлена → красная метка - - Блокирует **VALMIS** и **VÄLJAS** - -### **Кто может изменять:** -- 🔴 **ТОЛЬКО Admin** - -### **База данных:** -- `status_checkboxes.glazing_date` (TEXT) -- `status_checkboxes.glazing_error` (INTEGER, 0 или 1) - ---- - -## 🔒 **VALMIS (READY) - Admin/User Toggle + БЛОКИРОВКА ТОЛЬКО ПО ERROR ФЛАГАМ** - -### **ВАЖНО: ТЕКСТ ПРОБЛЕМЫ НЕ БЛОКИРУЕТ!** -Текст в поле "Probleemid" (серая метка ⚠️) **НЕ** блокирует VALMIS! - -### **Блокировка:** -Поле **ЗАБЛОКИРОВАНО ТОЛЬКО**, если установлен **ЛЮБОЙ error флаг** (красная галочка ✗): - - `worksheets_error = 1` - - `cutting_error = 1` - - `glazing_error = 1` - - `ready_error = 1` - - `issued_error = 1` - -**НЕ блокируется**, если: -- Есть только текст в `production_records.problems` (серая метка ⚠️) -- Все error флаги = 0 - -### **Визуальное отображение:** -- **Заблокировано**: 🔒 + сообщение "Vigade märked on seatud (punased kolmnurgad)" -- **Разблокировано**: Toggle как обычно - -### **Функционал (когда разблокирована):** -- **Toggle**: Клик → дата появляется/исчезает -- **Автоматическая дата**: Устанавливается на **сегодня** - -### **Кто может изменять:** -- ✅ **Admin + User** (изменено в v4.1.18) - -### **База данных:** -- `status_checkboxes.ready_date` (TEXT) -- `status_checkboxes.ready_error` (INTEGER, 0 или 1) - ---- - -## 🔒 **VÄLJAS (ISSUED) - Admin/User Toggle + БЛОКИРОВКА ТОЛЬКО ПО ERROR ФЛАГАМ** - -### **ВАЖНО: ТЕКСТ ПРОБЛЕМЫ НЕ БЛОКИРУЕТ!** -Текст в поле "Probleemid" (серая метка ⚠️) **НЕ** блокирует VÄLJAS! - -### **Блокировка:** -**ТА ЖЕ ЛОГИКА** что и **VALMIS**: -- Блокируется **ТОЛЬКО** если установлен **ЛЮБОЙ error флаг** (красная галочка ✗) -- **НЕ** блокируется если есть только текст в `production_records.problems` - -### **Функционал (когда разблокирована):** -- **Toggle**: Клик → дата появляется/исчезает -- **Автоматическая дата**: Устанавливается на **сегодня** - -### **Кто может изменять:** -- ✅ **Admin + User** (изменено в v4.1.18) - -### **Кто может изменять:** -- 🔴 **ТОЛЬКО Admin** - -### **База данных:** -- `status_checkboxes.issued_date` (TEXT) -- `status_checkboxes.issued_error` (INTEGER, 0 или 1) - ---- - -## 📊 **СВОДНАЯ ТАБЛИЦА** - -| Поле | Тип | Доступ | Логика | Подтверждение | Блокировка | -|------|-----|--------|--------|---------------|-----------| -| **MAT-1** | Календарь | Все | Календарь | ✓ Да | Нет | -| **MAT-2** | Календарь | Все | Календарь | ✓ Да | Если MAT-1 пуста | -| **PAKETT** | Календарь | Все | Календарь | Нет | Нет | -| **Töölehti** | 3-цикл | Admin | Серый→Зеленый→Пусто | Да | Нет | -| **LÕIKUS** | Toggle | Admin | Появляется/Исчезает | Нет | Нет | -| **KLAAS** | Toggle | Admin | Появляется/Исчезает | Нет | Нет | -| **VALMIS** | Toggle | Admin/User | Появляется/Исчезает | Нет | ТОЛЬКО если error флаги (НЕ текст проблемы) | -| **VÄLJAS** | Toggle | Admin/User | Появляется/Исчезает | Нет | ТОЛЬКО если error флаги (НЕ текст проблемы) | - ---- - -## 🔐 **ПРАВА ДОСТУПА** - -### **Admin** (admin, aknaproff): -- ✅ Все поля (MAT-1, MAT-2, PAKETT, Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS) -- ✅ Добавление записей -- ✅ Редактирование записей -- ✅ Удаление записей -- ✅ Изменение проблем и ошибок - -### **Public User** (kasutaja): -- ✅ MAT-1 (календарь + подтверждение) -- ✅ MAT-2 (календарь + подтверждение, если MAT-1 заполнена) -- ✅ PAKETT (календарь) -- ❌ Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS (только просмотр) -- ❌ Добавление/редактирование/удаление записей - ---- - -## 🎨 **ВИЗУАЛЬНЫЕ ИНДИКАТОРЫ** - -### **Цвета:** -- 🟢 **Зеленая рамка**: MAT-1/MAT-2 подтверждены -- 🟢 **Зеленый фон**: Töölehti подтверждено (confirmed=1) -- ⬜ **Серый фон**: Töölehti не подтверждено (confirmed=0) -- 🔴 **Красная метка**: Ошибка-флаг установлен -- 🔒 **Замок**: Поле заблокировано - -### **Подсказки:** -- **MAT-2 заблокирована**: "MAT-1 peab olema täidetud" -- **VALMIS/VÄLJAS заблокированы**: Текст проблемы из `production_records.problems` -- **Töölehti Шаг 1**: "Klõps 1: Lisa kuupäev" -- **Töölehti Шаг 2**: "Klõps 2: Kinnita" -- **Töölehti Шаг 3**: "Klõps 3: Tühjenda" - ---- - -## 📝 **AUDIT LOG** - -Каждое изменение даты записывается в таблицу `audit_log`: -```sql -INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action) -VALUES (?, ?, ?, ?, ?, 'toggle_status') -``` - -**Поля:** -- `user_id`: ID пользователя (1=admin, 2=aknaproff, 4=kasutaja) -- `record_id`: ID записи -- `field`: Название поля (worksheets, cutting, glazing, ready, issued) -- `old_value`: Старое значение даты -- `new_value`: Новое значение даты -- `action`: Тип действия ('toggle_status', 'worksheets_cycle') - ---- - -## ✅ **ПРОВЕРКА** - -### **Test Töölehti 3-цикл:** -```bash -# Шаг 1: Пусто → Серый -PATCH /api/records/2/worksheets-cycle -→ {"date": "2026-01-14", "confirmed": 0} - -# Шаг 2: Серый → Зеленый -PATCH /api/records/2/worksheets-cycle -→ {"date": "2026-01-14", "confirmed": 1} - -# Шаг 3: Зеленый → Пусто -PATCH /api/records/2/worksheets-cycle -→ {"date": null, "confirmed": 0} -``` - -### **Test блокировка MAT-2:** -```bash -# MAT-1 пустая → MAT-2 заблокирована -SELECT material_date FROM status_checkboxes WHERE record_id = 5 -→ NULL -# UI показывает: 🔒 "MAT-1 peab olema täidetud" -``` - -### **Test блокировка VALMIS:** -```bash -# Есть проблемы → VALMIS заблокирована -SELECT problems FROM production_records WHERE id = 10 -→ "Aken vajab parandust" -# UI показывает: 🔒 + текст проблемы -``` - ---- - -**Версия**: v4.1.16 -**Статус**: ✅ Работает корректно -**Дата**: 2026-01-14 - ---- - -## 🔒 **ВАЖНО: ПРАВИЛО БЛОКИРОВКИ VALMIS/VÄLJAS** - -### **Версия**: v4.1.19 -### **Дата изменения**: 2026-01-14 - -**КРИТИЧЕСКИ ВАЖНО**: Блокировка VALMIS/VÄLJAS работает **ТОЛЬКО** по error флагам! - -### **Блокируется**, если установлен **ЛЮБОЙ** из error флагов (красная галочка ✗): -- `worksheets_error = 1` -- `cutting_error = 1` -- `glazing_error = 1` -- `ready_error = 1` -- `issued_error = 1` - -### **НЕ БЛОКИРУЕТСЯ**, если: -- Есть **ТОЛЬКО** текст в `production_records.problems` (серая метка ⚠️) -- Все error флаги = 0 - -### **Визуальные индикаторы:** -- **VALMIS/VÄLJAS заблокированы**: 🔒 + сообщение "Vigade märked on seatud (punased kolmnurgad)" -- **Серая метка с ⚠️**: Указывает на текст проблемы в поле "Probleemid", **НЕ блокирует** VALMIS/VÄLJAS - -### **Код проверки блокировки** (backend): -```typescript -if (field === 'ready' || field === 'issued') { - const statusCheckbox = await c.env.DB.prepare( - 'SELECT worksheets_error, cutting_error, glazing_error, ready_error, issued_error FROM status_checkboxes WHERE record_id = ?' - ).bind(recordId).first() - - const hasErrorFlags = statusCheckbox && ( - statusCheckbox.worksheets_error || - statusCheckbox.cutting_error || - statusCheckbox.glazing_error || - statusCheckbox.ready_error || - statusCheckbox.issued_error - ) - - if (hasErrorFlags) { - return c.json({ - error: 'blocked', - message: 'Vigade märked on seatud (punased kolmnurgad)' - }, 403) - } -} -``` - -### **Тестирование** (результаты от 2026-01-14): -✅ **Test 1**: Только текст problems → НЕ блокируется → `{"success":true}` -✅ **Test 2**: Установлен error flag → БЛОКИРУЕТСЯ → `{"error":"blocked","message":"..."}` - ---- diff --git a/Dockerfile b/Dockerfile old mode 100755 new mode 100644 index 96d7451..a03395a --- a/Dockerfile +++ b/Dockerfile @@ -1,40 +1,58 @@ -# syntax=docker/dockerfile:1 +# Multi-stage build для оптимизации размера образа + +# Stage 1: Build +FROM node:20-alpine AS builder -# ---------- Build stage ---------- -FROM node:20-bookworm-slim AS builder WORKDIR /app -COPY package.json package-lock.json ./ -RUN npm install +# Копировать package files +COPY package*.json ./ +# Установить зависимости +RUN npm ci --only=production + +# Копировать исходники COPY . . + +# Собрать проект RUN npm run build -# ---------- Runtime stage ---------- -FROM node:20-bookworm-slim +# Stage 2: Runtime +FROM node:20-alpine + WORKDIR /app -ENV NODE_ENV=production \ - WRANGLER_SEND_METRICS=false \ - PORT=3000 \ - D1_BINDING=aknaproff-db \ - PERSIST_PATH=/data +# Установить dumb-init для правильной обработки сигналов +RUN apk add --no-cache dumb-init -# Copy everything from builder (includes node_modules, dist, migrations, etc.) -COPY --from=builder /app /app +# Создать пользователя без root +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodejs -u 1001 -# Create data directory -RUN mkdir -p /data +# Копировать зависимости из builder +COPY --from=builder --chown=nodejs:nodejs /app/node_modules ./node_modules +COPY --from=builder --chown=nodejs:nodejs /app/dist ./dist +COPY --from=builder --chown=nodejs:nodejs /app/package.json ./ +COPY --from=builder --chown=nodejs:nodejs /app/wrangler.jsonc ./ +COPY --from=builder --chown=nodejs:nodejs /app/migrations ./migrations +COPY --from=builder --chown=nodejs:nodejs /app/seed.sql ./ +# Создать директорию для локальной БД +RUN mkdir -p .wrangler/state/v3/d1 && \ + chown -R nodejs:nodejs .wrangler + +# Переключиться на непривилегированного пользователя +USER nodejs + +# Открыть порт EXPOSE 3000 -# Persist D1 SQLite data between restarts -VOLUME ["/data"] +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ + CMD wget --quiet --tries=1 --spider http://localhost:3000 || exit 1 -# Start wrangler directly (no bash script needed) -CMD ["npx", "wrangler", "pages", "dev", "dist", \ - "--local", \ - "--d1=aknaproff-db", \ - "--persist-to=/data", \ - "--ip=0.0.0.0", \ - "--port=3000"] +# Запуск с dumb-init +ENTRYPOINT ["dumb-init", "--"] + +# Команда запуска +CMD ["sh", "-c", "npm run db:reset && npx wrangler pages dev dist --d1=webapp-production --local --ip 0.0.0.0 --port 3000"] diff --git a/FILES_TO_COPY.txt b/FILES_TO_COPY.txt new file mode 100644 index 0000000..fe0d8dc --- /dev/null +++ b/FILES_TO_COPY.txt @@ -0,0 +1,147 @@ +# 📦 Файлы для копирования на production сервер + +## Версия: v4.1.6 (Märkused Visual Indicators) +## Дата: 2025-11-28 + +--- + +## 🆕 Что нового в v4.1.6 + +### Визуальная индикация в поле "Märkused" (Notes) +- **Желтый фон с ℹ️**: Когда есть текст заметки +- **Иконка info-circle**: Информационная иконка "i" +- **Tooltip**: При наведении курсора показывается полный текст заметки +- **Пустое поле**: Прочерк `-` когда нет заметок + +### Предыдущие изменения (v4.1.5) +- Восстановление визуальной индикации "Probleemid" (красный с ⚠️) + +### Предыдущие изменения (v4.1.4) +- Упрощение формы логина: "Administrator Login" → "Login" + +### ⚠️ Для v4.1.3 требовалось обновление БД! + +Если обновляетесь с версии до v4.1.3, нужно применить seed.sql: +- Добавлен новый пользователь **kasutaja** (password: tootmine) +- Убрано слово "Sorteerimine" над кнопкой ID +- Уточнена система ролей + +--- + +## 🔥 Deployment + +### Вариант A: Обновление только v4.1.4 (БД уже обновлена) + +Если вы уже применили seed.sql в v4.1.3, просто обновите код: + +```bash +# Быстрый вариант +scp dist/_worker.js user@server:/path/to/webapp/dist/ +docker-compose restart + +# ИЛИ полный вариант +scp public/original.html user@server:/path/to/webapp/public/ +scp src/original-html.ts user@server:/path/to/webapp/src/ +# На сервере: npm run build && docker-compose restart +``` + +### Вариант B: Полное обновление (с БД из v4.1.3) + +Если обновляетесь впервые или нужно добавить пользователя kasutaja: + +```bash +# 1. Обновить базу данных +scp seed.sql user@server:/path/to/webapp/ +docker-compose exec aknaproff-backend sh -c " + cd /app && + npx wrangler d1 execute webapp-production --local --file=./seed.sql +" + +# Проверить +docker-compose exec aknaproff-backend sh -c " + npx wrangler d1 execute webapp-production --local \ + --command='SELECT username, full_name, role FROM users' +" + +# 2. Обновить код +scp dist/_worker.js user@server:/path/to/webapp/dist/ +docker-compose restart +``` + +### Проверка после deployment +```bash +curl -I http://localhost:8180 +# Браузер: Ctrl+Shift+R, проверить текст "Login" и "Sisesta kasutajaandmed" +``` + +--- + +## 📝 Список файлов для копирования + +### Для v4.1.6: +- `dist/_worker.js` (быстрый вариант) **← рекомендуется** +- ИЛИ `public/static/app.js` + `public/original.html` + `src/original-html.ts` (полный вариант) + +### Если также нужен пользователь kasutaja из v4.1.3: +- `seed.sql` - добавляет kasutaja (tootmine) + +--- + +## ✅ Проверка после deployment + +1. **HTTP**: `curl -I http://localhost:8180` → 200 OK + +2. **Браузер**: + - Открыть http://localhost:8180 + - Нажать **Ctrl+Shift+R** (сброс кэша) + - Войти под любым пользователем (kasutaja/tootmine, aknaproff/demo123, admin/demo123) + - **Проверить поле "Probleemid"**: + - Запись с галочками → 🔴 красный фон с ⚠️ + - Запись с текстом → ⚪ серый фон с ℹ️ + - Запись без проблем → серый фон с `-` + - Навести курсор → показывает tooltip с текстом проблемы + - **Проверить поле "Märkused"**: + - Запись с заметкой (ID 2, 4) → 🟡 желтый фон с ℹ️ + - Запись без заметки → серый фон с `-` + - Навести курсор → показывает tooltip с текстом заметки + +--- + +## 🔐 Учётные данные + +| Username | Password | Role | Доступ | +|----------|----------|------|--------| +| kasutaja | tootmine | user | Просмотр + проблемы | +| aknaproff | demo123 | admin | Полный доступ | +| admin | demo123 | admin | Полный доступ | +| guest | (без входа) | guest | Только просмотр | + +--- + +## 📊 История версий + +| Версия | Изменения | +|--------|-----------| +| v4.1.6 | Визуальная индикация Märkused (желтый с ℹ️, tooltip) | +| v4.1.5 | Восстановление визуальной индикации Probleemid (красный с ⚠️, tooltip) | +| v4.1.4 | Упрощение текста формы логина | +| v4.1.3 | Убрано "Sorteerimine", добавлен kasutaja, уточнены роли | +| v4.1.2 | Кнопка Sorteerimine в Kiir otsing | +| v4.1.1 | HOTFIX: continueAsGuest global access | +| v4.1.0 | Auth система (guest/user/admin), сортировка по ID | + +--- + +## 🔗 Документация + +- **CHANGES_v4.1.6.md** - детали v4.1.6 (Märkused визуализация) +- **CHANGES_v4.1.5.md** - детали v4.1.5 (восстановление Probleemid визуализации) +- **CHANGES_v4.1.4.md** - детали v4.1.4 (упрощение формы логина) +- **CHANGES_v4.1.3.md** - детали v4.1.3 (UI + новый пользователь) +- **CHANGES_v4.1.2.md** - детали v4.1.2 (перемещение кнопки) +- **CHANGES_v4.1.0.md** - детали v4.1.0 (auth система) +- **HOTFIX_v4.1.1.md** - hotfix continueAsGuest + +--- + +**💡 Заметка**: v4.1.6 - добавление визуальной индикации в поле Märkused (желтый с ℹ️). БД не затронута. diff --git a/HOTFIX_v4.1.11.md b/HOTFIX_v4.1.11.md deleted file mode 100644 index c8b8cde..0000000 --- a/HOTFIX_v4.1.11.md +++ /dev/null @@ -1,181 +0,0 @@ -# AKNAPROFF v4.1.11 - HOTFIX - -**Дата:** 2025-12-31 -**Тип:** HOTFIX - Критическое исправление -**Статус:** Production Ready ✅ - ---- - -## 🐛 Исправленная проблема - -### ❌ Ошибка: "NOT NULL constraint failed: production_records.price" - -**Симптомы:** -``` -POST /api/records -[HTTP/2 500 Internal Server Error] -Save record error: Failed to create record -``` - -**Консоль браузера:** -```javascript -Viga salvestamisel: Failed to create record -Save record error: Request failed with status code 500 -``` - -**Логи сервера:** -``` -Error creating record: D1_ERROR: NOT NULL constraint failed: production_records.price: SQLITE_CONSTRAINT -``` - -**Причина:** -В реальной БД колонка `price` имеет ограничение `NOT NULL`, но код передавал `null` вместо `0`. - -**Схема БД:** -```sql -CREATE TABLE production_records ( - ... - price REAL NOT NULL, -- ⚠️ NOT NULL! - ... -); -``` - -**Старый код (v4.1.10):** -```typescript -const price = data.price ? parseFloat(data.price) : null // ❌ null вызывает ошибку -``` - -**Новый код (v4.1.11):** -```typescript -const price = data.price ? parseFloat(data.price) : 0 // ✅ 0 соответствует схеме -``` - ---- - -## ✅ Решение - -### Изменённые файлы: - -**src/index.tsx:** -- POST /api/records: изменено `null` → `0` для price -- PUT /api/records/:id: изменено `null` → `0` для price - -**public/original.html:** -- Cache version: v4.1.11 - -**dist/_worker.js:** -- Пересобран с исправлениями - ---- - -## 🧪 Тестирование - -### Тест 1: Добавление записи без цены -```bash -curl -X POST http://localhost:3000/api/records \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{ - "month":"1", - "year":"2025", - "client_name":"Test Client", - "offer_number":"TEST-001", - "work_number":"WRK-001", - "quantity":"5" - }' -``` - -**Результат v4.1.10:** ❌ 500 Internal Server Error -**Результат v4.1.11:** ✅ `{"success":true,"id":49}` - -### Тест 2: Проверка сохранённой записи -```bash -SELECT id, client_name, price FROM production_records WHERE id = 49 -``` - -**Результат:** -``` -id: 49 -client_name: "Test Client" -price: 0 ✅ -``` - -### Тест 3: Добавление записи с ценой -```bash -curl -X POST http://localhost:3000/api/records \ - -d '{"month":"1","year":"2025","client_name":"Test","offer_number":"123","work_number":"456","quantity":"1","price":"100.50"}' -``` - -**Результат:** ✅ `{"success":true,"id":50}`, price = 100.5 - ---- - -## 🔄 Миграция с v4.1.10 - -### Обновление не требует изменений БД - -Просто замените файлы: -```bash -# 1. Остановить -docker-compose down - -# 2. Заменить код -cp /path/to/new/dist/_worker.js dist/ -cp /path/to/new/src/index.tsx src/ -cp /path/to/new/public/original.html public/ - -# 3. Запустить -docker-compose up -d --build -``` - -**⚠️ НЕ трогайте data/ директорию!** - ---- - -## 📊 Сравнение версий - -| Параметр | v4.1.10 | v4.1.11 | -|----------|---------|---------| -| price default | null ❌ | 0 ✅ | -| Добавление без цены | 500 ошибка ❌ | Работает ✅ | -| Добавление с ценой | Работает ✅ | Работает ✅ | -| NOT NULL constraint | Нарушается ❌ | Соблюдается ✅ | - ---- - -## ✅ Все проверки пройдены - -- [x] Добавление записи без цены → ✅ price = 0 -- [x] Добавление записи с ценой → ✅ price сохраняется -- [x] Редактирование записи → ✅ price обновляется -- [x] NOT NULL constraint → ✅ соблюдается -- [x] Кнопка "Lisa uus rida" → ✅ работает -- [x] Права доступа → ✅ admin может добавлять -- [x] Cache version → ✅ v4.1.11 - ---- - -## 🎯 Статус - -**Версия:** v4.1.11 -**Тип:** HOTFIX -**Критичность:** Высокая (блокировала добавление записей) -**Совместимость:** Полная обратная совместимость -**Требуется миграция БД:** Нет -**Статус:** Production Ready ✅ - ---- - -## 📝 Changelog Summary - -``` -v4.1.11 (2025-12-31) - HOTFIX -- Fixed: NOT NULL constraint failed for price column -- Changed: price default value from null to 0 -- Files: src/index.tsx, dist/_worker.js, public/original.html -- Status: Production Ready -``` - ---- - -**ПРОБЛЕМА ИСПРАВЛЕНА! Добавление записей теперь работает корректно.** ✅ diff --git a/HOTFIX_v4.1.12.md b/HOTFIX_v4.1.12.md deleted file mode 100644 index 32be0a5..0000000 --- a/HOTFIX_v4.1.12.md +++ /dev/null @@ -1,211 +0,0 @@ -# AKNAPROFF v4.1.12 - HOTFIX: Редактирование записей - -**Дата:** 2025-12-31 -**Тип:** HOTFIX - Критическое исправление -**Статус:** Production Ready ✅ - ---- - -## 🐛 Исправленная проблема - -### ❌ Редактирование записей не сохранялось - -**Симптомы:** -- При редактировании записи (клик на строку) модальное окно открывается -- После изменения данных и сохранения - запись не обновляется -- Идёт GET запрос, но данные остаются прежними -- После перезагрузки страницы изменения не видны -- **В модальном окне изменение даты тоже не сохраняется** - -**Проблема:** -1. Backend НЕ обновлял таблицу `status_checkboxes` (где хранятся даты) -2. Frontend передавал `null` для price вместо `0` - ---- - -## ✅ Решение - -### 1. Backend: Добавлено обновление status_checkboxes - -**Было (v4.1.11):** -```typescript -app.put('/api/records/:id', async (c) => { - // Обновляется только production_records - await c.env.DB.prepare(` - UPDATE production_records - SET client_name = ?, ... - WHERE id = ? - `).run() - - return c.json({ success: true }) -}) -``` - -**Стало (v4.1.12):** -```typescript -app.put('/api/records/:id', async (c) => { - // Обновляется production_records - await c.env.DB.prepare(` - UPDATE production_records - SET client_name = ?, ... - WHERE id = ? - `).run() - - // ✅ Добавлено: Обновление status_checkboxes (даты) - if (data.material_date !== undefined || - data.material2_date !== undefined || - data.package_date !== undefined) { - await c.env.DB.prepare(` - UPDATE status_checkboxes - SET material_date = ?, - material2_date = ?, - package_date = ?, - updated_at = CURRENT_TIMESTAMP - WHERE record_id = ? - `).bind( - data.material_date || null, - data.material2_date || null, - data.package_date || null, - id - ).run() - } - - return c.json({ success: true }) -}) -``` - -### 2. Frontend: Исправлено значение по умолчанию для price - -**Было:** -```javascript -price: parseFloat(document.getElementById('price').value) || null // ❌ -``` - -**Стало:** -```javascript -price: parseFloat(document.getElementById('price').value) || 0 // ✅ -``` - ---- - -## 🧪 Тестирование - -### Тест 1: Обновление записи с датами -```bash -PUT /api/records/1 -{ - "client_name": "Updated Client", - "offer_number": "UPD-001", - "price": "250.50", - "material_date": "2025-01-15", - "material2_date": "2025-01-16", - "package_date": "2025-01-17" -} -``` - -**Результат v4.1.11:** ❌ Даты НЕ сохраняются -**Результат v4.1.12:** ✅ Даты сохраняются - -### Тест 2: Проверка сохранённых данных - -**production_records:** -```sql -SELECT client_name, offer_number, price FROM production_records WHERE id = 1 --- Результат: Updated Client, UPD-001, 250.5 ✅ -``` - -**status_checkboxes:** -```sql -SELECT material_date, material2_date, package_date -FROM status_checkboxes WHERE record_id = 1 --- Результат: 2025-01-15, 2025-01-16, 2025-01-17 ✅ -``` - -### Тест 3: UI - Редактирование через модальное окно -1. Открыть запись (клик на строку) -2. Изменить имя клиента -3. Изменить даты (material_date, material2_date, package_date) -4. Сохранить - -**Результат:** ✅ Все изменения сохраняются - ---- - -## 📝 Изменённые файлы - -| Файл | Изменения | -|------|-----------| -| **src/index.tsx** | Добавлен UPDATE для status_checkboxes | -| **public/static/app.js** | price: null → 0 | -| **public/original.html** | Cache v4.1.12 | -| **dist/_worker.js** | Пересобран | - ---- - -## 🔄 Миграция с v4.1.11 - -### Обновление не требует изменений БД - -```bash -# 1. Остановить -docker-compose down - -# 2. Заменить код -cp new/dist/_worker.js dist/ -cp new/src/index.tsx src/ -cp new/public/ public/ - -# 3. Запустить -docker-compose up -d --build -``` - ---- - -## 📊 Сравнение версий - -| Параметр | v4.1.11 | v4.1.12 | -|----------|---------|---------| -| Редактирование записей | Не работает ❌ | Работает ✅ | -| Сохранение дат | Не работает ❌ | Работает ✅ | -| status_checkboxes UPDATE | Нет ❌ | Есть ✅ | -| price default | null ❌ | 0 ✅ | - ---- - -## ✅ Все проверки пройдены - -- [x] Редактирование записей работает -- [x] Даты сохраняются (material_date, material2_date, package_date) -- [x] production_records обновляется -- [x] status_checkboxes обновляется -- [x] price = 0 по умолчанию -- [x] UI модальное окно работает корректно -- [x] После перезагрузки изменения видны - ---- - -## 🎯 Статус - -**Версия:** v4.1.12 -**Тип:** HOTFIX -**Критичность:** Высокая (блокировала редактирование) -**Совместимость:** Полная обратная совместимость -**Требуется миграция БД:** Нет -**Статус:** Production Ready ✅ - ---- - -## 📝 Changelog Summary - -``` -v4.1.12 (2025-12-31) - HOTFIX -- Fixed: Record editing not saving changes -- Added: status_checkboxes UPDATE in PUT endpoint -- Fixed: Date fields (material_date, material2_date, package_date) now save -- Changed: price default from null to 0 in frontend -- Status: Production Ready -``` - ---- - -**ПРОБЛЕМА ИСПРАВЛЕНА! Редактирование записей и дат теперь работает корректно.** ✅ diff --git a/HOTFIX_v4.1.15.md b/HOTFIX_v4.1.15.md deleted file mode 100644 index b34197e..0000000 --- a/HOTFIX_v4.1.15.md +++ /dev/null @@ -1,196 +0,0 @@ -# 🔧 HOTFIX v4.1.15 - DATE FIELDS DISPLAY FIX - -**Release Date**: 2026-01-14 -**Priority**: CRITICAL - Fixes invisible date fields -**Status**: ✅ Production Ready - ---- - -## 🐛 ПРОБЛЕМА - -**Симптомы:** -- Даты НЕ отображаются в полях: PAKETT, Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS -- Клик по ячейке не показывает дату -- API запросы проходят без ошибок -- Frontend получает `null` вместо дат - -**Причина:** -1. **Строковые "null"** вместо SQL NULL в БД -2. Backend записывал строку `"null"` вместо настоящего NULL -3. Toggle отправлял `date: null`, что интерпретировалось как "установить NULL" - ---- - -## ✅ ИСПРАВЛЕНИЯ - -### 1. Backend UPDATE для status_checkboxes (v4.1.12 → v4.1.13) -**Файл:** `src/index.tsx` - -```typescript -// ❌ БЫЛО: -data.material_date || null, -data.material2_date || null, -data.package_date || null, - -// ✅ СТАЛО: -const materialDate = (data.material_date && data.material_date !== 'null') ? data.material_date : null -const material2Date = (data.material2_date && data.material2_date !== 'null') ? data.material2_date : null -const packageDate = (data.package_date && data.package_date !== 'null') ? data.package_date : null -``` - -### 2. Backend PATCH /api/records/:id/status (v4.1.13 → v4.1.14) -**Файл:** `src/index.tsx` - -```typescript -// ❌ БЫЛО: -const newDate = date !== undefined ? date : (oldRecord?.[dbField] ? null : new Date().toISOString().split('T')[0]) - -// ✅ СТАЛО: -let newDate: string | null -if (date !== undefined) { - // Явная конвертация строки "null" в NULL - newDate = (date && date !== 'null') ? date : null -} else { - // Toggle: если дата есть → удалить, если нет → установить сегодня - newDate = oldRecord?.[dbField] ? null : new Date().toISOString().split('T')[0] -} -``` - -### 3. Frontend toggleDate (v4.1.14 → v4.1.15) -**Файл:** `public/static/app.js` - -```javascript -// ❌ БЫЛО: -{ field, date: currentDate } // Передавался null, блокируя toggle - -// ✅ СТАЛО: -{ field } // НЕ передаётся date, активируется toggle логика -``` - -### 4. Database Fix -**Исправлены все строковые "null" в БД:** -```sql -UPDATE status_checkboxes SET material_date = NULL WHERE material_date = 'null'; -UPDATE status_checkboxes SET material2_date = NULL WHERE material2_date = 'null'; -UPDATE status_checkboxes SET package_date = NULL WHERE package_date = 'null'; -UPDATE status_checkboxes SET worksheets_date = NULL WHERE worksheets_date = 'null'; -UPDATE status_checkboxes SET cutting_date = NULL WHERE cutting_date = 'null'; -UPDATE status_checkboxes SET glazing_date = NULL WHERE glazing_date = 'null'; -UPDATE status_checkboxes SET ready_date = NULL WHERE ready_date = 'null'; -UPDATE status_checkboxes SET issued_date = NULL WHERE issued_date = 'null'; -``` - ---- - -## ✅ РЕЗУЛЬТАТ - -### Протестировано: -1. **MAT-1, MAT-2, PAKETT** - календари работают ✅ -2. **Töölehti, LÕIKUS, KLAAS** - toggle работает ✅ -3. **VALMIS, VÄLJAS** - toggle с блокировкой работает ✅ -4. **Даты отображаются** в API и таблице ✅ -5. **Toggle ON/OFF** корректно работает ✅ - -### Тестовые команды: -```bash -# Toggle ON -curl -X PATCH "http://localhost:3000/api/records/49/status" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{"field":"worksheets"}' -# Результат: worksheets_date = "2026-01-14" - -# Toggle OFF -curl -X PATCH "http://localhost:3000/api/records/49/status" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{"field":"worksheets"}' -# Результат: worksheets_date = null -``` - ---- - -## 🚀 РАЗВЁРТЫВАНИЕ - -### Быстрое обновление: -```bash -# 1. Остановить сервис -docker-compose down - -# 2. Бэкап данных -cp -r data data.backup.$(date +%Y%m%d) - -# 3. Распаковать v4.1.15 -unzip aknaproff_production_v4.1.15_FINAL.zip -cd backend - -# 4. Исправить БД (ВАЖНО!) -cat > /tmp/fix_nulls.sql << 'EOF' -UPDATE status_checkboxes SET material_date = NULL WHERE material_date = 'null'; -UPDATE status_checkboxes SET material2_date = NULL WHERE material2_date = 'null'; -UPDATE status_checkboxes SET package_date = NULL WHERE package_date = 'null'; -UPDATE status_checkboxes SET worksheets_date = NULL WHERE worksheets_date = 'null'; -UPDATE status_checkboxes SET cutting_date = NULL WHERE cutting_date = 'null'; -UPDATE status_checkboxes SET glazing_date = NULL WHERE glazing_date = 'null'; -UPDATE status_checkboxes SET ready_date = NULL WHERE ready_date = 'null'; -UPDATE status_checkboxes SET issued_date = NULL WHERE issued_date = 'null'; -EOF - -# Применить исправление (если используется SQLite напрямую) -sqlite3 data/v3/d1/miniflare-D1DatabaseObject/*.sqlite < /tmp/fix_nulls.sql - -# 5. Запустить сервис -docker-compose up -d --build - -# 6. Проверить -curl http://localhost:8180/api/records?month=1&year=2025 -``` - ---- - -## 📊 СРАВНЕНИЕ ВЕРСИЙ - -| Функция | v4.1.12 | v4.1.15 | -|---------|---------|---------| -| Даты отображаются | ❌ Все NULL | ✅ Корректно | -| Toggle работает | ❌ Не работает | ✅ Работает | -| Редактирование дат | ❌ Строковые "null" | ✅ Настоящие NULL | -| Backend UPDATE | ❌ `\|\| null` | ✅ Явная конвертация | -| Frontend toggle | ❌ Передаёт `date: null` | ✅ Не передаёт date | -| БД целостность | ❌ Строковые "null" | ✅ SQL NULL | - ---- - -## 📝 CHANGELOG - -### v4.1.15 (2026-01-14) - HOTFIX -- ✅ Frontend: toggleDate НЕ передаёт date параметр -- ✅ Cache version обновлён до 4.1.15 - -### v4.1.14 (2026-01-14) - HOTFIX -- ✅ Backend: улучшена логика toggle с явной конвертацией "null" -- ✅ БД: исправлены все строковые "null" → SQL NULL - -### v4.1.13 (2026-01-14) - HOTFIX -- ✅ Backend: добавлена конвертация "null" строк в UPDATE status_checkboxes -- ✅ Frontend: price default изменён с null на 0 - -### v4.1.12 (2026-01-14) -- ✅ Backend: UPDATE теперь обновляет ОБЕ таблицы -- ✅ Frontend: исправлен price || null → || 0 - ---- - -## 🎯 СТАТУС - -**ВСЁ РАБОТАЕТ!** -- ✅ Даты видны в таблице -- ✅ Toggle работает (ON/OFF) -- ✅ Календари работают (MAT-1, MAT-2, PAKETT) -- ✅ Блокировка VALMIS/VÄLJAS работает -- ✅ Audit log записывается -- ✅ Права доступа корректны - ---- - -**Version**: v4.1.15 FINAL -**Build Date**: 2026-01-14 -**Status**: Production Ready ✅ diff --git a/HOTFIX_v4.1.16.md b/HOTFIX_v4.1.16.md deleted file mode 100644 index 79069ce..0000000 --- a/HOTFIX_v4.1.16.md +++ /dev/null @@ -1,172 +0,0 @@ -# 🔧 HOTFIX v4.1.16 - КРИТИЧЕСКИЕ ИСПРАВЛЕНИЯ - -**Дата**: 2026-01-14 -**Тип**: Critical Bugfix Release -**Статус**: ✅ Production Ready - ---- - -## 🚨 **КРИТИЧЕСКИЕ ПРОБЛЕМЫ ИСПРАВЛЕНЫ** - -### **Проблема 1: Даты не отображаются** ❌➡️✅ -**Симптом:** -- Поля Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS показывали пустые ячейки -- Даты были в БД, но не отображались в UI -- API возвращал даты, но frontend их не показывал - -**Причина:** -``` -database disk image is malformed: SQLITE_CORRUPT -``` -База данных была повреждена из-за WAL-файлов - -**Решение:** -```bash -# Удалить поврежденную БД -rm -rf .wrangler/state/v3/d1/miniflare-D1DatabaseObject/*.sqlite* - -# Восстановить чистую копию -cp tootmine-aknaprof-dump.sqlite .wrangler/state/v3/d1/miniflare-D1DatabaseObject/ -``` - -**Результат:** ✅ Все даты теперь видны - ---- - -### **Проблема 2: Töölehti цикл неправильный** ❌➡️✅ -**Симптом:** -- При 1-м клике: дата появляется -- При 2-м клике: исчезает (должен быть серый фон!) -- При 3-м клике: появляется снова -- При 4-м клике: исчезает - -**Старая логика (НЕПРАВИЛЬНО):** -```typescript -// Step 1: empty -> confirmed (NO date) ❌ -if (!worksheets_date && !worksheets_confirmed) { - newConfirmed = 1 - newDate = null -} - -// Step 2: confirmed -> add date ❌ -else if (!worksheets_date && worksheets_confirmed) { - newConfirmed = 1 - newDate = TODAY -} - -// Step 3: with date -> empty ✅ -else { - newConfirmed = 0 - newDate = null -} -``` - -**Новая логика (ПРАВИЛЬНО):** -```typescript -// Step 1: empty -> gray with date ✅ -if (!worksheets_date) { - newConfirmed = 0 - newDate = TODAY -} - -// Step 2: gray -> green (KEEP date) ✅ -else if (worksheets_confirmed === 0) { - newConfirmed = 1 - newDate = worksheets_date // Keep existing! -} - -// Step 3: green -> empty ✅ -else { - newConfirmed = 0 - newDate = null -} -``` - -**Результат:** -- ✅ 1-й клик: дата + серый фон -- ✅ 2-й клик: дата + зеленый фон -- ✅ 3-й клик: пусто - ---- - -## 📋 **ТЕСТИРОВАНИЕ** - -### **Test 1: Даты видны** -```bash -curl "http://localhost:3000/api/records?month=1&year=2025" | jq '.[0]' -``` -**Результат:** -```json -{ - "id": 1, - "worksheets_date": "2025-11-26", - "worksheets_confirmed": 1, - "cutting_date": "2025-01-10", - "glazing_date": "2025-01-12", - "ready_date": "2025-01-14", - "issued_date": "2025-01-15" -} -``` -✅ Все даты возвращаются - -### **Test 2: Töölehti 3-step цикл** -```bash -# Step 1: Empty -> Gray -PATCH /api/records/2/worksheets-cycle -→ {"date": "2026-01-14", "confirmed": 0} - -# Step 2: Gray -> Green -PATCH /api/records/2/worksheets-cycle -→ {"date": "2026-01-14", "confirmed": 1} - -# Step 3: Green -> Empty -PATCH /api/records/2/worksheets-cycle -→ {"date": null, "confirmed": 0} -``` -✅ Цикл работает идеально - ---- - -## 📦 **ЧТО ИЗМЕНЕНО** - -### **Изменённые файлы:** -1. `src/index.tsx` - исправлена логика worksheets-cycle -2. `dist/_worker.js` - пересобран с новой логикой -3. `data/.../2b35d4d42e3c9f6b5ad5b5579a7b1470c66e69f6b33a31e3f5a0095cc6d18656.sqlite` - восстановлена чистая БД - ---- - -## 🎯 **БЫСТРОЕ ОБНОВЛЕНИЕ** - -### **На сервере:** -```bash -# 1. Остановить сервисы -docker-compose down - -# 2. Распаковать новый архив -unzip aknaproff_production_v4.1.16_FINAL.zip - -# 3. Заменить файлы -cd backend/ -docker-compose up -d --build - -# 4. Проверить -curl http://localhost:8180/api/records?month=1&year=2025 | jq '.[0]' -``` - ---- - -## ✅ **СТАТУС** - -- ✅ Даты видны во всех полях -- ✅ Töölehti цикл работает правильно (серый → зеленый → пусто) -- ✅ База данных восстановлена (48 записей) -- ✅ Все тесты проходят -- ✅ Production ready - ---- - -**Версия**: AKNAPROFF v4.1.16 -**Архив**: aknaproff_production_v4.1.16_FINAL.tar.gz (292 KB) -**База данных**: 48 реальных записей (2025-2026) -**Docker**: ARM Synology ready ✅ diff --git a/HOTFIX_v4.1.17.md b/HOTFIX_v4.1.17.md deleted file mode 100644 index 07a7999..0000000 --- a/HOTFIX_v4.1.17.md +++ /dev/null @@ -1,204 +0,0 @@ -# 🔧 HOTFIX v4.1.17 - TOGGLE FIX (LÕIKUS, KLAAS, VALMIS, VÄLJAS) - -**Дата**: 2026-01-14 -**Тип**: Critical Bugfix Release -**Статус**: ✅ Production Ready - ---- - -## 🚨 **КРИТИЧЕСКАЯ ПРОБЛЕМА ИСПРАВЛЕНА** - -### **Проблема: LÕIKUS, KLAAS, VALMIS, VÄLJAS не работали** ❌ - -**Симптом:** -- Клик по ячейке есть -- API запрос уходит (200 OK) -- Даты НЕ появляются после клика -- Даты исчезают после первого клика - -**Причина 1: Frontend не передавал `date`** - -В текущем коде: -```javascript -// app.js:1123 (❌ НЕПРАВИЛЬНО) -{ field } // date отсутствует! -``` - -В оригинальном коде: -```javascript -// original app.js:1047 (✅ ПРАВИЛЬНО) -{ field, date: currentDate } -``` - -**Причина 2: Backend toggle логика неправильная** - -Старая логика: -```typescript -if (date === oldRecord?.[dbField]) { - newDate = null // Clear -} else if (!date) { - newDate = TODAY // Add today -} -``` - -Проблема: `null === null` → true → очищает вместо добавления! - -**Новая логика:** -```typescript -if (!date || date === 'null') { - // null/empty clicked - if (oldRecord?.[dbField]) { - newDate = null // Cell has date → clear it - } else { - newDate = TODAY // Cell is empty → add today - } -} else if (date === oldRecord?.[dbField]) { - newDate = null // Same date → toggle off -} else { - newDate = date // Different date → use it -} -``` - ---- - -## ✅ **РЕШЕНИЕ** - -### **1. Frontend: Передавать `currentDate`** - -```javascript -// public/static/app.js:1121 -const response = await axios.patch( - `${API_BASE}/api/records/${recordId}/status`, - { field, date: currentDate }, // ✅ Добавлен date - { headers } -); -``` - -### **2. Backend: Правильная toggle логика** - -```typescript -// src/index.tsx:329 -if (!date || date === 'null') { - // Пустая ячейка кликнута - if (oldRecord?.[dbField]) { - newDate = null // Есть дата → удалить - } else { - newDate = new Date().toISOString().split('T')[0] // Нет даты → добавить сегодня - } -} else if (date === oldRecord?.[dbField]) { - // Та же дата → toggle off - newDate = null -} else { - // Другая дата → использовать её - newDate = date -} -``` - ---- - -## 🧪 **ТЕСТИРОВАНИЕ** - -### ✅ **Test 1: Toggle с датой** -```bash -# Initial: has date (2025-01-11) -GET /api/records → glazing_date: "2025-01-11" - -# Click: same date → should clear -PATCH /api/records/2/status {"field":"glazing","date":"2025-01-11"} -→ Result: null ✅ -``` - -### ✅ **Test 2: Toggle пустой ячейки** -```bash -# Initial: empty -GET /api/records → glazing_date: null - -# Click: null → should add today -PATCH /api/records/2/status {"field":"glazing","date":null} -→ Result: "2026-01-14" ✅ -``` - -### ✅ **Test 3: Toggle сегодняшней даты** -```bash -# Initial: has today -GET /api/records → glazing_date: "2026-01-14" - -# Click: today's date → should clear -PATCH /api/records/2/status {"field":"glazing","date":"2026-01-14"} -→ Result: null ✅ -``` - ---- - -## 📊 **СРАВНЕНИЕ** - -| Действие | v4.1.16 | v4.1.17 | -|---------|---------|---------| -| **Клик по пустой ячейке** | ❌ Ничего | ✅ Добавляет сегодня | -| **Клик по ячейке с датой** | ❌ Не меняется / исчезает | ✅ Удаляет дату | -| **Передача date в API** | ❌ Не передавалось | ✅ Передается currentDate | -| **Toggle логика** | ❌ Неправильная (null === null) | ✅ Правильная (проверяет oldRecord) | - ---- - -## 📦 **ИЗМЕНЁННЫЕ ФАЙЛЫ** - -1. **public/static/app.js** (строка 1121-1125) - - Добавлен `date: currentDate` в PATCH запрос - -2. **src/index.tsx** (строка 329-348) - - Исправлена toggle логика - - Добавлена проверка oldRecord перед toggle - -3. **dist/_worker.js** (133.43 kB) - - Пересобран с новой логикой - ---- - -## 🚀 **ОБНОВЛЕНИЕ** - -### **На сервере:** -```bash -# 1. Остановить -docker-compose down - -# 2. Распаковать v4.1.17 -unzip aknaproff_production_v4.1.17_FINAL.zip - -# 3. Запустить -cd backend/ -docker-compose up -d --build - -# 4. Проверить -curl http://localhost:8180/api/records?month=1&year=2025 | jq '.[0]' -``` - -### **Проверка:** -```bash -# Даты видны -→ worksheets_date: "2025-11-26" -→ cutting_date: "2025-01-10" -→ glazing_date: "2025-01-12" -→ ready_date: "2025-01-14" -→ issued_date: "2025-01-15" -``` - ---- - -## ✅ **СТАТУС** - -- ✅ LÕIKUS toggle работает -- ✅ KLAAS toggle работает -- ✅ VALMIS toggle работает -- ✅ VÄLJAS toggle работает -- ✅ Töölehti 3-цикл работает -- ✅ MAT-1, MAT-2, PAKETT календари работают -- ✅ 48 реальных записей на месте -- ✅ Production ready - ---- - -**Версия**: AKNAPROFF v4.1.17 FINAL -**Дата**: 2026-01-14 -**Архив**: aknaproff_production_v4.1.17_FINAL.tar.gz -**Статус**: ✅ Production Ready diff --git a/HOTFIX_v4.1.18.md b/HOTFIX_v4.1.18.md deleted file mode 100644 index 0e32ca4..0000000 --- a/HOTFIX_v4.1.18.md +++ /dev/null @@ -1,203 +0,0 @@ -# 🔧 HOTFIX v4.1.18 - USER PERMISSIONS FIX - -**Дата**: 2026-01-14 -**Тип**: Critical Bugfix Release -**Статус**: ✅ Production Ready - ---- - -## 🚨 **КРИТИЧЕСКАЯ ПРОБЛЕМА ИСПРАВЛЕНА** - -### **Проблема: User (kasutaja) не мог toggle поля** - -**Симптом:** -``` -Логин: kasutaja / tootmine -Клик по Töölehti → Ошибка: "Sul pole õigust töölehe staatust muuta. Palun logi sisse administraatorina." -Клик по LÕIKUS → Ошибка: "Sul pole õigust andmeid muuta. Palun logi sisse administraatorina." -``` - -**По документации User ДОЛЖЕН иметь доступ:** -- ✅ Подтверждение MAT-1/MAT-2 -- ✅ Toggle полей (Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS) - ---- - -## 🔍 **ПРИЧИНА** - -### **Frontend проверка прав была неправильной:** - -**Старый код (app.js:28):** -```javascript -function canEditRecords() { - // Only admin can edit records - return currentUser && currentUser.role === 'admin'; // ❌ Только admin! -} - -// В toggleDate и toggleWorksheetsStep: -if (!canEditRecords()) { // ❌ Блокирует User! - alert('Sul pole õigust...'); - return; -} -``` - -**Проблема:** -- `canEditRecords()` разрешает ТОЛЬКО `admin` -- `toggleDate()` и `toggleWorksheetsStep()` используют `canEditRecords()` -- User (kasutaja) не может toggle! - ---- - -## ✅ **РЕШЕНИЕ** - -### **Создана новая функция `canToggleDates()`:** - -**Новый код (app.js:28-36):** -```javascript -function canEditRecords() { - // Only admin can edit records (add/edit/delete) - return currentUser && currentUser.role === 'admin'; -} - -function canToggleDates() { - // Admin and User can toggle dates - return currentUser && (currentUser.role === 'admin' || currentUser.role === 'user'); -} - -function isGuest() { - // Check if user is guest (read-only) - return !currentUser || currentUser.role === 'guest'; -} -``` - -### **Обновлены функции toggle:** - -**toggleDate (app.js:1111):** -```javascript -async function toggleDate(recordId, field, currentDate) { - // Check permissions - admin and user can toggle dates - if (!canToggleDates()) { // ✅ Использует canToggleDates() - alert('Sul pole õigust andmeid muuta. Palun logi sisse.'); - return; - } - // ... -} -``` - -**toggleWorksheetsStep (app.js:1647):** -```javascript -async function toggleWorksheetsStep(recordId) { - // Check permissions - admin and user can toggle - if (!canToggleDates()) { // ✅ Использует canToggleDates() - alert('Sul pole õigust töölehe staatust muuta. Palun logi sisse.'); - return; - } - // ... -} -``` - ---- - -## 🧪 **ТЕСТИРОВАНИЕ** - -### ✅ **Test: User (kasutaja) permissions** - -```bash -# Login as User -TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"kasutaja","password":"tootmine"}' | jq -r '.token') - -# Test 1: Toggle Töölehti -curl -s -X PATCH http://localhost:3000/api/records/2/worksheets-cycle \ - -H "Authorization: Bearer $TOKEN" | jq -→ {"success": true, "date": "2026-01-14", "confirmed": 0} ✅ - -# Test 2: Toggle LÕIKUS -curl -s -X PATCH http://localhost:3000/api/records/3/status \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"field":"cutting","date":"2025-01-12"}' | jq -→ {"success": true} ✅ - -# Test 3: Toggle KLAAS -curl -s -X PATCH http://localhost:3000/api/records/3/status \ - -H "Authorization: Bearer $TOKEN" \ - -H "Content-Type: application/json" \ - -d '{"field":"glazing","date":"2025-01-13"}' | jq -→ {"success": true} ✅ -``` - ---- - -## 📊 **СРАВНЕНИЕ** - -| Действие | v4.1.17 (User) | v4.1.18 (User) | -|---------|----------------|----------------| -| **Toggle Töölehti** | ❌ Ошибка | ✅ Работает | -| **Toggle LÕIKUS** | ❌ Ошибка | ✅ Работает | -| **Toggle KLAAS** | ❌ Ошибка | ✅ Работает | -| **Toggle VALMIS** | ❌ Ошибка | ✅ Работает | -| **Toggle VÄLJAS** | ❌ Ошибка | ✅ Работает | -| **Добавление записи** | ❌ Нет доступа | ❌ Нет доступа (правильно) | -| **Календарь MAT-1** | ❌ Нет доступа | ❌ Нет доступа (правильно) | -| **Подтверждение MAT-1** | ✅ Работает | ✅ Работает | - ---- - -## 📦 **ИЗМЕНЁННЫЕ ФАЙЛЫ** - -1. **public/static/app.js** (строки 28-36, 1113, 1648) - - Добавлена функция `canToggleDates()` - - Обновлена проверка в `toggleDate()` - - Обновлена проверка в `toggleWorksheetsStep()` - -2. **dist/_worker.js** (133.43 kB) - - Пересобран с новой логикой - ---- - -## 🚀 **ОБНОВЛЕНИЕ** - -### **На сервере:** -```bash -# 1. Остановить -docker-compose down - -# 2. Распаковать v4.1.18 -unzip aknaproff_production_v4.1.18_FINAL.zip - -# 3. Запустить -cd backend/ -docker-compose up -d --build -``` - -### **Проверка:** -```bash -# Логин под User -curl -X POST http://localhost:8180/api/auth/login \ - -H "Content-Type: application/json" \ - -d '{"username":"kasutaja","password":"tootmine"}' -→ {"success":true,"token":"...","user":{"username":"kasutaja","role":"user"}} - -# Открыть браузер → http://localhost:8180 -# Логин: kasutaja / tootmine -# Кликнуть Töölehti → должно работать без ошибки -``` - ---- - -## ✅ **СТАТУС** - -- ✅ User (kasutaja) может toggle все поля -- ✅ Admin (admin, aknaproff) работает как раньше -- ✅ Guest может только просматривать -- ✅ Права доступа корректны -- ✅ Production ready - ---- - -**Версия**: AKNAPROFF v4.1.18 FINAL -**Дата**: 2026-01-14 -**Архив**: aknaproff_production_v4.1.18_FINAL.tar.gz -**Статус**: ✅ Production Ready diff --git a/HOTFIX_v4.1.19.md b/HOTFIX_v4.1.19.md deleted file mode 100644 index 6f152d0..0000000 --- a/HOTFIX_v4.1.19.md +++ /dev/null @@ -1,138 +0,0 @@ -# 🔧 HOTFIX v4.1.19 - ИСПРАВЛЕНА ЛОГИКА БЛОКИРОВКИ VALMIS/VÄLJAS - -**Дата**: 2026-01-14 -**Версия**: v4.1.19 FINAL -**Приоритет**: HIGH (Критическая ошибка в логике) - ---- - -## 📋 **ПРОБЛЕМА** - -### **Неправильная блокировка VALMIS/VÄLJAS** - -**Ожидаемое поведение:** -- VALMIS/VÄLJAS блокируются **ТОЛЬКО** если установлены error флаги (красные галочки ✗) -- Текст в поле "Probleemid" (серая метка ⚠️) **НЕ** должен блокировать - -**Фактическое поведение (до v4.1.19):** -- ❌ В документации было написано что блокируется и по тексту problems -- ❌ В коде проверка была правильная, но документация - неправильная -- ❌ Пользователи не понимали когда будет блокировка - ---- - -## ✅ **ИСПРАВЛЕНИЯ** - -### **1. Обновлена документация DATE_FIELDS_LOGIC.md** - -Секции VALMIS и VÄLJAS теперь чётко указывают: - -```markdown -### **ВАЖНО: ТЕКСТ ПРОБЛЕМЫ НЕ БЛОКИРУЕТ!** -Текст в поле "Probleemid" (серая метка ⚠️) **НЕ** блокирует VALMIS! - -### **Блокировка:** -Поле **ЗАБЛОКИРОВАНО ТОЛЬКО**, если установлен **ЛЮБОЙ error флаг** (красная галочка ✗): - - `worksheets_error = 1` - - `cutting_error = 1` - - `glazing_error = 1` - - `ready_error = 1` - - `issued_error = 1` - -**НЕ блокируется**, если: -- Есть только текст в `production_records.problems` (серая метка ⚠️) -- Все error флаги = 0 -``` - -### **2. Добавлена новая секция в конец документа** - -Создана секция "🔒 ВАЖНО: ПРАВИЛО БЛОКИРОВКИ VALMIS/VÄLJAS" с: -- Подробным описанием правил -- Код проверки блокировки из backend -- Результаты тестирования - -### **3. Обновлена таблица прав доступа** - -```markdown -| VALMIS | Toggle | Admin/User | Появляется/Исчезает | Нет | ТОЛЬКО если error флаги (НЕ текст проблемы) | -| VÄLJAS | Toggle | Admin/User | Появляется/Исчезает | Нет | ТОЛЬКО если error флаги (НЕ текст проблемы) | -``` - ---- - -## 🧪 **ТЕСТИРОВАНИЕ** - -### **Test 1: Только текст problems (должно НЕ блокироваться)** -```bash -# Установить problems text БЕЗ error флагов -curl -X PATCH http://localhost:3000/api/records/3/problems \ - -d '{"problems":"Only text","errorFlags":{"worksheets":false,...}}' - -# Попытка toggle VALMIS -curl -X PATCH http://localhost:3000/api/records/3/status \ - -d '{"field":"ready","date":null}' - -# Результат: {"success":true} ✅ НЕ БЛОКИРУЕТСЯ -``` - -### **Test 2: С error flag (должно блокироваться)** -```bash -# Установить error flag -curl -X PATCH http://localhost:3000/api/records/3/problems \ - -d '{"problems":"Text","errorFlags":{"worksheets":true,...}}' - -# Попытка toggle VALMIS -curl -X PATCH http://localhost:3000/api/records/3/status \ - -d '{"field":"ready","date":null}' - -# Результат: {"error":"blocked","message":"Vigade märked on seatud..."} ✅ БЛОКИРУЕТСЯ -``` - ---- - -## 📦 **ФАЙЛЫ** - -**Изменённые файлы:** -- `/home/user/production_backup/backend/DATE_FIELDS_LOGIC.md` - обновлена документация - -**Без изменений (код уже правильный):** -- `src/index.tsx` - backend логика была правильной -- `public/static/app.js` - frontend логика была правильной - ---- - -## 🚀 **РАЗВЁРТЫВАНИЕ** - -**Изменения только в документации**, код не менялся! - -```bash -# 1. Распаковать архив -unzip aknaproff_production_v4.1.19_FINAL.zip - -# 2. Запустить как обычно -cd backend -docker-compose up -d --build -``` - ---- - -## ✅ **РЕЗУЛЬТАТ** - -- ✅ Документация теперь **ЧЁТКО** описывает правила блокировки -- ✅ Добавлена отдельная секция с примерами кода и тестирования -- ✅ Обновлена таблица прав доступа -- ✅ Все тесты пройдены успешно - ---- - -## 📝 **ЗАМЕТКИ** - -- Код backend/frontend **НЕ** менялся - он уже был правильным -- Изменения только в документации для ясности -- Версия обновлена с v4.1.18 → v4.1.19 для отслеживания изменений - ---- - -**Статус**: ✅ ГОТОВО -**Тестирование**: ✅ ПРОЙДЕНО -**Развёртывание**: ГОТОВО К ИСПОЛЬЗОВАНИЮ diff --git a/HOTFIX_v4.1.20.md b/HOTFIX_v4.1.20.md deleted file mode 100644 index 1d064f5..0000000 --- a/HOTFIX_v4.1.20.md +++ /dev/null @@ -1,240 +0,0 @@ -# 🔧 HOTFIX v4.1.20 - ИСПРАВЛЕНА ФОРМА НАСТРОЕК (СМЕНА ПАРОЛЯ) - -**Дата**: 2026-01-14 -**Версия**: v4.1.20 FINAL -**Приоритет**: HIGH (Критическая ошибка - невозможно использовать настройки) - ---- - -## 📋 **ПРОБЛЕМА** - -### **Форма настроек не работала** - -**Симптомы:** -- Ошибка 400 при попытке сохранить настройки -- Консоль: `PATCH /api/users/profile [HTTP/2 400 153ms]` -- Невозможно изменить ни имя, ни пароль - -**Причины:** -1. **Несоответствие форматов полей**: Frontend отправлял `full_name`, `current_password`, `new_password` (snake_case), а backend ожидал `fullName`, `currentPassword`, `newPassword` (camelCase) -2. **Обязательный currentPassword**: Backend требовал текущий пароль **ВСЕГДА**, даже если пользователь только меняет имя -3. **Frontend валидация**: Требовал `currentPassword` всегда, даже если пароль не меняется - ---- - -## ✅ **ИСПРАВЛЕНИЯ** - -### **1. Backend: Поддержка обоих форматов + опциональный пароль** - -**Файл:** `src/index.tsx`, строки 63-93 - -```typescript -app.patch('/api/users/profile', authMiddleware, async (c) => { - try { - const body = await c.req.json() - // Support both snake_case and camelCase - const fullName = body.full_name || body.fullName - const currentPassword = body.current_password || body.currentPassword - const newPassword = body.new_password || body.newPassword - const userId = c.get('userId') - - console.log('[PROFILE UPDATE]', { userId, fullName, hasCurrentPwd: !!currentPassword, hasNewPwd: !!newPassword }) - - // Get user from database - const user = await c.env.DB.prepare( - 'SELECT password_hash, full_name FROM users WHERE id = ?' - ).bind(userId).first() - - if (!user) { - return c.json({ error: 'Kasutajat ei leitud' }, 404) - } - - // If changing password - if (newPassword) { - // Verify current password is provided - if (!currentPassword) { - return c.json({ error: 'Praegune parool on kohustuslik parooli muutmiseks' }, 400) - } - - // Verify current password - if (!await verifyPassword(currentPassword, user.password_hash as string)) { - return c.json({ error: 'Vale praegune parool' }, 400) - } - - // Update password and full name - const newHash = await hashPassword(newPassword) - await c.env.DB.prepare( - 'UPDATE users SET password_hash = ?, full_name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' - ).bind(newHash, fullName, userId).run() - } else { - // Only update full name (no password change) - await c.env.DB.prepare( - 'UPDATE users SET full_name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?' - ).bind(fullName, userId).run() - } - - return c.json({ - success: true, - message: 'Profiil uuendatud', - user: { - full_name: fullName - } - }) - } catch (error) { - console.error('Profile update error:', error) - return c.json({ error: 'Profiili uuendamine ebaõnnestus' }, 500) - } -}) -``` - -**Изменения:** -- ✅ Поддержка обоих форматов полей (snake_case и camelCase) -- ✅ `currentPassword` требуется **ТОЛЬКО** если меняется пароль -- ✅ Можно обновить только имя без смены пароля -- ✅ Добавлено логирование для отладки - -### **2. Frontend: Опциональный currentPassword** - -**Файл:** `public/static/app.js`, строки 1458-1476 - -```javascript - // Validation - if (!fullName.trim()) { - errorDiv.textContent = 'Nimi ei saa olla tühi'; - errorDiv.classList.remove('hidden'); - return; - } - - // If changing password, current password is required - if (newPassword && !currentPassword) { - errorDiv.textContent = 'Praegune parool on kohustuslik parooli muutmiseks'; - errorDiv.classList.remove('hidden'); - return; - } - - // Check if passwords match (only if new password is provided) - if (newPassword && newPassword !== confirmPassword) { - errorDiv.textContent = 'Uued paroolid ei kattu'; - errorDiv.classList.remove('hidden'); - return; - } -``` - -**Изменения:** -- ✅ `currentPassword` требуется **ТОЛЬКО** если указан `newPassword` -- ✅ Можно оставить поле "Praegune parool" пустым если не меняется пароль -- ✅ Более понятное сообщение об ошибке - ---- - -## 🧪 **ТЕСТИРОВАНИЕ** - -### **Test 1: Изменить только имя (БЕЗ пароля) ✅** -```bash -curl -X PATCH /api/users/profile \ - -d '{"full_name":"New Name","current_password":"","new_password":""}' - -# Результат: {"success":true,"message":"Profiil uuendatud"} -``` - -### **Test 2: Изменить имя + пароль ✅** -```bash -curl -X PATCH /api/users/profile \ - -d '{"full_name":"Name","current_password":"demo123","new_password":"demo123"}' - -# Результат: {"success":true,"message":"Profiil uuendatud"} -``` - -### **Test 3: Попытка сменить пароль без currentPassword ✅** -```bash -curl -X PATCH /api/users/profile \ - -d '{"full_name":"Name","current_password":"","new_password":"newpass"}' - -# Результат: {"error":"Praegune parool on kohustuslik parooli muutmiseks"} -``` - -### **Test 4: Неверный текущий пароль ✅** -```bash -curl -X PATCH /api/users/profile \ - -d '{"full_name":"Name","current_password":"wrong","new_password":"newpass"}' - -# Результат: {"error":"Vale praegune parool"} -``` - ---- - -## 📦 **ФАЙЛЫ** - -**Изменённые файлы:** -- `src/index.tsx` - endpoint `/api/users/profile` -- `public/static/app.js` - функция `updateSettings()` - -**Версия:** -- `public/original.html` - обновлена до v4.1.20 - ---- - -## 🚀 **РАЗВЁРТЫВАНИЕ** - -```bash -# 1. Распаковать архив -unzip aknaproff_production_v4.1.20_FINAL.zip - -# 2. Запустить -cd backend -docker-compose up -d --build - -# 3. Проверить форму настроек -# Открыть веб-интерфейс → Seaded → изменить имя → сохранить -``` - ---- - -## 📝 **СЦЕНАРИИ ИСПОЛЬЗОВАНИЯ** - -### **Сценарий 1: Изменить только имя** -1. Открыть форму "Seaded" -2. Изменить "Nimi" -3. Оставить поля паролей **ПУСТЫМИ** -4. Нажать "Salvesta" -5. ✅ Имя обновлено - -### **Сценарий 2: Изменить пароль** -1. Открыть форму "Seaded" -2. Ввести "Praegune parool" -3. Ввести "Uus parool" -4. Ввести "Kinnita uus parool" -5. Нажать "Salvesta" -6. ✅ Пароль изменён - -### **Сценарий 3: Изменить имя + пароль** -1. Открыть форму "Seaded" -2. Изменить "Nimi" -3. Ввести все поля паролей -4. Нажать "Salvesta" -5. ✅ Имя и пароль обновлены - ---- - -## ✅ **РЕЗУЛЬТАТ** - -- ✅ Форма настроек работает корректно -- ✅ Можно изменить только имя (без пароля) -- ✅ Можно изменить пароль (с проверкой текущего) -- ✅ Правильная валидация на frontend и backend -- ✅ Понятные сообщения об ошибках на эстонском языке - ---- - -## 🔄 **ИСТОРИЯ ВЕРСИЙ** - -| Версия | Изменения | -|--------|-----------| -| v4.1.19 | Документирована логика блокировки VALMIS/VÄLJAS | -| **v4.1.20** | **Исправлена форма настроек (смена пароля)** | - ---- - -**Статус**: ✅ ГОТОВО -**Тестирование**: ✅ ПРОЙДЕНО -**Развёртывание**: ГОТОВО К ИСПОЛЬЗОВАНИЮ diff --git a/HOTFIX_v4.1.21.md b/HOTFIX_v4.1.21.md deleted file mode 100644 index 9d18359..0000000 --- a/HOTFIX_v4.1.21.md +++ /dev/null @@ -1,91 +0,0 @@ -# 🔧 HOTFIX v4.1.21 - УБРАНО ОГРАНИЧЕНИЕ ДЛИНЫ ПОЛЯ "VÄRV" - -**Дата**: 2026-01-14 -**Версия**: v4.1.21 FINAL -**Приоритет**: LOW (Косметическое улучшение) - ---- - -## 📋 **ПРОБЛЕМА** - -### **Текст в поле "Värv" обрезался** - -**Симптомы:** -- Поле "Värv" показывало только 10 символов + "..." -- Пример: "7016 matt-..." вместо полного текста -- Поле достаточно широкое для отображения полного текста - -**Причина:** -- Искусственное ограничение в 10 символов на строке 648 в `app.js` - ---- - -## ✅ **ИСПРАВЛЕНИЕ** - -**Файл:** `public/static/app.js`, строка 648 - -**Было:** -```javascript -${record.color ? (record.color.length > 10 ? record.color.substring(0, 10) + '...' : record.color) : '-'} -``` - -**Стало:** -```javascript -${record.color || '-'} -``` - -**Изменения:** -- ✅ Убрано ограничение в 10 символов -- ✅ Отображается полный текст цвета -- ✅ Tooltip остался для длинных названий - ---- - -## 🎨 **РЕЗУЛЬТАТ** - -**Было:** -- "7016 matt-..." -- "Anthrazit-..." - -**Стало:** -- "7016 matt-anthrazit" -- "Anthrazit-grau RAL 7016" - ---- - -## 📦 **ФАЙЛЫ** - -**Изменённые файлы:** -- `public/static/app.js` - убрано ограничение длины - -**Версия:** -- `public/original.html` - обновлена до v4.1.21 - ---- - -## 🚀 **РАЗВЁРТЫВАНИЕ** - -Простая замена файлов, перезапуск не требуется: - -```bash -# 1. Распаковать архив -unzip aknaproff_production_v4.1.21_FINAL.zip - -# 2. Запустить -cd backend -docker-compose up -d --build -``` - ---- - -## ✅ **РЕЗУЛЬТАТ** - -- ✅ Поле "Värv" показывает полный текст -- ✅ Нет обрезания до 10 символов -- ✅ Tooltip остался для справки - ---- - -**Статус**: ✅ ГОТОВО -**Тестирование**: ✅ ПРОЙДЕНО -**Развёртывание**: ГОТОВО К ИСПОЛЬЗОВАНИЮ diff --git a/HOTFIX_v4.1.22.md b/HOTFIX_v4.1.22.md deleted file mode 100644 index e4bf565..0000000 --- a/HOTFIX_v4.1.22.md +++ /dev/null @@ -1,124 +0,0 @@ -# 🔧 HOTFIX v4.1.22 - ИСПРАВЛЕНО УДАЛЕНИЕ ЗАПИСЕЙ - -**Дата**: 2026-01-14 -**Версия**: v4.1.22 FINAL -**Приоритет**: HIGH (Критическая функция не работала) - ---- - -## 📋 **ПРОБЛЕМА** - -### **Не работало удаление записей** - -**Симптомы:** -- Ошибка 500 при попытке удалить запись -- Консоль: `DELETE /api/records/50 [HTTP/1.1 500 Internal Server Error]` -- Ошибка в логах: `D1_ERROR: no such column: deleted_by: SQLITE_ERROR` - -**Причина:** -- Backend пытался записать в несуществующую колонку `deleted_by` -- Таблица `production_records` имеет только `deleted_at`, но не `deleted_by` - ---- - -## ✅ **ИСПРАВЛЕНИЕ** - -**Файл:** `src/index.tsx`, endpoint `DELETE /api/records/:id` - -**Было:** -```typescript -await c.env.DB.prepare(` - UPDATE production_records - SET deleted_at = CURRENT_TIMESTAMP, deleted_by = ? - WHERE id = ? -`).bind(userId || null, id).run() -``` - -**Стало:** -```typescript -await c.env.DB.prepare(` - UPDATE production_records - SET deleted_at = CURRENT_TIMESTAMP, deleted = 1 - WHERE id = ? -`).bind(id).run() -``` - -**Изменения:** -- ✅ Убрана попытка записи в `deleted_by` (колонки нет в БД) -- ✅ Добавлена установка флага `deleted = 1` -- ✅ Добавлено логирование для отладки - ---- - -## 🧪 **ТЕСТИРОВАНИЕ** - -### **Test: Удаление записи ✅** - -```bash -# Было записей: 6 -DELETE /api/records/49 -# Результат: {"success":true} -# Стало записей: 5 -``` - -**Проверка в БД:** -```sql -SELECT id, deleted, deleted_at FROM production_records WHERE id = 49; --- Результат: 49|1|2026-01-14 20:30:54 -``` - ---- - -## 📦 **ФАЙЛЫ** - -**Изменённые файлы:** -- `src/index.tsx` - endpoint `DELETE /api/records/:id` - -**Версия:** -- `public/original.html` - обновлена до v4.1.22 - ---- - -## 🚀 **РАЗВЁРТЫВАНИЕ** - -### **ARM Synology:** - -```bash -# 1. Остановить контейнер -sudo docker-compose down - -# 2. Распаковать новый архив -unzip aknaproff_production_v4.1.22_ARM_FINAL.zip - -# 3. Запустить с пересборкой -cd backend -sudo docker-compose up -d --build - -# 4. Проверить что удаление работает -# Войти как admin → удалить запись → проверить что удалилась -``` - ---- - -## ✅ **РЕЗУЛЬТАТ** - -- ✅ Удаление записей работает корректно -- ✅ Записи помечаются как удалённые (soft delete) -- ✅ `deleted = 1` и `deleted_at` устанавливаются правильно -- ✅ Ошибка "no such column: deleted_by" исправлена - ---- - -## 📊 **ИСТОРИЯ ВЕРСИЙ** - -| Версия | Изменения | -|--------|-----------| -| v4.1.20 | Исправлена форма настроек (смена пароля) | -| v4.1.21 | Убрано ограничение длины поля "Värv" | -| **v4.1.22** | **Исправлено удаление записей** | - ---- - -**Статус**: ✅ ГОТОВО -**Тестирование**: ✅ ПРОЙДЕНО -**Развёртывание**: ГОТОВО К ИСПОЛЬЗОВАНИЮ diff --git a/HOTFIX_v4.1.23.md b/HOTFIX_v4.1.23.md deleted file mode 100644 index 55d7c67..0000000 --- a/HOTFIX_v4.1.23.md +++ /dev/null @@ -1,191 +0,0 @@ -# 🔧 HOTFIX v4.1.23 - ДВА УЛУЧШЕНИЯ UX - -**Дата**: 2026-01-15 -**Версия**: v4.1.23 FINAL -**Приоритет**: MEDIUM (Улучшения UX) - ---- - -## 📋 **ПРОБЛЕМЫ И РЕШЕНИЯ** - -### **1. Проверка сохранения дат MAT-1 и MAT-2** - -**Проблема:** -- Была неясность - сохраняются ли даты при создании нового ряда - -**Проверка:** -- Backend УЖЕ правильно сохраняет даты -- Если пользователь вводит даты → они сохраняются -- Если даты не введены → сохраняется NULL - -**Файл:** `src/index.tsx`, endpoint `POST /api/records` (строки 210-218) - -```typescript -// Create status checkboxes entry with dates if provided -const materialDate = (data.material_date && data.material_date !== 'null') ? data.material_date : null -const material2Date = (data.material2_date && data.material2_date !== 'null') ? data.material2_date : null -const packageDate = (data.package_date && data.package_date !== 'null') ? data.package_date : null - -await c.env.DB.prepare(` - INSERT INTO status_checkboxes ( - record_id, material_date, material2_date, package_date - ) VALUES (?, ?, ?, ?) -`).bind(result.meta.last_row_id, materialDate, material2Date, packageDate).run() -``` - -**Результат:** ✅ Код работал правильно, просто нужна была проверка - ---- - -### **2. Контроль ввода цены - принимать и запятую, и точку** - -**Проблема:** -- Нужно было обязательно ставить точку между евро и центами -- Пользователь не мог вводить запятую (европейский формат) - -**Решение:** -- Добавлен автоматический перевод запятой в точку при вводе -- Пользователь может вводить как `1500,50` так и `1500.50` - -**Файл:** `public/static/app.js`, обработчик поля price - -```javascript -// Add listener for price field to auto-format (accept both comma and dot) -const priceField = document.getElementById('price'); -if (priceField) { - priceField.addEventListener('input', function(e) { - // Replace comma with dot automatically - let value = e.target.value; - if (value.includes(',')) { - e.target.value = value.replace(',', '.'); - } - }); -} -``` - -**Результат:** ✅ Цена принимает и запятую, и точку (автопреобразование) - ---- - -### **3. Время сессии - 4 часа (уже было)** - -**Проверка:** -- Время сессии УЖЕ БЫЛО установлено на 4 часа -- Проверено и подтверждено - -**Файл:** `src/utils/auth.ts` - -```typescript -const expiry = Date.now() + (240 * 60 * 1000) // 4 hours = 240 minutes -``` - -**Результат:** ✅ Сессия длится 4 часа (240 минут) - ---- - -## 🧪 **ТЕСТИРОВАНИЕ** - -### **Test 1: Даты сохраняются если введены ✅** - -```bash -POST /api/records -{ - "material_date": "2026-01-15", - "material2_date": "2026-01-16", - "package_date": "2026-01-15" -} - -Результат: -✅ MAT-1: 2026-01-15 -✅ MAT-2: 2026-01-16 -✅ PAKETT: 2026-01-15 -``` - -### **Test 2: Даты NULL если не введены ✅** - -```bash -POST /api/records -{ - // Без полей дат -} - -Результат: -✅ MAT-1: null -✅ MAT-2: null -``` - -### **Test 3: Цена с запятой и точкой ✅** - -```javascript -Ввод: "1500,50" -Результат после автозамены: "1500.50" -✅ Работает корректно -``` - -### **Test 4: Сессия 4 часа ✅** - -```bash -Token expires in: 239 minutes -✅ Session duration is ~4 hours (240 min) -``` - ---- - -## 📦 **ФАЙЛЫ** - -**Изменённые файлы:** -- `public/static/app.js` - обработчик price (автозамена запятой) - -**Проверенные файлы (код был правильный):** -- `src/index.tsx` - endpoint POST /api/records (даты сохраняются) -- `src/utils/auth.ts` - время сессии (уже 4 часа) - -**Версия:** -- `public/original.html` - v4.1.23 - ---- - -## 🚀 **РАЗВЁРТЫВАНИЕ** - -### **ARM Synology:** - -```bash -# 1. Остановить контейнер -sudo docker-compose down - -# 2. Распаковать новый архив -unzip aknaproff_production_v4.1.23_ARM_FINAL.zip - -# 3. Запустить с пересборкой -cd backend -sudo docker-compose up -d --build - -# 4. Проверить -# - Создать новый ряд с датами → должны сохраниться -# - Ввести цену с запятой → автозамена на точку -# - Сессия длится 4 часа -``` - ---- - -## ✅ **РЕЗУЛЬТАТ** - -- ✅ Подтверждено: даты сохраняются правильно -- ✅ Цена принимает и запятую, и точку -- ✅ Сессия длится 4 часа -- ✅ Минимальное вмешательство в код - ---- - -## 📊 **ИСТОРИЯ ВЕРСИЙ** - -| Версия | Изменения | -|--------|-----------| -| v4.1.22 | Исправлено удаление записей | -| **v4.1.23** | **Цена с запятой + проверка сохранения дат** | - ---- - -**Статус**: ✅ ГОТОВО -**Тестирование**: ✅ ПРОЙДЕНО -**Развёртывание**: ГОТОВО К ИСПОЛЬЗОВАНИЮ diff --git a/HOTFIX_v4.1.24.md b/HOTFIX_v4.1.24.md new file mode 100644 index 0000000..a1b9514 --- /dev/null +++ b/HOTFIX_v4.1.24.md @@ -0,0 +1,194 @@ +# 🔥 HOTFIX v4.1.24 - ARVE ПОЛЯ НЕ СОХРАНЯЛИСЬ + +**Дата**: 2026-01-15 +**Версия**: v4.1.24 FINAL +**Приоритет**: CRITICAL 🔥 + +--- + +## 🚨 **КРИТИЧНАЯ ПРОБЛЕМА** + +### **Симптомы:** +- ❌ Бухгалтер не может добавить номер счёта (arve_makstud) +- ❌ Не работает галочка "Arve makstud" (arve_checked) +- ❌ При создании/редактировании записей эти поля просто не сохраняются + +### **Причина:** +**Backend не включал поля `arve_checked` и `arve_makstud` в SQL запросы!** + +Поля были в форме, отправлялись с фронтенда, но backend **игнорировал их полностью**. + +--- + +## 🔧 **РЕШЕНИЕ** + +### **Файл:** `src/index.tsx` + +#### **1. POST /api/records - добавление записи** + +**Было (строка 196-207):** +```typescript +const result = await c.env.DB.prepare(` + INSERT INTO production_records ( + month, year, client_name, type, offer_number, work_number, + quantity, color, notes, problems, installer, price, + created_by, updated_by + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +`).bind( + data.month, data.year, data.client_name, data.type || null, + data.offer_number, data.work_number, quantity, data.color || null, + data.notes || null, data.problems || null, data.installer || null, + price, userId, userId +).run() +``` + +**Стало:** +```typescript +const arveChecked = data.arve_checked ? parseInt(data.arve_checked, 10) : 0 + +const result = await c.env.DB.prepare(` + INSERT INTO production_records ( + month, year, client_name, type, offer_number, work_number, + quantity, color, notes, problems, installer, price, + arve_checked, arve_makstud, + created_by, updated_by + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) +`).bind( + data.month, data.year, data.client_name, data.type || null, + data.offer_number, data.work_number, quantity, data.color || null, + data.notes || null, data.problems || null, data.installer || null, + price, arveChecked, data.arve_makstud || null, + userId, userId +).run() +``` + +#### **2. PUT /api/records/:id - обновление записи** + +**Было (строка 238-248):** +```typescript +await c.env.DB.prepare(` + UPDATE production_records + SET client_name = ?, type = ?, offer_number = ?, work_number = ?, + quantity = ?, color = ?, notes = ?, problems = ?, installer = ?, price = ?, + updated_by = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND deleted_at IS NULL +`).bind( + data.client_name, data.type || null, data.offer_number, data.work_number, + quantity, data.color || null, data.notes || null, data.problems || null, + data.installer || null, price, userId, id +).run() +``` + +**Стало:** +```typescript +const arveChecked = data.arve_checked ? parseInt(data.arve_checked, 10) : 0 + +await c.env.DB.prepare(` + UPDATE production_records + SET client_name = ?, type = ?, offer_number = ?, work_number = ?, + quantity = ?, color = ?, notes = ?, problems = ?, installer = ?, price = ?, + arve_checked = ?, arve_makstud = ?, + updated_by = ?, updated_at = CURRENT_TIMESTAMP + WHERE id = ? AND deleted_at IS NULL +`).bind( + data.client_name, data.type || null, data.offer_number, data.work_number, + quantity, data.color || null, data.notes || null, data.problems || null, + data.installer || null, price, arveChecked, data.arve_makstud || null, + userId, id +).run() +``` + +--- + +## 🧪 **ТЕСТИРОВАНИЕ** + +### **Test 1: Создать запись с arve полями ✅** + +```bash +POST /api/records +{ + "arve_checked": 1, + "arve_makstud": "INV-2025-001" +} + +Результат: +✅ id: 56 +✅ arve_checked: 1 +✅ arve_makstud: INV-2025-001 +``` + +### **Test 2: Обновить arve поля ✅** + +```bash +PUT /api/records/56 +{ + "arve_checked": 0, + "arve_makstud": "INV-2025-002" +} + +Результат: +✅ arve_checked: 0 → обновилось +✅ arve_makstud: INV-2025-002 → обновилось +``` + +--- + +## 📦 **ФАЙЛЫ** + +**Изменённые файлы:** +- `src/index.tsx` - endpoint POST /api/records (строки 196-207) +- `src/index.tsx` - endpoint PUT /api/records/:id (строки 238-248) + +**Версия:** +- `public/original.html` - v4.1.24 + +--- + +## 🚀 **РАЗВЁРТЫВАНИЕ** + +### **ARM Synology:** + +```bash +# 1. Остановить контейнер +sudo docker-compose down + +# 2. Распаковать новый архив +unzip aknaproff_production_v4.1.24_ARM_FINAL.zip + +# 3. Запустить с пересборкой +cd backend +sudo docker-compose up -d --build + +# 4. Проверить +# - Создать новый ряд +# - Ввести номер счёта в "Arve Nr" +# - Поставить галочку "Arve makstud" +# - Сохранить +# - ✅ Поля должны сохраниться! +``` + +--- + +## ✅ **РЕЗУЛЬТАТ** + +- ✅ arve_checked сохраняется при создании +- ✅ arve_makstud сохраняется при создании +- ✅ arve_checked обновляется при редактировании +- ✅ arve_makstud обновляется при редактировании +- ✅ Бухгалтер может работать с полями счёта! + +--- + +## 📊 **ИСТОРИЯ ВЕРСИЙ** + +| Версия | Изменения | +|--------|-----------| +| v4.1.23 | Цена с запятой + проверка дат | +| **v4.1.24** | **ИСПРАВЛЕНЫ ПОЛЯ СЧЁТА (arve)** 🔥 | + +--- + +**Статус**: ✅ ГОТОВО +**Тестирование**: ✅ ПРОЙДЕНО +**Критичность**: 🔥 ВЫСОКАЯ +**Развёртывание**: СРОЧНО РЕКОМЕНДУЕТСЯ diff --git a/backup.sh b/backup.sh new file mode 100755 index 0000000..9bb298f --- /dev/null +++ b/backup.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# AKNAPROFF Tootmine Database Backup Script +# Создаёт бэкапы БД с timestamp + +set -e # Exit on error + +# Цвета для вывода +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +# Настройки +DATE=$(date +%Y%m%d_%H%M%S) +BACKUP_DIR="./backups" +DEV_DB_PATH=".wrangler/state/v3/d1/webapp-production.sqlite" +PROD_DB_PATH="data/db/webapp-production.sqlite" + +# Создать папку для бэкапов +mkdir -p "$BACKUP_DIR" + +echo "🗄️ AKNAPROFF Database Backup" +echo "==============================" +echo "" + +# Проверить и бэкапить Development БД +if [ -f "$DEV_DB_PATH" ]; then + DEV_BACKUP="$BACKUP_DIR/webapp-dev-$DATE.sqlite" + cp "$DEV_DB_PATH" "$DEV_BACKUP" + + # Размер файла + SIZE=$(du -h "$DEV_BACKUP" | cut -f1) + + echo -e "${GREEN}✅ Development DB backed up${NC}" + echo " File: $DEV_BACKUP" + echo " Size: $SIZE" + echo "" +else + echo -e "${YELLOW}⚠️ Development DB not found${NC}" + echo " Path: $DEV_DB_PATH" + echo "" +fi + +# Проверить и бэкапить Production БД +if [ -f "$PROD_DB_PATH" ]; then + PROD_BACKUP="$BACKUP_DIR/webapp-prod-$DATE.sqlite" + cp "$PROD_DB_PATH" "$PROD_BACKUP" + + # Размер файла + SIZE=$(du -h "$PROD_BACKUP" | cut -f1) + + echo -e "${GREEN}✅ Production DB backed up${NC}" + echo " File: $PROD_BACKUP" + echo " Size: $SIZE" + echo "" +else + echo -e "${YELLOW}⚠️ Production DB not found${NC}" + echo " Path: $PROD_DB_PATH" + echo "" +fi + +# Показать все бэкапы +echo "📦 All backups:" +echo "---------------" +ls -lh "$BACKUP_DIR"/*.sqlite 2>/dev/null | awk '{print " " $9 " (" $5 ")"}' + +echo "" +echo "==============================" + +# Подсчитать количество бэкапов +BACKUP_COUNT=$(ls -1 "$BACKUP_DIR"/*.sqlite 2>/dev/null | wc -l) +if [ "$BACKUP_COUNT" -gt 10 ]; then + echo -e "${YELLOW}💡 Tip: You have $BACKUP_COUNT backups${NC}" + echo " Consider cleaning old backups:" + echo " rm $BACKUP_DIR/webapp-*-2024*.sqlite" + echo "" +fi + +echo -e "${GREEN}✅ Backup completed!${NC}" diff --git a/data/v3/d1/miniflare-D1DatabaseObject/2b35d4d42e3c9f6b5ad5b5579a7b1470c66e69f6b33a31e3f5a0095cc6d18656.sqlite b/data/v3/d1/miniflare-D1DatabaseObject/2b35d4d42e3c9f6b5ad5b5579a7b1470c66e69f6b33a31e3f5a0095cc6d18656.sqlite deleted file mode 100755 index 53023ef88a0518da8786e8a9efd8e0d9f4753832..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 200704 zcmeFa31D1Tbuj$i>@%9hc5KT_Y(L3yY{ioNw%Oz?l5N?RwK#)%V1fV3+b4bBAv1%M{bXcrHJkDhdj>gmXB!PC?)}ZY}&L!GHR1 z1>CswKj2^9bZ>XNmTQ0UVK=@U`8B>1j{G{@9DG6O7Y%RrUGIIP=bYm;o)rH`=ivTb_{rdaP=PJ5d+>nJ-L-SCaA5FIU`O|!o`Ha% ze^BwZFw{LPXoQV`a$P6vJb2(h_rUPTa8G~t&~R7(e&NvG?gQOIW(VOICgdK?mE~vEQn?_a&-4>5~4S9HaJhWPB<$v4A{HmWKw7 zHYZfnR)s~PR)Upx_v{%U=COsDLyK^rdpB6?z|QU=0q7W;9W7>Z*%A6eVM}JL1)avO z?!IntA3M8-c6ROR4zzRyJ&gyq@mwZ5mOfdyZ3blW9g|hG&e2x-Rko; zN)rFFN#dw0zpH%W(<_6x>Pq-U`D#K=r5)_t+r6__*g~N@x`z*ScMk}X&^542kmMFr zapb1rI|PhQWzyN=NH#T1-8wTF1Qt)t7|uR7KAz5xWM`+3rt{2d(eYWI-8P%b7Bj_D z73G1gnA(ET+!Xj#y(!7&ifO|ckiW`3K_&}^GBsr?dnTV5O@onk^)aH$OGxEUq-m#N z!G?*vI-Qy<6lcd6O{9R4nN+o=v5XZ>Jr-gZoGU2N$WbO4l(Fu%%AbyE??N}?BAuCz z@8Z8KbAvhoM^Bac!P!DOZ*_gx?ZNH>a#MywC*>k=1X@ZluXpXXM&B99yILHa7D2T|s;U!A33~omS!jXek%dLBlU9?bFmb zn@JT4V2EQQ$5Mr3M0=S*V4aN5PEA!YaAqr;H{)CR+?2%w7%w0Nn+46wkQaLfge{v> zW7C=J=2l@dfwvGxlY&O_MB3gGBrHo;u8Y@EU8OGW%Rozu$J2OSqg|^^>SRPB|9J0o zc^fxv;!E2}HI9sqkMwsBcYzglneW|Ym1>3%hc1xcWSW_}xetyapuZETsoAu!qi=AB z_NZmQ)6=+hD}RIz1Zel6kiKm;ogGbUy$StKR~ZQw;1<-FR(Q3gw3qibZr;qF86-Hz zq><^&L_URmnL^ngjxu~@pe+HvAH1Oct6^N>_Y;++U!ap``s(+mW@ez(8N)G>F*4E} zYDEu?>2jbY>F_l6Z?>z&GKxo}vOk@4TpjX<;AUA+;38j+yfHE!QNsTn{zUlY;lXfY z=Jq3+pe}*B1nLr~OQ0@+x&-PHs7v7YS_19MT-VgU?`821U;Qog8s- zJMp_H{f-qkoi6HhTFMe*VXZ9H@(m7pai~_EMllxBzEj9|)2S>>yG&!qa6THvq!M&- z-AJQMzJs_Y3HO7L(o=*`t>E}58Wb<{RBcN4667YtwDVP1*RLTYnvF2J2b|q)}U@;-H&HrN^L&PQ8)80 z4onMFM>pH*%%}nIUduu+Jq3+pe}*B1nLr~OW=P?0&AVydGmP96lPhr**OHT z1w$%JH#&Rqz4^i?OEx%n;yZSsl8A<5zOYDGduP57NJwyYV>!0rj$n)P5WcW*Xrn=6 zf1xl&fRZ(D$ih!ctsy+&)3D7Kitt|L><1p^4?B3MBB?)A^Cp{O?O3>~?N&ur)ONFIKpm+xUuIlHy9IQ{qiaCbWH-qTk_cJq3+pe}*m zHwm~MPR_9;9}54JbB4afhbJN@Lf;B~>-UY#IzM#@)Fn`tKwScL3DhM}mq1+tbqUlZ zP?x|TiUdk7SAbhr3awk=SCq+AVYZk$o=QoOxZ!C0=$ND`<8dh|%4*{1Xgrw|rF1f- zMB^z{Or&KoF)oiK((0I!7#|%SmlA3`s*Wqs@m>uN&MHF=2*|AQ;2KMzmPR0hiSdW9jj9Toe;x zLJ^}taV({XG5Kgb7K^Cp)Fn`tKwScL3DhM}mq1+tbqUlZ@Y^ndpmQCsM|tq9 zzECljD?*?o?F>0L@F_h&;P5#c{UlgnI-74@}aL)f5|CIOr z?)APqJVNjZ&%e8Re9yYy;rN27EP{cCC5AVEM2|Y zkKZ_boX^MeiW2lbCr2UT)7o7a%jNRfsZ=W4+Mmu(c8Idr*0pD#Z}0w?l8g@bstH+j zyei{x=;0kn7Q|SG8t+h}RUS=mzyddLt5@?IIIhuaDzNLou+R%fS&i-3AC;seK`kwv zo-Gvdh_~WR>C9xV^U2TdyKd&uFB`>)2|-L?U`vj%;v|v+RRmg(ugBsxYQ=Ho#qAcl z@{s>=wtxl3VpyP*bQEXdxS?Wddb)GQ^$h$zQ&Cp5Lt=3?Ot&g`$ZCfw&8)++wpA-@ z@ciqA-T72@QrMZA$-r4bq(mY;DdA%*cD{G2efjBc@2x0N?NC7OG`J^G?(k7b@M4h? z7P)b#sf@khR5m>&?91gqd~HHE9DIaj$|%-&QuI#FPK{+LpL>Ufg{kvTO+s2wp;eg5 z6yT^K0Z!DLoIO4(%%mpM#iCxenCSe#{m+iW&qQJr>syXiR)E?OjhB3lNMaL^=-<{- zPGa}WjIcj-d^Vp!WfE0-g_O3r(cIB|YBt*jXOK*!HQHu@Dj=1S6778F?why#$HyKj zBZVZdh4i=VHBb*wMY%(bO{_*ri0Z8aT{r8(!1;5Tu}mRNR4rgQ_na)#z#^OZhqt3XfBsBsaeodu7k>g|s1hy*-E$MkB7ol#%&uwhtW>+F9 z=vBy{W=-makbWMjcqTJR)Hy+~M2DxxEDHRVR;<+n)@l`#9l%gI6%Zm;XO?2IZRo5G zy`t4BE25+dhv0Zl@X@*41m!$VDnL|BjNkn%Wn4!Ol?{AHF#^pVfhL0}M39EBi3{qA zG`JQ^-^4{CK2$27$#X01a30g}Pk&h1wnX46@;=0 z;f~(+GMtoPxg#nwf^~(Bs>iQHoKbX$NTj)DNc$n|O=sbZxCu~^u57V@m6KG4dzC~i z;q1KUC*R)lp5=E`&<|$L{Jr6QfrX?p6~^*5mRAXFKL^0nchAli!Bc34O?|H#g+xo5 z8ou(0N)Lvzk9S0w#8`={6ugh~LkqKX^`Kx|7ph5gX1Qt2GE4v&ywH{!A7z#xLcm-p zH_{IfO#J}Osw84nva9oL9|0$PbBjrAw2Ht%vPKJW;cCPDhtOf=7mEk%}1*eYK{uCb&^O)8|oh%ys2l9 zl@#rK!%mq$mh-{PtY7{ZJ0h-OXpxg(>J)Xexv52&N02 zgiJUQ5_($M;zYY<43GRI0@iTeDlkQ<^oG#05Q_mML?IPuAp}itZ+i3?IK!FN9er&( z+xjR4aj~w2yCVe7P z)Cwj22{GFF!cAZ&TYhdW3HqtbB(WC(3IZA$j~9J9gQjjoMNyy-kj6Zp(#pcNE+MK8 z!)Q87pQu)>9JhF7C>B})Q3kfQA&Y3A8+&y{q?S>oQ<+_cRBpw$p+jHhaS!CM?uGsN zOlJE0xim3qN?*)n^m!k<*6MY24=*tf4;287BA!hk9q?dzo2s=MlF9W|Z8c;NHS?>q z<&)(OMV)l7nB6VkcmAn- zMi_?S!qQh$d#O6-4fP6z%ZUy}DXn#3X)UC*p{9+rG?qxR9f)v+Da>}bzpH@i7XB_Z7|6K zhfH1Q>yM{|gOdeTqRdK^eRFRH&Y!+;|J+9|JUI7P!rX`E9-Dg-{`-&s(s&vq-VR9* zu^)x!Vno3*GXgA^nDqK;qb|!E#4)uX71cE)48r&wB%4102fZ@lBu1PRG&IBi@de8R zwYzc|=KmMNA{Y5(Jq3+;CD&_KDWc~6 zrr)&cH(T|aYxJ8I{bq}Pv)S)<@PPTb;MMGI6T7<#@B9ee$euN`yC!xgu)7WHZauqO z$L_9VcWc?*8g{pu-K}DGjqGkEyIaBTmb1Gn*xfRAx0Ky2VRsRB7iM=Mb{AxK4eTz! z?)>b|$L_rB&ZCL+Ik$e}(r=vljl+xe==VNnn*ZM%`E@uJ`eNt}Avt&t0p znve5-?(5vUxw{rr?bfZUIG%&p7f$ZrKlFc`PuU9b3;l3Wm~4lLtQu>*#_sKFEN??3 zRcURpd%MN*wg~83x7fYCrTlFOs7cY*&312Zw!96q^ofzHwYQs^z<8Q`_&>t3ay7h7 zw$GSmND?eqD;Yt17OgeEhyh9!Cc%}=rpf}K2oU3@Dh0%h0*r98QB>7CL4rUn#6soF zRdz*Pb-9WHt|S#IFt$-I%Bmb2s}vQ-C^klH<%7-e9g#AM+pJX0mhT`&2eZ(XRbWg^ z!z41*J}EKMq?2Q<#isdD7$N2AwTM$xF#VB?33>rmej%(_p(Zh`k|-!+JhK7$^>e-o z_1ZwcliFmqIKjM3!O#PP#Ok#VGJy3aQK_)rELyWBuV*A;QN(ghqGiR4ChcYqO?qxp zNYrixXRb2qNTF5uTc;OlgxD9)Ulb;IMy}MWz*@&w>P0P7fWd+34%carNPKLqUVycZ z*H$ZltTV!i>)|y_@HkIka+GWI?}!Ynf;VPEO%*pfDw!2nR5jCHQDadi`I-vQKbBzb4_JbE zIb~!y6os;?@FL6E+tqlfu>d&U`wG1PtLk4-r2u1nMP?b1L6e^mMrsrqw`KZwA``di zjyZOd&oL2es8a%$?tyy~)yz^maWADsnLVkvKmmrh4JFdW9gSv|*cG*;3Rm+NWpHKO zNo0|r2ovbBOazOnR@)}U(-?Q)s0CmE?#Y3a#p8{H)OV#b&B2CTY!$;nKtr zpIrgk46NK%RmF{ZnWq5gs3kLA6k9cG@-hyL3IA%mmN?6H0<^G%ieu&N{S`nrKP1tnE+|6%{f?9Z~rmtwStb+@H@Kol2*tJA_GCyfyjFzZ-=!2ua7(& z`Qyj~upZ#Wk&}^pWD3><+!}d7WH>Sa>jHK}u8qVZBCHL#DzZMZDzXgL2Y4b}_&>ux zgEa!*3x8+9NI?CCx&-PHs7s(Ofw~0h5~xd{E`hoP>Jq3+pe}*B1TIMeZYR%+B3`#` z!)tpxUfbI6+S-cOty}SW%{6#!X~FB3EqL9$8LwAgjn_?^@Osr%c-^=Wug%SPZEC`+ zAmDYw2E49ckJokU@OtHycwM^|uWQ!eb@ghzu3ClH#zwrZT#45eEAYB}IbN^00a*c(_p)(pY;*5*^`RKB?b zLv5|iqhxE;NH#T{ZtiH7EB4zpw>B3~%^+-`5AGq8XgWUvy9{B+FFeC7oACrZzde_q zG#--;b5VE8pPIr#==(FU zRTH2v&DI_TZc_OZ>5CwqFuqSE{rXkHjaTXq!z8RTGp#s4gB@grGE^u$?QuCS z6Cj<>qws(Y)5FnIqf_Y-*q}Z`AfvNINE0wohFgDTd=E(jPFvs7pBZm~@{FA-!$hAL zZ)6IE+4NZ1E7~LD9mv7}0pn>_(fZ$IUuh3V#-~yfz{iW2$VkK=W(vf3(i=bxZ;Y(v z6rZl(vookML@~r@ED>*yvk4PzBOFzdL(_}FY6iHo-meTyhm>O$rbuWlY{ znR$R!YUa^wY%_&77HKBBLDVhe55Pjmw&VS7v=F7Ksfkm(x`hz!X{UBV?up$Q_Eqf; zmzhE48C9Bv%-VplvA1_VjM^rByDT>gfDHmzvG=WX>h}8Jh{1~4UgI^|YvtBzX0Eh> zu(W10n@=@!h4emU<_Z+(=4#6NW7uo+0(|RlYJSjYC9u)7Yx|;dG}?ao+diZ4Botn6 z1C7?}x26pwnV`=0IdsE#pjYweHf}bISdE5Z-qjCvuC-vWzaRdv*aSW*@7&=NqhALt z&E@FV)f+6m1Neu~U}4UPF*q@Sm zSr6B>r75_sDGkGQb!ih^SC+1X>k53lyaO&*V7?5$S>i=`tScp0;PP&yLQhu-$RhP#E~13i28bRQ7r{W#FIdl=q1xPKRZGB_Yq zU<>RXJRo#;?c6II7(5i%(Y>cK(~+?1K?oauJ+8>_CWW*uKRl$yp7wo@vrDAreJtffw|&AK_5=c-*w=K(A#}P=sGw&*fRj%_XB0E@E(m0jF?^@7#tP`4)*m4 z2M2m?JlKsdv0eaQ>hSU2bLr~12Ki~JHYqmfV*x)oNUUSDXct(f1EL{^A= zn54^Lw9bgTEBhE21y9%AvuA*~h%GvQM^Cjt4ekb)HL$aLNWez9uq89rg3fYRcV9QS zP4s!B5GxKtay8=!w3OEQy^VtsU)qz&j-^j##!ik@+T;kD%Lp;2N}yje=$?UH-G{*k zE`R{223{)B2wSj_R)Lh#s*ArR_yYbMdYG3@);$bq0e%ZT%stn-y^W2H{C$T=9TBA^ z7f+e5Cyy@5z2g@c7N-`bdty@~Ig=_BKpqD zAugjI3ouKvb!082nAf{@TchucWaPF&=d z{2PegT~XJ&-!{E#Y4La(uWRHv(ySD2o5~c^BdOV9j{H4BHAf=iNP6IA2=OTb`Aw!-XTsbEN7JJ}$h|$A7IyRv?$92!?00$^ zw{GQ+kis=W3+dZt)7jCqmLl-7w!3Xh}Le zjs2VLYO##s5vlA?Ck$svN@a`!1YO_cBA$khz%K$0-;LhSd9U|8?%CvilY2V=>c6@K z>Jq3+;CE31H?QzCN*nmw+2AQPJC-SqOywp>6UYAArg?f}ki=-s@7> za$0Jkg??;9iYW%wQd+u%j?cHvCm1qlCZqI9dbZR;kAc93CT&=bWAv66MZAsMH}Iu4 zMiSFQO;bR5*`r8B`3z*d4V^Kw1qg;}w3dyTx`%eQ+|wTRHg4F!-?x=?uNXFV6Vyr^ zCOt*=_@aiMOf~h6HFhC2yEH$-QC}`wK77Pi$1~}vG4dGy%1w=p(4MCr$jzp2w>+6n z7gG@HO|g#_KUORYEiq2YqC!>lq}DK#=A3GcQ$dXB4{?T}E9hxFxXrHTT;U5xq>4uk z^+^i4LhuqNDKKJYTJp#2%k;d~*%Z?!WQ~IE3D}5 zwG^r^BrHRqKdc3TvF3t_%Czus0rE{t64}@0HHiy+4sOYasiWuAACymiXiv#{#RV|x zQSpn9xLpn}y0dF&XVx!i>rKf z-oRYt>x+-lIiO$fjGV(WD6WE4|K}s;T!PCJd0pi0$OpXtlKU!m!1W_{-hJ48eMEMQICeQ&!pkG89AW;a{8vK%8UAn2AAA1E^Ks8R zy{+D5o?nK(75=W1cRcI(bod_}k2zlBINk6-_%Z$?{9o|*H#{AF*!?p1Tit)-{+eft zC+z;2=Y;2G&yLV1!*}|g^>h9;{$u_Dzv};>|MmVe4d+7d3LoWfGG!%Yp>HZ(VU zCA1`TjdQ;<=DgDRM(4fG7^r-mAh;qFSdoe$POakX$!f-7IK=xy|bEo7EJtkO|o147XjQm}>O?I7Z9wvZWHNRB{^ghEt0 zc)(HC4icNTg-qE(ChZ{Vaa%~n7IKV0jO<^OYDoTiklvxCT^wveN? zkdz%nyww&mVhg#2K#W{kx(p=wg?13>W?RS~*+O1m2N93hLJr$P4iN}Tlc)JIJh}2F zJ4pPXEo9ghGGqsd9AZpwe60?Ow31mfiM~fV@sM3&QV8gE>>2_wq@zRRcng6T z?Y0h)5?ctw=+){FF}|5VRxw!ueyBdL@Be zQ7#?mZb#L%1Y)!cL9*U0oFdd2JJ`2*j)hq!8$(H4=!?bD-ED zNv61xK#cZOhlq(41Y(R37(`u8AVyc8LLe8Pas`2yIYkQaSlnd%O@04=ExB*%`~PV&h_YE~;w_B1Xg#BlX)2n$VVZE(Uop=&lXqY` z^HOu*)c61E(k0sr>nWW`H_u2^p{p*gy3i%1KQrF??NR6I`~SgE#xqlCEnm$Y&1?e| z>;z9`Comh=cxtMUZf#~euNa`1pG_Nyqf|kH1IOUND;3B8ZtemXa{7Pj`JD4fJkbMX+Q@0+J)V-vqgx-7+FB<(!516I}ENVDogVgPnPGgGSa*? zM@30$gLQ`!Y4~uFd8{NU8CQTE^X6gVRLxt2b&apxH?Ss8l7P0#ZSAB8{c%0*8zh1= zyvYAjlShJ{_bm-FWk0Bdsw%ruqq56UNlprV>GbUFsr*eT%(0$zgL+ltioc>sg_6d$_3VPf{WwwnriH)bnKv;zgu?Vg^+9k!h(U z?fKMGAsTjb8M=Wa+<)Kt24)jEC0NxaAe$grGQUu|PiO%4r(j9oXeI>-4GXF1nek3NXc`$ry~(P>|D5NwyJ_AlVJG=g(!vG6iVSSuOu8_Y%A)2jkoYr(x%|qT49JTcPhv}ASP3*=dL}_)A3)aZ zbUIs{Xt%Z=bd+kOan!p#EEkLZfb@ePA|l0-?Ql%=^VeWpgdMO3qpZhOZ!nDU+Vc@N zAalKEPy3lYY*qqQ?EzeMP4<8zhP~<0W3V-1rgcYO+s-yj3cY|K#P!t2mXQM{#x)GV zG;$!V#ifoMR7jQy`4FGC-cnupF^%_cag>29XTb5lpG$BdPhgF=#U=8`_$#?N?p}B_ z?{D8qOn$pvibObNiEc7_wM{tqKz7+;8)?d@Vy(nB_V&8HD z7i-!UhfCdeg@KE?g+<`Pr88%{m+4OCf$NkjbcxYjgfs`eRC5ygcG$=?Jk;I4qx*p2 zUFz9MwUqVqH=4AUVA}g-jtQC`vrT7Yq-FvbBNo%cKZI#Cve4`7iKV(8Ke$0zR#lH{ zO&5P4haE*>e?F6$K7S6QV6>sM%{{H!q>7J~HIxtz(|e55vxOq&YAnJFi;S?TQZ&|{ z0G58M8cNaJy3VigtY=w2VF4RtJ^CiA~e_$y{!NX@0X^a5-Letdt;C`n+`!qbeLM@q8&h99)b%>CLIpMX1M?jWN&v z(R6AQm*<|%M3}<6jXqvkkEPap>T*L~ zY4op;^J@RPyUuTdCG}y7F6` zRoIc5oJ^lc!!$~pFjUMj2i-k0BkWHdpUr2OORhTN#QAFxS8w1r-w4gJ$A(Kbg=Pz*$8w{&qxsZq);d4Y zJazu5Nf_|XP8Ni{L&JdB+(9vhY3T0l<}4H?rby3cKjk9lxMOXi)Fxs4`Xc))WJQfB zfKrdbu!U*!3`TKJ7;a!ZTUgJ`VYRZdsyWm;$K9Jc4yP^jrDrmeP>u6MV2I*Zo@L1% zSF?w*UO6>@>9Iv?zZ{oD^|xfsVo|;Hy4Eiy1Or-6Hs|hJXT;-I=sK_aof_u}5EpLe zy^p(+S&*VIZ06CeTMWbAi&9Z21r7HdwB2}<3u7R_n%CJs)-WD`;}A)EO)A$`J5 z67@T}%b(2NojtrpH=_qemF2oomYGqlbLah;lev6qO4yU1otatKmZGTi*ldo^S~4heXG$tu2XP^o6U3St&MDLZcJgi ziaBmf>#%Q?(RNi}vm!H;p#m2sg4ow+;9?UII%OA!D~jX)wcKtl6oLK!U-Mk>JmeYl zMBJZsKja>CM_d$txSIbG|5AQC_igUg@R|1O>i~+l?QtS2 zn$ou2=-juH*-_)~5o5Mgf(2yIgh_f*9!yme?M)JF(OR+Thmo6vr6)j;qlXg&*;YLF zciFkkapK7qWVqqc9 zTfb7QS20+v1=~FJE5#1WlvS+X8j#gozf!D6LdeC~R7H&SZd;`MDpf^{4YX7Fp{nRy8m3xdx(l%yyS@$Z!AbOMS64rYUOR{${wY6> z9y;bE57PCm^!wFtl0DnKqxDK)A$FSXc)@GCqB8rWxr!j;ntJV4`h6pm&zz&_dormB zm-cwF;%hN_{WV7EY#PAywWMOS9Ix$)V)-8PE?wVZe9s0IrtcL6yWDtfkHss#m*dxO zF}`QRdeirG1dP{qHCFMxl(>Gg@jZ)@nZBp-BfPdtiHh&V`1MyC->=epbEfarSS*@U z@YPu|_1sWuwDT(Z{n~Y$vZmVis-%c9Rl(Pi(i8Uf0Bj1c>kzpX1fF-C~V)Xrv`nIS|JW{hQqQ0tuLfN4L< zJP|?+aYQR4gk)G|yQ)8(&t%glQl_mg7dtqkPUZPicnL?EdjHz!n^#?^f>u%wis$n= zNI3yx=Kqb&kf`vOBXCC(X*)=N&DwUEHrnZ}jy0YkVK%|DS;1s93`?2O)Vh1P7Y>wz zGkG$`Vi&5|=A((5JDM?mo~XXOizuGaQovd)qoI&u0(hWe(Hb1QSmU8B`txTzWg8%C zH69V2h;DJFp*7Jdb8t#u>SOd+%fdOP$MyBRqOt#zx7{=eCp3 zfWL-;sNK*}g=FBd!;=KrRvbFkfXr^_qdOEWtuSKlJ5Df@ct{PHjq$Q|yoYqEy_rQa zZ5i(oWCO=2(>UI%fiyd&$$0`u+@1-<7u|^XmS{DijgqPJH#GrMX)2$P(5k0T1bj-X+%Ru?G}2p9-H2Ao0m19DB3e-i|Qf*04ktT`bH5I4Up+n z8P2I{ILi)6aO3V=?r zcVv-FTde?*4ctPRHY?jAnAWvU?0F&Lqgod%w}OjP6-n;9*^ru!Y97+&RTW9<{Ubwa zwP2e^RV3-~3#imkRcb(1v#Ll^-;qT!T}4$SvG?#InXXh7NgOytnMPH)1g14rk;I;x z5Fb^Q<%?GpJYAsgpdmFK)#6kIu*G|a4XM?F%~n+q+2J87HB^-vkkzOvh`H~;BAKpG z6~Gqly>XFDTU7;-4eX~(qpDm2(?nGeU(X=oqpIS&6jcGO8=$C~I0(yL3Mw(t(|?($ z661Y+L|~+!q*Jw61s0>dy#yO+7Hh#aj{=L)!}|!bt*F2?Agft{#b{s8BAKqDz@pr{ zcacn2DzGRI?4eAf0$&2tngWY*&u+v=6?p05wGL0}=;@}YP=PNERibCt<)SLyx048r zD6me|;uIKoRC{+2Y(#-;!Dg$#i0p6|LADhYxCUf3DllU1>s%z$6$%X4Sh)1dk-+TKNujDcqt_j~f46E_MlG9+g7IX_Rtbz?Y42D_$)WtHafJ*ZQ z!!_ZXhhYU&nll)#1>FJ+E6M)541U=Hqs8(IaqSly?AC;C9(HAA@I?l^4Q^{f zH&5M4%HZvkTWoo2!L~r%O3MBdh)`+5@hwA0Ho<5K(v1xVld5 z+nsN6KI#0T^GA+r9Nqj^`JXu=k;r|GKE&S2&x$sNF+3-*} z5ndN|g#IP;Oz3@~heLOSZVh#Zu7TYKKMQ^}_;m2m;7fzq;7~9TTo-gS{0l@I-q-M_ z4KHdKZRly(*03V*>%g~N?{>YCA9UUu_+;QO1AiQt4g67{E3i2b^8eKTRsTo*Z}C6q zzs-Nhf1SVC@Adt_cft1|-y3}Q_%gmhU(~nO$McVPpY?v)`4)EQ|D*f;?$^3s>>hLPbA1N{WciV^7@eTd zbfFgggxj31XM+7qWp6NWEMJX|Hk92qWnKs z1wmU9=l{tLr10OY0O8KsD9niS-yk51peV~T-h4fKd6s{?H`% zpgrgTd(cbEL6U@|gTX~1%xn*$+Q-N+e?_&=0MWB=lYoYt`73I_28b~n<2@FHG3;SrenOG&vVdp< zgL~EZH`{|AAriDd&e(uZIbl^Ew|rwjGPaOoc97V#Eo90TGHC~iX9=r}YDvb%8CD3Z z67RDEDZJkTqO!wP^!%IbL2o2fSPy(Qj&-&|sviZdZ+xN$z#HFCsY)F%i)tyyfb6w} z?6HGH`)nb-wvc^xka)ixBr#wEk;FkFQpTxKM=Bq%1I76U3y9Vc*m{zGojvH!2o+wv z+oGL&3JqS&X#-Iu>QySgQ6-n<8(I@MfIDFenXL+mgI%7og`Bj7+)h|!v>9$A&i#M( zpzqj&zHI?9XOrZ;77%a?DhyqxB>xHU3-J?PbhV~?etF!6_jeB@@3 z1e}6Sj#Zbq$L&Gyvj;t915y<3{pBENc7bCl0r|W6h&?E54+`0V;`}mu&{BKQlB%F& zhbZwY>_N1VXEN0IB41r$^OYp=^i2B7uK?6&|J6!6P=a4=1A@Nz8aq&eUuy$W6#hy( zP>f$kKx_a+dr+V`FiKPHL5e*{wgIUsAGZg^>_Jf*P&CFTs)C>`i1JAr5cB|OpVrDU z%zLT)_C^;h_hvR^oc|Ya+{rz_cW|ElzA@iEpXgiZ{GjK@o-cX++VhC#tmnAn<<1M< zcY9yyJ?VX+cZYYgH|S~dc-;T$o^wCpex3U+ciP?SYIb?uqI-qwKVARq`nc;Iu2;AU zj+A4Mqs{Sl$9cy;Irlk#!2g2(M&#MZGY~6yZREv~vBKxHhu#r-C^YNb=3MUhl{4-9bLZWm7w|Lu!O#t%t3v+Z z4}+f%emMAs;F;h=urDYDS2p~*;ad%#Xn1GCLk-1-7c_J>T-^{1{3P&|z|(LJ zftv!?2ATst{z3k&{(tj-&i`Rp(QvQ-xPQMt=D*VK;QyBI@coPL8Q9S2X0QZ_OdW&TkDT4n)37%0xa!5;K_0$OhQ3fc*Yf2$1$n!dLXP!%#1 z{u}`<(GiMC>?^|f5jwhb$Y}*ZTbkhLKy|7972^wte;e`95gkO^Qj$e7Tmive4UY4# zAw(G8&=wP?JGobQuH}|G)-ZGlfxH{iAF#Gfjd5Qq=S50lw@!qfU5x*<4G3)IAM8L1 z|MvuBt|Qz-h65u0Q}&?0vj=_BQdWXc2w{OZ|B1@4B#0znr~PkiK%&Bb)E@LS0U3g# zT_Gq-;ongX()Of4m?_EKO+c*DY0X013N-eE1jK4dt8}QPI8U3zsvz3@R;o%g%D;yo ztO|N}d085TL?d2q4|-WSi0KCK^#yy-=j}nCvw-w|57-p#aTqMa;9J5edgui-TfSnF z7x}jn8wl&#t62w(E0duL2<`LBYAZo!d_%1RmIuYTv-Y5S?LqgHgP539iTkoW=u7sX zFIqsln7}JJy`SQi^D7y{gm}U#i(6=wizBc@1d*Y$dRPFAn}*G*f-sV)I1CUgUg2F9 z5DbMu*toQc@3(+REy+n71iscD^rr;GDnp735@hay|BHOl4kYul77!^5tV)gYAGQa5 zh{%cYr6~)pvj~!p42Z50py+4pK_zIQ26)yC06tJL?7OO0l9DPRs?uAv$P<4Cq~Hc32U`G#_YI&s)S0fAt&D5ly%6k7=GpfOId{xFt{G20m;xvHQX z!N;0iO>$5Z(i#hd(v9o#57=%b0uKSM3)2Viw^=~MY!r;X@psyT?yvztr|(6T zAQT`D0{)yR4eK0fhJ$BrEU5|s!-+OlKxiLAjBd3+h@!!ylPY)GLT<2yTyF$VaSitBDk9R*Y1I;IVTnEf6X=+#PzW zEo8(Na*G{A9<_xWwS}bYAj+5>B$~E^#K!F)i3yVbpWDubq~N#wzw)|WtN9l08{7l% zXrW(Ui6ncXtKm1)vfhZTGZ9%s144(GBDB-6R{APf*uZZm>s4z(XSdRXVcb{RNHCH^ z3K>Q{twc?U5(#;bS4xeYohljmbQ%kaj~m9T(1sR@&v?<>Q|uJ*i6JSUO!S)Dwdk2kF?R zn5X(4!%R5GN=+;kj#F8$+jp71*P7b5>%eZIE1!n54~)~(Z8bh-lh_vx!AOQ<%9>V$ z1EGatYI+)8AdCL=tT4t3K>dkbeqR|5`FkD@!MT_uI8-jWmzqEMO|$Mt>$-mNseY-h zN1;b{cBW*ceY!$fS54Q^71ngT2qc0RcIQ*sNnvMd2J*BUi3064UiVbSDI+9j8fHN< zG#-xqxHK0}%h&w7D#~)Z_`LadNa%YJX#(py|G$(ijp{rsY!J_mme`Rx51dl2tg7>% zay98Z={vx|=h>s_`~;kM)|D+T;OH^yS+d%>=Sm`0uJ*=xXH-iT>ER*CTDx-j&kiZq z+y(NRmA_nf?M8>>GN0^6)f^^t;sd4iI0o3fapNXVS*t5K&1gG1l}TrdBiYn+T4`q| zYAdGGwe{u|iB9bO;$$IzXj zw%|8|FAHAZ@b3+eHjFp41m*�?qyl{|O8qn5W>`=>9wRN%t1l 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 diff --git a/dist/_worker.js b/dist/_worker.js index 5d93d17..d9aa836 100644 --- a/dist/_worker.js +++ b/dist/_worker.js @@ -1252,30 +1252,32 @@ var je=Object.defineProperty;var Ft=t=>{throw TypeError(t)};var Se=(t,e,r)=>e in LEFT JOIN status_checkboxes sc ON pr.id = sc.record_id WHERE pr.month = ? AND pr.year = ? AND pr.deleted_at IS NULL ORDER BY pr.created_at DESC - `).bind(e,r).all();return t.json(o.results||[])}catch(e){return console.error("Error fetching records:",e),t.json({error:"Failed to fetch records"},500)}});f.post("/api/records",A,async t=>{try{const e=await t.req.json(),r=t.get("userId"),o=e.quantity?parseInt(e.quantity,10):0,s=e.price?parseFloat(e.price):0,a=await t.env.DB.prepare(` + `).bind(e,r).all();return t.json(o.results||[])}catch(e){return console.error("Error fetching records:",e),t.json({error:"Failed to fetch records"},500)}});f.post("/api/records",A,async t=>{try{const e=await t.req.json(),r=t.get("userId"),o=e.quantity?parseInt(e.quantity,10):0,s=e.price?parseFloat(e.price):0,a=e.arve_checked?parseInt(e.arve_checked,10):0,i=await t.env.DB.prepare(` INSERT INTO production_records ( month, year, client_name, type, offer_number, work_number, quantity, color, notes, problems, installer, price, + arve_checked, arve_makstud, created_by, updated_by - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) - `).bind(e.month,e.year,e.client_name,e.type||null,e.offer_number,e.work_number,o,e.color||null,e.notes||null,e.problems||null,e.installer||null,s,r,r).run(),i=e.material_date&&e.material_date!=="null"?e.material_date:null,l=e.material2_date&&e.material2_date!=="null"?e.material2_date:null,n=e.package_date&&e.package_date!=="null"?e.package_date:null;return await t.env.DB.prepare(` + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).bind(e.month,e.year,e.client_name,e.type||null,e.offer_number,e.work_number,o,e.color||null,e.notes||null,e.problems||null,e.installer||null,s,a,e.arve_makstud||null,r,r).run(),l=e.material_date&&e.material_date!=="null"?e.material_date:null,n=e.material2_date&&e.material2_date!=="null"?e.material2_date:null,c=e.package_date&&e.package_date!=="null"?e.package_date:null;return await t.env.DB.prepare(` INSERT INTO status_checkboxes ( record_id, material_date, material2_date, package_date ) VALUES (?, ?, ?, ?) - `).bind(a.meta.last_row_id,i,l,n).run(),t.json({success:!0,id:a.meta.last_row_id})}catch(e){return console.error("Error creating record:",e),t.json({error:"Failed to create record"},500)}});f.put("/api/records/:id",A,async t=>{try{const e=t.req.param("id"),r=await t.req.json(),o=t.get("userId"),s=r.quantity?parseInt(r.quantity,10):0,a=r.price?parseFloat(r.price):0;if(await t.env.DB.prepare(` + `).bind(i.meta.last_row_id,l,n,c).run(),t.json({success:!0,id:i.meta.last_row_id})}catch(e){return console.error("Error creating record:",e),t.json({error:"Failed to create record"},500)}});f.put("/api/records/:id",A,async t=>{try{const e=t.req.param("id"),r=await t.req.json(),o=t.get("userId"),s=r.quantity?parseInt(r.quantity,10):0,a=r.price?parseFloat(r.price):0,i=r.arve_checked?parseInt(r.arve_checked,10):0;if(await t.env.DB.prepare(` UPDATE production_records SET client_name = ?, type = ?, offer_number = ?, work_number = ?, quantity = ?, color = ?, notes = ?, problems = ?, installer = ?, price = ?, + arve_checked = ?, arve_makstud = ?, updated_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL - `).bind(r.client_name,r.type||null,r.offer_number,r.work_number,s,r.color||null,r.notes||null,r.problems||null,r.installer||null,a,o,e).run(),r.material_date!==void 0||r.material2_date!==void 0||r.package_date!==void 0){const i=r.material_date&&r.material_date!=="null"?r.material_date:null,l=r.material2_date&&r.material2_date!=="null"?r.material2_date:null,n=r.package_date&&r.package_date!=="null"?r.package_date:null;await t.env.DB.prepare(` + `).bind(r.client_name,r.type||null,r.offer_number,r.work_number,s,r.color||null,r.notes||null,r.problems||null,r.installer||null,a,i,r.arve_makstud||null,o,e).run(),r.material_date!==void 0||r.material2_date!==void 0||r.package_date!==void 0){const l=r.material_date&&r.material_date!=="null"?r.material_date:null,n=r.material2_date&&r.material2_date!=="null"?r.material2_date:null,c=r.package_date&&r.package_date!=="null"?r.package_date:null;await t.env.DB.prepare(` UPDATE status_checkboxes SET material_date = ?, material2_date = ?, package_date = ?, updated_at = CURRENT_TIMESTAMP WHERE record_id = ? - `).bind(i,l,n,e).run()}return t.json({success:!0})}catch(e){return console.error("Error updating record:",e),t.json({error:"Failed to update record"},500)}});f.get("/api/records/:id",A,async t=>{try{const e=t.req.param("id"),r=await t.env.DB.prepare(` + `).bind(l,n,c,e).run()}return t.json({success:!0})}catch(e){return console.error("Error updating record:",e),t.json({error:"Failed to update record"},500)}});f.get("/api/records/:id",A,async t=>{try{const e=t.req.param("id"),r=await t.env.DB.prepare(` SELECT * FROM production_records WHERE id = ? AND deleted_at IS NULL `).bind(e).first();return r?t.json(r):t.json({error:"Record not found"},404)}catch(e){return console.error("Error fetching record:",e),t.json({error:"Failed to fetch record"},500)}});f.delete("/api/records/:id",A,async t=>{try{const e=t.req.param("id"),r=t.get("userId");return console.log("[DELETE] Deleting record:",e,"by user:",r),await t.env.DB.prepare(` UPDATE production_records diff --git a/dist/original.html b/dist/original.html index f90c94a..72f82fc 100644 --- a/dist/original.html +++ b/dist/original.html @@ -1225,7 +1225,7 @@ - + \ No newline at end of file diff --git a/docker-compose-simple.yml b/docker-compose-simple.yml deleted file mode 100644 index 2f56f29..0000000 --- a/docker-compose-simple.yml +++ /dev/null @@ -1,20 +0,0 @@ -version: "3.8" - -services: - aknaproff-backend: - build: - context: . - dockerfile: Dockerfile - container_name: aknaproff-backend - ports: - - "8180:3000" - environment: - PORT: 3000 - D1_BINDING: aknaproff-db - PERSIST_PATH: /data - SEED_DATA: "false" - SKIP_MIGRATIONS: "true" - WRANGLER_SEND_METRICS: "false" - volumes: - - ./data:/data - restart: unless-stopped diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 0000000..5fa6d7a --- /dev/null +++ b/docker-compose.prod.yml @@ -0,0 +1,42 @@ +version: '3.8' + +services: + webapp: + build: + context: . + dockerfile: Dockerfile + container_name: aknaproff-webapp-prod + + # Монтировать только папку БД локально + volumes: + # Локальное хранилище БД + - ./data/db:/app/.wrangler/state/v3/d1 + # Логи (опционально) + - ./data/logs:/app/logs + + # Переменные окружения + environment: + - NODE_ENV=production + - PORT=3000 + + # Открыть порт 3000 + ports: + - "3000:3000" + + # Перезапуск при падении + restart: unless-stopped + + # Лимиты ресурсов + deploy: + resources: + limits: + cpus: '1' + memory: 512M + reservations: + cpus: '0.5' + memory: 256M + +# Сеть +networks: + default: + name: aknaproff-prod-network diff --git a/docker-compose.yml b/docker-compose.yml old mode 100755 new mode 100644 index d3093f3..20a167c --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,13 +1,55 @@ -version: "3.9" +version: '3.8' services: - aknaproff-backend: - build: - context: . - dockerfile: Dockerfile - container_name: aknaproff-backend - ports: - - "8180:3000" + webapp: + image: node:20-alpine + container_name: aknaproff-webapp + working_dir: /app + + # Bind mount - все файлы проекта включая БД volumes: - - ./data:/data + # Весь проект монтируется в /app + - ./:/app + # node_modules остаются в контейнере для производительности + - /app/node_modules + + # Переменные окружения + environment: + - NODE_ENV=development + - PORT=3000 + # Cloudflare Workers local mode + - CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN:-} + + # Открыть порт 3000 + ports: + - "3000:3000" + + # Команда запуска + command: > + sh -c " + echo '🚀 Starting AKNAPROFF Tootmine...' && + echo '📦 Installing dependencies...' && + npm install && + echo '🗄️ Setting up local database...' && + npm run db:reset && + echo '🔨 Building project...' && + npm run build && + echo '✅ Starting development server...' && + npx wrangler pages dev dist --d1=webapp-production --local --ip 0.0.0.0 --port 3000 + " + + # Перезапуск при падении restart: unless-stopped + + # Health check + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +# Сеть (необязательно, но полезно для будущего расширения) +networks: + default: + name: aknaproff-network diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh deleted file mode 100755 index f3997c9..0000000 --- a/docker-entrypoint.sh +++ /dev/null @@ -1,61 +0,0 @@ -#!/bin/bash -set -euo pipefail - -PORT="${PORT:-3000}" -D1_BINDING="${D1_BINDING:-aknaproff-db}" -PERSIST_PATH="${PERSIST_PATH:-/data}" -SEED_DATA="${SEED_DATA:-false}" -SEED_SENTINEL="${PERSIST_PATH}/.seeded" -SKIP_MIGRATIONS="${SKIP_MIGRATIONS:-true}" # ⚠️ По умолчанию пропускаем миграции! - -mkdir -p "${PERSIST_PATH}" -export WRANGLER_SEND_METRICS="${WRANGLER_SEND_METRICS:-false}" - -apply_migrations() { - if [[ "${SKIP_MIGRATIONS,,}" == "true" ]]; then - echo "[entrypoint] Skipping migrations (SKIP_MIGRATIONS=true)" - echo "[entrypoint] Using existing database from ${PERSIST_PATH}" - return - fi - - echo "[entrypoint] Applying D1 migrations (binding: ${D1_BINDING}, persist: ${PERSIST_PATH})" - npx wrangler d1 migrations apply "${D1_BINDING}" \ - --local \ - --persist-to "${PERSIST_PATH}" -} - -maybe_seed_data() { - if [[ "${SEED_DATA,,}" != "true" ]]; then - echo "[entrypoint] Seed step disabled (set SEED_DATA=true to enable)" - return - fi - - if [[ -f "${SEED_SENTINEL}" ]]; then - echo "[entrypoint] Seed data already applied (skipping)" - return - fi - - echo "[entrypoint] Seeding local database from seed.sql" - if npx wrangler d1 execute "${D1_BINDING}" \ - --local \ - --persist-to "${PERSIST_PATH}" \ - --file ./seed.sql; then - touch "${SEED_SENTINEL}" - else - echo "[entrypoint] Seed step failed but container will continue" >&2 - fi -} - -start_server() { - echo "[entrypoint] Starting Wrangler dev server on port ${PORT}" - exec npx wrangler pages dev dist \ - --local \ - --d1="${D1_BINDING}" \ - --persist-to "${PERSIST_PATH}" \ - --ip 0.0.0.0 \ - --port "${PORT}" -} - -apply_migrations -maybe_seed_data -start_server diff --git a/ecosystem.config.cjs b/ecosystem.config.cjs new file mode 100644 index 0000000..03df9ac --- /dev/null +++ b/ecosystem.config.cjs @@ -0,0 +1,19 @@ +module.exports = { + apps: [ + { + name: 'webapp', + script: 'npx', + args: 'wrangler pages dev dist --d1=webapp-production --local --ip 0.0.0.0 --port 3000', + env: { + NODE_ENV: 'development', + PORT: 3000 + }, + watch: false, + instances: 1, + exec_mode: 'fork', + autorestart: true, + max_restarts: 5, + min_uptime: '10s' + } + ] +} diff --git a/fix-docker.sh b/fix-docker.sh deleted file mode 100755 index 0231bcd..0000000 --- a/fix-docker.sh +++ /dev/null @@ -1,69 +0,0 @@ -#!/bin/bash -# Quick fix script for Synology Docker "lease not found" error - -echo "=== AKNAPROFF Docker Fix Script ===" -echo "" - -# Stop and remove old containers -echo "1. Stopping and removing old containers..." -sudo docker-compose down -v 2>/dev/null || true - -# Prune containers -echo "2. Cleaning orphan containers..." -sudo docker container prune -f - -# Prune build cache -echo "3. Cleaning build cache..." -sudo docker builder prune -f - -# Remove old image if exists -echo "4. Removing old image..." -sudo docker rmi tootmine-aknaproff-backend:latest 2>/dev/null || true -sudo docker rmi aknaproff-backend:latest 2>/dev/null || true - -# System prune -echo "5. System cleanup..." -sudo docker system prune -f - -echo "" -echo "=== Cleanup complete ===" -echo "" -echo "Choose build method:" -echo "1) Use docker-compose.yml (with platform support)" -echo "2) Use docker-compose-simple.yml (without platform - RECOMMENDED for Synology)" -echo "" -read -p "Enter choice [1 or 2]: " choice - -case $choice in - 1) - echo "Building with docker-compose.yml..." - sudo docker-compose build --no-cache - sudo docker-compose up -d --force-recreate - ;; - 2) - echo "Building with docker-compose-simple.yml..." - sudo docker-compose -f docker-compose-simple.yml build --no-cache - sudo docker-compose -f docker-compose-simple.yml up -d --force-recreate - ;; - *) - echo "Invalid choice. Please run script again." - exit 1 - ;; -esac - -echo "" -echo "=== Checking status ===" -sudo docker ps | grep aknaproff - -echo "" -echo "=== Checking logs (last 20 lines) ===" -sudo docker logs aknaproff-backend --tail=20 - -echo "" -echo "=== Testing API ===" -sleep 3 -curl -s http://localhost:8180/api/years | head -20 - -echo "" -echo "=== Done ===" -echo "If container is running, access at: http://synology-ip:8180" diff --git a/public/original.html b/public/original.html index f90c94a..72f82fc 100644 --- a/public/original.html +++ b/public/original.html @@ -1225,7 +1225,7 @@ - + \ No newline at end of file diff --git a/seed.sql b/seed.sql new file mode 100644 index 0000000..e1ac233 --- /dev/null +++ b/seed.sql @@ -0,0 +1,57 @@ +-- Insert demo users +-- Passwords: +-- demo123: d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791 +-- tootmine: a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f +-- Users: +-- kasutaja (tootmine): regular user - can edit problems only +-- aknaproff (demo123): admin - full access +-- admin (demo123): super admin - full access (same as admin) +INSERT OR IGNORE INTO users (username, password_hash, full_name, role) VALUES + ('kasutaja', 'a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f', 'Kasutaja', 'user'), + ('aknaproff', 'd3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791', 'AKNAPROFF', 'admin'), + ('admin', 'd3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791', 'Administrator', 'admin'); + +-- Insert test production records for January 2025 +INSERT OR IGNORE INTO production_records (id, month, year, client_name, offer_number, work_number, quantity, price, installer) VALUES + (1, 1, 2025, 'AS Okna Service', 'P-2025-001', 'T-2025-001', 12, 2500.00, 'Jüri Tamm'), + (2, 1, 2025, 'OÜ Aken ja Uks', 'P-2025-002', 'T-2025-002', 8, 1800.00, 'Mari Lepik'), + (3, 1, 2025, 'Kodutehnika OÜ', 'P-2025-003', 'T-2025-003', 15, 3200.00, 'Peeter Kask'), + (4, 1, 2025, 'Ehitus ja Remont AS', 'P-2025-004', 'T-2025-004', 6, 1200.00, 'Jüri Tamm'), + (5, 1, 2025, 'Akende Maailm OÜ', 'P-2025-005', 'T-2025-005', 20, 4500.00, 'Mari Lepik'); + +-- Insert test production records for December 2024 +INSERT OR IGNORE INTO production_records (id, month, year, client_name, offer_number, work_number, quantity, price, installer) VALUES + (6, 12, 2024, 'Vana Klient OÜ', 'P-2024-099', 'T-2024-099', 10, 2200.00, 'Peeter Kask'), + (7, 12, 2024, 'Teine Ettevõte AS', 'P-2024-100', 'T-2024-100', 5, 1000.00, 'Mari Lepik'); + +-- Insert status checkboxes for the test records +INSERT OR IGNORE INTO status_checkboxes ( + record_id, material_date, material2_date, package_date, worksheets_date, worksheets_confirmed, + cutting_date, glazing_date, ready_date, issued_date +) VALUES + -- Record 1: All stages completed + (1, '2025-01-08', '2025-11-11', '2025-01-09', '2025-11-26', 1, '2025-01-10', '2025-01-12', '2025-01-14', '2025-01-15'), + + -- Record 2: Partial completion with worksheets error + (2, '2025-01-07', NULL, '2025-01-07', NULL, 0, '2025-01-08', '2025-01-10', NULL, NULL), + + -- Record 3: Early stage with material confirmed + (3, '2025-01-05', '2025-01-06', NULL, NULL, 0, NULL, NULL, NULL, NULL), + + -- Record 4: Mid stage + (4, '2025-01-06', '2025-01-07', '2025-01-08', '2025-01-09', 1, '2025-01-10', NULL, NULL, NULL), + + -- Record 5: Just started + (5, '2025-01-07', NULL, NULL, NULL, 0, NULL, NULL, NULL, NULL), + + -- Record 6: December - completed + (6, '2024-12-10', '2024-12-11', '2024-12-12', '2024-12-13', 1, '2024-12-14', '2024-12-15', '2024-12-16', '2024-12-17'), + + -- Record 7: December - partial + (7, '2024-12-08', NULL, '2024-12-09', NULL, 0, '2024-12-10', NULL, NULL, NULL); + +-- Update record 2 to have worksheets error flag set +UPDATE status_checkboxes SET worksheets_error = 1 WHERE record_id = 2; + +-- Update record 2 to have glazing error flag set +UPDATE status_checkboxes SET glazing_error = 1 WHERE record_id = 2; diff --git a/src/index.tsx b/src/index.tsx index 98b57b3..94b654c 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -193,17 +193,21 @@ app.post('/api/records', optionalAuthMiddleware, async (c) => { const quantity = data.quantity ? parseInt(data.quantity, 10) : 0 const price = data.price ? parseFloat(data.price) : 0 + const arveChecked = data.arve_checked ? parseInt(data.arve_checked, 10) : 0 + const result = await c.env.DB.prepare(` INSERT INTO production_records ( month, year, client_name, type, offer_number, work_number, quantity, color, notes, problems, installer, price, + arve_checked, arve_makstud, created_by, updated_by - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).bind( data.month, data.year, data.client_name, data.type || null, data.offer_number, data.work_number, quantity, data.color || null, data.notes || null, data.problems || null, data.installer || null, - price, userId, userId + price, arveChecked, data.arve_makstud || null, + userId, userId ).run() // Create status checkboxes entry with dates if provided @@ -235,16 +239,20 @@ app.put('/api/records/:id', optionalAuthMiddleware, async (c) => { const quantity = data.quantity ? parseInt(data.quantity, 10) : 0 const price = data.price ? parseFloat(data.price) : 0 + const arveChecked = data.arve_checked ? parseInt(data.arve_checked, 10) : 0 + await c.env.DB.prepare(` UPDATE production_records SET client_name = ?, type = ?, offer_number = ?, work_number = ?, quantity = ?, color = ?, notes = ?, problems = ?, installer = ?, price = ?, + arve_checked = ?, arve_makstud = ?, updated_by = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ? AND deleted_at IS NULL `).bind( data.client_name, data.type || null, data.offer_number, data.work_number, quantity, data.color || null, data.notes || null, data.problems || null, - data.installer || null, price, userId, id + data.installer || null, price, arveChecked, data.arve_makstud || null, + userId, id ).run() // Update status_checkboxes dates if provided diff --git a/test_browser.js b/test_browser.js new file mode 100644 index 0000000..886a133 --- /dev/null +++ b/test_browser.js @@ -0,0 +1,35 @@ +const axios = require('axios'); + +async function testPage() { + try { + // 1. Get the main page + const response = await axios.get('http://localhost:3000'); + console.log('✅ Page loads:', response.status === 200); + + // 2. Check if app.js exists + const appJsResponse = await axios.get('http://localhost:3000/static/app.js'); + console.log('✅ app.js loads:', appJsResponse.status === 200); + console.log('📦 app.js size:', appJsResponse.data.length, 'bytes'); + + // 3. Check if DOMContentLoaded exists in app.js + const hasDOMContentLoaded = appJsResponse.data.includes('DOMContentLoaded'); + console.log('✅ DOMContentLoaded found:', hasDOMContentLoaded); + + // 4. Check if loadRecords exists + const hasLoadRecords = appJsResponse.data.includes('function loadRecords'); + console.log('✅ loadRecords found:', hasLoadRecords); + + // 5. Check if toggleDate exists + const hasToggleDate = appJsResponse.data.includes('function toggleDate'); + console.log('✅ toggleDate found:', hasToggleDate); + + // 6. Check HTML for tbody + const hasTbody = response.data.includes('