chore: remove APAW from tracking; gitignore agent-generated files

This commit is contained in:
Deploy Bot
2026-06-20 01:32:54 +01:00
parent d6dedfb513
commit 6aa41381ac
362 changed files with 48506 additions and 13261 deletions

15
.editorconfig Normal file
View File

@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = space
indent_size = 4
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.yml]
indent_size = 2

5
.gitattributes vendored Normal file
View File

@@ -0,0 +1,5 @@
* text=auto
*.css linguist-vendored
*.scss linguist-vendored
*.js linguist-vendored
CHANGELOG.md export-ignore

5
.htaccess Normal file
View File

@@ -0,0 +1,5 @@
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteRule ^(.*)$ public/$1 [L]
</IfModule>

13
.styleci.yml Normal file
View File

@@ -0,0 +1,13 @@
php:
preset: laravel
disabled:
- unused_use
finder:
not-name:
- index.php
- server.php
js:
finder:
not-name:
- webpack.mix.js
css: true

View File

@@ -1,199 +0,0 @@
# AKNAPROFF Tootmine - Изменения v4.1.0
**Дата:** 28.11.2025
**Версия:** v4.1.0
---
## 🎯 Реализованные изменения
### 1. ✅ Логика блокировки изменена
**Было:** Проблемы блокировали поля при наличии текста ИЛИ галочек ошибок
**Стало:** Блокировка только по галочкам ошибок, текст - только пояснение
**Технические детали:**
- Убрана проверка `(record.problems && record.problems.trim())` из условия hasProblems
- Блокировка происходит только при установленных флагах ошибок
### 2. ✅ Система авторизации и ролей
**Реализовано 3 уровня доступа:**
| Роль | Доступ | Описание |
|------|--------|----------|
| **guest** | Только просмотр | Гость без входа - ничего не может изменять |
| **user** | Просмотр + Проблемы | Простой пользователь - может изменять только Problems |
| **admin** | Полный доступ | Администратор - может изменять всё |
**Функциональность:**
- При открытии без логина показывается форма входа
- Guest (гость) - только read-only просмотр
- User - может редактировать проблемы (текст + галочки ошибок)
- Admin - полный доступ ко всем функциям
**Защищённые функции:**
- toggleDate() - только admin
- toggleMaterialConfirmed() - только admin
- toggleMaterial2Confirmed() - только admin
- toggleWorksheetsStep() - только admin
- togglePricePaid() - только admin
- saveNotes() - только admin
- saveProblems() - user и admin
### 3. ✅ Кнопка сортировки по ID
**Добавлена кнопка "ID" перед фильтрами:**
- Клик 1: Сортировка по возрастанию (↑)
- Клик 2: Сортировка по убыванию (↓)
- Клик 3: Отключение сортировки
- Иконка показывает текущее состояние
---
## 📂 Измененные файлы
### Для копирования на сервер:
```bash
# Frontend файлы (обязательно)
public/static/app.js
public/original.html
# Backend файлы (обязательно)
src/index.tsx # Если были изменения в API
src/original-html.ts # Embedded HTML
# Build файлы (создаются автоматически)
dist/_worker.js # Скомпилированный backend
```
---
## 🚀 Инструкция по обновлению на сервере
### Вариант 1: Полное обновление (РЕКОМЕНДУЕТСЯ)
```bash
# 1. Остановить контейнер
docker-compose down
# 2. Скопировать файлы из локальной машины на сервер
scp public/static/app.js user@server:/path/to/webapp/public/static/
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
# 3. Пересобрать на сервере
cd /path/to/webapp
npm run build
# 4. Запустить контейнер
docker-compose up -d --build
```
### Вариант 2: Только frontend (БЕЗ ПЕРЕСБОРКИ)
Если НЕ ИЗМЕНЯЛСЯ backend (src/index.tsx), можно обновить только frontend:
```bash
# 1. Скопировать файлы
scp public/static/app.js user@server:/path/to/webapp/public/static/
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
# 2. Пересобрать
docker-compose exec aknaproff-backend npm run build
# 3. Перезапустить (НЕ пересобирать образ)
docker-compose restart
```
### Вариант 3: Копирование готового dist/
```bash
# 1. Собрать локально (уже сделано)
npm run build
# 2. Скопировать dist на сервер
scp dist/_worker.js user@server:/path/to/webapp/dist/
# 3. Перезапустить
docker-compose restart
```
---
## ⚠️ ВАЖНО: База данных
**БД НЕ ИЗМЕНЯЛАСЬ!** Миграции не требуются.
Все изменения только в логике приложения:
- Логика hasProblems (frontend)
- Система прав (frontend)
- Кнопка сортировки (frontend)
**Данные в БД сохранятся полностью.**
---
## ✅ Проверка после обновления
### 1. Проверить вход
```bash
# Открыть в браузере
http://your-server:8180
# Должна показаться форма входа
```
### 2. Тестировать роли
**Guest (без входа):**
-Не может ничего изменять
- ✅ Может только просматривать
**User (простой пользователь):**
- ✅ Может изменять Problems (текст + галочки)
-Не может изменять даты, notes, статусы
**Admin:**
- ✅ Может изменять всё
### 3. Проверить сортировку
- Нажать кнопку "ID" перед фильтрами
- Проверить сортировку: ↑ → ↓ → отключено
### 4. Проверить логику блокировки
- Поставить галочку ошибки без текста
- Поле VALMIS/VÄLJAS должно заблокироваться (красное)
- Убрать галочку, добавить только текст
- Поле VALMIS/VÄLJAS НЕ должно блокироваться
---
## 📊 Сводка изменений
| Компонент | Изменено | Тип |
|-----------|----------|-----|
| Логика hasProblems | ✅ | Frontend |
| Система авторизации | ✅ | Frontend |
| Проверки прав | ✅ | Frontend |
| Кнопка сортировки | ✅ | Frontend |
| База данных | ❌ | Не изменялась |
| Backend API | ❌ | Не изменялся |
---
## 🔧 Откат изменений (если что-то пошло не так)
```bash
# 1. Вернуться к предыдущей версии
git checkout v4.0.13
# 2. Пересобрать
npm run build
# 3. Перезапустить
docker-compose up -d --build
```
---
**Готово к deployment!** 🚀

View File

@@ -1,116 +0,0 @@
# 🔄 CHANGES v4.1.2 - UI Layout Update
**Дата**: 2025-11-28
**Версия**: v4.1.2
**Тип**: UI Layout Improvement
---
## 📝 Что изменено
**Перемещение кнопки сортировки**:
- Кнопка **"Sorteerimine"** (сортировка по ID) перенесена из секции **"Filters"** в секцию **"Kiir otsing"** (Быстрый поиск)
- Теперь кнопка находится **ПЕРЕД** полем **"Klient"**
---
## 🎨 Визуальная структура
### ДО (v4.1.1):
```
┌─ Filters ─────────────────────────┐
│ [Sorteerimine: ID ↕] │
│ [Kuu] [Aasta] [Lisa uus rida] │
└───────────────────────────────────┘
┌─ Kiir otsing ─────────────────────┐
│ [Klient] [Tüüp] [Pakkum. Nr] │
│ [Töö Nr] [Aasta filter] │
└───────────────────────────────────┘
```
### ПОСЛЕ (v4.1.2):
```
┌─ Filters ─────────────────────────┐
│ [Kuu] [Aasta] [Lisa uus rida] │
└───────────────────────────────────┘
┌─ Kiir otsing ─────────────────────┐
│ [Sorteerimine: ID ↕] [Klient] │
│ [Tüüp] [Pakkum. Nr] [Töö Nr] │
│ [Aasta filter] │
└───────────────────────────────────┘
```
---
## 💡 Логика изменения
**Почему так лучше**:
1. **Логическая группировка**: Сортировка и поиск - это функции фильтрации данных, поэтому логично их держать вместе
2. **Меньше секций**: Секция Filters теперь содержит только основные фильтры (месяц/год) и кнопку добавления
3. **Удобнее использовать**: Сортировка и поиск теперь в одном месте
---
## 📦 Изменённые файлы
1. **public/original.html**
- Удалена кнопка Sorteerimine из Filters (строки 123-131)
- Добавлена кнопка Sorteerimine в Kiir otsing перед Klient
- Обновлена версия: `app.js?v=4.1.1``app.js?v=4.1.2`
2. **src/original-html.ts**
- Регенерирован embedded HTML с новой структурой
3. **dist/_worker.js**
- Пересобран с обновлённым HTML
---
## ✅ Тестирование
**Проверено**:
- ✅ Кнопка Sorteerimine отображается в секции Kiir otsing
- ✅ Кнопка находится ПЕРЕД полем Klient
- ✅ Функционал сортировки работает (↑ ↓ ↕)
-Все поля поиска работают
- ✅ Layout адаптивный (flex-wrap)
- ✅ Консоль браузера чистая
---
## 🔗 URLs
- **Production**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Git Commit**: b541aff
---
## 📊 Статус
**Версия**: v4.1.2
**Статус**: ✅ Production Ready
**HTTP Status**: 200 OK
**JavaScript Errors**: 0
---
## 📝 Для deployment на production
```bash
# Вариант 1: Быстрый (рекомендуется)
scp dist/_worker.js user@server:/path/to/webapp/dist/
docker-compose restart
# Вариант 2: Полный
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
cd /path/to/webapp && npm run build && docker-compose restart
```
**После deployment**: Нажмите **Ctrl+Shift+R** в браузере для сброса кэша.
---
**🎯 Готово!** Кнопка Sorteerimine перенесена в секцию Kiir otsing перед полем Klient.

View File

@@ -1,228 +0,0 @@
# 🔧 CHANGES v4.1.3 - UI Polish & New User
**Дата**: 2025-11-28
**Версия**: v4.1.3
**Тип**: UI Improvement + User Management
---
## 📝 Что изменено
### 1. Убрано слово "Sorteerimine"
- **ДО**: Кнопка имела label "Sorteerimine" над ней
- **ПОСЛЕ**: Только кнопка "ID" с иконкой сортировки (↕)
- **Причина**: Более компактный и чистый UI
### 2. Добавлен новый пользователь "kasutaja"
- **Username**: kasutaja
- **Password**: tootmine
- **Full Name**: Kasutaja
- **Role**: user (обычный пользователь)
### 3. Уточнена система ролей
**Три пользователя с двумя уровнями доступа**:
| Username | Password | Full Name | Role | Уровень доступа |
|----------|----------|-----------|------|-----------------|
| kasutaja | tootmine | Kasutaja | user | Обычный пользователь |
| aknaproff | demo123 | AKNAPROFF | admin | Администратор |
| admin | demo123 | Administrator | admin | Администратор |
**Права доступа**:
#### User (kasutaja)
- ✅ Просмотр всех записей
- ✅ Изменение проблем (текст + галочки)
- ✅ Просмотр Notes (read-only)
- ❌ Изменение дат
- ❌ Изменение MAT-1/MAT-2
- ❌ Добавление/редактирование/удаление записей
#### Admin (aknaproff, admin)
-Все права User
- ✅ Изменение дат (toggle)
- ✅ Изменение MAT-1/MAT-2 (toggle)
- ✅ Изменение LÕIKUS/KLAAS/VALMIS/VÄLJAS (3-step cycle)
- ✅ Добавление записей
- ✅ Редактирование записей
- ✅ Удаление записей
**Примечание**: aknaproff и admin имеют одинаковый уровень доступа (оба admin).
---
## 🎨 Визуальные изменения
### ДО (v4.1.2):
```
┌─ Kiir otsing ─────────────────────────────┐
│ Sorteerimine │
│ [ID ↕] │
│ │
│ Klient │
│ [Otsi kliendi järgi...] │
└───────────────────────────────────────────┘
```
### ПОСЛЕ (v4.1.3):
```
┌─ Kiir otsing ─────────────────────────────┐
│ [ID ↕] [Klient: Otsi kliendi järgi...] │
└───────────────────────────────────────────┘
```
**Преимущества**:
- Более компактный UI
- Меньше визуального шума
- Интуитивно понятно, что кнопка ID для сортировки
---
## 📦 Изменённые файлы
### 1. public/original.html
- Убран label с текстом "Sorteerimine"
- Кнопка ID теперь без label (только иконка + текст)
- Обновлена версия: `app.js?v=4.1.2``app.js?v=4.1.3`
### 2. seed.sql
- Добавлен пользователь "kasutaja" с паролем "tootmine"
- Обновлены комментарии с пояснением ролей
- Добавлен SHA-256 hash для пароля "tootmine"
### 3. src/original-html.ts
- Регенерирован embedded HTML
### 4. dist/_worker.js
- Пересобран с обновлениями
---
## 🧪 Тестирование
### Проверка UI
✅ Кнопка ID отображается без label "Sorteerimine"
✅ Кнопка компактная и выровнена с другими полями
✅ Функционал сортировки работает (↑ ↓ ↕)
### Проверка нового пользователя
✅ Вход: kasutaja / tootmine
✅ Роль: user
✅ Доступ только к просмотру и изменению проблем
✅ Остальные функции заблокированы (alert)
### Проверка существующих пользователей
✅ aknaproff / demo123 - admin права
✅ admin / demo123 - admin права
Оба имеют полный доступ
---
## 🔐 Учётные данные
### Production Users
**Обычный пользователь**:
- Username: `kasutaja`
- Password: `tootmine`
- Доступ: просмотр + изменение проблем
**Администраторы** (одинаковые права):
- Username: `aknaproff` | Password: `demo123`
- Username: `admin` | Password: `demo123`
- Доступ: полный
**Гость** (без входа):
- Кнопка: "Vaata ainult"
- Доступ: только просмотр (read-only)
---
## 🔗 URLs
- **Production**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Git Commit**: Будет добавлен после коммита
---
## 📊 Статус
**Версия**: v4.1.3
**Статус**: ✅ Production Ready
**HTTP Status**: 200 OK
**JavaScript Errors**: 0
**Database**: Обновлена (добавлен пользователь kasutaja)
---
## 🚀 Deployment Instructions
### ⚠️ ВАЖНО: Требуется обновление базы данных!
**На production сервере**:
```bash
# 1. Скопировать seed.sql с новым пользователем
scp seed.sql user@server:/path/to/webapp/
# 2. На сервере применить seed.sql
docker-compose exec aknaproff-backend sh -c "
cd /app &&
npx wrangler d1 execute webapp-production --local --file=./seed.sql
"
# 3. Проверить, что пользователь добавлен
docker-compose exec aknaproff-backend sh -c "
npx wrangler d1 execute webapp-production --local \
--command='SELECT username, full_name, role FROM users'
"
# Должно вывести:
# - kasutaja | Kasutaja | user
# - aknaproff | AKNAPROFF | admin
# - admin | Administrator | admin
# 4. Скопировать обновлённые файлы
scp dist/_worker.js user@server:/path/to/webapp/dist/
# или
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
# и потом: npm run build
# 5. Перезапустить
docker-compose restart
```
### Проверка после deployment
```bash
# 1. Проверить HTTP
curl -I http://localhost:8180
# 2. Войти в браузере под kasutaja/tootmine
# 3. Проверить, что доступно только изменение проблем
# 4. Войти под aknaproff/demo123 или admin/demo123
# 5. Проверить полный доступ
```
---
## 💾 Password Hashes
Для справки (SHA-256):
- **demo123**: `d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791`
- **tootmine**: `a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f`
---
## 📝 Заметки
1. **База данных изменена**: Добавлен новый пользователь, требуется применение seed.sql на production
2. **Роли**: aknaproff и admin - оба admin, kasutaja - user
3. **UI**: Более компактный, убран label "Sorteerimine"
4. **Cache-busting**: v4.1.3, нужен Ctrl+Shift+R после deployment
---
**🎯 Готово!** UI упрощён, добавлен пользователь kasutaja (tootmine), система ролей уточнена.

View File

@@ -1,152 +0,0 @@
# 📝 CHANGES v4.1.4 - Login Form Simplification
**Дата**: 2025-11-28
**Версия**: v4.1.4
**Тип**: UI Text Improvement
---
## 📝 Что изменено
### Упрощение текста формы логина
**ДО (v4.1.3)**:
```
┌─────────────────────────────────┐
│ 🔒 │
│ Administrator Login │
│ Sisesta admin kasutajaandmed │
│ │
│ [Kasutajanimi] │
│ [Parool] │
│ [Logi sisse] │
└─────────────────────────────────┘
```
**ПОСЛЕ (v4.1.4)**:
```
┌─────────────────────────────────┐
│ 🔒 │
│ Login │
│ Sisesta kasutajaandmed │
│ │
│ [Kasutajanimi] │
│ [Parool] │
│ [Logi sisse] │
└─────────────────────────────────┘
```
### Изменения:
1. **Заголовок**: "Administrator Login" → **"Login"**
2. **Подзаголовок**: "Sisesta admin kasutajaandmed" → **"Sisesta kasutajaandmed"**
### Причина:
- Форма теперь используется **всеми типами пользователей** (kasutaja, aknaproff, admin)
- Не только администраторами, поэтому слово "Administrator" и "admin" вводят в заблуждение
- Более универсальный и понятный текст
---
## 📦 Изменённые файлы
### 1. public/original.html
- Строка 47: `Administrator Login``Login`
- Строка 48: `Sisesta admin kasutajaandmed``Sisesta kasutajaandmed`
- Обновлена версия: `app.js?v=4.1.3``app.js?v=4.1.4`
### 2. src/original-html.ts
- Регенерирован embedded HTML с новыми текстами
### 3. dist/_worker.js
- Пересобран с обновлениями
---
## ✅ Тестирование
**Проверено**:
- ✅ Заголовок формы: "Login" (не "Administrator Login")
- ✅ Подзаголовок: "Sisesta kasutajaandmed" (не "admin kasutajaandmed")
-Все функции входа работают
- ✅ Вход для kasutaja/tootmine работает
- ✅ Вход для aknaproff/demo123 работает
- ✅ Вход для admin/demo123 работает
- ✅ Кнопка "Vaata ainult" (guest) работает
---
## 🚀 Deployment
### Быстрое обновление (только UI)
```bash
# Скопировать только dist
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
```
### Проверка
```bash
# HTTP
curl -I http://localhost:8180
# Браузер (Ctrl+Shift+R)
# Проверить текст: "Login" и "Sisesta kasutajaandmed"
```
---
## ⚠️ Важно
- **База данных**: НЕ изменена
- **Миграции**: НЕ требуются
- **Изменения**: только UI текст
- **Cache-busting**: v4.1.4
---
## 🔗 URLs
- **Production**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Git Commit**: f9c5e0a
---
## 📊 Статус
**Версия**: v4.1.4
**Статус**: ✅ Production Ready
**HTTP Status**: 200 OK
**JavaScript Errors**: 0
**Database**: Не изменена
---
## 🔐 Учётные данные (без изменений)
| Username | Password | Role | Доступ |
|----------|----------|------|--------|
| kasutaja | tootmine | user | Просмотр + проблемы |
| aknaproff | demo123 | admin | Полный доступ |
| admin | demo123 | admin | Полный доступ |
| (guest) | без входа | guest | Только просмотр |
---
## 💡 Для чего это изменение
**Проблема**: Текст "Administrator Login" и "admin kasutajaandmed" создавал впечатление, что форма только для администраторов.
**Решение**: Универсальный текст "Login" и "kasutajaandmed" показывает, что форма для всех типов пользователей.
**Результат**: Более понятный и дружелюбный интерфейс для всех пользователей.
---
**🎯 Готово!** Форма логина теперь с универсальным текстом для всех пользователей.

View File

@@ -1,200 +0,0 @@
# 🔴 CHANGES v4.1.5 - Restore Probleemid Visual Indicators
**Дата**: 2025-11-28
**Версия**: v4.1.5
**Тип**: Bug Fix / Visual Restoration
---
## 📝 Что восстановлено
### Поле "Probleemid" (Проблемы) - визуальная индикация
**Проблема**: Поле Probleemid не показывало визуальные индикаторы проблем.
**Восстановлено**:
#### 1. Красный фон с восклицательным знаком ⚠️
Когда есть галочки ошибок (error flags):
- **Цвет**: Красный фон (`bg-red-500`)
- **Иконка**: Восклицательный треугольник `<i class="fas fa-exclamation-triangle"></i>`
- **Tooltip**: Текст проблемы при наведении курсора
- **Условие**: Хотя бы одна галочка проблемы установлена (worksheets_error, cutting_error, glazing_error, ready_error, issued_error)
#### 2. Серый фон с информационной иконкой
Когда есть только текст проблемы, но нет галочек:
- **Цвет**: Серый фон (`bg-gray-300`)
- **Иконка**: Информационный круг `<i class="fas fa-info-circle"></i>`
- **Tooltip**: Текст проблемы при наведении курсора
- **Условие**: Есть текст в поле problems, но нет галочек ошибок
#### 3. Пустое поле (нет проблем)
Когда нет ни галочек, ни текста:
- **Цвет**: Светло-серый (`bg-gray-100`)
- **Символ**: Прочерк `-`
- **Действие**: Клик открывает модальное окно для добавления проблем
---
## 🎨 Визуальные состояния
### Состояние 1: Есть галочки ошибок
```
┌─────────────┐
│ 🔴 ⚠️ │ ← Красный с восклицательным знаком
└─────────────┘
↑ Наведение показывает текст проблемы
```
### Состояние 2: Только текст, нет галочек
```
┌─────────────┐
│ ⚪ │ ← Серый с информационной иконкой
└─────────────┘
↑ Наведение показывает текст проблемы
```
### Состояние 3: Нет проблем
```
┌─────────────┐
│ - │ ← Светло-серый с прочерком
└─────────────┘
↑ Клик для добавления проблемы
```
---
## 🔍 Логика отображения
```javascript
// Проверка галочек ошибок
const hasProblems = record.worksheets_error === 1 ||
record.cutting_error === 1 ||
record.glazing_error === 1 ||
record.ready_error === 1 ||
record.issued_error === 1;
// Если есть галочки → КРАСНЫЙ с ⚠️
if (hasProblems) {
return RED + exclamation-triangle icon + tooltip
}
// Если только текст → СЕРЫЙ с
if (problems && problems.trim()) {
return GRAY + info-circle icon + tooltip
}
// Иначе → пустой
return LIGHT_GRAY + "-"
```
---
## 📦 Изменённые файлы
### 1. public/static/app.js
- Функция `renderProblemsCell()` полностью переписана
- Добавлена проверка `hasProblems` (галочки ошибок)
- Добавлены 3 состояния: красный/серый/пустой
- Добавлены иконки Font Awesome
- Добавлен tooltip с текстом проблемы
- Обновлена версия: `app.js?v=4.1.4``app.js?v=4.1.5`
### 2. public/original.html
- Обновлена версия cache-busting
### 3. src/original-html.ts
- Регенерирован
### 4. dist/_worker.js
- Пересобран
---
## ✅ Тестирование
### Сценарий 1: Запись с галочками проблем
**Запись ID 2** (worksheets_error=1, glazing_error=1):
- ✅ Показывает КРАСНЫЙ фон
- ✅ Показывает иконку ⚠️
- ✅ При наведении показывает текст "qqqq"
- ✅ Клик открывает модальное окно с отмеченными галочками
### Сценарий 2: Запись с текстом, но без галочек
Если добавить текст проблемы без галочек:
- ✅ Показывает СЕРЫЙ фон
- ✅ Показывает иконку
- ✅ При наведении показывает текст
- ✅ Клик открывает модальное окно
### Сценарий 3: Запись без проблем
**Записи ID 1, 4, 5**:
- ✅ Показывает светло-серый фон с прочерком `-`
- ✅ Клик открывает модальное окно для добавления
---
## 🚀 Deployment
```bash
# Быстрый вариант
scp dist/_worker.js user@server:/path/to/webapp/dist/
docker-compose restart
# Полный вариант
scp public/static/app.js user@server:/path/to/webapp/public/static/
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
```
### Проверка
```bash
curl -I http://localhost:8180
# Браузер: Ctrl+Shift+R
# Проверить поле Probleemid:
# - Записи с галочками → красный фон с ⚠️
# - Записи с текстом → серый фон с
# - Записи без проблем → серый с "-"
# - Наведение курсора → показывает tooltip
```
---
## 🔗 URLs
- **Production**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Git Commit**: d79f236
---
## 📊 Статус
**Версия**: v4.1.5
**Статус**: ✅ Production Ready
**HTTP Status**: 200 OK
**JavaScript Errors**: 0
**Database**: Не изменена
---
## 💡 Ключевые моменты
1. **Галочки = красный** - Если хотя бы одна галочка установлена, поле красное с ⚠️
2. **Текст = информация** - Если есть только текст, поле серое с
3. **Tooltip = подсказка** - При наведении курсора показывается полный текст проблемы
4. **Font Awesome** - Используются иконки:
- `fa-exclamation-triangle` для ошибок
- `fa-info-circle` для информации
---
## 📚 Связанные версии
Эта функциональность связана с изменениями в v4.1.0:
- Галочки проблем блокируют поля VALMIS/VÄLJAS
- Текст проблемы - только комментарий, не блокирует
---
**🎯 Готово!** Поле Probleemid теперь правильно показывает красный фон с ⚠️ и tooltip с текстом проблемы!

View File

@@ -1,149 +0,0 @@
# 🟡 CHANGES v4.1.6 - Märkused Visual Indicators
**Дата**: 2025-11-28
**Версия**: v4.1.6
**Тип**: Visual Improvement
---
## 📝 Что изменено
### Поле "Märkused" (Notes/Заметки) - визуальная индикация
**Добавлено**:
- **Желтый фон** с белым текстом
- **Иконка "i"** (`fa-info-circle`)
- **Tooltip** при наведении курсора с полным текстом заметки
---
## 🎨 Визуальные состояния
### Состояние 1: Есть текст заметки
```
┌─────────────┐
│ 🟡 │ ← Желтый фон с иконкой "i"
└─────────────┘
↑ Наведение показывает полный текст заметки
```
### Состояние 2: Нет заметки
```
┌─────────────┐
│ - │ ← Серый фон с прочерком
└─────────────┘
↑ Клик для добавления заметки
```
---
## 🔍 Логика отображения
```javascript
// Если есть текст заметки → ЖЕЛТЫЙ с
if (notes && notes.trim()) {
return YELLOW + info-circle icon + tooltip
}
// Иначе → пустой (серый с прочерком)
return LIGHT_GRAY + "-"
```
---
## 📦 Изменённые файлы
### 1. public/static/app.js
- Функция `renderNotesCell()` обновлена
- Добавлен желтый фон: `bg-yellow-400`
- Изменена иконка: `fa-exclamation``fa-info-circle`
- Добавлен tooltip с полным текстом заметки
- Удалена зависимость от `notesDate` (показывает желтый только если есть текст)
### 2. public/original.html
- Обновлена версия: `app.js?v=4.1.5``app.js?v=4.1.6`
### 3. src/original-html.ts, dist/_worker.js
- Регенерированы и пересобраны
---
## ✅ Тестирование
### Сценарий 1: Запись с заметкой
**Запись ID 2** (notes="Срочный заказ до 20.01"):
- ✅ Показывает ЖЕЛТЫЙ фон
- ✅ Показывает иконку
- ✅ При наведении показывает "Срочный заказ до 20.01"
- ✅ Клик открывает модальное окно с текстом
### Сценарий 2: Запись без заметки
**Записи ID 1, 3, 5**:
- ✅ Показывает серый фон с прочерком `-`
- ✅ Клик открывает модальное окно для добавления
---
## 🚀 Deployment
```bash
# Быстрый вариант
scp dist/_worker.js user@server:/path/to/webapp/dist/
docker-compose restart
# Полный вариант
scp public/static/app.js user@server:/path/to/webapp/public/static/
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
```
### Проверка
```bash
curl -I http://localhost:8180
# Браузер: Ctrl+Shift+R
# Проверить поле Märkused:
# - Запись ID 2 → желтый фон с
# - Навести курсор → показывает tooltip с текстом
```
---
## 🔗 URLs
- **Production**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Git Commit**: dbc5c25
---
## 📊 Статус
**Версия**: v4.1.6
**Статус**: ✅ Production Ready
**HTTP Status**: 200 OK
**JavaScript Errors**: 0
**Database**: Не изменена
---
## 🎨 Сравнение с Probleemid
| Поле | Цвет | Иконка | Условие |
|------|------|--------|---------|
| **Probleemid** | 🔴 Красный | ⚠️ exclamation-triangle | Есть галочки ошибок |
| **Probleemid** | ⚪ Серый | info-circle | Только текст, без галочек |
| **Märkused** | 🟡 Желтый | info-circle | Есть текст заметки |
| **Пустое** | ⚪ Серый | - | Нет данных |
---
## 💡 Ключевые моменты
1. **Желтый = заметка** - Визуальное отличие от красных проблем
2. **Иконка "i"** - Информационный характер заметок
3. **Tooltip** - Полный текст при наведении курсора
4. **Без даты** - Показывает желтый только если есть текст
---
**🎯 Готово!** Поле Märkused теперь с желтым фоном, иконкой и tooltip при наведении!

View File

@@ -1,231 +0,0 @@
# AKNAPROFF v4.1.8 - Permissions Fix & UI Improvements
**Дата:** 2025-12-30
**Версия:** v4.1.8
**Тип:** Bug Fix + UI Improvement
---
## Изменения
### 1. ✅ Исправлены права доступа для Märkused (Notes)
**Проблема:** Пользователь `kasutaja` (role: user) не мог редактировать заметки
**Причина:** Код использовал `canEditRecords()` (только admin) вместо `canEditProblems()` (user и admin)
**Исправлено:**
- `openNotesModal()`: изменена проверка с `canEditRecords()` на `canEditProblems()`
- `saveNotes()`: изменена проверка с `canEditRecords()` на `canEditProblems()`
**Результат:**
- ✅ Admin может редактировать заметки
- ✅ User (kasutaja) может редактировать заметки
- ❌ Guest не может редактировать заметки (только просмотр)
---
### 2. ✅ Исправлены права доступа для Probleemid (Problems)
**Проблема:** Пользователь `kasutaja` (role: user) не мог редактировать проблемы
**Причина:**
1. Отсутствовали колонки `problems` и `problems_date` в таблице `production_records`
2. Неправильные названия колонок в `audit_log` (использовались `field_name` и `action_type` вместо `field` и `action`)
**Исправлено:**
- Добавлены колонки `problems` и `problems_date` в таблицу `production_records`
- Исправлены названия колонок в SQL запросах к `audit_log`:
- `field_name``field`
- `action_type``action`
**Результат:**
- ✅ Admin может редактировать проблемы и галочки ошибок
- ✅ User (kasutaja) может редактировать проблемы и галочки ошибок
- ❌ Guest не может редактировать проблемы (только просмотр)
---
### 3. ✅ Скрыта кнопка "Lisa uus rida" для не-админов
**Проблема:** Кнопка "Lisa uus rida" (Добавить новую строку) была видна всем пользователям
**Требование:** Только админы должны видеть кнопку добавления новой записи
**Исправлено:**
- Добавлен `id="addNewRowBtn"` к div с кнопкой в `original.html`
- Добавлена логика скрытия в `showMainApp()`:
- **Admin:** кнопка видна
- **User:** кнопка скрыта
- **Guest:** кнопка скрыта
**Результат:**
- ✅ Admin видит кнопку "Lisa uus rida"
- ❌ User (kasutaja) НЕ видит кнопку
- ❌ Guest НЕ видит кнопку
---
## Изменённые Файлы
### Frontend
- `public/static/app.js`:
- Исправлены права доступа для `openNotesModal()` и `saveNotes()`
- Добавлена логика скрытия кнопки "Lisa uus rida" в `showMainApp()`
- `public/original.html`:
- Добавлен `id="addNewRowBtn"` к div с кнопкой
- `src/original-html.ts`:
- Регенерирован с версией v4.1.8
### Backend
- `src/index.tsx`:
- Исправлены названия колонок в SQL запросах к `audit_log` (4 места)
- `field_name``field`
- `action_type``action`
### База данных
- `production_records`:
- Добавлена колонка `problems TEXT DEFAULT NULL`
- Добавлена колонка `problems_date DATE DEFAULT NULL`
---
## Тестирование
### Backend API Tests
#### ✅ User (kasutaja) может редактировать Notes
```bash
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"kasutaja","password":"tootmine"}' | jq -r .token)
curl -X PATCH http://localhost:3000/api/records/2/notes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"notes":"Test note from user"}'
# → {"success": true} ✅
```
#### ✅ User (kasutaja) может редактировать Problems
```bash
curl -X PATCH http://localhost:3000/api/records/2/problems \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{"problems":"Test problem","errorFlags":{"worksheets_error":1}}'
# → {"success": true} ✅
```
### Frontend Tests
- ✅ Admin видит кнопку "Lisa uus rida"
- ✅ User (kasutaja) НЕ видит кнопку "Lisa uus rida"
- ✅ Guest НЕ видит кнопку "Lisa uus rida"
- ✅ User может открыть модальное окно заметок и сохранить
- ✅ User может открыть модальное окно проблем и сохранить
- ✅ Guest может открыть модальные окна (read-only)
---
## Deployment на Production
### ⚠️ КРИТИЧНО: База данных изменена
**Шаг 1: Обновить БД (ОБЯЗАТЕЛЬНО)**
```bash
# 1. Добавить колонки problems в production_records
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='
ALTER TABLE production_records ADD COLUMN problems TEXT DEFAULT NULL;
ALTER TABLE production_records ADD COLUMN problems_date DATE DEFAULT NULL;
'"
# 2. Проверка
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='
PRAGMA table_info(production_records);
'" | grep problems
# Должно показать: problems | TEXT | NULL
# problems_date | DATE | NULL
```
**Шаг 2: Обновить код**
Быстрый вариант (рекомендуется):
```bash
scp dist/_worker.js user@server:/path/to/webapp/dist/
docker-compose restart
```
Полный вариант:
```bash
scp public/static/app.js user@server:/path/to/webapp/public/static/
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
scp src/index.tsx user@server:/path/to/webapp/src/
cd /path/to/webapp && npm run build && docker-compose restart
```
**Шаг 3: Проверка**
```bash
# Тест пользователя kasutaja
curl -X POST http://localhost:8180/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"kasutaja","password":"tootmine"}'
# Проверить кнопку "Lisa uus rida":
# - Admin: видна
# - User: скрыта
# - Guest: скрыта
```
---
## Матрица Прав Доступа
| Действие | Admin | User (kasutaja) | Guest |
|----------|-------|-----------------|-------|
| **Просмотр данных** | ✅ | ✅ | ✅ |
| **Редактировать Märkused** | ✅ | ✅ | ❌ |
| **Редактировать Probleemid** | ✅ | ✅ | ❌ |
| **Редактировать даты** | ✅ | ❌ | ❌ |
| **Добавить новую запись** | ✅ | ❌ | ❌ |
| **Удалить запись** | ✅ | ❌ | ❌ |
| **Редактировать все поля** | ✅ | ❌ | ❌ |
| **Изменить настройки** | ✅ | ❌ | ❌ |
---
## Статус
- **Версия:** v4.1.8
- **Статус:** ✅ Production Ready
- **Production URL:** https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **HTTP:** 200 OK
- **JavaScript Errors:** 0
- **База данных:** ✅ Обновлена
---
## Changelog
### v4.1.8 (2025-12-30)
- 🔧 **FIX:** User (kasutaja) может редактировать заметки
- 🔧 **FIX:** User (kasutaja) может редактировать проблемы
- 🔧 **FIX:** Исправлены колонки audit_log (field_name → field, action_type → action)
- 📦 **ADD:** Колонки `problems` и `problems_date` в таблицу `production_records`
- 🎨 **UI:** Скрыта кнопка "Lisa uus rida" для user и guest
-**VERIFY:** Все права доступа работают корректно
---
## Известные Ограничения
1. **ERR_BLOCKED_BY_CLIENT** - AdBlock блокирует ресурс (не критично)
2. **Tailwind CSS CDN** - в production рекомендуется PostCSS
3. **SHA-256 пароли** - в production рекомендуется bcrypt
---
## Итог
**Проблемы решены:**
1. User (kasutaja) может редактировать заметки и проблемы
2. Кнопка "Lisa uus rida" видна только админам
3. Все права доступа работают корректно
🎯 **Готово к deployment на production**
Не забудьте выполнить SQL команды для добавления колонок `problems` и `problems_date`!

View File

@@ -1,192 +0,0 @@
# AKNAPROFF v4.1.9 - Fix Notes Permissions (User → View Only)
**Дата:** 2025-12-30
**Версия:** v4.1.9
**Тип:** Permission Fix
---
## Изменение
### Märkused (Notes) - изменены права доступа
**Было (v4.1.8):**
- ✅ Admin: редактирование заметок
- ✅ User (kasutaja): редактирование заметок ❌ **НЕПРАВИЛЬНО**
- ❌ Guest: только просмотр
**Стало (v4.1.9):**
- ✅ Admin: редактирование заметок
- 👁️ User (kasutaja): **только просмотр** (кнопка "Salvesta" скрыта)
- 👁️ Guest: только просмотр
---
## Исправления
### Frontend (public/static/app.js)
1. **openNotesModal():**
- Было: `const readOnly = !canEditProblems()` (user + admin)
- Стало: `const readOnly = !canEditRecords()` (только admin)
2. **saveNotes():**
- Было: `if (!canEditProblems())` (user + admin)
- Стало: `if (!canEditRecords())` (только admin)
### Backend (src/index.tsx)
1. **PATCH /api/records/:id/notes:**
- Изменён middleware: `optionalAuthMiddleware``authMiddleware` (требует авторизации)
- Добавлена проверка роли:
```typescript
const userRole = c.get('role')
if (userRole !== 'admin') {
return c.json({ error: 'Permission denied. Only admin can edit notes.' }, 403)
}
```
2. **PATCH /api/records/:id/problems:**
- Изменён middleware: `optionalAuthMiddleware` → `authMiddleware`
- Добавлена проверка роли:
```typescript
const userRole = c.get('role')
if (userRole !== 'admin' && userRole !== 'user') {
return c.json({ error: 'Permission denied. Only admin and user can edit problems.' }, 403)
}
```
---
## Тестирование
### Backend API Tests
#### ✅ User НЕ может редактировать Notes
```bash
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-d '{"username":"kasutaja","password":"tootmine"}' | jq -r .token)
curl -X PATCH http://localhost:3000/api/records/2/notes \
-H "Authorization: Bearer $TOKEN" \
-d '{"notes":"Should not work"}'
# → {"error": "Permission denied. Only admin can edit notes."} ✅ 403
```
#### ✅ User может редактировать Problems
```bash
curl -X PATCH http://localhost:3000/api/records/2/problems \
-H "Authorization: Bearer $TOKEN" \
-d '{"problems":"Test","errorFlags":{"worksheets_error":1}}'
# → {"success": true} ✅ 200
```
#### ✅ Admin может редактировать Notes
```bash
TOKEN=$(curl -s -X POST http://localhost:3000/api/auth/login \
-d '{"username":"admin","password":"demo123"}' | jq -r .token)
curl -X PATCH http://localhost:3000/api/records/2/notes \
-H "Authorization: Bearer $TOKEN" \
-d '{"notes":"Admin can edit"}'
# → {"success": true} ✅ 200
```
### Frontend Tests
- ✅ Admin открывает Notes → поле редактируемое + кнопка "Salvesta" видна
- ✅ User открывает Notes → поле read-only + кнопка "Salvesta" скрыта
- ✅ Guest открывает Notes → поле read-only + кнопка "Salvesta" скрыта
- ✅ User может редактировать Problems (кнопка видна)
---
## Матрица Прав Доступа (Финальная)
| Действие | Admin | User (kasutaja) | Guest |
|----------|-------|-----------------|-------|
| **Просмотр данных** | ✅ | ✅ | ✅ |
| **Märkused (просмотр)** | ✅ | ✅ | ✅ |
| **Märkused (редактирование)** | ✅ | ❌ | ❌ |
| **Probleemid (просмотр)** | ✅ | ✅ | ✅ |
| **Probleemid (редактирование)** | ✅ | ✅ | ❌ |
| **Lisa uus rida** | ✅ | ❌ | ❌ |
| **Редактировать даты** | ✅ | ❌ | ❌ |
| **Удалить запись** | ✅ | ❌ | ❌ |
| **Редактировать все поля** | ✅ | ❌ | ❌ |
---
## Deployment на Production
### Файлы для Копирования
**Быстрый вариант (рекомендуется):**
```bash
scp dist/_worker.js user@server:/path/to/webapp/dist/
docker-compose restart
```
**Полный вариант:**
```bash
scp public/static/app.js user@server:/path/to/webapp/public/static/
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
scp src/index.tsx user@server:/path/to/webapp/src/
cd /path/to/webapp && npm run build && docker-compose restart
```
### Проверка после deployment
```bash
# 1. Войти как kasutaja
# 2. Открыть запись → нажать на Märkused (желтая ячейка)
# 3. Проверить: поле read-only, кнопка "Salvesta" скрыта
# 4. Открыть запись → нажать на Probleemid (красная ячейка)
# 5. Проверить: поле редактируемое, кнопка "Salvesta" видна
```
---
## Статус
- **Версия:** v4.1.9
- **Статус:** ✅ Production Ready
- **Production URL:** https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **HTTP:** 200 OK
- **JavaScript Errors:** 0
- **База данных:** Не изменена
---
## Changelog
### v4.1.9 (2025-12-30) - Permission Fix
- 🔧 **FIX:** User (kasutaja) теперь может только **просматривать** заметки (было: редактировать)
- 🔧 **FIX:** Добавлены backend проверки роли для `/api/records/:id/notes` (только admin)
- 🔧 **FIX:** Добавлены backend проверки роли для `/api/records/:id/problems` (admin + user)
- 🎨 **UI:** Кнопка "Salvesta" скрыта для user при просмотре заметок
-**VERIFY:** Все права доступа работают корректно (frontend + backend)
---
## Сравнение с v4.1.8
| Изменение | v4.1.8 | v4.1.9 |
|-----------|---------|---------|
| **User редактирует Notes** | ✅ (неправильно) | ❌ (исправлено) |
| **User просматривает Notes** | ✅ | ✅ |
| **User редактирует Problems** | ✅ | ✅ |
| **Backend проверка прав** | ❌ | ✅ |
| **Кнопка "Lisa uus rida" для user** | скрыта | скрыта |
---
## Итог
**Исправлено согласно требованию:**
- User (kasutaja) может только **просматривать** заметки
- Кнопка "Salvesta" скрыта для user в модальном окне заметок
- Backend проверяет роль и возвращает 403 для user при попытке редактирования
- User по-прежнему может редактировать проблемы
🎯 **Готово к deployment на production!**

View File

@@ -1,306 +0,0 @@
# 🔍 ПОЛНЫЙ ОБЗОР ЛОГИКИ КЛИКОВ - AKNAPROFF Tootmine
## 📋 История изменений из переписки
### Версия 1: Оригинальный архив (aknaproff.zip)
- **Структура**: Один HTML файл с встроенным JavaScript
- **Кнопки**: `<button onclick="openModal()">Lisa uus rida</button>`
- **Функции**: Все функции встроены в `<script>` теги внутри HTML
- **Аутентификация**: НЕТ - приложение работало без логина
- **Статус**: ✅ Все клики работали
### Версия 2: Разделение на фронт + бэк (v3.x - v4.0.6)
**Что было сделано:**
1. HTML извлечён в `public/original.html`
2. JavaScript извлечён в `public/static/app.js`
3. Backend создан с Hono + D1 Database
4. Добавлена JWT аутентификация
**Проблемы:**
- ❌ Кнопки требовали admin роль, но Public User не мог их видеть
- ❌ Backend требовал JWT токен (authMiddleware)
- ❌ Frontend показывал только для `body.role-admin`
### Версия 3: Текущая (v4.0.7)
**Что исправлено:**
- ✅ Backend: `authMiddleware``optionalAuthMiddleware` (13 endpoints)
- ✅ Backend: `userId || null` для audit_log
- ✅ Backend: Все PATCH/POST/PUT/DELETE работают без токена
- ✅ Default month: Изменён на January (1) где есть демо-данные
- ✅ Cache busting: Добавлен `?v=4.0.6` к app.js
**Что НЕ ИСПРАВЛЕНО:**
- ❌ Frontend: Кнопка "Lisa uus rida" всё ещё скрыта для Public User
- ❌ Frontend: `openModal()` проверяет `role === 'admin'` и блокирует
- ❌ CSS: `.admin-only-block { display: none }` скрывает кнопки
---
## 🎯 ТЕКУЩЕЕ СОСТОЯНИЕ КЛИКОВ
### ✅ Что РАБОТАЕТ
#### 1. Клики по ячейкам таблицы (Date Toggle)
```html
<td onclick="toggleDate(1, 'cutting', '2025-01-10')">
```
**Функция:**
```javascript
async function toggleDate(recordId, field, date) {
await axios.patch(`/api/records/${recordId}/status`, { field, date });
await loadRecords();
}
```
**Status:** ✅ РАБОТАЕТ (без токена, optionalAuthMiddleware)
#### 2. Клики по Worksheets Cycle
```html
<button onclick="toggleWorksheetsStep(1)">
```
**Функция:**
```javascript
async function toggleWorksheetsStep(recordId) {
await axios.patch(`/api/records/${recordId}/worksheets-cycle`);
await loadRecords();
}
```
**Status:** ✅ РАБОТАЕТ (без токена)
#### 3. Фильтры (Month/Year)
```html
<select id="monthFilter">
```
**Event Listener:**
```javascript
document.getElementById('monthFilter').addEventListener('change', loadRecords);
```
**Status:** ✅ РАБОТАЕТ
#### 4. Поиск (Search Inputs)
```html
<input id="searchClient">
```
**Event Listener:**
```javascript
document.getElementById('searchClient').addEventListener('input', handleSearchFilter);
```
**Status:** ✅ РАБОТАЕТ
### ❌ Что НЕ РАБОТАЕТ
#### 1. Кнопка "Lisa uus rida" (Add Record)
```html
<div class="admin-only-block">
<button onclick="openModal()">Lisa uus rida</button>
</div>
```
**CSS:**
```css
.admin-only-block { display: none; }
body.role-admin .admin-only-block { display: block; }
```
**Проблема:** Кнопка СКРЫТА для Public User
**Функция:**
```javascript
function openModal() {
if (!token || !currentUser || currentUser.role !== 'admin') {
alert('Ainult administraator saab lisada uusi kirjeid');
openLoginModal();
return; // ❌ БЛОКИРУЕТ для Public User
}
document.getElementById('recordModal').classList.add('active');
}
```
**Status:**НЕ РАБОТАЕТ (скрыта + блокируется)
#### 2. Edit Record Button
```html
<button onclick="editRecord(1)">
```
**Функция:**
```javascript
function editRecord(recordId) {
if (!token || !currentUser || currentUser.role !== 'admin') {
alert('Ainult administraator saab muuta kirjeid');
openLoginModal();
return; // ❌ БЛОКИРУЕТ
}
// ... load and edit
}
```
**Status:** ❌ ЧАСТИЧНО (видна, но блокируется)
#### 3. Delete Record Button
```html
<button onclick="confirmDelete(1)" class="delete-btn">
```
**CSS:**
```css
.delete-btn { display: none; }
```
**JavaScript Toggle:**
```javascript
function toggleDeleteButtons() {
const isAdmin = currentUser?.role === 'admin';
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.style.display = isAdmin ? 'inline-block' : 'none';
});
}
```
**Status:**НЕ РАБОТАЕТ (скрыта для Public User)
---
## 🔧 КОРНЕВАЯ ПРИЧИНА
### Несоответствие Frontend ↔ Backend
**Backend (v4.0.6):**
```typescript
// ✅ Разрешает публичный доступ
app.post('/api/records', optionalAuthMiddleware, async (c) => {
// userId может быть null для Public User
})
```
**Frontend (app.js):**
```javascript
// ❌ Блокирует публичный доступ
function openModal() {
if (currentUser.role !== 'admin') {
alert('Только администратор...');
return; // БЛОК!
}
}
```
**CSS:**
```css
/* ❌ Скрывает кнопки для Public User */
.admin-only-block { display: none; }
body.role-admin .admin-only-block { display: block; }
```
---
## 📊 СРАВНЕНИЕ: Оригинал vs Текущий
| Функция | Оригинал | Текущий v4.0.7 | Причина проблемы |
|---------|----------|----------------|------------------|
| Toggle Date | ✅ Работает | ✅ Работает | Backend: optionalAuth |
| Worksheets | ✅ Работает | ✅ Работает | Backend: optionalAuth |
| Add Record | ✅ Работает | ❌ Скрыта | CSS: admin-only-block |
| Edit Record | ✅ Работает | ❌ Блокируется | JS: role check |
| Delete Record | ✅ Работает | ❌ Скрыта | JS: toggleDeleteButtons |
| Filters | ✅ Работает | ✅ Работает | No auth needed |
| Search | ✅ Работает | ✅ Работает | No auth needed |
---
## 🎯 ЧТО НУЖНО ИСПРАВИТЬ
### Проблема 1: Кнопка "Lisa uus rida" скрыта
**Файл:** `public/original.html`
**Сейчас:**
```html
<div class="admin-only-block">
<button onclick="openModal()">Lisa uus rida</button>
</div>
```
**Исправить на:**
```html
<div>
<button onclick="openModal()">Lisa uus rida</button>
</div>
```
### Проблема 2: openModal() блокирует Public User
**Файл:** `public/static/app.js`
**Сейчас:**
```javascript
function openModal() {
if (!token || !currentUser || currentUser.role !== 'admin') {
alert('Ainult administraator...');
openLoginModal();
return;
}
// ...
}
```
**Исправить на:**
```javascript
function openModal() {
// Allow all users to open modal
editingRecordId = null;
document.getElementById('modalTitle').textContent = 'Lisa uus kirje';
document.getElementById('recordForm').reset();
document.getElementById('recordModal').classList.add('active');
updateMat2State();
}
```
### Проблема 3: editRecord() блокирует Public User
**Исправить на:**
```javascript
function editRecord(recordId) {
// Allow all users to edit
editingRecordId = recordId;
// ... load and show modal
}
```
### Проблема 4: Delete buttons скрыты
**Файл:** `public/static/app.js`
**Сейчас:**
```javascript
function toggleDeleteButtons() {
const isAdmin = currentUser?.role === 'admin';
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.style.display = isAdmin ? 'inline-block' : 'none';
});
}
```
**Исправить на:**
```javascript
function toggleDeleteButtons() {
// Show delete buttons for all users
document.querySelectorAll('.delete-btn').forEach(btn => {
btn.style.display = 'inline-block';
});
}
```
---
## ✅ ИТОГО
**РЕАЛЬНО РАБОТАЕТ:**
- ✅ Клики по ячейкам таблицы (toggleDate)
- ✅ Worksheets cycle button
- ✅ Фильтры (month/year)
- ✅ Поиск (client/type/offer/work)
- ✅ Сортировка (sortRecords)
- ✅ Backend API (26/26 endpoints с optionalAuth)
**НЕ РАБОТАЕТ (причина - Frontend блокировки):**
- ❌ Кнопка "Lisa uus rida" - СКРЫТА (admin-only-block CSS)
- ❌ openModal() - БЛОКИРУЕТСЯ (role check в JS)
- ❌ editRecord() - БЛОКИРУЕТСЯ (role check в JS)
- ❌ Delete buttons - СКРЫТЫ (toggleDeleteButtons)
**ПРИЧИНА:**
Backend был сделан публичным (optionalAuthMiddleware), но **Frontend всё ещё требует admin роль**.
**РЕШЕНИЕ:**
Убрать все проверки `role === 'admin'` из app.js и удалить класс `admin-only-block` у кнопок.

File diff suppressed because it is too large Load Diff

View File

@@ -1,228 +0,0 @@
# DB FIX v4.1.7 - Fix Login and Remove tootmine User
**Дата:** 2025-12-30
**Версия:** v4.1.7
**Тип:** Database Fix + Security Update
---
## Проблема
1. **Login failed** - все попытки логина возвращали ошибку
2. Пользователь `tootmine` нужно удалить (дубликат kasutaja)
---
## Анализ Проблемы
### 1. Несовпадение хэшей паролей
- **Бэкап БД**: использовал bcrypt хэши (`$2a$...`)
- **Код приложения**: использует SHA-256 хэши
- **Результат**: `verifyPassword()` не могла проверить пароли
### 2. Отсутствие колонки deleted_at
- Код: `WHERE deleted_at IS NULL`
- БД: колонка `deleted_at` отсутствовала в таблице `users`
- **Результат**: SQL ошибка → 500 Internal Server Error
---
## Решение
### 1. Обновление паролей (SHA-256)
```sql
-- admin / demo123
UPDATE users SET password_hash = 'd3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791'
WHERE username = 'admin';
-- aknaproff / demo123
UPDATE users SET password_hash = 'd3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791'
WHERE username = 'aknaproff';
-- kasutaja / tootmine
UPDATE users SET password_hash = 'a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f'
WHERE username = 'kasutaja';
```
### 2. Удаление пользователя tootmine
```sql
DELETE FROM users WHERE username = 'tootmine';
```
### 3. Добавление колонки deleted_at
```sql
-- Для таблицы users
ALTER TABLE users ADD COLUMN deleted_at DATETIME DEFAULT NULL;
-- Для таблицы production_records (уже добавлено ранее)
ALTER TABLE production_records ADD COLUMN deleted_at DATETIME DEFAULT NULL;
```
---
## Текущее Состояние БД
### Пользователи (3 total)
| ID | Username | Password | Role | Access |
|----|----------|----------|------|--------|
| 1 | admin | demo123 | admin | Полный доступ |
| 2 | aknaproff | demo123 | admin | Полный доступ |
| 4 | kasutaja | tootmine | user | Просмотр + проблемы |
**Удалено:**
- ~~3. tootmine / tootmine / user~~ ❌ (дубликат, удалён)
### Production Records
- **Всего записей:** 38
- **Годы:** 2025, 2026
- **Месяцы:** Январь-Декабрь
### Schema Updates
`users.deleted_at` - добавлена
`production_records.deleted_at` - добавлена
Все индексы сохранены
---
## Тестирование
### Backend API Tests
```bash
# ✅ Admin login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"demo123"}'
# → success: true, token: eyJ1c2VySWQiOjE...
# ✅ Aknaproff login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"aknaproff","password":"demo123"}'
# → success: true, token: eyJ1c2VySWQiOjI...
# ✅ Kasutaja login
curl -X POST http://localhost:3000/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"kasutaja","password":"tootmine"}'
# → success: true, token: eyJ1c2VySWQiOjQ...
```
### Frontend Tests
- ✅ Страница загружается
- ✅ Форма логина отображается
- ✅ Вход admin работает
- ✅ Вход aknaproff работает
- ✅ Вход kasutaja работает
- ✅ Guest режим работает
- ✅ Таблица с данными отображается
- ✅ Нет JavaScript ошибок
---
## Deployment на Production
### ⚠️ КРИТИЧНО: База данных изменена
Нужно выполнить 3 SQL команды на production:
```bash
# 1. Добавить колонку deleted_at в users
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='ALTER TABLE users ADD COLUMN deleted_at DATETIME DEFAULT NULL'"
# 2. Удалить пользователя tootmine
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='DELETE FROM users WHERE username = \"tootmine\"'"
# 3. Обновить пароли (SHA-256)
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='
UPDATE users SET password_hash = \"d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791\" WHERE username = \"admin\";
UPDATE users SET password_hash = \"d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791\" WHERE username = \"aknaproff\";
UPDATE users SET password_hash = \"a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f\" WHERE username = \"kasutaja\"
'"
```
### Проверка после deployment
```bash
# Проверить пользователей
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='SELECT username, role FROM users'"
# Должно вернуть:
# admin | admin
# aknaproff | admin
# kasutaja | user
# (3 пользователя, tootmine удалён)
```
---
## Файлы для Копирования
**НЕ ТРЕБУЕТСЯ** - код не изменён, только база данных.
Если нужно обновить код (для v4.1.6):
```bash
# Быстрый вариант (рекомендуется)
scp dist/_worker.js user@server:/path/to/webapp/dist/
docker-compose restart
# ИЛИ Полный вариант
scp public/static/app.js user@server:/path/to/webapp/public/static/
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
cd /path/to/webapp && npm run build && docker-compose restart
```
---
## Статус
- **Версия:** v4.1.7 (DB Fix)
- **Код:** v4.1.6 (без изменений)
- **База данных:** ✅ Исправлена
- **Production URL:** https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **HTTP Status:** 200 OK
- **JavaScript Errors:** 0
- **Login Status:** ✅ Работает для всех пользователей
---
## Changelog
### v4.1.7 (2025-12-30) - Database Fix
- 🔧 **FIX:** Обновлены пароли с bcrypt на SHA-256
- 🔧 **FIX:** Добавлена колонка `deleted_at` в таблицу `users`
- 🗑️ **REMOVE:** Удалён пользователь `tootmine` (дубликат kasutaja)
-**VERIFY:** Все логины работают (admin, aknaproff, kasutaja)
- 📊 **DB:** 3 пользователя, 38 production записей
---
## Security Notes
### Пароли (SHA-256)
⚠️ **ВАЖНО:** SHA-256 используется для demo, в production рекомендуется bcrypt
**Текущие хэши:**
- `demo123``d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791`
- `tootmine``a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f`
### Рекомендации
1. В production использовать bcrypt или argon2
2. Включить rate limiting для /api/auth/login
3. Добавить CSRF защиту
4. Использовать HTTPS только
---
## Итог
**Проблема решена:**
- Login работает для всех пользователей
- Пользователь tootmine удалён
- База данных полностью совместима с кодом v4.1.6
🎯 **Готово к deployment на production**
Не забудьте выполнить SQL команды на production сервере!

View File

@@ -1,193 +0,0 @@
# 💾 DB RESTORE REPORT - Production Data Merge
**Дата**: 2025-12-30
**Файл бекапа**: backup_30.12.25.zip
**Версия**: v4.1.6
**Статус**: ✅ Успешно выполнено
---
## 📋 Что было сделано
### 1. Анализ бекапа
**Источник**: `/home/user/uploaded_files/backend/data/v3/d1/miniflare-D1DatabaseObject/`
- Размер БД: 148 KB
- Количество записей: 38 (production данные)
- Пользователи в бекапе:
- admin (Administraator) - admin
- aknaproff (Aknaproff) - admin
- tootmine (Tootmine kasutaja) - user
### 2. Сохранение текущей БД
- Создан бекап текущей БД: `/tmp/webapp_old.sqlite` (72 KB)
- Количество записей: 7 (демо данные)
- Пользователи до восстановления:
- admin (Admin Test) - admin
- aknaproff (AKNAPROFF) - admin
- kasutaja (Kasutaja) - user
### 3. Восстановление production данных
**Действия**:
1. Остановлен PM2
2. Очищена директория `.wrangler/state/v3/d1/miniflare-D1DatabaseObject/`
3. Скопирована БД из бекапа
4. Применена миграция: добавлена колонка `deleted_at` (soft delete support)
### 4. Слияние пользователей
**Добавлен пользователь из v4.1.3**:
- **kasutaja** (Kasutaja) - user role
- Password hash: `a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f` (password: tootmine)
**Итоговые пользователи** (4 в БД):
1. admin (Administraator) - admin
2. aknaproff (Aknaproff) - admin
3. tootmine (Tootmine kasutaja) - user
4. kasutaja (Kasutaja) - user
---
## 📊 Итоговое состояние БД
### Production Records
- **Количество записей**: 38
- **Данные**: Реальные production данные с production сервера
- **Месяцы**: Январь 2025, другие месяцы
- **Годы**: 2025, 2026
### Пользователи (4)
| ID | Username | Full Name | Role | Password |
|----|----------|-----------|------|----------|
| 1 | admin | Administraator | admin | demo123 |
| 2 | aknaproff | Aknaproff | admin | demo123 |
| 3 | tootmine | Tootmine kasutaja | user | tootmine |
| 4 | kasutaja | Kasutaja | user | tootmine |
**Примечание**: Оба пользователя "tootmine" и "kasutaja" имеют одинаковый пароль "tootmine" и роль "user".
---
## ✅ Проверка после восстановления
### API Endpoints
- ✅ GET /api/years → `[2025, 2026]`
- ✅ GET /api/records?month=1&year=2025 → 5 записей
- ✅ HTTP Status: 200 OK
### Браузер
- ✅ Страница загружается
- ✅ Форма логина работает
- ✅ Таблица показывает данные
- ✅ Консоль: 0 критических ошибок
-Все функции работают (проверено визуально)
### Вход в систему
Проверены все 4 пользователя:
- ✅ admin / demo123 → admin права
- ✅ aknaproff / demo123 → admin права
- ✅ tootmine / tootmine → user права
- ✅ kasutaja / tootmine → user права
- ✅ Guest (без входа) → read-only
---
## 🔧 Технические детали
### Миграция БД
Добавлена отсутствующая колонка:
```sql
ALTER TABLE production_records ADD COLUMN deleted_at DATETIME DEFAULT NULL;
```
**Проблема**: Бекап БД был сделан до применения миграции soft delete (v4.0.6).
**Решение**: Добавлена колонка `deleted_at` для поддержки soft delete.
### Структура БД
- **production_records**: 38 записей (+ deleted_at column)
- **status_checkboxes**: связанные данные
- **users**: 4 пользователя
- **audit_log**: история изменений
---
## 📝 Изменения в коде
**Код не изменялся!**
Все изменения только в БД:
- Восстановлены production данные
- Добавлен пользователь kasutaja
- Добавлена колонка deleted_at
**Текущая версия**: v4.1.6
- Все визуальные индикаторы работают (Probleemid, Märkused)
- Все правки v4.1.0 - v4.1.6 применены
- Код полностью совместим с production данными
---
## 🔐 Учётные данные
### Администраторы (полный доступ)
- Username: `admin` | Password: `demo123`
- Username: `aknaproff` | Password: `demo123`
### Обычные пользователи (просмотр + проблемы)
- Username: `tootmine` | Password: `tootmine`
- Username: `kasutaja` | Password: `tootmine`
### Гость (только просмотр)
- Кнопка: "Vaata ainult" (без входа)
---
## 🚀 Deployment на production
### Для применения на production сервере:
```bash
# 1. Создать бекап текущей production БД (КРИТИЧНО!)
docker-compose exec aknaproff-backend sh -c "
cp /data/v3/d1/miniflare-D1DatabaseObject/*.sqlite /data/backup_before_merge.sqlite
"
# 2. Скопировать восстановленную БД из sandbox
scp /home/user/webapp/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/2b35d4d42e3c9f6b5ad5b5579a7b1470c66e69f6b33a31e3f5a0095cc6d18656.sqlite \
user@server:/tmp/webapp_merged.sqlite
# 3. На сервере остановить docker
docker-compose down
# 4. Заменить БД файл
mv /path/to/data/v3/d1/miniflare-D1DatabaseObject/*.sqlite \
/path/to/data/v3/d1/miniflare-D1DatabaseObject/webapp.sqlite.bak
cp /tmp/webapp_merged.sqlite \
/path/to/data/v3/d1/miniflare-D1DatabaseObject/d6ce1225f7b3fe02cb9b5e87aebb58192679c5a5fb4dd87257777cedd013c0cc.sqlite
# 5. Запустить docker
docker-compose up -d
# 6. Проверить
curl http://localhost:8180/api/years
curl "http://localhost:8180/api/records?month=1&year=2025"
```
---
## 💡 Важные примечания
1. **Данные**: Теперь в БД 38 реальных production записей вместо 7 демо записей
2. **Пользователи**: Добавлен kasutaja (4-й пользователь), остальные из production бекапа
3. **Совместимость**: Код v4.1.6 полностью совместим с production данными
4. **Миграция**: Добавлена колонка deleted_at для поддержки soft delete
5. **Бекап**: Старая БД сохранена в `/tmp/webapp_old.sqlite`
---
## 🔗 URLs
- **Sandbox URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Бекап файл**: /home/user/uploaded_files/backend/
- **Старая БД**: /tmp/webapp_old.sqlite
---
**🎯 Итог**: Production данные успешно восстановлены, пользователь kasutaja добавлен, миграция применена, всё работает! ✅

View File

@@ -1,102 +0,0 @@
# 🚀 Quick Deployment Instructions - AKNAPROFF v4.1.7
**Версия:** v4.1.7
**Дата:** 2025-12-30
**Статус:** ✅ Ready to Deploy
---
## ⚡ Быстрый Старт (3 минуты)
### Шаг 1: SQL Команды (ОБЯЗАТЕЛЬНО)
```bash
# На production сервере выполнить:
# 1. Добавить deleted_at колонку
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='ALTER TABLE users ADD COLUMN deleted_at DATETIME DEFAULT NULL'"
# 2. Удалить дубликат пользователя
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='DELETE FROM users WHERE username = \"tootmine\"'"
# 3. Обновить пароли (SHA-256)
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='
UPDATE users SET password_hash = \"d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791\" WHERE username IN (\"admin\", \"aknaproff\");
UPDATE users SET password_hash = \"a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f\" WHERE username = \"kasutaja\"
'"
```
### Шаг 2: Перезапуск
```bash
docker-compose restart
```
### Шаг 3: Проверка
```bash
# Test admin login
curl -X POST http://localhost:8180/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"demo123"}'
# Должно вернуть: {"success":true, "token":"...", ...}
```
---
## 📋 Новые Учётные Данные
| Username | Password | Role | Доступ |
|----------|----------|------|--------|
| admin | demo123 | admin | Полный |
| aknaproff | demo123 | admin | Полный |
| kasutaja | tootmine | user | Просмотр + проблемы |
**Удалено:** ~~tootmine~~ (дубликат)
---
## 🔍 Проверка После Deployment
### Backend
```bash
✅ curl http://localhost:8180
✅ curl http://localhost:8180/api/years
✅ curl http://localhost:8180/api/records?month=1&year=2025
```
### Frontend
1. ✅ Открыть `http://localhost:8180`
2.**Ctrl+Shift+R** (hard refresh)
3. ✅ Войти как **admin / demo123**
4. ✅ Проверить таблицу с данными
5. ✅ Нажать **Vaata ainult** (👁) - guest режим
6. ✅ F12 → Console: нет ошибок
---
## ⚠️ Важно
### Что изменилось
- ✅ Пароли теперь SHA-256 (было bcrypt)
- ✅ Удалён пользователь `tootmine`
- ✅ Добавлена колонка `deleted_at` в `users`
### Если что-то не работает
1. Проверить выполнение SQL команд
2. Проверить логи: `docker-compose logs -f aknaproff-backend`
3. Перезапустить: `docker-compose restart`
4. Проверить БД: `npx wrangler d1 execute webapp-production --local --command='SELECT * FROM users'`
---
## 📄 Полная Документация
- **FINAL_REPORT_v4.1.7.md** - подробный отчёт
- **DB_FIX_v4.1.7.md** - детали исправления БД
- **FILES_TO_COPY.txt** - опциональное обновление кода (v4.1.6 UI)
---
🎉 **Готово! После выполнения SQL команд и перезапуска всё будет работать!**

View File

@@ -1,246 +0,0 @@
# 🚀 DEPLOYMENT REPORT - AKNAPROFF v4.1.1
**Дата**: 2025-11-28 23:45
**Версия**: v4.1.1 (HOTFIX)
**Статус**: ✅ Ready for Production
---
## 📋 Краткое резюме
**Версия v4.1.1 - Critical Hotfix:**
- Исправлена ошибка `continueAsGuest is not defined`
- Кнопка "Vaata ainult" (Только просмотр) теперь работает
- Guest режим полностью функционален
- Cache-busting обновлён до v4.1.1
---
## 🔄 История версий (краткая)
| Версия | Дата | Описание |
|--------|------|----------|
| v4.0.13 | 28.11 | Calendar picker для MAT-1/MAT-2 для всех пользователей |
| v4.1.0 | 28.11 | Auth система, роли (guest/user/admin), сортировка по ID |
| v4.1.1 | 28.11 | **HOTFIX**: исправлена функция continueAsGuest |
---
## 🐛 Что было исправлено в v4.1.1
### Проблема
```
Uncaught ReferenceError: continueAsGuest is not defined
```
### Решение
```javascript
// До
function continueAsGuest() { ... }
// После
window.continueAsGuest = function() { ... }
```
### Результат
✅ Кнопка "Vaata ainult" работает
✅ Guest режим активируется
✅ Форма логина закрывается
✅ Консоль браузера чистая (0 критических ошибок)
---
## 📦 Изменённые файлы
### Для копирования на production:
**Вариант 1: Полный (с пересборкой)**
```
public/static/app.js
public/original.html
src/original-html.ts
```
**Вариант 2: Быстрый (только dist)**
```
dist/_worker.js
```
---
## 🧪 Тестирование
### Sandbox Environment
- **URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Status**: ✅ Online
- **HTTP**: 200 OK
- **Page Load**: ~8-9s
- **JavaScript Errors**: 0 (кроме AdBlock)
### Функциональные тесты
#### 1. Guest Mode (Режим гостя)
✅ Открытие страницы → показывается форма логина
✅ Кнопка "Vaata ainult" → закрывает форму
✅ Таблица загружается с данными
✅ Фильтры работают (месяц/год)
✅ Поиск работает
✅ Сортировка по колонкам работает
✅ Кнопка сортировки по ID работает
✅ При попытке изменить данные → alert "read-only"
✅ Кнопка "Logi sisse" в хедере доступна
#### 2. User Mode (Простой пользователь)
✅ Вход: aknaproff / demo123
✅ Просмотр всех данных
✅ Изменение проблем (текст + галочки)
✅ Просмотр Notes (read-only)
✅ Блокировка других функций (даты, записи)
#### 3. Admin Mode (Администратор)
✅ Вход: admin / demo123
✅ Полный доступ ко всем функциям
✅ Изменение дат (toggle)
✅ Изменение MAT-1/MAT-2 (toggle)
✅ Изменение LÕIKUS/KLAAS/VALMIS/VÄLJAS (3-step cycle)
✅ Добавление записей
✅ Редактирование записей
✅ Удаление записей (с настройкой)
#### 4. Блокировка по проблемам
✅ Без галочек проблем → VALMIS/VÄLJAS работают
С галочками проблем → VALMIS/VÄLJAS заблокированы
✅ Текст проблемы НЕ блокирует (только галочки)
#### 5. Сортировка по ID
✅ Первый клик → сортировка по возрастанию (↑)
✅ Второй клик → сортировка по убыванию (↓)
✅ Третий клик → отключение сортировки (↕)
✅ При сортировке по другой колонке → сброс ID
---
## 📊 Статистика приложения
### Frontend
- **HTML**: public/original.html (~300 строк)
- **JavaScript**: public/static/app.js (~2233 строк)
- **CSS**: Tailwind CSS (CDN)
- **Icons**: Font Awesome (CDN)
### Backend
- **Framework**: Hono (Cloudflare Workers)
- **API Endpoints**: 26
- **Database**: Cloudflare D1 (SQLite)
- **Auth**: JWT (localStorage)
### Database
- **Tables**: 4 (production_records, status_checkboxes, users, audit_log)
- **Demo Records**: 5 (January 2025)
- **Users**: 2 (admin, aknaproff)
---
## 🔐 Учётные данные
**Demo Accounts**:
- **Admin**: admin / demo123
- **User**: aknaproff / demo123
- **Guest**: Без входа (кнопка "Vaata ainult")
---
## 🚀 Deployment Instructions
### На production сервере:
```bash
# 1. Скопировать файлы (выберите один из вариантов)
# Вариант A: Полный (требует npm run build)
scp public/static/app.js user@server:/path/to/webapp/public/static/
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
# На сервере:
cd /path/to/webapp && npm run build && docker-compose restart
# Вариант B: Быстрый (только dist)
scp dist/_worker.js user@server:/path/to/webapp/dist/
# На сервере:
docker-compose restart
# 2. Проверка после deployment
curl -I http://localhost:8180
# Должен вернуть: HTTP/1.1 200 OK
# 3. Тест в браузере
# Открыть http://your-server:8180
# Нажать Ctrl+Shift+R (сброс кэша)
# Проверить кнопку "Vaata ainult"
# Проверить консоль браузера (F12)
```
---
## ⚠️ Важные замечания
1. **Cache-busting**: Версия app.js обновлена до `?v=4.1.1`
2. **База данных**: НЕ изменялась, миграции НЕ требуются
3. **Docker volume**: Данные в `./data/` сохранены
4. **Backup**: Рекомендуется сделать backup БД перед deployment
5. **Browser cache**: Пользователям нужно сделать Ctrl+Shift+R
---
## 📝 Git History
```
f3e95a9 - Update FILES_TO_COPY.txt for v4.1.1
8484b21 - HOTFIX v4.1.1: Fix continueAsGuest global access and cache-busting
31c9262 - Fix: Add "Continue as Guest" button for read-only access
565f820 - v4.1.0 - Major Update: Auth System, Permissions & Sort by ID
```
---
## 🔗 Полезные ссылки
- **Production URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Git Repository**: /home/user/webapp
- **Documentation**:
- HOTFIX_v4.1.1.md (детали hotfix)
- CHANGES_v4.1.0.md (история v4.1.0)
- COMPLETE_PROJECT_HISTORY.md (полная история)
- DOCUMENTATION_INDEX.md (навигация по документации)
---
## ✅ Финальный чеклист
- [x] Все тесты пройдены
- [x] Консоль браузера чистая (0 критических ошибок)
- [x] Guest режим работает
- [x] User режим работает
- [x] Admin режим работает
- [x] База данных не тронута
- [x] Git коммиты созданы
- [x] Документация обновлена
- [x] FILES_TO_COPY.txt обновлён
- [x] Cache-busting настроен
---
## 🎯 Итог
**AKNAPROFF Tootmine v4.1.1** готов к production deployment!
**Статус**: ✅ Production Ready
**Критических багов**: 0
**Функциональность**: 100%
**Документация**: Полная
---
**Развёрнуто**: 2025-11-28 23:45
**Разработчик**: Claude + User
**Следующий шаг**: Production deployment на ваш сервер

View File

@@ -1,629 +0,0 @@
# 🐳 Docker Guide - AKNAPROFF Tootmine
## 📋 Оглавление
1. [Быстрый старт](#быстрый-старт)
2. [Структура файлов](#структура-файлов)
3. [Development режим](#development-режим)
4. [Production режим](#production-режим)
5. [База данных](#база-данных)
6. [Команды Docker](#команды-docker)
7. [Troubleshooting](#troubleshooting)
---
## 🚀 Быстрый старт
### Prerequisite
Установите Docker и Docker Compose:
- **Docker**: https://docs.docker.com/get-docker/
- **Docker Compose**: обычно идёт в комплекте с Docker Desktop
### Development режим (с локальной БД)
```bash
# 1. Клонировать проект (или быть в папке проекта)
cd /path/to/webapp
# 2. Запустить Docker Compose
docker-compose up
# 3. Открыть браузер
http://localhost:3000
```
**✅ БД будет храниться в `.wrangler/state/v3/d1/` внутри проекта!**
---
## 📁 Структура файлов
### Созданные Docker файлы:
```
webapp/
├── docker-compose.yml # Development конфигурация
├── docker-compose.prod.yml # Production конфигурация
├── Dockerfile # Production образ
├── .dockerignore # Исключения для Docker build
└── DOCKER_GUIDE.md # Эта инструкция
```
### Где хранится БД:
#### Development режим (docker-compose.yml):
```
./ # Корень проекта
└── .wrangler/
└── state/
└── v3/
└── d1/
└── webapp-production.sqlite # ✅ БД здесь!
```
#### Production режим (docker-compose.prod.yml):
```
./ # Корень проекта
└── data/
└── db/
└── webapp-production.sqlite # ✅ БД здесь!
```
---
## 🔧 Development режим
### Запуск
```bash
# Запустить в foreground (видеть логи)
docker-compose up
# Запустить в background (daemon)
docker-compose up -d
# Посмотреть логи
docker-compose logs -f
```
### Особенности Development режима:
1. **Hot reload**: Изменения в коде сразу видны (bind mount)
2. **Локальная БД**: Хранится в `.wrangler/state/v3/d1/`
3. **Автоматический rebuild**: При изменении `package.json`
4. **Debug режим**: Все логи в консоль
### Структура docker-compose.yml:
```yaml
volumes:
# Весь проект монтируется
- ./:/app
# node_modules остаются в контейнере
- /app/node_modules
```
**Почему это работает:**
- ✅ Весь проект доступен в контейнере
-`.wrangler/` создаётся внутри проекта
- ✅ БД физически на вашем диске в проекте
- ✅ Можно бэкапить вместе с проектом
### Остановка
```bash
# Остановить контейнеры
docker-compose down
# Остановить и удалить volumes (НЕ УДАЛЯЕТ ЛОКАЛЬНЫЕ ФАЙЛЫ)
docker-compose down -v
```
---
## 🚀 Production режим
### Build и запуск
```bash
# 1. Создать директорию для БД
mkdir -p data/db data/logs
# 2. Build образа
docker-compose -f docker-compose.prod.yml build
# 3. Запуск
docker-compose -f docker-compose.prod.yml up -d
# 4. Проверить логи
docker-compose -f docker-compose.prod.yml logs -f
```
### Особенности Production режима:
1. **Multi-stage build**: Оптимизированный образ
2. **Non-root user**: Безопасность
3. **Health checks**: Автоматическая проверка
4. **Resource limits**: CPU и Memory limits
5. **Локальная БД**: Хранится в `./data/db/`
### Структура docker-compose.prod.yml:
```yaml
volumes:
# Только БД монтируется локально
- ./data/db:/app/.wrangler/state/v3/d1
- ./data/logs:/app/logs
```
**Почему отдельная папка `data/`:**
- ✅ Чистое разделение данных и кода
- ✅ Легко бэкапить: `tar -czf backup.tar.gz data/`
- ✅ Можно смонтировать на отдельный диск
- ✅ Безопасность: права доступа только к `data/`
---
## 🗄️ База данных
### Где находится БД
#### Development:
```bash
# Путь к БД
./webapp/.wrangler/state/v3/d1/webapp-production.sqlite
# Посмотреть БД
sqlite3 .wrangler/state/v3/d1/webapp-production.sqlite
```
#### Production:
```bash
# Путь к БД
./webapp/data/db/webapp-production.sqlite
# Посмотреть БД
sqlite3 data/db/webapp-production.sqlite
```
### Бэкап БД
#### Автоматический бэкап (рекомендуется):
```bash
# Создать скрипт backup.sh
cat > backup.sh << 'EOF'
#!/bin/bash
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_DIR="./backups"
mkdir -p $BACKUP_DIR
# Development БД
if [ -f ".wrangler/state/v3/d1/webapp-production.sqlite" ]; then
cp .wrangler/state/v3/d1/webapp-production.sqlite \
$BACKUP_DIR/webapp-dev-$DATE.sqlite
echo "✅ Dev DB backed up: $BACKUP_DIR/webapp-dev-$DATE.sqlite"
fi
# Production БД
if [ -f "data/db/webapp-production.sqlite" ]; then
cp data/db/webapp-production.sqlite \
$BACKUP_DIR/webapp-prod-$DATE.sqlite
echo "✅ Prod DB backed up: $BACKUP_DIR/webapp-prod-$DATE.sqlite"
fi
EOF
chmod +x backup.sh
# Запустить бэкап
./backup.sh
```
#### Ручной бэкап:
```bash
# Development
cp .wrangler/state/v3/d1/webapp-production.sqlite \
webapp-backup-$(date +%Y%m%d).sqlite
# Production
cp data/db/webapp-production.sqlite \
webapp-backup-$(date +%Y%m%d).sqlite
```
### Восстановление БД
```bash
# Development
cp webapp-backup-20251128.sqlite \
.wrangler/state/v3/d1/webapp-production.sqlite
# Production
cp webapp-backup-20251128.sqlite \
data/db/webapp-production.sqlite
# Перезапустить контейнер
docker-compose restart # Development
docker-compose -f docker-compose.prod.yml restart # Production
```
### Сброс БД
```bash
# Зайти в контейнер
docker-compose exec webapp sh
# Внутри контейнера
npm run db:reset
# Или снаружи
docker-compose exec webapp npm run db:reset
```
### Миграции
```bash
# Применить миграции
docker-compose exec webapp npm run db:migrate:local
# Залить seed данные
docker-compose exec webapp npm run db:seed
```
---
## 📦 Команды Docker
### Базовые команды
```bash
# Запустить
docker-compose up
# Запустить в background
docker-compose up -d
# Остановить
docker-compose down
# Остановить и удалить volumes
docker-compose down -v
# Перезапустить
docker-compose restart
# Посмотреть логи
docker-compose logs -f
# Посмотреть логи конкретного сервиса
docker-compose logs -f webapp
```
### Управление контейнерами
```bash
# Список контейнеров
docker-compose ps
# Зайти в контейнер
docker-compose exec webapp sh
# Выполнить команду в контейнере
docker-compose exec webapp npm run build
# Посмотреть использование ресурсов
docker stats aknaproff-webapp
```
### Build и образы
```bash
# Build заново
docker-compose build
# Build без кэша
docker-compose build --no-cache
# Посмотреть образы
docker images | grep webapp
# Удалить неиспользуемые образы
docker image prune -a
```
### Logs и debugging
```bash
# Все логи
docker-compose logs
# Последние 100 строк
docker-compose logs --tail=100
# Follow (live)
docker-compose logs -f
# Только ошибки
docker-compose logs | grep ERROR
# Экспорт логов в файл
docker-compose logs > webapp-logs.txt
```
---
## 🔍 Troubleshooting
### Проблема: Контейнер не запускается
**Симптомы:**
```
Error: Cannot find module 'hono'
```
**Решение:**
```bash
# Удалить node_modules и пересобрать
docker-compose down -v
docker-compose build --no-cache
docker-compose up
```
---
### Проблема: Порт 3000 занят
**Симптомы:**
```
Error: bind: address already in use
```
**Решение 1: Убить процесс на порту 3000**
```bash
# Linux/Mac
lsof -ti:3000 | xargs kill -9
# Или
fuser -k 3000/tcp
```
**Решение 2: Изменить порт в docker-compose.yml**
```yaml
ports:
- "3001:3000" # Host:Container
```
---
### Проблема: БД не создаётся
**Симптомы:**
```
Error: no such table: users
```
**Решение:**
```bash
# Зайти в контейнер
docker-compose exec webapp sh
# Сбросить БД
npm run db:reset
# Выйти
exit
```
---
### Проблема: БД пропала после перезапуска
**Причина:** Использовали `docker-compose down -v` (удаляет volumes)
**Решение:**
```bash
# НЕ используйте -v если нужно сохранить данные
docker-compose down # ✅ Правильно
# Используйте -v только для полной очистки
docker-compose down -v # ❌ Удалит volumes
```
**Но в нашем случае:**
- ✅ БД хранится в `./` или `./data/` (bind mount)
-`docker-compose down -v` НЕ удалит файлы на хосте
- ✅ Безопасно использовать `-v`
---
### Проблема: Изменения в коде не видны
**Development режим:**
```bash
# Проверить bind mount
docker-compose exec webapp ls -la /app
# Если bind mount не работает, перезапустить
docker-compose down
docker-compose up
```
**Production режим:**
```bash
# Production требует rebuild
docker-compose -f docker-compose.prod.yml build
docker-compose -f docker-compose.prod.yml up -d
```
---
### Проблема: Недостаточно памяти
**Симптомы:**
```
JavaScript heap out of memory
```
**Решение: Увеличить memory limit**
```yaml
# docker-compose.prod.yml
deploy:
resources:
limits:
memory: 1G # Было 512M
```
---
## 📊 Мониторинг
### Использование ресурсов
```bash
# Real-time статистика
docker stats aknaproff-webapp
# Вывод:
# CONTAINER CPU % MEM USAGE / LIMIT MEM %
# aknaproff... 0.5% 120MB / 512MB 23%
```
### Health checks
```bash
# Проверить health status
docker-compose ps
# Вывод:
# NAME STATUS
# aknaproff-webapp Up (healthy)
```
### Логи приложения
```bash
# Live логи
docker-compose logs -f webapp
# Ошибки
docker-compose logs webapp | grep ERROR
# Экспорт в файл
docker-compose logs webapp > logs.txt
```
---
## 🎯 Best Practices
### 1. Регулярные бэкапы
```bash
# Добавить в cron (каждый день в 3:00)
0 3 * * * cd /path/to/webapp && ./backup.sh
```
### 2. Мониторинг дискового пространства
```bash
# Проверить размер БД
du -sh .wrangler/state/v3/d1/
du -sh data/db/
# Проверить свободное место
df -h
```
### 3. Обновление образов
```bash
# Обновить base образы
docker-compose pull
# Rebuild с новыми образами
docker-compose build --no-cache
docker-compose up -d
```
### 4. Безопасность
```bash
# Использовать .env для секретов
echo "CLOUDFLARE_API_TOKEN=your_token" > .env
# Добавить в .gitignore
echo ".env" >> .gitignore
# В docker-compose.yml
env_file:
- .env
```
### 5. Логирование
```bash
# Ротация логов (добавить в docker-compose.yml)
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
```
---
## 📚 Полезные ссылки
- **Docker Docs**: https://docs.docker.com/
- **Docker Compose**: https://docs.docker.com/compose/
- **Cloudflare Workers**: https://developers.cloudflare.com/workers/
- **Wrangler CLI**: https://developers.cloudflare.com/workers/wrangler/
- **Hono Framework**: https://hono.dev/
---
## ✅ Проверочный чек-лист
### Development:
- [ ] `docker-compose up` запускается без ошибок
- [ ] Приложение доступно на http://localhost:3000
- [ ] БД создалась в `.wrangler/state/v3/d1/`
- [ ] Демо-данные загрузились (5 записей за январь 2025)
- [ ] Hot reload работает (изменения видны)
- [ ] Health check: `docker-compose ps``(healthy)`
### Production:
- [ ] `docker-compose -f docker-compose.prod.yml build` успешно
- [ ] `docker-compose -f docker-compose.prod.yml up -d` запустился
- [ ] Приложение доступно на http://localhost:3000
- [ ] БД создалась в `./data/db/`
- [ ] Демо-данные загрузились
- [ ] Health check: `(healthy)`
- [ ] Бэкап работает: `./backup.sh`
---
## 🎉 Готово!
**AKNAPROFF Tootmine теперь работает в Docker с локальной БД!**
**Production URL (Docker):**
http://localhost:3000
**Demo Accounts:**
- `admin` / `demo123`
- `aknaproff` / `demo123`
- **Public User** (без логина)
**База данных:**
- **Development**: `.wrangler/state/v3/d1/webapp-production.sqlite`
- **Production**: `./data/db/webapp-production.sqlite`
**Бэкап:**
```bash
./backup.sh
```
---
*Создано: 28.11.2025*
*Версия проекта: v4.0.13*

View File

@@ -1,184 +0,0 @@
# 🚀 Docker Quick Start - AKNAPROFF Tootmine
## Быстрый запуск в 3 команды
### Development режим:
```bash
# 1. Перейти в папку проекта
cd /path/to/webapp
# 2. Запустить Docker Compose
docker-compose up
# 3. Открыть браузер
http://localhost:3000
```
**БД будет храниться в `.wrangler/state/v3/d1/webapp-production.sqlite`**
---
## Где хранится база данных?
### Development:
```
./webapp/.wrangler/state/v3/d1/webapp-production.sqlite ✅ На вашем диске!
```
### Production:
```
./webapp/data/db/webapp-production.sqlite ✅ На вашем диске!
```
**Важно:** БД НЕ в Docker volume, а в папке проекта!
---
## Основные команды
```bash
# Запустить
docker-compose up
# Запустить в background
docker-compose up -d
# Остановить
docker-compose down
# Посмотреть логи
docker-compose logs -f
# Зайти в контейнер
docker-compose exec webapp sh
# Бэкап БД
./backup.sh
```
---
## Production режим
```bash
# 1. Создать папку для БД
mkdir -p data/db
# 2. Build и запуск
docker-compose -f docker-compose.prod.yml up --build -d
# 3. Проверить логи
docker-compose -f docker-compose.prod.yml logs -f
```
**БД будет в `./data/db/webapp-production.sqlite`**
---
## Бэкап базы данных
```bash
# Автоматический бэкап
./backup.sh
# Ручной бэкап
cp .wrangler/state/v3/d1/webapp-production.sqlite \
backup-$(date +%Y%m%d).sqlite
```
Бэкапы сохраняются в папке `./backups/`
---
## Сброс базы данных
```bash
# Через Docker
docker-compose exec webapp npm run db:reset
# Или удалить и пересоздать
docker-compose down
rm -rf .wrangler/state/v3/d1
docker-compose up
```
---
## Troubleshooting
### Порт 3000 занят
```bash
# Убить процесс
fuser -k 3000/tcp
# Или изменить порт в docker-compose.yml
ports:
- "3001:3000"
```
### Контейнер не запускается
```bash
# Пересобрать без кэша
docker-compose down -v
docker-compose build --no-cache
docker-compose up
```
### БД не создаётся
```bash
# Зайти в контейнер и сбросить БД
docker-compose exec webapp sh
npm run db:reset
exit
```
---
## Полная документация
Смотрите **[DOCKER_GUIDE.md](DOCKER_GUIDE.md)** для подробной информации:
- Структура файлов
- Мониторинг
- Best practices
- Полный troubleshooting
---
## ✅ Проверка
После запуска проверьте:
1. **Приложение работает:**
```bash
curl http://localhost:3000
```
2. **БД создалась:**
```bash
ls -lh .wrangler/state/v3/d1/webapp-production.sqlite
```
3. **Health check OK:**
```bash
docker-compose ps
# Должно быть: (healthy)
```
4. **Демо-данные загружены:**
```bash
sqlite3 .wrangler/state/v3/d1/webapp-production.sqlite \
"SELECT COUNT(*) FROM production_records"
# Должно быть: 7
```
---
**🎉 Готово! AKNAPROFF работает в Docker с локальной БД!**
**URL:** http://localhost:3000
**Demo:** `admin` / `demo123` или **Public User** (без логина)
**БД:** `.wrangler/state/v3/d1/webapp-production.sqlite`

View File

@@ -1,270 +0,0 @@
# 📚 ИНДЕКС ДОКУМЕНТАЦИИ - AKNAPROFF Tootmine
**Проект**: AKNAPROFF Tootmine
**Текущая версия**: v4.0.13
**Дата**: 28.11.2025
**Общий размер документации**: 152KB
**Файлов документации**: 11
---
## 📖 БЫСТРАЯ НАВИГАЦИЯ
### 🚀 Для быстрого старта
1. **[README.md](README.md)** (8.6KB) - Начните здесь!
- Обзор проекта
- Установка и запуск
- API endpoints
- Структура базы данных
### 📊 Для понимания структуры
2. **[PROJECT_STRUCTURE.md](PROJECT_STRUCTURE.md)** (12KB)
- Полная структура директорий
- 26 API endpoints с описанием
- 4 таблицы БД со схемами
- Статистика кода (~3885 строк)
### 📋 Для краткого обзора версий
3. **[VERSION_SUMMARY.md](VERSION_SUMMARY.md)** (3.3KB)
- Таблица версий v4.0.5 → v4.0.13
- Проблемы и решения
- Инструкции по тестированию
### 📚 Для полной истории (с версии 1)
4. **[COMPLETE_PROJECT_HISTORY.md](COMPLETE_PROJECT_HISTORY.md)** (38KB) ⭐ **НОВЫЙ**
- **Полная история с v1.0 до v4.0.13**
- 5 фаз разработки
- 37 git commits анализ
- Все технические решения
- Все уроки и выводы
### 🔍 Для детальной истории v4.0.5-v4.0.13
5. **[FULL_DEVELOPMENT_HISTORY.md](FULL_DEVELOPMENT_HISTORY.md)** (25KB)
- Детальная история версий v4.0.5-v4.0.13
- Все запросы пользователей
- Все проблемы и решения
- Тесты и результаты
---
## 📂 КАТЕГОРИИ ДОКУМЕНТАЦИИ
### 🏗️ Восстановление проекта
**[RESTORE_REPORT.md](RESTORE_REPORT.md)** (11KB)
- Отчёт о восстановлении v3.20.3
- Что было восстановлено из архива
- Структура проекта
- Результаты тестирования
- **Дата**: 28.11.2025, 10:00
### 🔧 Исправления по версиям
**[FIX_REPORT_v3.20.7.md](FIX_REPORT_v3.20.7.md)** (7.6KB)
- Исправление критических ошибок v3.20.7
- Database binding fix
- API years format fix
- Modal windows fix
- tfoot fix
**[FIXED_v4.0.1.md](FIXED_v4.0.1.md)** (6.4KB)
- Исправление путей к ресурсам
- FontAwesome CDN
- Axios CDN
- App.js путь
**[FIXED_v4.0.5.md](FIXED_v4.0.5.md)** (4.6KB)
- Default month filter fix
- Empty table → Working clicks
- Демо-данные январь 2025
**[FIXED_v4.0.6.md](FIXED_v4.0.6.md)** (7.7KB)
- HTTP 401 fix
- Public access enable
- optionalAuthMiddleware
- userId || null fix
### 🔍 Анализ логики
**[CLICK_LOGIC_REVIEW.md](CLICK_LOGIC_REVIEW.md)** (9.1KB)
- Полный обзор логики кликов
- Сравнение оригинал vs текущий
- Frontend vs Backend несоответствия
- Что работает / что не работает
- Решения проблем
---
## 📊 СВОДНАЯ ИНФОРМАЦИЯ
### Версии проекта
```
v1.0 - Оригинальный архив (aknaproff.zip)
v3.20.3-3.20.8 - Восстановление (8 версий)
v4.0.0-4.0.4 - Полная реставрация (5 версий)
v4.0.5-4.0.8 - Исправление кликов (4 версии)
v4.0.9-4.0.13 - Исправление MAT-1/MAT-2 (5 версий)
---
ВСЕГО: 32 версии
```
### Git коммиты
```
37 коммитов (28.11.2025)
~5000 insertions
~200 deletions
```
### Статистика кода
```
Backend: 1400 lines (50KB)
Frontend: 3302 lines (75KB)
Database: 250 lines (10KB)
Config: 150 lines (5KB)
---
ВСЕГО: 5102 lines (140KB)
```
### Документация
```
README.md - 8.6KB - Основное руководство
PROJECT_STRUCTURE.md - 12KB - Структура проекта
VERSION_SUMMARY.md - 3.3KB - Краткая сводка версий
COMPLETE_PROJECT_HISTORY.md - 38KB - Полная история (v1→v4.0.13) ⭐
FULL_DEVELOPMENT_HISTORY.md - 25KB - Детальная история (v4.0.5→v4.0.13)
RESTORE_REPORT.md - 11KB - Отчёт восстановления
FIX_REPORT_v3.20.7.md - 7.6KB - Исправления v3.20.7
FIXED_v4.0.1.md - 6.4KB - Исправления v4.0.1
FIXED_v4.0.5.md - 4.6KB - Исправления v4.0.5
FIXED_v4.0.6.md - 7.7KB - Исправления v4.0.6
CLICK_LOGIC_REVIEW.md - 9.1KB - Анализ логики кликов
---
ВСЕГО: 152KB документации
```
### API Endpoints
```
26 endpoints:
- 2 Authentication
- 5 Records CRUD
- 10 Status updates
- 1 Utility
- 1 Static serving
13 endpoints с optionalAuthMiddleware (public access)
```
### База данных
```
4 таблицы:
- users (2 демо-пользователя)
- production_records (7 демо-записей)
- status_checkboxes (статусы и флаги)
- audit_log (история изменений)
```
---
## 🎯 РЕКОМЕНДУЕМЫЙ ПУТЬ ЧТЕНИЯ
### Для нового разработчика:
1. **README.md** - Общий обзор
2. **PROJECT_STRUCTURE.md** - Понять структуру
3. **COMPLETE_PROJECT_HISTORY.md** - Узнать полную историю
4. **CLICK_LOGIC_REVIEW.md** - Понять логику работы
### Для решения проблем:
1. **VERSION_SUMMARY.md** - Найти похожую проблему
2. **FULL_DEVELOPMENT_HISTORY.md** - Детали решения v4.0.5+
3. **COMPLETE_PROJECT_HISTORY.md** - Все решения с v1.0
4. **Соответствующий FIX_REPORT** - Конкретная версия
### Для deployment:
1. **README.md** - Инструкции
2. **PROJECT_STRUCTURE.md** - Requirements
3. **RESTORE_REPORT.md** - Setup process
---
## 🌐 ССЫЛКИ
**Production URL:**
https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
**Demo Accounts:**
- `admin` / `demo123`
- `aknaproff` / `demo123`
- **Public User** (no login needed)
**GitHub:**
- Repository: (not deployed yet)
---
## ✅ ФИНАЛЬНЫЙ СТАТУС
**Версия**: v4.0.13
**Дата**: 28.11.2025, 19:00
**Статус**: ✅ Production Ready
**Что работает:**
-Все клики (date toggle, calendar, buttons)
- ✅ MAT-1/MAT-2 calendar picker (все пользователи)
- ✅ MAT-1/MAT-2 checkbox toggle (все пользователи)
- ✅ Public access (no login required)
- ✅ 26/26 API endpoints
- ✅ D1 Database
- ✅ Audit logging
- ✅ Lock logic
- ✅ All modals
**Консоль браузера:**
- ✅ 0 JavaScript errors
- ✅ 0 HTTP errors
-Все ресурсы загружаются
---
## 📝 ПОСЛЕДНИЕ ОБНОВЛЕНИЯ
### 28.11.2025, 22:30
- ✅ Добавлен **COMPLETE_PROJECT_HISTORY.md** (38KB)
- Полная история с v1.0 до v4.0.13
- 5 фаз разработки
- 32 версии
- 37 git commits
- Все технические и процессные уроки
- ✅ Создан **DOCUMENTATION_INDEX.md** (этот файл)
- Индекс всей документации
- Навигация по файлам
- Сводная статистика
- Рекомендуемый путь чтения
**Общий объём документации:** 152KB (11 файлов)
---
## 🎓 КЛЮЧЕВЫЕ ВЫВОДЫ
### Технические:
1. ✅ Используйте оригинальный архив как BASE
2. ✅ Синхронизируйте Frontend ↔ Backend auth
3.`<label for>` + `left:-9999px` для date picker
4. ✅ Cache busting с версиями (`?v=X`)
5. ✅ Default filters на данные, не на текущую дату
6.`userId || null` для optional FK
### Процессные:
1. ✅ Frequent git commits (37 за 9 часов)
2. ✅ Документируйте каждый major fix
3. ✅ Тестируйте через curl → browser
4. ✅ Очищайте browser cache (Ctrl+Shift+R)
5. ✅ Сохраняйте историю для анализа
---
**🎉 Проект полностью задокументирован и готов к использованию!**
*Создано: 28.11.2025, 22:30*
*Последнее обновление: 28.11.2025, 22:30*

View File

@@ -1,147 +0,0 @@
# 📦 Файлы для копирования на 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 (желтый с ). БД не затронута.

View File

@@ -1,359 +0,0 @@
# AKNAPROFF v4.1.7 - Final Report
**Дата:** 2025-12-30
**Версия:** v4.1.7
**Статус:** ✅ Production Ready
---
## Краткое Резюме
### Проблемы (Исправлено)
1.**Login failed** → ✅ Все логины работают
2. ❌ Пользователь `tootmine` (дубликат) → ✅ Удалён
### Что Сделано
1. ✅ Восстановлена production база (38 записей вместо 7 демо)
2. ✅ Исправлены пароли (bcrypt → SHA-256)
3. ✅ Добавлена колонка `deleted_at` в таблицу `users`
4. ✅ Удалён пользователь `tootmine`
5. ✅ Проверены все логины (admin, aknaproff, kasutaja)
---
## Текущая Версия: v4.1.7
### Код
- **Версия:** v4.1.6 (без изменений)
- **Последний коммит:** dbc5c25
- **Функции:** Все v4.1.0-v4.1.6 фичи работают
### База Данных
- **Версия:** v4.1.7 (исправлена)
- **Пользователи:** 3 (admin, aknaproff, kasutaja)
- **Записи:** 38 production записей
- **Schema:** Все колонки актуальны
---
## Учётные Данные
| Username | Password | Role | Доступ |
|----------|----------|------|--------|
| **admin** | demo123 | admin | Полный доступ |
| **aknaproff** | demo123 | admin | Полный доступ |
| **kasutaja** | tootmine | user | Просмотр + проблемы |
| *(guest)* | без входа | guest | Только просмотр |
**Удалено:**
- ~~tootmine~~ ❌ (дубликат kasutaja)
---
## Production URLs
### Sandbox (Текущий)
- **URL:** https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Status:** ✅ Online
- **HTTP:** 200 OK
- **JS Errors:** 0
### Production Server
- **URL:** `http://your-server:8180`
- **Status:** Ожидает deployment
- **Требуется:** Обновление БД (см. ниже)
---
## Deployment на Production Server
### ⚠️ КРИТИЧНО: Сначала База Данных
**Шаг 1: Остановить приложение**
```bash
docker-compose stop
```
**Шаг 2: Выполнить SQL команды**
```bash
# 1. Добавить deleted_at в users
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='ALTER TABLE users ADD COLUMN deleted_at DATETIME DEFAULT NULL'"
# 2. Удалить tootmine
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='DELETE FROM users WHERE username = \"tootmine\"'"
# 3. Обновить пароли (SHA-256)
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='
UPDATE users SET password_hash = \"d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791\" WHERE username = \"admin\";
UPDATE users SET password_hash = \"d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791\" WHERE username = \"aknaproff\";
UPDATE users SET password_hash = \"a1026b7bd143f7190248bc79901e9a357a408e208f2d8e4d38fccf184754f35f\" WHERE username = \"kasutaja\"
'"
```
**Шаг 3: Проверка БД**
```bash
docker-compose exec aknaproff-backend sh -c \
"npx wrangler d1 execute webapp-production --local --command='SELECT username, role FROM users'"
# Должно показать:
# admin | admin
# aknaproff | admin
# kasutaja | user
```
**Шаг 4: Обновить код (опционально, для v4.1.6 фич)**
```bash
# Быстрый вариант (рекомендуется)
scp dist/_worker.js user@server:/path/to/webapp/dist/
# ИЛИ Полный вариант
scp public/static/app.js user@server:/path/to/webapp/public/static/
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
cd /path/to/webapp && npm run build
```
**Шаг 5: Перезапуск**
```bash
docker-compose restart
```
**Шаг 6: Проверка**
```bash
# HTTP статус
curl -I http://localhost:8180
# Тест логина
curl -X POST http://localhost:8180/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"demo123"}'
# Должно вернуть: success: true, token: eyJ...
```
---
## Проверка После Deployment
### Backend Tests
```bash
# ✅ Admin login
curl -X POST http://localhost:8180/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"admin","password":"demo123"}'
# ✅ Kasutaja login
curl -X POST http://localhost:8180/api/auth/login \
-H "Content-Type: application/json" \
-d '{"username":"kasutaja","password":"tootmine"}'
# ✅ Records API
curl http://localhost:8180/api/records?month=1&year=2025
# ✅ Years API
curl http://localhost:8180/api/years
```
### Frontend Tests
1. Открыть `http://localhost:8180`
2. **Ctrl+Shift+R** (hard refresh)
3. Проверить форму логина:
- Заголовок: "Login" (не "Administrator Login")
- Подзаголовок: "Sisesta kasutajaandmed"
4. Войти как **admin / demo123**
5. Проверить таблицу с данными
6. Проверить фильтры (Kuu, Aasta, Kiir otsing)
7. Проверить кнопку "ID ↕" (без label "Sorteerimine")
8. Проверить поля:
- **Probleemid**: красный фон с ⚠️ при проблемах
- **Märkused**: желтый фон с при заметках
9. Нажать **Vaata ainult** (👁) - должен открыться guest режим
10. F12 → Console: проверить отсутствие ошибок
---
## История Версий
### v4.1.7 (2025-12-30) - Database Fix
- 🔧 Исправлены пароли (bcrypt → SHA-256)
- 🔧 Добавлена колонка `deleted_at` в `users`
- 🗑️ Удалён пользователь `tootmine`
-Все логины работают
### v4.1.6 (2025-12-30)
- 🎨 Märkused поле: желтый фон + + tooltip
### v4.1.5 (2025-12-30)
- 🎨 Probleemid поле: красный фон + ⚠️ + tooltip
### v4.1.4 (2025-12-30)
- 📝 Login форма: упрощен текст (убрано "Administrator")
### v4.1.3 (2025-12-30)
- 👤 Добавлен пользователь `kasutaja / tootmine`
- 📝 Удалена надпись "Sorteerimine"
- 📋 Уточнены роли пользователей
### v4.1.2 (2025-12-30)
- 🔄 Кнопка "Sorteerimine" перенесена в "Kiir otsing"
### v4.1.1 (2025-12-30)
- 🐛 Fix: `continueAsGuest()` глобальная функция
- 🔄 Cache-busting для app.js
### v4.1.0 (2025-12-29)
- 🔐 Система авторизации (admin, user, guest)
- 🔒 Права доступа по ролям
- 👁️ Guest режим (только просмотр)
- 🆔 Сортировка по ID
---
## Структура Проекта
```
webapp/
├── src/
│ ├── index.tsx # Main Hono app
│ ├── routes/ # API routes
│ ├── middleware/ # Auth middleware
│ └── utils/ # Auth utils (SHA-256)
├── public/
│ ├── static/
│ │ ├── app.js # Frontend JS
│ │ └── styles.css # Custom CSS
│ └── original.html # HTML template
├── migrations/
│ └── 0001_initial_schema.sql
├── .wrangler/
│ └── state/v3/d1/ # Local SQLite DB
├── dist/
│ └── _worker.js # Compiled Cloudflare Worker
├── DB_FIX_v4.1.7.md # Отчёт о фиксе БД
├── CHANGES_v4.1.6.md # Changelog v4.1.6
├── FILES_TO_COPY.txt # Список файлов для deployment
├── package.json
├── wrangler.jsonc
└── README.md
```
---
## База Данных
### Таблицы
- **users** (3 записи)
- Колонки: id, username, password_hash, full_name, role, active, created_at, updated_at, `deleted_at`
- **production_records** (38 записей)
- Колонки: id, month, year, client_name, type, offer_number, work_number, quantity, color, notes, notes_date, problems, problems_date, price, material_checked, material2_checked, installer_name, created_by_user_id, created_at, updated_at, `deleted_at`
- **status_checkboxes** (38 записей)
- Связь с production_records
### Индексы
- `idx_records_month_year`
- `idx_records_client`
- `idx_records_offer`
- `idx_status_record`
---
## Известные Ограничения
### 1. SHA-256 вместо bcrypt
⚠️ **Для demo** - в production рекомендуется bcrypt или argon2
### 2. Tailwind CSS через CDN
⚠️ **Warning** - в production использовать PostCSS или Tailwind CLI
### 3. ERR_BLOCKED_BY_CLIENT
**Не критично** - AdBlock блокирует какой-то ресурс (не влияет на функционал)
---
## Рекомендации для Production
### Безопасность
1. ✅ Использовать bcrypt/argon2 для паролей
2. ✅ Добавить rate limiting для `/api/auth/login`
3. ✅ Включить CSRF защиту
4. ✅ HTTPS только
5. ✅ Secure cookies для токенов
### Производительность
1. ✅ Использовать PostCSS для Tailwind CSS
2. ✅ Минификация CSS/JS
3. ✅ Кэширование статических файлов
4. ✅ CDN для статики
### Мониторинг
1. ✅ Логирование ошибок
2. ✅ Мониторинг производительности
3. ✅ Алерты на критичные ошибки
---
## Контакты и Поддержка
### Документация
- **DB_FIX_v4.1.7.md** - детали исправления БД
- **CHANGES_v4.1.6.md** - changelog v4.1.6
- **FILES_TO_COPY.txt** - инструкции по deployment
- **README.md** - общая документация проекта
### Git Commits
- `d116d5b` - v4.1.7: Fix login (SHA-256 passwords) and remove tootmine user
- `1e7a9e7` - Add DB restore report - production data merged with kasutaja user
- `8bb7cff` - Update FILES_TO_COPY.txt for v4.1.6
---
## Итоговый Статус
### ✅ Готово к Production
- [x] База данных исправлена
- [x] Все логины работают
- [x] Пользователь tootmine удалён
- [x] Production данные (38 записей) загружены
- [x] Все фичи v4.1.0-v4.1.6 работают
- [x] Нет JavaScript ошибок
- [x] HTTP 200 OK
- [x] Документация готова
### 📋 TODO (После Deployment)
- [ ] Выполнить SQL команды на production сервере
- [ ] Проверить все логины на production
- [ ] Сделать Ctrl+Shift+R в браузере
- [ ] Проверить таблицу с данными
- [ ] Проверить фильтры и сортировку
- [ ] Проверить guest режим
---
## 🎯 Финальный Чеклист
**Перед deployment:**
1. ✅ Бэкап текущей БД сделан (`/tmp/webapp_old.sqlite`)
2. ✅ Новые SQL команды подготовлены
3. ✅ Код v4.1.6 готов (опционально для обновления UI)
**После deployment:**
1. ⏳ Выполнить SQL команды
2. ⏳ Перезапустить docker-compose
3. ⏳ Проверить логины
4. ⏳ Проверить данные
5. ⏳ Проверить UI
6. ⏳ Проверить guest режим
---
**Версия:** v4.1.7
**Дата:** 2025-12-30
**Статус:** ✅ Production Ready
**Автор:** AI Assistant
---
🎉 **Всё готово! Можете копировать файлы на production сервер и выполнять SQL команды!**

View File

@@ -1,220 +0,0 @@
# ✅ AKNAPROFF Tootmine v4.0.1 - Все проблемы решены
**Дата:** 28.11.2025
**Статус:****Production Ready** - Полное восстановление + Исправлены пути
---
## 🎯 Что было исправлено в v4.0.1
### ❌ Проблемы v4.0.0:
1. Иконки не загружались (404 для `/webfonts/fa-solid-900.woff2`)
2. Axios не загружался (неправильный путь `./AKNAPROFF Tootmine_files/axios.min.js.Без названия`)
3. App.js не загружался (неправильный путь `/static/app.js.Без названия`)
4. Ошибка `openLoginModal is not defined` (функция есть в app.js, но он не загрузился)
### ✅ Решения v4.0.1:
#### 1. FontAwesome через CDN
**Было:**
```html
<link href="/static/all.min.css" rel="stylesheet">
```
Проблема: Шрифты искались локально в `/webfonts/` (404 ошибки)
**Стало:**
```html
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
```
Результат: ✅ Все иконки загружаются с CDN
---
#### 2. Axios через CDN
**Было:**
```html
<script src="./AKNAPROFF Tootmine_files/axios.min.js.Без названия"></script>
```
Проблема: Неправильный путь из сохранённой страницы
**Стало:**
```html
<script src="https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js"></script>
```
Результат: ✅ Axios загружается корректно
---
#### 3. App.js правильный путь
**Было:**
```html
<script src="/static/app.js.Без названия"></script>
```
Проблема: Путь содержал `.Без названия` (кириллица)
**Стало:**
```html
<script src="/static/app.js"></script>
```
Результат: ✅ Оригинальный app.js (73KB, 2079 строк) загружается
---
## 📊 Результаты тестирования v4.0.1
### ✅ Страница загружается:
```
<title>AKNAPROFF Tootmine</title> ✅
```
### ✅ Ресурсы (без 404 ошибок):
```
- FontAwesome CDN: ✅
- Axios CDN: ✅
- App.js (73KB): ✅
```
### ✅ Оригинальные элементы:
```
- Button openModal(): ✅
- Text 'Lisa uus rida': ✅
```
### ✅ API endpoints работают:
```json
GET /api/years {"years":[2024,2025,2026]}
```
---
## 🎨 Загрузка иконок FontAwesome
### Иконки в приложении:
```html
<!-- Login -->
<i class="fas fa-lock"></i>
<!-- Edit button -->
<i class="fas fa-edit"></i>
<!-- Delete button -->
<i class="fas fa-trash"></i>
<!-- Add button -->
<i class="fas fa-plus"></i>
<!-- Settings -->
<i class="fas fa-cog"></i>
<!-- Report -->
<i class="fas fa-chart-bar"></i>
```
**Статус:**Все иконки загружаются с CDN
---
## 🚀 Консоль браузера
### ❌ Было в v4.0.0:
```
downloadable font: download failed (font-family: "Font Awesome 6 Free")
Загрузка <script> по адресу «.../axios.min.js.Без названия» не удалась
Загрузка <script> по адресу «.../app.js.Без названия» не удалась
Uncaught ReferenceError: openLoginModal is not defined
```
### ✅ Стало в v4.0.1:
```
(Консоль чистая - 0 критических ошибок)
```
---
## 📦 Структура ресурсов v4.0.1
### External CDN (не требуют локальных файлов):
```
https://cdn.tailwindcss.com
https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css
https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js
```
### Local (из архива):
```
/static/app.js (73KB, 2079 lines - оригинальный из aknaproff.zip)
```
---
## 🔧 Технические детали
### Изменённые файлы:
1. `public/original.html` - исправлены пути к ресурсам
2. `src/original-html.ts` - перегенерирован с новыми путями
### Команды для воспроизведения:
```bash
# Исправление путей в HTML
sed -i 's|./AKNAPROFF Tootmine_files/axios.min.js.Без названия|https://cdn.jsdelivr.net/npm/axios@1.6.0/dist/axios.min.js|g' public/original.html
sed -i 's|/static/app.js.Без названия|/static/app.js|g' public/original.html
sed -i 's|/static/all.min.css|https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css|g' public/original.html
# Пересоздание embedded HTML
python3 -c "
with open('public/original.html', 'r', encoding='utf-8') as f:
html = f.read()
html_escaped = html.replace('\\\\', '\\\\\\\\').replace('\`', '\\\\\`').replace('\${', '\\\\\${')
with open('src/original-html.ts', 'w', encoding='utf-8') as f:
f.write(f'export const ORIGINAL_HTML = \`{html_escaped}\`\\n')
"
# Пересборка
npm run build
pm2 restart webapp
```
---
## ✅ Итоговый статус
**AKNAPROFF Tootmine v4.0.1** - **Production Ready**
### Checklist:
- ✅ Frontend: 100% соответствие оригиналу из архива
- ✅ Ресурсы: Все загружаются через CDN
- ✅ Иконки: FontAwesome работают
- ✅ JavaScript: Axios + App.js (73KB) загружены
- ✅ Backend: 26 API endpoints работают
- ✅ Database: D1 с тестовыми данными
- ✅ Консоль: 0 критических ошибок
- ✅ Функции: Все названия из оригинала
- ✅ Кнопки: Оригинальные тексты
- ✅ Стили: Оригинальные из архива
---
## 🌐 Доступ к приложению
**URL:** https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
**Логин:**
- `admin` / `demo123`
- `aknaproff` / `demo123`
---
## 📝 Git история
```bash
811c741 - Update README to v4.0.1
ae0b05a - Fix resource paths: use CDN for FontAwesome and Axios (v4.0.1)
75637ae - Update README for v4.0.0 - Full archive restoration strategy
6d22b04 - FULL RESTORE: Use original HTML/CSS/JS from archive as base (v4.0.0)
```
---
**Все проблемы решены! Приложение готово к работе! 🎉**

View File

@@ -1,171 +0,0 @@
# ✅ AKNAPROFF Tootmine v4.0.5 - FIXED!
**Date**: 28.11.2025
**Status**: ✅ **Production Ready**
**Commit**: `a775738 - Fix: Set default month to January (1) to show demo data`
---
## 🐛 Problem Report
**User Issue**: "Сейчас вообще ни один клик не работает и в консоле нет ошибок"
**Root Cause**: Empty table because of incorrect default month filter
- `initFilters()` function was setting month filter to **current month** (November/December 2025)
- Demo data in database exists only for **January 2025** (month=1)
- Empty table meant **no clickable elements** were rendered
- No JavaScript errors because code was correct - table was just empty!
---
## 🔍 Debugging Process
### 1. Initial Checks
✅ Page loads: HTTP 200 OK
`app.js` loads: 73KB, 2079 lines
✅ DOMContentLoaded: Found
✅ Functions exist: `toggleDate`, `loadRecords`, `openModal`
✅ HTML structure: `<tbody id="recordsTable">` present
✅ API works: `/api/records?month=1&year=2025` returns 5 records
### 2. Problem Identification
❌ Table was rendering **empty** by default
`initFilters()` set month to `now.getMonth() + 1` = **11 or 12**
❌ No data for November/December → empty table → **no clicks work**
### 3. Solution
✅ Changed default month from `now.getMonth() + 1` to `1` (January)
✅ Table now loads with 5 demo records by default
✅ All click handlers now work correctly
---
## 🛠️ Changes Made
### File: `public/static/app.js`
**Before**:
```javascript
async function initFilters() {
const now = new Date();
document.getElementById('monthFilter').value = now.getMonth() + 1; // Current month!
// ...
}
```
**After**:
```javascript
async function initFilters() {
// Set to January (month 1) by default since that's where demo data exists
document.getElementById('monthFilter').value = 1;
// ...
}
```
---
## ✅ Testing Results
### 1. HTML Rendering
```bash
curl http://localhost:3000 | grep -o "toggleDate(" | wc -l
# Result: 16 click handlers found ✅
```
### 2. API Data
```bash
curl http://localhost:3000/api/records?month=1&year=2025 | jq 'length'
# Result: 5 records ✅
```
### 3. Browser Console
- ✅ Page loads in 7.92s
- ✅ No JavaScript errors
- ✅ No "undefined" function errors
- ✅ Table renders with data
### 4. Click Functionality
All click types now work:
- ✅ Cell clicks for date toggle
- ✅ Calendar icon clicks
- ✅ Edit/Delete buttons
- ✅ Filter/Search inputs
- ✅ Add record button
- ✅ Modal windows
---
## 📊 Current Application Status
### ✅ Frontend
- **100% from original archive** (app.js 73KB, all.min.css 100KB)
- **Original HTML structure preserved**
- **All original function names** (openModal, toggleDate, etc.)
- **Original Estonian text** (Lisa uus rida, etc.)
### ✅ Backend
- **26 API endpoints working**
- **D1 Database with migrations**
- **JWT Authentication**
- **All CRUD operations functional**
### ✅ Data
- **5 demo records** for January 2025
- **Default month filter**: January (1)
- **Default year filter**: 2025
- **All status fields working** (cutting_date, glazing_date, etc.)
---
## 🎯 Key Learnings
1. **Empty UI ≠ Broken code**: Code was correct, but no data to display
2. **Default filters matter**: Always set defaults to show demo data
3. **Console can be clean**: No errors doesn't mean everything works
4. **Test data location**: Check where demo data exists (January, not December!)
---
## 🚀 Access & Credentials
**Production URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
**Demo Accounts**:
- **Admin**: `admin` / `demo123`
- **User**: `aknaproff` / `demo123`
---
## 📝 Git History (Last 5 Commits)
```
a775738 - Fix: Set default month to January (1) to show demo data (v4.0.5)
0e320b1 - Update README to v4.0.4
39f5d2f - Fix status toggle: add _date suffix to field names (v4.0.4)
cea6ca4 - Update README to v4.0.3
51c5919 - Fix all API endpoints: add GET /api/records/:id, fix status/problems (v4.0.3)
```
---
## ✅ Final Status
**AKNAPROFF Tootmine v4.0.5 is now fully functional!** 🎉
All features working:
- ✅ Cell clicks for status updates
- ✅ Date toggle and calendar
- ✅ Add/Edit/Delete records
- ✅ Filters and search
- ✅ Notes and problems
- ✅ User authentication
- ✅ Admin permissions
**Browser Console**: 0 critical errors
**API Endpoints**: 26/26 working
**Database**: D1 SQLite with demo data
**Git History**: Clean with descriptive commits
---
**Remember**: Clear browser cache (Ctrl+Shift+R / Cmd+Shift+R) or use incognito mode to load the latest version!

View File

@@ -1,268 +0,0 @@
# ✅ AKNAPROFF Tootmine v4.0.6 - CLICKS WORKING!
**Date**: 28.11.2025
**Status**: ✅ **Production Ready**
**Commit**: `ec9214b - Fix: Allow public access (no login required) for all endpoints`
---
## 🐛 Problem Report
**User Issue**: "Не работает логика кликов" + **HTTP 401 Unauthorized** errors
**Console Errors**:
```
Toggle date error: Object { message: "Request failed with status code 401" }
PATCH /api/records/1/status [HTTP/2 401]
Toggle worksheets step error: Object { message: "Request failed with status code 401" }
PATCH /api/records/1/worksheets-cycle [HTTP/2 401]
```
**Root Cause**: Authentication mismatch between frontend and backend
- **Frontend**: Creates `Public User` when no JWT token exists (line 75 in app.js)
- **Backend**: Required JWT token (`authMiddleware`) for **ALL** PATCH/POST/PUT/DELETE endpoints
- **Result**: All clicks failed with **401 Unauthorized** because public user had no token
---
## 🔍 Analysis
### Frontend Behavior (Original Archive)
```javascript
// public/static/app.js:60-76
if (token) {
currentUser = JSON.parse(localStorage.getItem('user'));
// ... session validation
} else {
// Set default public user (no login required)
currentUser = { username: 'Public', full_name: 'Public User', role: 'user' };
}
```
**Design**: Application works **WITHOUT LOGIN** for basic operations.
### Backend Behavior (Before Fix)
```typescript
// All endpoints used authMiddleware - REQUIRES JWT token
app.patch('/api/records/:id/status', authMiddleware, async (c) => {
// Returns 401 if no Authorization header
})
```
**Problem**: Backend rejected all requests from public users.
---
## 🛠️ Solution
### 1. Replace `authMiddleware` → `optionalAuthMiddleware`
**Changed 13 endpoints** to allow public access:
**Record Management**:
- `POST /api/records` - Create new record
- `PUT /api/records/:id` - Update record
- `DELETE /api/records/:id` - Soft delete record
**Status Updates** (9 endpoints):
- `PATCH /api/records/:id/status` - Toggle date fields
- `PATCH /api/status/:recordId/:field` - Update specific status
- `PATCH /api/status/:recordId/:field/error` - Toggle error flags
- `PATCH /api/status/:recordId/:field/confirm` - Confirm status
- `PATCH /api/records/:id/worksheets-cycle` - Cycle worksheets status
- `PATCH /api/records/:id/notes` - Update notes
- `PATCH /api/records/:id/problems` - Update problems
- `PATCH /api/records/:id/material-confirmed` - Confirm material
- `PATCH /api/records/:id/material2-confirmed` - Confirm material 2
- `PATCH /api/records/:id/price-paid` - Update payment status
### 2. Fix `userId` Handling for Public Users
**Problem**: `userId` is `undefined` for public users, causing SQL errors:
```
Error: Type 'undefined' not supported for value 'undefined'
```
**Solution**: Allow `NULL` values in audit_log and deleted_by:
**Before**:
```typescript
.bind(userId, recordId, field, oldValue, newValue) // ❌ userId = undefined
```
**After**:
```typescript
.bind(userId || null, recordId, field, oldValue, newValue) // ✅ userId = null
```
**Fixed 6 locations**:
- 5× audit_log INSERT statements
- 1× deleted_by in soft delete
---
## ✅ Testing Results
### 1. Status Toggle (Public User)
```bash
curl -X PATCH http://localhost:3000/api/records/1/status \
-H "Content-Type: application/json" \
-d '{"field":"cutting","date":"2025-03-26"}'
# Response: {"success": true} ✅
```
### 2. Worksheets Cycle (Public User)
```bash
curl -X PATCH http://localhost:3000/api/records/1/worksheets-cycle
# Response: {"success": true, "date": null, "confirmed": 0} ✅
```
### 3. Create Record (Public User)
```bash
curl -X POST http://localhost:3000/api/records \
-d '{"month":1,"year":2025,"client_name":"Test","quantity":5}'
# Response: {"success": true, "id": 13} ✅
```
### 4. Browser Console
```
Page load time: 7.54s
JavaScript Errors: 0 (only AdBlock warnings)
Page title: AKNAPROFF Tootmine ✅
```
---
## 📊 How `optionalAuthMiddleware` Works
```typescript
// src/middleware/auth.ts:52-83
export async function optionalAuthMiddleware(c, next) {
const authHeader = c.req.header('Authorization')
if (authHeader && authHeader.startsWith('Bearer ')) {
// Has token → verify and set user context
const payload = verifyToken(token)
if (payload) {
c.set('userId', payload.userId)
c.set('username', payload.username)
c.set('role', user.role)
}
} else {
// No token → set public user
c.set('username', 'Public')
// userId remains undefined → converted to null in SQL
}
await next() // ✅ Always continues (never returns 401)
}
```
**Key Difference**:
- `authMiddleware`: Returns **401** if no token
- `optionalAuthMiddleware`: Sets `username='Public'` and **continues**
---
## 🔒 Security Considerations
### What Still Requires Login?
- `PATCH /api/users/profile` - User settings (still uses `authMiddleware`)
- Admin-only features (delete buttons, price fields) - controlled by frontend role
### What Works Without Login?
- ✅ View records
- ✅ Add records
- ✅ Edit records (all fields)
- ✅ Update status dates
- ✅ Toggle error flags
- ✅ Add notes/problems
- ✅ Soft delete records
**Design Philosophy**: This is a **demo/development application** where ease of use matters more than strict authentication. For production, you may want to re-enable `authMiddleware` for sensitive operations.
---
## 📊 Current Application Status
### ✅ Frontend
- **100% from original archive**
- **No modifications** to app.js logic
- **Public User mode works** as designed
### ✅ Backend
- **13 endpoints** converted to optional auth
- **26 API endpoints** total (all working)
- **D1 Database** with migrations
- **Audit log** supports NULL user_id
### ✅ Data
- **5 demo records** for January 2025
- **Default month**: January (1)
- **All fields** working correctly
---
## 🎯 Key Learnings
1. **Frontend-Backend alignment**: Always match authentication requirements
2. **Optional auth pattern**: Use `optionalAuthMiddleware` for public-facing apps
3. **NULL handling**: Use `|| null` for optional foreign keys in SQL
4. **401 vs logic errors**: 401 means auth problem, not business logic
---
## 🚀 Access & Testing
**Production URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
**How to Test**:
1. Open URL in browser
2. **No login needed** - you're automatically "Public User"
3. Click any cell to toggle dates → **Works!**
4. Click worksheets cycle button → **Works!**
5. Click "Lisa uus rida" to add record → **Works!**
**Optional Login**:
- Click top-right icon to login as admin: `admin` / `demo123`
- Unlocks: Delete buttons, price fields, user settings
---
## 📝 Git History (Last 5 Commits)
```
ec9214b - Fix: Allow public access (no login required) for all endpoints (v4.0.6)
64946ab - Add comprehensive fix report for v4.0.5
a775738 - Fix: Set default month to January (1) to show demo data (v4.0.5)
0e320b1 - Update README to v4.0.4
39f5d2f - Fix status toggle: add _date suffix to field names (v4.0.4)
```
---
## ✅ Final Status
**AKNAPROFF Tootmine v4.0.6 - ALL CLICKS WORKING!** 🎉
**Fixed Issues**:
- ✅ HTTP 401 errors → Now returns 200 OK
- ✅ Status toggle clicks work
- ✅ Worksheets cycle clicks work
- ✅ Add/Edit/Delete records work
- ✅ Notes and problems work
- ✅ All dates update correctly
**Technical Status**:
- **Browser Console**: 0 JavaScript errors
- **API Endpoints**: 26/26 working (13 with optional auth)
- **Database**: D1 SQLite with audit_log supporting NULL user_id
- **Authentication**: Optional (public access enabled)
---
**Remember**: Clear browser cache (Ctrl+Shift+R / Cmd+Shift+R) or use incognito mode to load the latest version!

View File

@@ -1,203 +0,0 @@
# 🔧 AKNAPROFF Tootmine v3.20.7 - Исправление критических ошибок
**Дата:** 28.11.2025
**Статус:****Все проблемы решены - Production Ready**
---
## 🎯 Решённые проблемы
### 1. ❌ Ошибка: `TypeError: can't access property "forEach", years is undefined`
**Причина:**
После перезапуска PM2 и сборки проекта, `wrangler pages dev` терял привязку к D1 базе данных. Это приводило к тому, что API `/api/years` не мог выполнить запрос к БД и возвращал ошибку вместо массива годов.
**Решение:**
```bash
# 1. Полная очистка кеша wrangler
rm -rf .wrangler
# 2. Пересоздание локальной базы данных
npm run db:migrate:local # Применение миграций
npm run db:seed # Загрузка тестовых данных
# 3. Пересборка и перезапуск
npm run build
pm2 restart webapp
```
**Результат:**
✅ API `/api/years` теперь возвращает: `{"years":[2024,2025,2026]}`
✅ Фронтенд корректно загружает фильтры по годам
✅ Ошибка `years is undefined` устранена
---
### 2. ❌ Ошибка: `can't access property "addEventListener", document.getElementById(...) is null`
**Причина:**
В HTML отсутствовали критические элементы модальных окон:
- `settingsForm`
- `reportStep0`, `reportStep1`, `reportStep2`, `reportStep3`
- `reportTableBody`
- `settingsError`, `settingsSuccess`
**Решение:**
Полная замена всех модальных окон оригинальными из архива `aknaproff.zip` (465 строк HTML).
**Результат:**
Все 7 модальных окон полностью функциональны
Все `getElementById()` находят свои элементы
✅ События `addEventListener()` работают корректно
---
### 3. ❌ Ошибка: `can't access property "innerHTML", tfoot is null`
**Причина:**
В таблице `<table id="recordsTable">` отсутствовал элемент `<tfoot id="recordsTableFooter">` для вывода итоговых сумм.
**Решение:**
Добавлен `<tfoot>` с полями:
```html
<tfoot id="recordsTableFooter" class="sticky bottom-0 bg-white border-t-2 border-gray-300">
<tr>
<td colspan="7" class="px-6 py-3 text-right font-bold text-gray-700">Summa:</td>
<td id="totalQuantity" class="px-6 py-3 font-bold text-gray-900">0</td>
<td id="totalPrice" class="px-6 py-3 font-bold text-gray-900">0.00</td>
<td colspan="7"></td>
</tr>
</tfoot>
```
**Результат:**
✅ Итоговые суммы отображаются корректно
✅ Ошибка `tfoot is null` устранена
---
### 4. ❌ 404 ошибки для API эндпоинтов
**Причина:**
Отсутствовали бэкенд роуты для фронтенд API вызовов:
- `PATCH /api/records/:id/worksheets-cycle`
- `PATCH /api/records/:id/status`
- `PATCH /api/records/:id/notes`
- `PATCH /api/records/:id/problems`
**Решение:**
Добавлено 7 новых API эндпоинтов на бэкенде (Hono).
**Результат:**
Все API вызовы возвращают 200 OK
✅ Функции toggle, сохранение заметок, обновление статусов работают
---
## 📊 Статистика изменений
### До исправлений (v3.20.6):
- ❌ 10+ критических ошибок в консоли браузера
- ❌ API `/api/years` не возвращал данные
- ❌ Таблица не загружалась из-за отсутствия `tfoot`
- ❌ События и клики в таблице не работали
- ❌ Модальные окна не открывались
### После исправлений (v3.20.7):
-**0 ошибок** в консоли браузера
-Все API эндпоинты работают корректно
- ✅ Таблица загружается и отображает данные
-Все события и клики обрабатываются
-Все 7 модальных окон полностью функциональны
---
## 🔍 Тестирование
### API эндпоинты (все ✅):
```bash
curl http://localhost:3000/api/years
# {"years":[2024,2025,2026]}
curl http://localhost:3000/api/records?month=1&year=2025
# [{"id":1,"client_name":"AS Okna Service",...},...]
```
### HTML элементы (все ✅):
- `recordsTable` - основная таблица
- `recordsTableFooter` - итоговая строка с суммами
- `settingsForm` - форма настроек
- `reportModal` - модальное окно отчётов
- `recordModal` - модальное окно записей
- `notesModal` - модальное окно заметок
- `problemsModal` - модальное окно проблем
- `blockedFieldModal` - модальное окно блокировки
- `loginModal` - модальное окно входа
---
## 🚀 Доступ к приложению
**URL:** https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
**Демо-аккаунты:**
- Admin: `admin` / `demo123`
- User: `aknaproff` / `demo123`
---
## 📦 Что было сделано
1. ✅ Полная очистка кеша `.wrangler`
2. ✅ Пересоздание D1 базы данных с миграциями
3. ✅ Загрузка тестовых данных (7 записей)
4. ✅ Замена всех модальных окон на оригинальные из архива
5. ✅ Добавление `<tfoot>` для итоговых сумм
6. ✅ Создание 7 недостающих API эндпоинтов
7. ✅ Полное тестирование всех функций
8. ✅ Git commit с описанием изменений
---
## 📝 Git история
```bash
f45b5a3 - Fix D1 database binding and API /api/years endpoint (v3.20.7)
[Previous commits...]
```
---
## ⚠️ Важные замечания
### Кеширование браузера
После обновления **необходимо очистить кеш браузера**:
- Chrome/Firefox: `Ctrl + Shift + R` (жёсткая перезагрузка)
- Или откройте в режиме инкогнито
### Перезапуск после изменений
При любых изменениях в коде **всегда выполняйте полный цикл**:
```bash
cd /home/user/webapp
rm -rf .wrangler
npm run db:migrate:local
npm run db:seed
npm run build
pm2 restart webapp
```
---
## ✅ Итоговый статус
**AKNAPROFF Tootmine v3.20.7** - **Production Ready**
-Все критические ошибки исправлены
- ✅ База данных работает корректно
-Все API эндпоинты функционируют
-Все модальные окна полностью функциональны
- ✅ Таблица загружается и отображает данные
- ✅ События и клики обрабатываются корректно
- ✅ Консоль браузера чистая (0 ошибок)
**Приложение готово к использованию! 🎉**

View File

@@ -1,705 +0,0 @@
# AKNAPROFF Tootmine - Полная история разработки и исправлений
**Проект:** AKNAPROFF Tootmine (Система управления производством окон)
**Период:** 28.11.2025
**Начальная версия:** v4.0.4
**Финальная версия:** v4.0.13
---
## 📋 Оглавление
1. [v4.0.5 - Исправление default month filter](#v405)
2. [v4.0.6 - Удаление HTTP 401 ошибок](#v406)
3. [v4.0.7 - Добавление cache busting](#v407)
4. [v4.0.8 - Удаление frontend role checks](#v408)
5. [v4.0.9 - Исправление MAT-1/MAT-2 checkbox toggle](#v409)
6. [v4.0.10 - Попытка исправления date picker через .click()](#v4010)
7. [v4.0.11 - Попытка через label for с pointer-events:none](#v4011)
8. [v4.0.12 - Исправление pointer-events:none](#v4012)
9. [v4.0.13 - Calendar picker для всех пользователей](#v4013)
---
## <a name="v405"></a>v4.0.5 - Исправление default month filter
### 🎯 Запрос пользователя
**"Не работает в MAT-1 MAT-2 при выборке дата не сохраняется и не реагирует на чекбокс"**
### 🔍 Проблема
- Клики вообще не работали нигде в таблице
- Таблица была пустая
- Пользователь видел пустой экран без данных
### 🐛 Найденная причина
```javascript
// ❌ public/static/app.js строка 281
document.getElementById('monthFilter').value = now.getMonth() + 1;
```
**Проблема:**
- `now.getMonth()` возвращает 10 (ноябрь) или 11 (декабрь)
- Фильтр устанавливался на текущий месяц
- База данных содержит только demo данные для **января 2025** (month=1)
- Результат: пустая таблица → клики не работают
### ✅ Решение
```javascript
// ✅ Исправлено
document.getElementById('monthFilter').value = 1; // January
```
### 📝 Результат
- Таблица загружает 5 demo records из января 2025
- Все клики начинают работать
- 16 обработчиков toggleDate() активны
### 🔗 Commit
```
git commit -m "Set default month to January (1) to show demo data"
```
---
## <a name="v406"></a>v4.0.6 - Удаление HTTP 401 ошибок
### 🎯 Запрос пользователя
**"Не работает логика кликов"**
### 🔍 Проблема
- HTTP 401 Unauthorized при клике на даты
- Ошибки при обновлении статусов
- Frontend создавал Public User без токена
- Backend требовал JWT токен для всех PATCH/POST/PUT/DELETE
### 🐛 Найденные ошибки
**1. Authentication mismatch:**
```javascript
// Frontend (app.js):
const currentUser = {
username: 'Public',
full_name: 'Public User',
role: 'user'
}; // Нет токена
// Backend (src/index.tsx):
app.patch('/api/records/:id/status', authMiddleware, async (c) => {
// authMiddleware требует JWT токен!
});
```
**2. userId undefined в audit log:**
```javascript
// ❌ Ошибка
.bind(userId, ...) // userId = undefined для Public User
// Error: Type 'undefined' not supported for value 'undefined'
```
### ✅ Решение
**1. Заменили authMiddleware на optionalAuthMiddleware:**
```typescript
// src/index.tsx - 13 endpoints
app.patch('/api/records/:id/status', optionalAuthMiddleware, async (c) => {
// Работает с токеном И без него
});
```
**2. Исправили userId handling:**
```typescript
// До:
.bind(userId, recordId, ...)
// После:
.bind(userId || null, recordId, ...)
```
### 📝 Изменённые endpoints (13 шт)
```
POST /api/records
PUT /api/records/:id
DELETE /api/records/:id
PATCH /api/records/:id/status
PATCH /api/status/:recordId/:field
PATCH /api/status/:recordId/:field/error
PATCH /api/status/:recordId/:field/confirm
PATCH /api/records/:id/worksheets-cycle
PATCH /api/records/:id/notes
PATCH /api/records/:id/problems
PATCH /api/records/:id/material-confirmed
PATCH /api/records/:id/material2-confirmed
PATCH /api/records/:id/price-paid
```
### 📊 Тестирование
```bash
# Test 1: Status toggle без токена
$ curl -X PATCH http://localhost:3000/api/records/1/status \
-H "Content-Type: application/json" \
-d '{"field":"cutting","date":"2025-03-26"}'
{"success":true}
# Test 2: Worksheets cycle без токена
$ curl -X PATCH http://localhost:3000/api/records/1/worksheets-cycle
{"success":true,"date":null,"confirmed":0}
# Test 3: POST record без токена
$ curl -X POST http://localhost:3000/api/records \
-H "Content-Type: application/json" \
-d '{...}'
{"success":true,"id":13}
```
### 🔗 Commit
```
git commit -m "Allow public access (no login required) for all endpoints (v4.0.6)"
```
---
## <a name="v407"></a>v4.0.7 - Добавление cache busting
### 🎯 Запрос пользователя
**"Вообще пропала реакция на клики или события"**
### 🔍 Проблема
- Пользователь видел старую кешированную версию app.js
- Браузер не загружал новый код
- Клики не работали из-за старого JavaScript
### ✅ Решение
```html
<!-- До: -->
<script src="/static/app.js"></script>
<!-- После: -->
<script src="/static/app.js?v=4.0.7"></script>
```
### 📝 Результат
- Браузер загружает свежую версию app.js
- Кеш не используется при изменении версии
- Пользователь видит актуальный код
### 🔗 Commit
```
git commit -m "Add cache busting version parameter to app.js (v4.0.7)"
```
---
## <a name="v408"></a>v4.0.8 - Удаление frontend role checks
### 🎯 Обнаруженная проблема
Backend публичный (v4.0.6), но frontend всё ещё блокирует Public User
### 🐛 Найденные блокировки
**1. CSS скрывает кнопки:**
```css
/* public/original.html */
.admin-only-block { display: none; }
body.role-admin .admin-only-block { display: block; }
```
**2. JavaScript role checks:**
```javascript
// public/static/app.js
// openModal()
if (currentUser.role !== 'admin') {
alert('Uute ridade lisamine on lubatud ainult administraatoritele');
document.getElementById('loginModal').classList.add('active');
return;
}
// editRecord()
if (currentUser.role !== 'admin') {
alert('Kirjete muutmine on lubatud ainult administraatoritele');
return;
}
// toggleDeleteButtons()
function toggleDeleteButtons() {
const allowDelete = currentUser?.role === 'admin';
// Скрывает кнопки Delete
}
```
### ✅ Решение
**1. Убрали CSS hiding:**
```html
<!-- До: -->
<div class="admin-only-block">
<button onclick="openModal()">Lisa uus rida</button>
</div>
<!-- После: -->
<button onclick="openModal()">Lisa uus rida</button>
```
**2. Удалили role checks в JavaScript:**
```javascript
// До:
if (currentUser.role !== 'admin') {
alert('...только для admin');
return;
}
// После:
// Никаких проверок - все пользователи могут редактировать
```
### 📝 Результат
- ✅ Кнопка "Lisa uus rida" видна всем
- ✅ openModal() работает для Public User
- ✅ editRecord() работает для всех
- ✅ Кнопки Delete видны всем
- ✅ Backend и frontend оба публичные
### 🔗 Commit
```
git commit -m "Remove all frontend authentication blocks for Public User (v4.0.8)"
```
---
## <a name="v409"></a>v4.0.9 - Исправление MAT-1/MAT-2 checkbox toggle
### 🎯 Запрос пользователя
**"Не работает в MAT-1 MAT-2 при выборке дата не сохраняется и не реагирует на чекбокс"**
### 🔍 Проблема
Checkbox для MAT-1/MAT-2 не переключался (не было toggle)
### 🐛 Анализ backend
**Backend endpoints уже были правильные (v4.0.6):**
```typescript
// src/index.tsx строка 542
app.patch('/api/records/:id/material-confirmed', optionalAuthMiddleware, async (c) => {
// Get current value
const current = await c.env.DB.prepare(
'SELECT material_confirmed FROM status_checkboxes WHERE record_id = ?'
).bind(recordId).first()
// Toggle value
const newValue = current?.material_confirmed === 1 ? 0 : 1
await c.env.DB.prepare(
'UPDATE status_checkboxes SET material_confirmed = ? WHERE record_id = ?'
).bind(newValue, recordId).run()
return c.json({ success: true })
})
```
**Frontend был правильный:**
```javascript
// public/static/app.js
async function toggleMaterialConfirmed(recordId) {
await axios.patch(`${API_BASE}/api/records/${recordId}/material-confirmed`, {});
await loadRecords();
}
```
### ✅ Добавлено логирование
**Для отладки:**
```typescript
console.log('[MAT1] Toggle request for record:', recordId)
console.log('[MAT1] Current value:', current?.material_confirmed)
console.log('[MAT1] New value:', newValue)
return c.json({ success: true, newValue })
```
### 📊 Тестирование
```bash
# Test 1: Toggle 0 -> 1
$ curl -X PATCH http://localhost:3000/api/records/1/material-confirmed
{"success":true,"newValue":1}
$ npx wrangler d1 execute webapp-production --local \
--command="SELECT material_confirmed FROM status_checkboxes WHERE record_id = 1"
material_confirmed = 1
# Test 2: Toggle 1 -> 0
$ curl -X PATCH http://localhost:3000/api/records/1/material-confirmed
{"success":true,"newValue":0}
$ npx wrangler d1 execute webapp-production --local \
--command="SELECT material_confirmed FROM status_checkboxes WHERE record_id = 1"
material_confirmed = 0
```
### 📝 Результат
- ✅ Toggle работает: 0 ↔ 1
- ✅ API возвращает newValue
- ✅ База данных обновляется корректно
### 🔗 Commit
```
git commit -m "Fix MAT-1/MAT-2 checkbox toggle endpoints (v4.0.9)"
```
---
## <a name="v4010"></a>v4.0.10 - Попытка исправления date picker через .click()
### 🎯 Запрос пользователя
**"MAT-1 MAT-2 не работает выбор даты при нажатии на дату хотя очистить кнопка работает"**
### 🔍 Проблема
- Клик на дату в MAT-1/MAT-2 не открывал date picker
- Кнопка "очистить" работала
- Checkbox toggle работал (v4.0.9)
### 🐛 Найденная причина
```javascript
// public/static/app.js
onclick="document.getElementById('${fieldId}').showPicker()"
```
**Проблемы с showPicker():**
- Не работает во всех браузерах
- Требует прямого user gesture
- Может блокироваться security policies
- Не работает через onclick в некоторых контекстах
### ✅ Попытка решения
```javascript
// До:
onclick="document.getElementById('${fieldId}').showPicker()"
// После:
onclick="document.getElementById('${fieldId}').click()"
// И изменили CSS:
class="absolute opacity-0 pointer-events-none"
// на:
class="absolute opacity-0 w-0 h-0"
```
### 📝 Результат
**НЕ СРАБОТАЛО** - calendar picker всё равно не открывался
### 🔗 Commit
```
git commit -m "Fix date picker click for MAT-1/MAT-2 (v4.0.10)"
```
---
## <a name="v4011"></a>v4.0.11 - Попытка через <label for> с pointer-events:none
### 🎯 Запрос пользователя
**"событие не происходит а в консоле накапливаеться счетчик"**
### 🔍 Проблема
- События клика не происходили
- В консоли накапливались ошибки
- Ошибка: "Игнорируем неподдерживаемые entryTypes: longtask"
### 🐛 Причина v4.0.10
```javascript
onclick="document.getElementById('${fieldId}').click()"
```
**Почему не работало:**
- Программный click() блокируется браузерами
- Скрытый input с pointer-events-none не получал события
- Console errors накапливались
### ✅ Новый подход - <label for>
```html
<!-- До: -->
<div onclick="document.getElementById('${fieldId}').click()">
<!-- После: -->
<input type="date" id="${fieldId}"
style="position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;" />
<label for="${fieldId}" class="cursor-pointer">
${formattedDate}
</label>
```
**Преимущества:**
- Нативное HTML5 поведение
- Не требует JavaScript
- Работает в всех браузерах
- Более семантично
- Более доступно для screen readers
### 📝 Результат
**НЕ СРАБОТАЛО** - calendar picker всё равно не открывался
**Причина (обнаружена позже):** `pointer-events: none` блокирует даже label!
### 🔗 Commit
```
git commit -m "Fix date picker using <label for=id> approach (v4.0.11)"
```
---
## <a name="v4012"></a>v4.0.12 - Исправление pointer-events:none
### 🎯 Запрос пользователя
**"Не работет не вслаывает календарь при клике нигде ни на одной строке или колонке таблицы"**
### 🔍 Критическая проблема
Calendar ВООБЩЕ не открывается нигде в таблице!
### 🐛 Найдена КРИТИЧЕСКАЯ ОШИБКА
```css
/* v4.0.11 - НЕ РАБОТАЛО: */
style="position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;"
```
**Почему pointer-events: none блокирует label:**
В HTML5, `<label for="id">` активирует связанный input через **событийную модель браузера**.
Но `pointer-events: none` говорит браузеру: **"этот элемент НЕ СУЩЕСТВУЕТ для событий"**.
Даже программная активация через `<label>` не работает, потому что input **полностью исключён из событийной модели**.
### ✅ Решение - Убрали pointer-events: none
```css
/* До: */
position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;
/* После: */
position: absolute; left: -9999px; opacity: 0;
```
**Почему это работает:**
1.`left: -9999px` — перемещает input за пределы экрана (но он активный)
2.`opacity: 0` — делает прозрачным (дополнительная защита)
3.**НЕТ** `pointer-events: none` — input остаётся интерактивным
4.`<label for>` теперь может активировать input
### 📝 Результат
**ДОЛЖНО РАБОТАТЬ** - input скрыт визуально, но активен для событий
### 🔗 Commit
```
git commit -m "Fix date picker: remove pointer-events:none (v4.0.12)"
```
---
## <a name="v4013"></a>v4.0.13 - Calendar picker для всех пользователей (ФИНАЛЬНОЕ ИСПРАВЛЕНИЕ)
### 🎯 Уточнение от пользователя
**"Поля MAT-1 MAT-2 при нажатии должны вызывать календарь в таблицу а поля Töölehti LÕIKUS KLAAS VALMIS VÄLJAS имеют 3 статуса по кругу серая чисто рамочка и дата зеленый и красный при ошибке и отсутсвие даты"**
### 🔍 Правильная логика полей
**Есть ДВА ТИПА полей:**
#### 1. Calendar Picker (MAT-1, MAT-2, PAKETT)
- Клик → Открывается календарь
- Пользователь выбирает дату из calendar picker
- Используют `renderCalendarCell()` с `<label for="input">`
#### 2. Toggle 3-step (Töölehti, LÕIKUS, KLAAS, VALMIS, VÄLJAS)
- Клик → Циклическое переключение
- 3 состояния:
1. **Пусто**: Серая рамка, белый фон `-`
2. **Дата**: Зелёный фон `DD.MM.YYYY`
3. **Ошибка**: Красный фон `DD.MM.YYYY`
- Используют `renderDateCell()` с `onclick="toggleDate()"`
### 🐛 Найденная проблема
```javascript
// public/static/app.js строка 523
const isAdmin = currentUser?.role === 'admin';
// Строки 542-544
${isAdmin ? renderCalendarCell(...) : renderReadOnlyCell(...)}
${isAdmin ? renderCalendarCell(...) : renderReadOnlyCell(...)}
${isAdmin ? renderCalendarCell(...) : renderReadOnlyCell(...)}
```
**Результат:**
- ❌ Admin видел calendar picker
- ❌ Public User видел **read-only** (только просмотр)
-Все предыдущие исправления (v4.0.10-4.0.12) не помогали, потому что Public User вообще не видел calendar picker!
### ✅ Решение
```javascript
// До:
${isAdmin ? renderCalendarCell(...) : renderReadOnlyCell(...)}
// После:
${renderCalendarCell(...)}
```
Убрали проверку `isAdmin` — теперь **ВСЕ пользователи** видят calendar picker!
### 📋 Правильное поведение (финальное)
| Поле | Тип | Клик | Результат |
|------|-----|------|-----------|
| **MAT-1** | Calendar | Клик | Открывается календарь 📅 |
| **MAT-2** | Calendar | Клик | Открывается календарь 📅 |
| **PAKETT** | Calendar | Клик | Открывается календарь 📅 |
| **Töölehti** | Toggle | Клик | 3-step: пусто → подтверждено → дата |
| **LÕIKUS** | Toggle | Клик | 3-step: пусто/белый → дата/зелёный → ошибка/красный |
| **KLAAS** | Toggle | Клик | 3-step: пусто/белый → дата/зелёный → ошибка/красный |
| **VALMIS** | Toggle | Клик | 3-step: пусто/белый → дата/зелёный → ошибка/красный |
| **VÄLJAS** | Toggle | Клик | 3-step: пусто/белый → дата/зелёный → ошибка/красный |
### 📝 Результат
**РАБОТАЕТ!** Все поля ведут себя правильно для всех пользователей
### 🔗 Commit
```
git commit -m "Fix: MAT-1/MAT-2 calendar picker for all users (v4.0.13)"
```
---
## 📊 Сводная таблица версий
| Версия | Проблема | Решение | Статус |
|--------|----------|---------|--------|
| v4.0.5 | Пустая таблица | Default month = 1 | ✅ |
| v4.0.6 | HTTP 401 | optionalAuthMiddleware | ✅ |
| v4.0.7 | Кеширование | Cache busting | ✅ |
| v4.0.8 | Frontend blocks | Убрали role checks | ✅ |
| v4.0.9 | Checkbox toggle | Логирование + newValue | ✅ |
| v4.0.10 | Date picker | .click() вместо .showPicker() | ❌ |
| v4.0.11 | События | <label for> с pointer-events | ❌ |
| v4.0.12 | label блокировка | Убрали pointer-events:none | ⚠️ |
| v4.0.13 | isAdmin check | Calendar для всех | ✅ |
---
## 🎯 Финальное состояние проекта
### Архитектура
- **Backend**: Hono + Cloudflare Workers + D1 Database
- **Frontend**: Vanilla JavaScript + Tailwind CSS (CDN)
- **Аутентификация**: Опциональная (Public User + Admin)
- **База данных**: Cloudflare D1 (local SQLite для dev)
### Функциональность
- ✅ Управление production records (CRUD)
- ✅ Calendar picker для MAT-1, MAT-2, PAKETT
- ✅ 3-step toggle для других date полей
- ✅ Checkbox confirmation для MAT-1, MAT-2
- ✅ 3-step worksheets cycle
- ✅ Audit logging для всех изменений
- ✅ Блокировка полей при наличии ошибок
- ✅ Public access без логина
- ✅ Admin features (опционально)
### API Endpoints (26 шт)
```
GET /api/years
GET /api/records
GET /api/records/:id
POST /api/records
PUT /api/records/:id
DELETE /api/records/:id
PATCH /api/records/:id/status
PATCH /api/records/:id/worksheets-cycle
PATCH /api/records/:id/notes
PATCH /api/records/:id/problems
PATCH /api/records/:id/material-confirmed
PATCH /api/records/:id/material2-confirmed
PATCH /api/records/:id/price-paid
PATCH /api/status/:recordId/:field
PATCH /api/status/:recordId/:field/error
PATCH /api/status/:recordId/:field/confirm
POST /api/auth/login
PATCH /api/users/profile
```
### Deployment
- **Development**: PM2 + wrangler pages dev (localhost:3000)
- **Production**: Cloudflare Pages
- **Database**: D1 (local для dev, remote для prod)
- **Static files**: public/static/ → /static/*
### URLs
- **Development**: http://localhost:3000
- **Sandbox**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Production**: https://webapp.pages.dev (при деплое)
---
## 📝 Ключевые уроки
### 1. Frontend-Backend синхронизация
**Проблема:** Backend был публичным (v4.0.6), но frontend блокировал Public User (до v4.0.8)
**Урок:** Всегда проверяйте согласованность frontend и backend политик доступа
### 2. Calendar picker и pointer-events
**Проблема:** `pointer-events: none` блокирует даже `<label for>`
**Урок:** Для скрытия input используйте `left: -9999px`, а НЕ `pointer-events: none`
### 3. Admin checks в нескольких местах
**Проблема:** isAdmin проверялся и в CSS, и в JavaScript, и в renderRecords
**Урок:** Централизуйте логику проверки прав доступа
### 4. Две разные логики для date fields
**Проблема:** Не сразу поняли, что есть два типа полей (calendar vs toggle)
**Урок:** Внимательно изучайте требования и оригинальный архив
---
## 🚀 Инструкции по тестированию
### Тест 1: Calendar Picker (MAT-1, MAT-2)
1. Откройте https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
2. Очистите кеш (Ctrl+Shift+R)
3. Кликните на MAT-1 (материал) → должен открыться календарь
4. Выберите дату → дата должна сохраниться
5. Кликните на checkbox справа → должен стать зелёным
6. Кликните ещё раз → должен стать серым
### Тест 2: Toggle (LÕIKUS, KLAAS, VALMIS, VÄLJAS)
1. Кликните на пустую ячейку LÕIKUS `-`
2. Должна появиться сегодняшняя дата с зелёным фоном
3. Кликните ещё раз → дата очистится (белый фон `-`)
4. Проверьте красный фон при ошибке
### Тест 3: Worksheets 3-step cycle
1. Кликните на пустую ячейку Töölehti `-`
2. Должна появиться галочка (подтверждено, без даты)
3. Кликните ещё раз → появится дата
4. Кликните ещё раз → очистится (пусто)
### Тест 4: CRUD операции
1. Кликните "Lisa uus rida" → модальное окно
2. Заполните данные → "Salvesta"
3. Кликните "Edit" на любой строке → модальное окно
4. Кликните "Delete" → строка удаляется (soft delete)
---
## 📞 Контакты и поддержка
**Production URL:** https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
**Git Repository:** /home/user/webapp
**Версия:** v4.0.13 (28.11.2025)
**Demo Accounts:**
- Admin: `admin` / `demo123`
- User: `aknaproff` / `demo123`
- Public: Без логина
---
**Конец документа**

View File

@@ -1,160 +0,0 @@
# 🔥 HOTFIX v4.1.1 - Guest Mode Function Fix
**Дата**: 2025-11-28
**Версия**: v4.1.1
**Статус**: ✅ Production Ready
---
## 🐛 Проблема
**Ошибка в консоли**:
```
Uncaught ReferenceError: continueAsGuest is not defined
onclick https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.gensparksite.com/:1
```
**Симптомы**:
- Кнопка "Vaata ainult" (Только просмотр) не работала
- При клике появлялась ошибка в консоли
- Форма логина не закрывалась
- Guest режим не активировался
---
## 🔍 Анализ
**Причина**: Функция `continueAsGuest` была определена как обычная функция внутри блока, но не была доступна глобально для HTML onclick обработчика.
**Код до исправления**:
```javascript
function continueAsGuest() {
currentUser = { username: 'Guest', full_name: 'Guest User', role: 'guest' };
closeLoginModal();
showMainApp();
loadRecords();
}
```
**HTML**:
```html
<button type="button" onclick="continueAsGuest()">
<i class="fas fa-eye mr-2"></i>Vaata ainult
</button>
```
---
## ✅ Решение
**Изменение**: Сделали функцию глобальной через `window.continueAsGuest`
**Код после исправления**:
```javascript
// Make continueAsGuest globally accessible for onclick
window.continueAsGuest = function() {
// User chose to continue as guest (read-only mode)
currentUser = { username: 'Guest', full_name: 'Guest User', role: 'guest' };
closeLoginModal();
showMainApp();
loadRecords();
}
```
**Cache-busting**: Обновлена версия app.js с `4.1.0``4.1.1`
---
## 📝 Изменённые файлы
1. **public/static/app.js**
- Изменена функция `continueAsGuest``window.continueAsGuest`
- Добавлен комментарий о глобальной доступности
2. **public/original.html**
- Обновлена версия: `app.js?v=4.1.0``app.js?v=4.1.1`
3. **src/original-html.ts**
- Регенерирован embedded HTML с новой версией
4. **dist/_worker.js**
- Пересобран с исправлениями
---
## 🧪 Тестирование
**Шаги**:
1. Открыть страницу → появляется форма логина
2. Нажать "Vaata ainult" → форма закрывается, таблица показывается
3. Проверить консоль браузера → ошибок нет
4. Попробовать изменить данные → появляется alert (read-only режим)
**Результаты**:
- ✅ Кнопка "Vaata ainult" работает
- ✅ Форма логина закрывается
- ✅ Guest режим активируется
- ✅ Таблица загружается
- ✅ Консоль чистая (нет ошибок)
---
## 📦 Deployment
**Для обновления на production сервере**:
```bash
# Скопировать файлы
scp public/static/app.js user@server:/path/to/webapp/public/static/
scp public/original.html user@server:/path/to/webapp/public/
scp src/original-html.ts user@server:/path/to/webapp/src/
# На сервере пересобрать
cd /path/to/webapp
npm run build
docker-compose restart
```
**Альтернативный вариант (только dist)**:
```bash
# Скопировать только собранный файл
scp dist/_worker.js user@server:/path/to/webapp/dist/
docker-compose restart
```
---
## 🔗 URLs
- **Production**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Git Commit**: Будет добавлен после коммита
---
## 📊 Итоговое состояние
**Версия**: v4.1.1
**Статус**: ✅ Production Ready
**JavaScript Errors**: 0
**HTTP Status**: 200 OK
**Page Load**: ~9s
**Функциональность**:
- ✅ Guest режим полностью работает
- ✅ Кнопка "Vaata ainult" функционирует
- ✅ Read-only доступ активируется
- ✅ Форма логина корректно закрывается
- ✅ Кнопка "Logi sisse" доступна в хедере
---
## 💡 Уроки
1. **Глобальные функции**: Функции, вызываемые из HTML onclick, должны быть глобальными (через `window.functionName`)
2. **Cache-busting**: При изменении JavaScript файлов обязательно обновлять версию (`?v=x.x.x`)
3. **Проверка консоли**: Всегда тестировать в браузере с открытой консолью
4. **Быстрые hotfix**: Для критических багов использовать минимальные изменения
---
**🎯 Статус**: Hotfix успешно применён, v4.1.1 готов к production deployment

View File

@@ -1,103 +0,0 @@
# 🔧 HOTFIX v4.1.26 - PRODUCTION FILES & DB
**Дата**: 2026-01-18
**Версия**: v4.1.26 FINAL
**Приоритет**: CRITICAL 🔥
---
## 🚨 **КРИТИЧНЫЕ ИЗМЕНЕНИЯ**
### **1. Рабочие файлы Docker**
Использованы ПРОВЕРЕННЫЕ рабочие файлы:
-`Dockerfile` - рабочий multi-stage build
-`docker-compose.yml` - рабочая конфигурация
-`docker-entrypoint.sh` - правильный entrypoint script
### **2. Правильное имя БД**
Во ВСЕХ конфигурациях установлено:
```
D1_BINDING=aknaproff-db
```
### **3. Production БД**
Включена рабочая БД:
- **Файл**: `data/aknaproff-db.sqlite`
- **Записей**: 67 production records
- **Размер**: 212 KB
---
## 📦 **СТРУКТУРА АРХИВА**
```
backend/
├── Dockerfile # Multi-stage build (рабочий)
├── docker-compose.yml # Конфигурация (рабочая)
├── docker-entrypoint.sh # Entrypoint script (рабочий)
├── wrangler.jsonc # D1_BINDING=aknaproff-db
├── package.json # NPM скрипты
├── src/ # Backend код
├── public/ # Frontend
├── dist/ # Built files
├── migrations/ # D1 миграции
├── seed.sql # Seed данные
└── data/
└── aknaproff-db.sqlite # PRODUCTION БД (67 записей)
```
---
## 🚀 **РАЗВЁРТЫВАНИЕ НА SYNOLOGY**
```bash
# 1. Остановить контейнер
sudo docker-compose down
# 2. Распаковать архив
unzip aknaproff_production_v4.1.26_ARM_FINAL.zip
# 3. Перейти в backend
cd backend
# 4. ВАЖНО: Проверить что БД на месте
ls -lh data/aknaproff-db.sqlite
# Должно быть: 212K
# 5. Запустить с пересборкой
sudo docker-compose up -d --build
# 6. Проверить логи
sudo docker-compose logs -f
# 7. Проверить БД
curl http://localhost:8180/api/records?month=1&year=2025
```
---
## ✅ **ЧТО ИСПРАВЛЕНО**
1. ✅ Использованы РАБОЧИЕ Docker файлы
2. ✅ Имя БД = `aknaproff-db` во всех конфигах
3. ✅ Включена production БД (67 записей)
4. ✅ Удалены `.env` файлы (не нужны)
5. ✅ Поля arve работают (v4.1.24)
6.Все предыдущие исправления сохранены
---
## 📊 **ИСТОРИЯ ВЕРСИЙ**
| Версия | Изменения |
|--------|-----------|
| v4.1.24 | Исправлены поля счёта (arve) |
| v4.1.25 | Попытка обратной совместимости |
| **v4.1.26** | **PRODUCTION FILES + DB** 🔥 |
---
**Статус**: ✅ ГОТОВО
**Тестирование**: ✅ ПРОВЕРЕНО
**Критичность**: 🔥 ВЫСОКАЯ
**Развёртывание**: ИСПОЛЬЗУЙТЕ ТОЛЬКО ЭТУ ВЕРСИЮ

View File

@@ -1,408 +0,0 @@
# AKNAPROFF Tootmine - Структура проекта
**Версия:** v4.0.13 (28.11.2025)
---
## 📁 Структура директорий
```
webapp/
├── src/ # Backend исходники (Hono + TypeScript)
│ ├── index.tsx # Главный файл приложения (26 API endpoints)
│ ├── original-html.ts # Embedded HTML для главной страницы
│ ├── middleware/ # Middleware для аутентификации
│ │ └── auth.ts # authMiddleware, optionalAuthMiddleware
│ └── utils/ # Утилиты
│ └── auth.ts # JWT, password hashing
├── public/ # Статические файлы
│ ├── original.html # Исходный HTML шаблон
│ ├── test-datepicker.html # Тестовая страница для date picker
│ └── static/ # Статические ресурсы (доступны как /static/*)
│ ├── app.js # Главный JavaScript файл (1500+ строк)
│ └── styles.css # Дополнительные стили
├── migrations/ # D1 Database миграции
│ ├── 0001_initial_schema.sql
│ ├── 0002_add_user_profiles.sql
│ └── meta/
├── dist/ # Build output (создаётся при npm run build)
│ ├── _worker.js # Скомпилированный Cloudflare Worker
│ └── _routes.json # Cloudflare routing config
├── .wrangler/ # Wrangler development state
│ └── state/v3/d1/ # Локальная D1 база данных
├── node_modules/ # NPM зависимости
├── .git/ # Git repository
├── package.json # NPM конфигурация и скрипты
├── wrangler.jsonc # Cloudflare Workers конфигурация
├── tsconfig.json # TypeScript конфигурация
├── vite.config.ts # Vite build конфигурация
├── ecosystem.config.cjs # PM2 конфигурация (для development)
├── .gitignore # Git ignore файл
├── seed.sql # Demo данные для локальной разработки
├── README.md # Основная документация проекта
├── FULL_DEVELOPMENT_HISTORY.md # Полная история разработки (25KB)
├── VERSION_SUMMARY.md # Краткая сводка версий (3KB)
└── PROJECT_STRUCTURE.md # Этот файл
```
---
## 📄 Ключевые файлы
### Backend (src/)
#### src/index.tsx (главный файл)
```typescript
// 665 строк кода
// 26 API endpoints
// Hono framework + Cloudflare Workers
// D1 Database integration
Основные секции:
- Type Bindings (D1Database, Variables)
- CORS middleware
- Static file serving
- Auth routes (login, profile)
- Data routes (CRUD для records)
- Status checkbox routes
- Material confirmed routes
- Main page route
```
#### src/middleware/auth.ts
```typescript
// JWT authentication middleware
// authMiddleware - требует токен
// optionalAuthMiddleware - опциональный токен
```
#### src/utils/auth.ts
```typescript
// JWT token generation
// Password hashing/verification (bcrypt-style)
```
#### src/original-html.ts
```typescript
// Embedded HTML template
// Создаётся из public/original.html
// Содержит полный HTML главной страницы
```
### Frontend (public/)
#### public/original.html
```html
<!-- Главный HTML шаблон -->
<!-- ~700 строк -->
<!-- Tailwind CSS (CDN) -->
<!-- FontAwesome icons -->
<!-- Axios для HTTP запросов -->
```
#### public/static/app.js
```javascript
// Главный JavaScript файл
// ~1500 строк кода
Основные функции:
- loadRecords() - загрузка records из API
- renderRecords() - рендеринг таблицы
- renderCalendarCell() - calendar picker для MAT-1/MAT-2
- renderDateCell() - 3-step toggle для других полей
- toggleDate() - toggle date fields
- toggleMaterialConfirmed() - toggle MAT-1 checkbox
- toggleMaterial2Confirmed() - toggle MAT-2 checkbox
- openModal() - открытие модального окна
- editRecord() - редактирование record
- deleteRecord() - удаление record
- handleLogin() - логин
- initFilters() - инициализация фильтров
```
### Database (migrations/)
#### migrations/0001_initial_schema.sql
```sql
-- production_records table
-- status_checkboxes table
-- users table
-- audit_log table
-- Indexes
```
### Configuration
#### package.json
```json
{
"scripts": {
"dev": "vite",
"dev:sandbox": "wrangler pages dev dist --ip 0.0.0.0 --port 3000",
"build": "vite build",
"deploy": "npm run build && wrangler pages deploy dist",
"db:migrate:local": "wrangler d1 migrations apply webapp-production --local",
"db:migrate:prod": "wrangler d1 migrations apply webapp-production"
},
"dependencies": {
"hono": "^4.0.0"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250705.0",
"@hono/vite-cloudflare-pages": "^0.4.2",
"vite": "^5.0.0",
"wrangler": "^3.78.0"
}
}
```
#### wrangler.jsonc
```jsonc
{
"name": "webapp",
"compatibility_date": "2024-01-01",
"pages_build_output_dir": "./dist",
"d1_databases": [
{
"binding": "DB",
"database_name": "webapp-production",
"database_id": "your-database-id"
}
]
}
```
#### ecosystem.config.cjs (PM2)
```javascript
module.exports = {
apps: [{
name: 'webapp',
script: 'npx',
args: 'wrangler pages dev dist --ip 0.0.0.0 --port 3000',
env: {
NODE_ENV: 'development',
PORT: 3000
}
}]
}
```
---
## 📊 Статистика кода
| Файл | Строки | Тип | Назначение |
|------|--------|-----|------------|
| src/index.tsx | ~665 | TypeScript | Backend API |
| public/static/app.js | ~1500 | JavaScript | Frontend logic |
| public/original.html | ~700 | HTML | UI template |
| src/middleware/auth.ts | ~50 | TypeScript | Authentication |
| src/utils/auth.ts | ~80 | TypeScript | JWT & passwords |
| migrations/0001_*.sql | ~100 | SQL | Database schema |
| FULL_DEVELOPMENT_HISTORY.md | ~790 | Markdown | Documentation |
**Всего:** ~3885 строк кода (без node_modules, dist, .wrangler)
---
## 🔌 API Endpoints (26 шт)
### Authentication (2)
```
POST /api/auth/login - Логин пользователя
PATCH /api/users/profile - Обновление профиля (требует auth)
```
### Data CRUD (5)
```
GET /api/years - Получить список доступных годов
GET /api/records - Получить records (по месяцу/году)
GET /api/records/:id - Получить один record
POST /api/records - Создать новый record
PUT /api/records/:id - Обновить record
DELETE /api/records/:id - Удалить record (soft delete)
```
### Status Management (13)
```
PATCH /api/records/:id/status - Toggle date field
PATCH /api/records/:id/worksheets-cycle - 3-step worksheets cycle
PATCH /api/records/:id/notes - Обновить notes
PATCH /api/records/:id/problems - Обновить problems + error flags
PATCH /api/records/:id/material-confirmed - Toggle MAT-1 checkbox
PATCH /api/records/:id/material2-confirmed - Toggle MAT-2 checkbox
PATCH /api/records/:id/price-paid - Обновить price paid status
PATCH /api/status/:recordId/:field - Обновить date field
PATCH /api/status/:recordId/:field/error - Обновить error flag
PATCH /api/status/:recordId/:field/confirm - Обновить confirmation flag
```
### Pages (6)
```
GET / - Главная страница (HTML)
GET /test-click - Тестовая страница для clicks
GET /test-datepicker - Тестовая страница для date picker
GET /static/* - Статические файлы (app.js, styles.css)
GET /favicon.ico - Favicon (empty response)
```
---
## 🗄️ Database Schema
### Tables (4)
#### production_records
```sql
- id INTEGER PRIMARY KEY
- month INTEGER -- 1-12
- year INTEGER -- 2024, 2025, etc.
- client_name TEXT
- type TEXT
- offer_number TEXT
- work_number TEXT
- quantity INTEGER
- color TEXT
- notes TEXT
- problems TEXT
- installer TEXT
- price REAL
- created_at DATETIME
- updated_at DATETIME
- deleted_at DATETIME
- deleted_by INTEGER
```
#### status_checkboxes
```sql
- id INTEGER PRIMARY KEY
- record_id INTEGER (FK production_records.id)
- material_date DATE
- material2_date DATE
- package_date DATE
- worksheets_date DATE
- cutting_date DATE
- glazing_date DATE
- ready_date DATE
- issued_date DATE
- worksheets_error BOOLEAN
- cutting_error BOOLEAN
- glazing_error BOOLEAN
- ready_error BOOLEAN
- issued_error BOOLEAN
- material_confirmed BOOLEAN
- material2_confirmed BOOLEAN
- worksheets_confirmed BOOLEAN
```
#### users
```sql
- id INTEGER PRIMARY KEY
- username TEXT UNIQUE
- password_hash TEXT
- full_name TEXT
- role TEXT (admin/user)
- created_at DATETIME
- updated_at DATETIME
- deleted_at DATETIME
```
#### audit_log
```sql
- id INTEGER PRIMARY KEY
- user_id INTEGER (FK users.id)
- record_id INTEGER (FK production_records.id)
- field_name TEXT
- old_value TEXT
- new_value TEXT
- action_type TEXT
- created_at DATETIME
```
---
## 🔄 Development Workflow
### 1. Локальная разработка
```bash
# 1. Build проекта
npm run build
# 2. Запуск с PM2
pm2 start ecosystem.config.cjs
# 3. Миграции базы данных
npm run db:migrate:local
# 4. Тестирование
curl http://localhost:3000/api/records?month=1&year=2025
# 5. Логи
pm2 logs webapp --nostream
```
### 2. Обновление кода
```bash
# 1. Изменить код
nano public/static/app.js
# 2. Обновить версию
sed -i 's/app.js?v=4.0.13/app.js?v=4.0.14/' public/original.html
# 3. Пересоздать embedded HTML
node -e "..."
# 4. Rebuild
npm run build
# 5. Restart PM2
pm2 restart webapp
```
### 3. Git workflow
```bash
git add .
git commit -m "Fix: описание изменения"
git push origin main
```
### 4. Deployment на Cloudflare
```bash
# 1. Build
npm run build
# 2. Deploy
npx wrangler pages deploy dist --project-name webapp
```
---
## 🔗 Важные ссылки
- **Sandbox URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Local URL**: http://localhost:3000
- **Git Repository**: /home/user/webapp
- **Cloudflare Pages**: (при деплое)
---
## 📚 Документация
| Файл | Размер | Описание |
|------|--------|----------|
| README.md | ~5KB | Основная документация |
| FULL_DEVELOPMENT_HISTORY.md | 25KB | Детальная история всех версий |
| VERSION_SUMMARY.md | 3KB | Краткая сводка версий |
| PROJECT_STRUCTURE.md | Этот файл | Структура проекта |
---
**Конец документа**

View File

@@ -1,303 +0,0 @@
# 📋 Отчёт о восстановлении проекта AKNAPROFF Tootmine
**Дата восстановления**: 28.11.2025
**Версия**: 3.20.3
**Статус**: ✅ Полностью восстановлен и работает
---
## 🎯 Цель восстановления
Восстановить полный рабочий проект AKNAPROFF Tootmine v3.20.3 после потери данных в sandbox-сессии, используя:
1. Frontend HTML/JS из архива `aknaproff.zip`
2. Backend API код из истории чата
3. Структуру базы данных D1 из истории разработки
---
## ✅ Выполненные задачи
### 1. Восстановление структуры проекта
- ✅ Создан новый Hono проект с шаблоном Cloudflare Pages
- ✅ Настроены все конфигурационные файлы:
- `wrangler.jsonc` - конфигурация Cloudflare
- `package.json` - скрипты и зависимости
- `ecosystem.config.cjs` - PM2 конфигурация
- `.gitignore` - исключения для Git
- `vite.config.ts` - сборка проекта
### 2. Извлечение и интеграция Frontend
- ✅ Извлечён `app.js` (73KB) из архива
- ✅ Размещён в `/public/static/app.js`
- ✅ Интегрирован полный HTML интерфейс в `src/index.tsx`
-Все CDN библиотеки подключены:
- TailwindCSS
- Font Awesome
- Axios
### 3. Восстановление Backend (Hono API)
Созданы файлы:
-`src/index.tsx` - главный файл с 19 API endpoints:
- `POST /api/auth/login` - авторизация
- `PATCH /api/users/profile` - смена пароля
- `GET /api/years` - диапазон лет
- `GET /api/records` - получение записей
- `POST /api/records` - создание записи
- `PUT /api/records/:id` - обновление записи
- `DELETE /api/records/:id` - удаление записи
- `PATCH /api/status/:recordId/:field` - обновление статуса
- `PATCH /api/status/:recordId/:field/error` - флаги ошибок
- `PATCH /api/status/:recordId/:field/confirm` - подтверждения
-`src/middleware/auth.ts` - middleware аутентификации:
- `authMiddleware` - обязательная авторизация
- `optionalAuthMiddleware` - опциональная авторизация с token refresh
-`src/utils/auth.ts` - утилиты аутентификации:
- `hashPassword()` - SHA-256 хеширование
- `verifyPassword()` - проверка пароля
- `generateToken()` - генерация JWT токена
- `refreshToken()` - обновление токена
- `verifyToken()` - валидация токена
### 4. Восстановление базы данных D1
-`migrations/0001_initial_schema.sql` - полная схема БД:
- `users` - пользователи
- `production_records` - производственные записи
- `status_checkboxes` - статусы и флаги
- `audit_log` - история изменений
- Все индексы
-`seed.sql` - тестовые данные:
- 2 пользователя: `admin`, `aknaproff` (пароль: `demo123`)
- 5 записей за январь 2025
- 2 записи за декабрь 2024
- Статусные данные для всех записей
- Флаги ошибок для демонстрации
### 5. Инициализация Git
- ✅ Создан репозиторий
- ✅ 3 коммита:
1. Initial commit с полным проектом
2. Comprehensive README
3. Fix authentication hash
### 6. Сборка и запуск
- ✅ Успешная сборка проекта (`npm run build`)
- ✅ Инициализация БД (`npm run db:reset`)
- ✅ Запуск с PM2 (`pm2 start ecosystem.config.cjs`)
-Все тесты пройдены
---
## 🧪 Результаты тестирования
### ✅ Все тесты пройдены:
1. **Login Test**
```
✅ Login as aknaproff/demo123 - SUCCESS
✅ Token generated: eyJ1c2VySWQiOjIsInVzZXJuYW1lIj...
```
2. **API Endpoints**
```
✅ GET /api/years - 200 OK
✅ GET /api/records?month=1&year=2025 - 200 OK (5 records)
✅ Authenticated request with Bearer token - 200 OK
```
3. **Database**
```
✅ 2 users created (admin, aknaproff)
✅ 7 production records loaded
✅ All status checkboxes populated
✅ Error flags configured
```
4. **Frontend**
```
✅ Main HTML page loads
✅ Static app.js served (73KB)
✅ All CDN resources accessible
```
---
## 🌐 Доступ к приложению
**Public URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
### Демо пользователи:
| Username | Password | Role |
|----------|----------|------|
| admin | demo123 | Admin (для разработчика) |
| aknaproff | demo123 | Admin (для клиента) |
---
## 📊 Статистика восстановления
### Файлы:
- **Создано**: 16 файлов
- **Общий размер**: ~150KB кода
- **Frontend JS**: 73KB
- **Backend TS**: 35KB
- **Database SQL**: 5KB
### Git коммиты:
- **Всего**: 3 коммита
- **Изменения**: 5764+ insertions
### Время восстановления:
- **Общее время**: ~20 минут
- **Основные этапы**:
- Извлечение архива: 1 мин
- Создание backend: 5 мин
- Настройка БД: 2 мин
- Сборка и тесты: 3 мин
- Документация: 5 мин
---
## 🔧 Технические детали
### Исправленные проблемы:
1. **Password Hash Mismatch**
- **Проблема**: Неправильный хеш в seed.sql
- **Решение**: Обновлён на корректный SHA-256 хеш `demo123`
- **Хеш**: `d3ad9315b7be5dd53b31a273b3b3aba5defe700808305aa16a3062b76658a791`
2. **Database Connection**
- **Проблема**: DB не открывается после PM2 restart
- **Решение**: Полная пересборка + `pm2 delete` перед стартом
3. **Static Files Serving**
- **Проблема**: app.js не доступен
- **Решение**: Правильная настройка `serveStatic` для `/static/*`
### Версии технологий:
- **Node.js**: v18+
- **Hono**: v4.10.7
- **Wrangler**: v4.51.0
- **Vite**: v6.4.1
- **PM2**: (pre-installed)
---
## 📚 Документация
Созданы файлы:
- ✅ `README.md` - Полное руководство (6.3KB)
- ✅ `RESTORE_REPORT.md` - Этот отчёт
README включает:
- Обзор проекта
- Инструкции по установке
- Описание API endpoints
- Структуру базы данных
- Руководство по разработке
- Информацию о безопасности
---
## 🎯 Функциональность
### Полностью восстановлено (v3.20.3):
#### Управление данными:
- ✅ CRUD операции для производственных записей
- ✅ Фильтрация по месяцу/году
- ✅ Быстрый поиск: Klient, Tüüp, Pakkum. Nr, Töö Nr
- ✅ Сортировка по колонкам
- ✅ Soft delete с audit log
#### Статусная система:
- ✅ 8 этапов производства с датами:
- MAT-1, MAT-2, PAKETT
- Töölehti, Lõikus, Klaas
- Valmis, Väljas
- ✅ Флаги ошибок для каждого этапа
- ✅ Флаги подтверждения
- ✅ Модальное окно "Проблемы" с чекбоксами
#### Логика блокировки:
- ✅ Замки появляются при тексте ИЛИ галочке ошибки
- ✅ Блокировка полей Valmis и Väljas
- ✅ Всплывающие подсказки с причиной блокировки
#### Аутентификация:
- ✅ JWT токены с 30-минутным сроком
- ✅ Автообновление токена при активности
- ✅ Сессионный таймер
- ✅ Смена пароля
- ✅ Публичный просмотр (без логина)
- ✅ Admin-only функции
---
## 🚀 Следующие шаги
### Для локальной разработки:
```bash
cd /home/user/webapp
npm run db:reset # Сбросить БД
npm run build # Собрать проект
pm2 start ecosystem.config.cjs # Запустить
pm2 logs webapp --nostream # Проверить логи
```
### Для deployment на Cloudflare:
```bash
# 1. Создать production D1 database
npx wrangler d1 create webapp-production
# 2. Обновить wrangler.jsonc с database_id
# 3. Применить миграции
npm run db:migrate:prod
# 4. Deploy
npm run deploy:prod
```
### Рекомендации для production:
1. ⚠️ Заменить auth utilities на bcrypt + настоящий JWT
2. ⚠️ Использовать httpOnly cookies вместо localStorage
3. ⚠️ Настроить CORS для production доменов
4. ⚠️ Включить rate limiting
5. ⚠️ Настроить мониторинг и алерты
---
## 📝 История версий (восстановленные)
- **v3.20.3** (26.11.2025): Логика замков (текст ИЛИ чекбокс)
- **v3.20.2** (26.11.2025): Переименование колонок
- **v3.20.1** (26.11.2025): Исправление смены пароля
- **v3.20.0** (26.11.2025): Пользователь aknaproff, сессия от активности
- **v3.19.8** (26.11.2025): Фильтр Tüüp
---
## ✅ Заключение
**Проект AKNAPROFF Tootmine v3.20.3 полностью восстановлен и работает!**
Все функции восстановлены из:
- ✅ Frontend архива (aknaproff.zip)
- ✅ Backend кода из истории чата
- ✅ Database структуры из документации
**Статус**: 🟢 Production Ready (для sandbox)
**Готово к**:
- Локальной разработке
- Тестированию
- Deployment на Cloudflare Pages (после настройки API ключа)
---
**Восстановление завершено**: 28.11.2025 10:10 UTC
**Sandbox URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai

View File

@@ -1,84 +0,0 @@
# AKNAPROFF Tootmine - Краткая сводка версий
**Проект:** AKNAPROFF Tootmine
**Дата:** 28.11.2025
**Версии:** v4.0.4 → v4.0.13
---
## 📊 Быстрая справка
| Версия | Проблема | Решение | Статус |
|--------|----------|---------|--------|
| v4.0.5 | Пустая таблица, клики не работали | Исправлен default month filter (January) | ✅ РАБОТАЕТ |
| v4.0.6 | HTTP 401 Unauthorized | optionalAuthMiddleware для 13 endpoints | ✅ РАБОТАЕТ |
| v4.0.7 | Браузер кеширует старый код | Cache busting (app.js?v=4.0.7) | ✅ РАБОТАЕТ |
| v4.0.8 | Frontend блокирует Public User | Удалены role checks и CSS hiding | ✅ РАБОТАЕТ |
| v4.0.9 | MAT-1/MAT-2 checkbox не toggle | Добавлено логирование, возврат newValue | ✅ РАБОТАЕТ |
| v4.0.10 | Date picker не открывается | Попытка через .click() | ❌ НЕ СРАБОТАЛО |
| v4.0.11 | События не происходят | Попытка через <label for> | ❌ НЕ СРАБОТАЛО |
| v4.0.12 | Calendar нигде не открывается | Убран pointer-events:none | ⚠️ ЧАСТИЧНО |
| v4.0.13 | Public User не видит calendar | Убран isAdmin check | ✅ РАБОТАЕТ |
---
## 🎯 Финальное решение (v4.0.13)
### Проблема
```javascript
// Public User видел read-only вместо calendar picker
const isAdmin = currentUser?.role === 'admin';
${isAdmin ? renderCalendarCell(...) : renderReadOnlyCell(...)}
```
### Решение
```javascript
// Все пользователи видят calendar picker
${renderCalendarCell(...)}
```
### Результат
**MAT-1, MAT-2, PAKETT**: Calendar picker для всех
**LÕIKUS, KLAAS, VALMIS, VÄLJAS**: 3-step toggle (пусто → дата → ошибка)
---
## 🔗 URLs
- **Sandbox**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
- **Git**: /home/user/webapp
- **Подробная история**: FULL_DEVELOPMENT_HISTORY.md (25KB)
---
## 📝 Ключевые изменения
### Backend (src/index.tsx)
- ✅ 13 endpoints с optionalAuthMiddleware
- ✅ userId || null в audit logs
- ✅ Toggle endpoints для MAT-1/MAT-2
### Frontend (public/static/app.js)
- ✅ Убраны role checks (openModal, editRecord, toggleDeleteButtons)
- ✅ Убран isAdmin check для renderCalendarCell
- ✅ <label for> без pointer-events:none
- ✅ Cache busting (app.js?v=4.0.13)
### HTML (public/original.html)
- ✅ Убран admin-only-block CSS
- ✅ Кнопка "Lisa uus rida" видна всем
---
## 🚀 Тестирование
**ВАЖНО: Очистите кеш (Ctrl+Shift+R) или используйте Incognito!**
1. **MAT-1/MAT-2**: Клик → Календарь открывается
2. **LÕIKUS/KLAAS**: Клик → Toggle (белый → зелёный → белый)
3. **Checkbox MAT-1/MAT-2**: Клик → Toggle (серый ↔ зелёный)
4. **CRUD**: "Lisa uus rida" → Модальное окно
---
**Полная документация:** См. FULL_DEVELOPMENT_HISTORY.md

View File

@@ -0,0 +1,44 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Artisan;
class Template extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'mpfg:template';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Seed template in the application';
/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();
}
/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
Artisan::call('migrate:refresh');
Artisan::call('db:seed');
}
}

38
app/Console/Kernel.php Normal file
View File

@@ -0,0 +1,38 @@
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')
// ->hourly();
if (config('app.env') == 'demo') {
$schedule->command('mpfg:template')
->daily();
}
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums\User;
enum PermissionEnum: string
{
case CREATE_FORM = 'create form';
case VIEW_FORM = 'view form';
case EDIT_FORM = 'edit form';
}

View File

@@ -0,0 +1,10 @@
<?php
namespace App\Enums\User;
enum RoleEnum: string
{
case SUPERVISOR = 'supervisor';
case ADMIN = 'admin';
case USER = 'user';
}

View File

@@ -0,0 +1,91 @@
<?php
namespace App\Exceptions;
use App\Mail\ExceptionOccured;
use Exception;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Mail;
use Symfony\Component\Debug\Exception\FlattenException;
use Symfony\Component\Debug\ExceptionHandler as SymfonyExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
\Illuminate\Auth\AuthenticationException::class,
\Illuminate\Auth\Access\AuthorizationException::class,
\Symfony\Component\HttpKernel\Exception\HttpException::class,
\Illuminate\Database\Eloquent\ModelNotFoundException::class,
\Illuminate\Session\TokenMismatchException::class,
\Illuminate\Validation\ValidationException::class,
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Throwable $exception
* @return void
*/
public function report(Throwable $exception)
{
if ($this->shouldReport($exception)) {
if (config('app.env') == 'demo') {
$this->sendEmail($exception); // sends an email in demo server
}
}
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
* @return \Illuminate\Http\Response
*/
public function render($request, Throwable $exception)
{
return parent::render($request, $exception);
}
/**
* Sends the exception email
*
* @param $exception
*/
public function sendEmail(Exception $exception)
{
try {
$e = FlattenException::create($exception);
$handler = new SymfonyExceptionHandler();
$html = $handler->getHtml($e);
$email = env('MAIL_USERNAME');
if (! empty($email)) {
Mail::to($email)->send(new ExceptionOccured($html));
}
} catch (Exception $ex) {
dd($ex);
}
}
}

69
app/Form.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Form extends Model
{
use \App\Traits\UsesUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
public $incrementing = false;
protected $keyType = 'string';
protected $appends = ['media_url'];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'schema' => 'array',
'mailchimp_details' => 'array',
'acelle_mail_info' => 'array',
'webhook_info' => 'array',
];
public function getMediaUrlAttribute()
{
return asset('/uploads/'.config('constants.doc_path').'/');
}
/**
* Get the data for the form.
*/
public function data()
{
return $this->hasMany(\App\FormData::class);
}
/**
* Get the created by for the form.
*/
public function createdBy()
{
return $this->belongsTo(\App\User::class, 'created_by');
}
/**
* Get the assigned user for form.
*/
public function assignedTo()
{
return $this->hasMany(\App\UserForm::class, 'form_id');
}
/**
* Get the visitors for form
*/
public function visitors()
{
return $this->hasMany(\App\Visitor::class, 'form_id');
}
}

56
app/FormData.php Normal file
View File

@@ -0,0 +1,56 @@
<?php
namespace App;
use App\Traits\UsesUuid;
use Illuminate\Database\Eloquent\Model;
class FormData extends Model
{
use UsesUuid;
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = [];
/**
* The attributes that should be cast to native types.
*
* @var array
*/
protected $casts = [
'data' => 'array',
];
public static function sources()
{
return ['app'];
}
/**
* Get the form for the form data
*/
public function form()
{
return $this->belongsTo(\App\Form::class);
}
/**
* Get the submitted by for the form.
*/
public function submittedBy()
{
return $this->belongsTo(\App\User::class, 'submitted_by');
}
/**
* Get the form's comment
*/
public function comments()
{
return $this->hasMany(\App\FormDataComment::class, 'form_data_id');
}
}

31
app/FormDataComment.php Normal file
View File

@@ -0,0 +1,31 @@
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class FormDataComment extends Model
{
/**
* The attributes that aren't mass assignable.
*
* @var array
*/
protected $guarded = ['id'];
/**
* Get the form_data that owns the comment.
*/
public function post()
{
return $this->belongsTo(\App\FormData::class);
}
/**
* Get the commentator for comment.
*/
public function commentedBy()
{
return $this->belongsTo(\App\User::class, 'user_id');
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ConfirmsPasswords;
class ConfirmPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Confirm Password Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password confirmations and
| uses a simple trait to include the behavior. You're free to explore
| this trait and override any functions that require customization.
|
*/
use ConfirmsPasswords;
/**
* Where to redirect users when the intended url fails.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
class ForgotPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset emails and
| includes a trait which assists in sending these notifications from
| your application to your users. Feel free to explore this trait.
|
*/
use SendsPasswordResetEmails;
}

View File

@@ -0,0 +1,74 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
class LoginController extends Controller
{
/*
|--------------------------------------------------------------------------
| Login Controller
|--------------------------------------------------------------------------
|
| This controller handles authenticating users for the application and
| redirecting them to your home screen. The controller uses a trait
| to conveniently provide its functionality to your applications.
|
*/
use AuthenticatesUsers;
/**
* Where to redirect users after login.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest')->except('logout');
}
/**
* The user has been authenticated.
* Check if user is active or not
*
* @param \Illuminate\Http\Request $request
* @param mixed $user
* @return mixed
*/
protected function authenticated(Request $request, $user)
{
if (! $user->is_active) {
Auth::logout();
return redirect('/login')->with(
'status',
['success' => false, 'msg' => __('messages.user_inactive')]
);
}
}
/**
* Where to redirect users after logout.
*
* @var string
*/
public function logout(Request $request)
{
Auth::logout();
return redirect('/login');
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Package;
use App\PackageSubscription;
use App\Providers\RouteServiceProvider;
use App\User;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
/*
|--------------------------------------------------------------------------
| Register Controller
|--------------------------------------------------------------------------
|
| This controller handles the registration of new users as well as their
| validation and creation. By default this controller uses a trait to
| provide this functionality without requiring any additional code.
|
*/
use RegistersUsers;
/**
* Where to redirect users after registration.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('guest');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\User
*/
protected function create(array $data)
{
$user = User::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => $data['password'],
]);
if (! empty($data['package_id'])) {
$subscription = PackageSubscription::createSubscription($user->id, $data['package_id']);
}
return $user;
}
/**
* Show the application registration form.
*
* @return \Illuminate\Http\Response
*/
public function showRegistrationForm()
{
$packages = Package::Where('is_active', 1)
->orderBy('sort_order', 'asc')
->get();
return view('auth.register')
->with(compact('packages'));
}
}

View File

@@ -0,0 +1,30 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\ResetsPasswords;
class ResetPasswordController extends Controller
{
/*
|--------------------------------------------------------------------------
| Password Reset Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling password reset requests
| and uses a simple trait to include this behavior. You're free to
| explore this trait and override any methods you wish to tweak.
|
*/
use ResetsPasswords;
/**
* Where to redirect users after resetting their password.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
}

View File

@@ -0,0 +1,42 @@
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Providers\RouteServiceProvider;
use Illuminate\Foundation\Auth\VerifiesEmails;
class VerificationController extends Controller
{
/*
|--------------------------------------------------------------------------
| Email Verification Controller
|--------------------------------------------------------------------------
|
| This controller is responsible for handling email verification for any
| user that recently registered with the application. Emails may also
| be re-sent if the user didn't receive the original email message.
|
*/
use VerifiesEmails;
/**
* Where to redirect users after verification.
*
* @var string
*/
protected $redirectTo = RouteServiceProvider::HOME;
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
$this->middleware('signed')->only('verify');
$this->middleware('throttle:6,1')->only('verify', 'resend');
}
}

View File

@@ -0,0 +1,307 @@
<?php
namespace App\Http\Controllers;
use App\Form;
use App\PackageSubscription;
use App\UserForm;
use App\Visitor;
use Carbon\Carbon;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
protected $statusCode;
public function getStatusCode()
{
return $this->statusCode;
}
public function setStatusCode($statusCode)
{
$this->statusCode = $statusCode;
return $this;
}
public function respondWithError($message = null)
{
return response()->json(
['success' => false, 'msg' => $message]
);
}
/**
* Returns a Unauthorized response.
*
* @param string $message
* @return \Illuminate\Http\Response
*/
public function respondUnauthorized($message = 'Unauthorized action.')
{
return $this->setStatusCode(403)
->respondWithError($message);
}
/**
* Returns a went wrong response.
*
* @param object $exception = null
* @return \Illuminate\Http\Response
*/
public function respondWentWrong($exception = null)
{
//If debug is enabled then send exception message
$message = (config('app.debug') && is_object($exception)) ? 'File:'.$exception->getFile().'Line:'.$exception->getLine().'Message:'.$exception->getMessage() : __('messages.something_went_wrong');
//TODO: show exception error message when error is enabled.
return $this->setStatusCode(200)
->respondWithError($message);
}
/**
* Returns a 200 response.
*
* @param object $message = null
* @return \Illuminate\Http\Response
*/
public function respondSuccess($message = null, $additional_data = [])
{
$message = is_null($message) ? __('messages.success') : $message;
$data = ['success' => true, 'msg' => $message];
if (! empty($additional_data)) {
$data = array_merge($data, $additional_data);
}
return $this->respond($data);
}
/**
* Returns a 200 response.
*
* @param array $data
* @return \Illuminate\Http\Response
*/
public function respond($data)
{
return response()->json($data);
}
/**
* Returns a disabled response for demo
*
* @return \Illuminate\Http\Response
*/
public function respondDemo()
{
$data = ['success' => false, 'msg' => __('messages.feature_disabled_in_demo')];
return $this->respond($data);
}
public function isDemo()
{
if (config('app.env') == 'demo') {
return true;
}
}
/**
* Checks if the feature is allowed in demo
*
* @return mixed
*/
public function notAllowedInDemo()
{
//Disable in demo
if (config('app.env') == 'demo') {
$output = ['success' => 0,
'msg' => __('messages.feature_disabled_in_demo'),
];
if (request()->ajax()) {
return $output;
} else {
return back()->with('status', $output);
}
}
}
/**
* Checks if the user is subscribed
* a package or not.
*
* @return bool
*/
public function isSubscribed($user)
{
$superadmins = env('SUPERADMIN_EMAILS');
$superadmin_emails = explode(',', $superadmins);
if (is_saas_enabled() && ! in_array($user->email, $superadmin_emails)) {
$today = Carbon::today()->toDateString();
$subscription = PackageSubscription::activeSubscription($user->id);
if (! isset($subscription->end_date) || $subscription->end_date <= $today) {
return false;
}
}
return true;
}
/**
* Checks if the user's package
* offers available or not.
*
* @return bool
*/
public function isQuotaAvailable($user_id)
{
if (is_saas_enabled()) {
$subscription = PackageSubscription::activeSubscription($user_id);
$subscription_start = '';
$subscription_end = '';
if (! empty($subscription)) {
$subscription_start = Carbon::parse($subscription->start_date)->toDateTimeString();
$subscription_end = Carbon::parse($subscription->end_date)->toDateTimeString();
}
$form_count = Form::where('created_by', $user_id)
->whereBetween('created_at', [$subscription_start, $subscription_end])
->where('is_template', 0)
->count();
if (isset($subscription->package_details['no_of_active_forms']) && $subscription->package_details['no_of_active_forms'] != 0) {
if ($form_count >= $subscription->package_details['no_of_active_forms']) {
return false;
}
}
}
return true;
}
/**
* Returns a subscription expired.
*
* @param string $message
* @return \Illuminate\Http\Response
*/
public function subscriptionExpired()
{
$output = ['success' => 0,
'msg' => __('messages.subscription_expired_pls_renew_it'),
'subscribe_btn' => 1,
];
return $output;
}
/**
* Returns a quota not available.
*
* @param string $message
* @return \Illuminate\Http\Response
*/
public function quotaNotAvailable()
{
$output = ['success' => 0,
'msg' => __('messages.active_no_of_forms_for_package_crossed_limit'),
'subscribe_btn' => 0,
];
return $output;
}
/**
* Returns the list of all configured payment gateway
*
* @return Response
*/
public function paymentGateways()
{
$gateways = [];
//Check if paypal is configured or not
if ((env('PAYPAL_SANDBOX_API_USERNAME') && env('PAYPAL_SANDBOX_API_PASSWORD') && env('PAYPAL_SANDBOX_API_SECRET')) || (env('PAYPAL_LIVE_API_USERNAME') && env('PAYPAL_LIVE_API_PASSWORD') && env('PAYPAL_LIVE_API_SECRET'))) {
$gateways['paypal'] = 'PayPal';
}
//Check if stripe is configured or not
if (! empty(config('constants.STRIPE_PUB_KEY')) && ! empty(config('constants.STRIPE_SECRET_KEY'))) {
$gateways['stripe'] = 'Stripe';
}
//check if offline payment enabled
if (config('constants.ENABLE_OFFLINE_PAYMENT')) {
$gateways['offline'] = 'Offline';
}
return $gateways;
}
public function doUserHavePermission($form_id, $permission)
{
$user_form = UserForm::where('form_id', $form_id)
->where('assigned_to', \Auth::id())
->first();
if (! empty($user_form)) {
return in_array($permission, $user_form->permissions);
} else {
return false;
}
}
public function postVisitorsCount($request, $form_id)
{
$is_unique = 0;
if (($request->session()->get('referrer') !=
$request->headers->get('referer')) ||
($request->session()->get('id') != $form_id)
) {
session([
'id' => $form_id,
'referrer' => $request->headers->get('referer'),
]);
$is_unique = 1;
}
$visitor = Visitor::create([
'form_id' => $form_id,
'is_unique' => $is_unique,
'referrer' => $request->headers->get('referer'),
]);
return $visitor;
}
public function checkIfUserIsFormCreatorOrSharedWith($form)
{
if (! is_null(\Auth::id())) {
$user_form = UserForm::where('form_id', $form->id)
->where('assigned_to', \Auth::id())
->first();
return ($form->created_by != \Auth::id()) ? ! empty($user_form) : true;
}
return false;
}
public function checkIfUserIsCreatorOfGivenForm($form_id)
{
$form = Form::where('id', $form_id)
->where('created_by', \Auth::id())
->first();
return ! empty($form);
}
}

View File

@@ -0,0 +1,731 @@
<?php
namespace App\Http\Controllers;
use App\Enums\User\RoleEnum;
use App\Form;
use App\PackageSubscription;
use App\User;
use App\UserForm;
use Carbon\Carbon;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use ZipArchive;
class FormController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
if (request()->ajax()) {
$user = request()->user();
$subscription_info = [];
if (! $this->isSubscribed($user)) {
$subscription_info = $this->subscriptionExpired();
} elseif (! $this->isQuotaAvailable($user->id)) {
$subscription_info = $this->quotaNotAvailable();
}
$templates = Form::where('is_template', 1)
->where('created_by', $user->id)
->orWhere('is_global_template', 1)
->pluck('name', 'id')
->toArray();
return view('form.create')
->with(compact('templates', 'subscription_info'));
}
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
$input = $request->only('name', 'description', 'slug');
$template_id = $request->input('template_id');
$id = $request->input('id');
if (! empty($id) || ! empty($template_id)) {
$form_id = ! empty($id) ? $id : $template_id;
$input['schema'] = Form::find($form_id)->schema;
}
$input['created_by'] = request()->user()->id;
$form = Form::create($input);
return redirect()->action([\App\Http\Controllers\FormController::class, 'edit'],
['form' => $form->id]
);
}
/**
* Display the specified resource.
* This menthod is available publicly.
*
* @param id_or_slug
* @return \Illuminate\Http\Response
*/
public function show(Request $request, $id_or_slug)
{
$form = Form::where('id', $id_or_slug)
->orWhere('slug', $id_or_slug)
->first();
if (empty($form)) {
abort(404);
}
//check if password protection is enabled or not
if (isset($form->schema['settings']['password_protection']) && $form->schema['settings']['password_protection']['is_enabled'] && ! session('validated_protected_form')) {
return redirect()->route('validate.protected.form', ['id' => $id_or_slug]);
}
//if token available get submitted data
$token = request()->get('token');
if (! empty($token)) {
$form->load(['data' => function ($query) use ($token, $form) {
$query->where('token', $token)
->where('form_id', $form->id);
}]);
}
$form_closed_msg = '';
$is_form_closed = false;
if (isset($form->schema['settings']['form_scheduling']['is_enabled']) && $form->schema['settings']['form_scheduling']['is_enabled']) {
//getting form scheduling values
$form_closed_msg = $form->schema['settings']['form_scheduling']['closed_msg'];
$today = Carbon::now();
$start_date_time = null;
$end_date_time = Carbon::parse($form->schema['settings']['form_scheduling']['end_date_time']);
//parse start date time if it is enabled set form to close by default
if (isset($form->schema['settings']['form_scheduling']['start_date_time'])) {
$is_form_closed = true;
$start_date_time = Carbon::parse($form->schema['settings']['form_scheduling']['start_date_time']);
}
//check if form will be open
if (((isset($start_date_time) && ($start_date_time->lessThanOrEqualTo($today))) && $end_date_time->greaterThanOrEqualTo($start_date_time))) {
$is_form_closed = false;
}
//check if form will be close
if ($end_date_time->lessThanOrEqualTo($today)) {
$is_form_closed = true;
}
}
if (! $this->checkIfUserIsFormCreatorOrSharedWith($form)) {
$this->postVisitorsCount($request, $form->id);
}
//flush session
$request->session()->forget('validated_protected_form');
$nav = false;
$iframe_enabled = $request->get('iframe', false);
$action_by = auth()->user()->roles->first()->name;
return view('form.show')
->with(compact('form', 'nav', 'is_form_closed', 'form_closed_msg', 'iframe_enabled', 'action_by'));
}
/**
* Show the form for editing the specified resource.
*
* @param \App\Form $form
* @return \Illuminate\Http\Response
*/
public function edit(Form $form)
{
$user_id = request()->user()->id;
$placeholder_img = asset('img/placeholder.png');
//check subscription if form is not shared to user
if (! $this->doUserHavePermission($form->id, 'can_design_form')) {
if (! $this->isSubscribed(request()->user())) {
return redirect('home')->with('status', $this->subscriptionExpired());
}
}
//check permission if user is not a creator
$has_permission = ($form->created_by != $user_id) ? $this->doUserHavePermission($form->id, 'can_design_form') : true;
if (!$form->created_by !== $user_id && !auth()->user()->hasRole([RoleEnum::ADMIN->value, RoleEnum::SUPERVISOR->value])) {
abort(404);
}
//if user is not creator then don't allow permission to save as template
$save_as_template = ($form->created_by != $user_id) ? false : true;
return view('form.edit')
->with(compact('form', 'placeholder_img', 'save_as_template'));
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param $id
* @return \Illuminate\Http\Response
*/
public function update($id)
{
try {
$input = request()->only('name', 'description', 'slug');
$form_data = [
'form' => request()->input('form'),
'emailConfig' => request()->input('email_config'),
'settings' => request()->input('settings'),
'additional_js_css' => request()->input('js_css'),
'conditional_fields' => request()->input('conditional_fields'),
'form_attributes' => request()->input('form_attributes'),
'contains_page_break' => request()->input('contains_page_break'),
];
$is_template = request()->input('is_template');
$input['schema'] = $form_data;
$form = Form::find($id);
$form->name = $input['name'];
$form->slug = $input['slug'];
$form->description = $input['description'];
$form->schema = $input['schema'];
$form->is_template = $is_template;
$form->mailchimp_details = request()->input('mailchimp_details');
$form->acelle_mail_info = request()->input('acelle_mail_info');
$form->webhook_info = request()->input('webhook_info');
$form->save();
$data['redirect'] = action([\App\Http\Controllers\HomeController::class, 'index']);
$data['preview'] = action([\App\Http\Controllers\FormController::class, 'show'], ['form' => $form->slug ?: $id]);
$data['form_name'] = $input['name'];
return $this->respondSuccess($message = null, $data);
} catch (Exception $e) {
return $this->respondWentWrong($e);
}
}
/**
* Remove the specified resource from storage.
*
* @param \App\Form $form
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
if (! empty($this->notAllowedInDemo())) {
return $this->notAllowedInDemo();
}
try {
if (request()->ajax()) {
$form = Form::findOrFail($id);
$form->data()->delete();
$form->assignedTo()->delete();
$form->visitors()->delete();
$form->delete();
}
return $this->respondSuccess(__('messages.deleted_successfully'));
} catch (Exception $e) {
return $this->respondWentWrong($e);
}
}
public function downloadCode($form_id)
{
if (! empty($this->notAllowedInDemo())) {
return $this->notAllowedInDemo();
}
try {
//check for form code download in subscription
if (is_saas_enabled()) {
$subscription = PackageSubscription::activeSubscription(request()->user()->id);
if (! $this->isSubscribed(request()->user()) || isset($subscription->package_details['is_form_downloadable']) && ! $subscription->package_details['is_form_downloadable']) {
exit('Form code download not allowed');
}
}
$form = Form::findOrFail($form_id);
$form_slug = Str::slug($form->name);
$validation_rules = [];
$signature_elements = [];
if (! empty($form->schema['form'])) {
foreach ($form->schema['form'] as $key => $element) {
$rules = [];
foreach ($element['validations'] as $index => $validation) {
$rule = 'data-rule-'.$validation['rule'];
$error_msg = 'data-msg-'.$validation['rule'];
$is_rule_required = ! empty($validation['value']) ? $validation['value'] : true;
$rules[$index]['rule'] = $rule;
$rules[$index]['error'] = $error_msg;
$rules[$index]['value'] = $is_rule_required;
$rules[$index]['msg'] = $validation['error_msg'];
}
$validation_rules[$key] = $rules;
//used in mail
if ($element['type'] == 'signature') {
$signature_elements[$element['name']] = $element['name'];
}
}
}
//Generate html, CSS, JS
$html = view('form.download')
->with(compact('form', 'form_slug', 'validation_rules'))
->render();
//Generate PHP backend code.
$this->GeneratorCommonPhp = new FormPhpGenerator();
$form_php = $this->GeneratorCommonPhp->phpStart();
//Check for recaptcha
if ($form->schema['settings']['recaptcha']['is_enable']) {
$form_php .= $this->GeneratorCommonPhp->reCaptcha($form->schema['settings']['recaptcha']['secret_key']);
}
$smtp = $form->schema['emailConfig']['smtp'];
if (isset($form->schema['emailConfig']['email']['enable']) && $smtp['use_system_smtp'] == 1) {
// my smtp settings
$user = request()->user();
$my_smtp_settings = $user->settings['smtp'];
$smtp['host'] = $my_smtp_settings['MAIL_HOST'];
$smtp['port'] = $my_smtp_settings['MAIL_PORT'];
$smtp['from_address'] = $my_smtp_settings['MAIL_FROM_ADDRESS'];
$smtp['from_name'] = $my_smtp_settings['MAIL_FROM_NAME'];
$smtp['encryption'] = $my_smtp_settings['MAIL_ENCRYPTION'];
$smtp['username'] = $my_smtp_settings['MAIL_USERNAME'];
$smtp['password'] = $my_smtp_settings['MAIL_PASSWORD'];
}
//check for attachments
$form_php .= $this->GeneratorCommonPhp->attachmentFields($form->schema['form']);
$success_variable = true;
//Send Email.
if (isset($form->schema['emailConfig']['email']['enable']) && $form->schema['emailConfig']['email']['enable'] == 1) {
$form_php .= $this->GeneratorCommonPhp->email($form->schema['emailConfig']['email'], $smtp, '$is_email_sent', false, $signature_elements);
$success_variable = '$is_email_sent';
}
//Check and Send Auto response Email.
if (isset($form->schema['emailConfig']['auto_response']['is_enable']) && $form->schema['emailConfig']['auto_response']['is_enable'] == 1) {
$form_php .= $this->GeneratorCommonPhp->email(
$form->schema['emailConfig']['auto_response'],
$smtp,
'$is_autoresponse_email_sent',
true,
$signature_elements
);
$success_variable = ($success_variable == true) ? '$is_autoresponse_email_sent' : true;
}
// $form_php .= $this->GeneratorCommonPhp->databaseIntegration($this->form_data);
//TODO: Error message in response.
$form_php .= $this->GeneratorCommonPhp->phpEnd($form->schema['settings']['notification'], $success_variable);
//Create a folder and put the contents in it.
$directory = storage_path(config('constants.doc_path').'/'.$form_slug.'_'.time());
$directory_library_php = $directory.'/library/';
if (! is_writable(storage_path(config('constants.doc_path')))) {
return back()
->with('status', ['success' => 0,
'msg' => 'Unable to create temporary folder, Please create a folder '.storage_path(config('constants.doc_path').' and provide writable permission'),
]);
}
mkdir($directory, 0777, true);
mkdir($directory_library_php);
if ($form->schema['settings']['background']['bg_type'] == 'bg_image' && ! empty($form->schema['settings']['color']['image_path'])) {
$image_name = $form->schema['settings']['color']['image_path'];
$src_file = public_path('uploads/'.config('constants.doc_path').'/'.$image_name);
$destination_file = $directory.'/asset/';
mkdir($destination_file);
copy($src_file, $destination_file.$image_name);
}
$download_html = $directory.'/'.$form_slug.'.html';
$download_php = $directory.'/'.$form_slug.'.php';
//Generate html file.
$f_html = fopen($download_html, 'w');
fwrite($f_html, $html);
fclose($f_html);
//Generate php file.
$f_php = fopen($download_php, 'w');
fwrite($f_php, $form_php);
fclose($f_php);
//Copy all files from form_bundle.
$src = __DIR__.'/form_bundle_libraries';
File::copyDirectory($src, $directory_library_php);
return $this->_zipAndDownload($directory, $form_slug.'.zip');
} catch (Exception $e) {
exit('Something Went Wrong');
}
}
protected function _zipAndDownload($folder, $zipName)
{
if (! class_exists('ZipArchive')) {
exit('ZipArchive class not found, enable it from your php.ini file.');
}
$zipName = storage_path().'/'.$zipName;
//Zip the folder and download it.
$files = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($folder),
\RecursiveIteratorIterator::LEAVES_ONLY
);
$zip = new ZipArchive();
$zip->open($zipName, ZipArchive::CREATE | ZipArchive::OVERWRITE);
foreach ($files as $name => $file) {
// Skip directories (they would be added automatically)
if (! $file->isDir()) {
// Get real and relative path for current file
$filePath = $file->getRealPath();
$relativePath = substr($filePath, strlen($folder));
// Add current file to archive
$zip->addFile($filePath, ltrim($relativePath, '/'));
}
}
// Zip archive will be created only after closing object
$zip->close();
Storage::deleteDirectory($folder);
return response()->download($zipName)
->deleteFileAfterSend();
}
public function generateWidget($id)
{
if (request()->ajax()) {
$form = Form::where('created_by', \Auth::id())
->findOrFail($id);
$url = action([\App\Http\Controllers\FormController::class, 'show'], ['form' => $form->slug ?: $form->id]);
$widget = '<iframe src="'.$url.'?iframe=true"'.' frameborder="0" width="100%"></iframe>';
$widget_script = '<script src="'.asset('/js/iframeResizer.js').'"></script><script src="'.asset('/js/widget.js').'"></script>';
$form = '<a href="'.$url.'"'.' target="_blank" class="btn btn-sm btn-info">Please fill out form</a>';
return view('form.generate_widget')
->with(compact('widget', 'form', 'widget_script'));
}
}
public function copyForm($id)
{
if (request()->ajax()) {
$user = request()->user();
$subscription_info = [];
if (! $this->isSubscribed($user)) {
$subscription_info = $this->subscriptionExpired();
} elseif (! $this->isQuotaAvailable($user->id)) {
$subscription_info = $this->quotaNotAvailable();
}
$form = Form::find($id);
return view('form.copy_form')
->with(compact('form', 'subscription_info'));
}
}
public function generateFormSlug(Request $request)
{
if ($request->ajax()) {
$form_name = $request->input('name').' '.Str::random(3);
$form_slug = Str::slug($form_name);
return $this->respond($form_slug);
}
}
/**
* get view to assign collab
*
* @param \App\Form $id
* @return \Illuminate\Http\Response
*/
public function getCollab($id)
{
if (request()->ajax()) {
$user = request()->user();
$form = Form::with('assignedTo', 'assignedTo.user')
->where('created_by', $user->id)
->find($id);
return view('form.collaborate')
->with(compact('form'));
}
}
/**
* store the collab data in db
*
* @param \App\Form $id
* @return \Illuminate\Http\Response
*/
public function postCollab(Request $request)
{
try {
if (request()->ajax()) {
$form_id = $request->input('form_id');
$permissions = $request->input('permissions');
$email = $request->input('email');
$user = User::where('email', $email)
->first();
$form = Form::findOrFail($form_id);
if (! empty($email) && empty($user)) {
return $this->respondWithError(__('messages.user_doesnt_exist'));
} elseif (! empty($user) && ($user->id == $form->created_by)) {
return $this->respondWithError(__('messages.form_cant_be_shared_with_creator'));
}
if (! empty($user) && ! empty($permissions)) {
UserForm::create([
'form_id' => $form_id,
'assigned_to' => $user->id,
'assigned_by' => \Auth::id(),
'permissions' => $permissions,
]);
}
//update form permission(user)
$edit_permissions = $request->input('edit_permissions');
$assgined_form_ids = $request->input('edit_assigned_id');
if (! empty($assgined_form_ids)) {
$non_existing_ids = [];
foreach ($assgined_form_ids as $key => $id) {
if (! empty($edit_permissions[$id])) {
$user_form = UserForm::find($id);
$user_form->permissions = $edit_permissions[$id];
$user_form->save();
} else {
$non_existing_ids[] = $id;
}
}
UserForm::whereIn('id', $non_existing_ids)
->delete();
}
}
return $this->respondSuccess(__('messages.success'));
} catch (Exception $e) {
return $this->respondWentWrong($e);
}
}
/**
* get form example views
*
* @return object
*/
public function getFormExamples()
{
if (! $this->isDemo()) {
abort(403, 'Feature disabled');
}
return view('form_examples');
}
public function validatePasswordForProtectedForm($id)
{
$nav = false;
return view('form.validate_password')
->with(compact('nav', 'id'));
}
public function postValidatePasswordForProtectedForm($id)
{
if (request()->ajax()) {
$form = Form::where('id', $id)
->orWhere('slug', $id)
->firstOrFail();
if (request()->input('password') == $form->schema['settings']['password_protection']['password']) {
$output['success'] = true;
$output['redirect'] = action([\App\Http\Controllers\FormController::class, 'show'], ['form' => $id]);
session(['validated_protected_form' => true]);
} else {
$output['success'] = false;
$output['msg'] = __('messages.invalid_password');
session(['validated_protected_form' => false]);
}
return $output;
}
}
public function toggleGlobalTemplate(Request $request)
{
if (! auth()->user()->can('superadmin')) {
abort(403, 'Unauthorized action.');
}
if (request()->ajax()) {
try {
$is_global_template = (bool) $request->input('is_checked');
$form_id = $request->input('form_id');
Form::where('id', $form_id)
->update(['is_global_template' => $is_global_template]);
$output = [
'success' => true,
'msg' => __('messages.success'),
];
} catch (Exception $e) {
$output = [
'success' => false,
'msg' => __('messages.something_went_wrong'),
];
}
return $output;
}
}
public function getAcelleMailListIds(Request $request)
{
try {
$api_token = trim($request->input('token'));
$request_uri = config('constants.ACELLE_MAIL_API').'/lists?api_token='.$api_token;
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $request_uri);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($curl);
curl_close($curl);
$result = json_decode($result, true);
$output = [
'success' => true,
'list' => $result,
'msg' => __('messages.success'),
];
} catch (Exception $e) {
$output = [
'success' => false,
'msg' => __('messages.something_went_wrong'),
];
}
return $output;
}
public function getAcelleMailListInfo(Request $request)
{
try {
$api_token = trim($request->input('token'));
$list_id = trim($request->input('list_id'));
$args = [
'api_token' => trim($api_token),
];
$request_uri = config('constants.ACELLE_MAIL_API').'/lists/'.$list_id.'?'.http_build_query($args);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $request_uri);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($curl);
curl_close($curl);
$result = json_decode($result, true);
$fields = isset($result['list']['fields']) ? $result['list']['fields'] : [];
$output = [
'success' => true,
'fields' => $fields,
'msg' => __('messages.success'),
];
} catch (Exception $e) {
$output = [
'success' => false,
'msg' => __('messages.something_went_wrong'),
];
}
return $output;
}
/**
* validate the input value for
* given element
*/
public function validateInputValue(Request $request)
{
if (
! empty($request->input('field')) &&
! empty($request->input('field_value'))
) {
$field = json_decode($request->input('field'), true);
$allowed_values = explode(PHP_EOL, $field['allowed_input']['values']);
$allowed_values = array_map('strtolower', $allowed_values);
if (in_array(strtolower($request->input('field_value')), $allowed_values)) {
echo 'true';
exit;
} else {
echo 'false';
exit;
}
}
echo 'true';
exit;
}
}

View File

@@ -0,0 +1,116 @@
<?php
namespace App\Http\Controllers;
use App\FormDataComment;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\View;
class FormDataCommentController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
//
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
try {
$input = $request->only('form_data_id', 'comment');
$input['user_id'] = \Auth::id();
$comment = FormDataComment::create($input);
$comments[] = FormDataComment::with('commentedBy')
->findOrFail($comment->id);
//generate comment html
$comment_html = View::make('form_data.partials.comment')
->with(compact('comments'))
->render();
$output = $this->respondSuccess(__('messages.success'), ['comment' => $comment_html]);
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
try {
$form_data_id = request()->input('form_data_id');
$comment = FormDataComment::where('form_data_id', $form_data_id)
->findOrFail($id);
$comment->delete();
$output = $this->respondSuccess();
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
}

View File

@@ -0,0 +1,948 @@
<?php
namespace App\Http\Controllers;
use App\Enums\User\RoleEnum;
use App\Form;
use App\FormData;
use App\Mail\FormSubmitted;
use App\Visitor;
use Carbon\Carbon;
use DB;
use DNS1D;
use DNS2D;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Str;
use Newsletter;
use PDF;
use Spatie\WebhookServer\WebhookCall;
use Image;
class FormDataController extends Controller
{
public function store($form_id, Request $request)
{
try {
//TODO: Check if form allowed to submit without login or not.
$form = Form::findOrFail($form_id);
$post_submit_action['notification'] = $form->schema['settings']['notification'];
$is_enable_recaptcha = $form->schema['settings']['recaptcha']['is_enable'];
$form_data = [];
parse_str($request->get('form_data'), $form_data['data']);
//if form is template don't save data
if ($form->is_template) {
return $this->respondSuccess($message = null, $post_submit_action);
}
//Verification for google reCaptcha
if (isset($is_enable_recaptcha) && $is_enable_recaptcha == 1) {
if (isset($form_data['data']['g-recaptcha-response']) && !empty($form_data['data']['g-recaptcha-response'])) {
//your site secret key
$secret_key = $form->schema['settings']['recaptcha']['secret_key'];
//get verify response data
$verify_response = file_get_contents('https://www.google.com/recaptcha/api/siteverify?secret=' . $secret_key . '&response=' . $form_data['data']['g-recaptcha-response']);
$response_data = json_decode($verify_response);
if (!$response_data->success) {
$msg = 'reCaptcha error';
return $this->respondWithError($msg);
}
} else {
$msg = 'reCaptcha error';
return $this->respondWithError($msg);
}
}
//If user is logged in then save user id.
if (Auth::check()) {
$form_data['submitted_by'] = $request->user()->id;
}
$form_data['source'] = 'app';
//status: if form data is draft(incomplete) or not
$form_data['status'] = 'complete';
if (isset($form_data['data']['status']) && $form_data['data']['status'] == 'incomplete') {
$form_data['status'] = 'incomplete';
}
//if token, get existing data for token
$existing_token_form_data = [];
if (!empty($request->get('token'))) {
$existing_token_form_data = FormData::where('form_id', $form->id)
->when(
$request->get('token') === 'null',
function (Builder $builder) {
$builder->whereNull('token');
}
)
->when(
$request->get('token') !== 'null',
function (Builder $builder) use ($request) {
$builder->where('token', $request->get('token'));
}
)
->findOrFail($request->get('form_data_id'));
}
//if draft(incomplete) generate token & edit form url for user
if ($form_data['status'] == 'incomplete') {
if (!empty($request->get('token'))) {
$form_data['token'] = $existing_token_form_data['token'];
} else {
$form_data['token'] = Str::random(4);
}
$post_submit_action['notification']['token'] = $form_data['token'];
$post_submit_action['notification']['form_editable_url'] = action([\App\Http\Controllers\FormController::class, 'show'], ['form' => $form->slug ?: $form_id]) . '?token=' . $form_data['token'];
} else {
//if form submission status:complete, set token as null & get form view url
$post_submit_action['notification']['view_form_url'] = action([\App\Http\Controllers\FormController::class, 'show'], ['form' => $form->slug ?: $form_id]);
$post_submit_action['notification']['qr_code_text'] = $this->_generateQrCodeText($form->schema, $form_data['data']);
$form_data['token'] = null;
}
//generate form submission reference number if enabled
$form_data['submission_ref'] = null;
if ((isset($form->schema['settings']['form_submision_ref']['is_enabled']) && $form->schema['settings']['form_submision_ref']['is_enabled']) && $form_data['status'] == 'complete') {
$start_no = $form->schema['settings']['form_submision_ref']['start_no'];
$min_digit = $form->schema['settings']['form_submision_ref']['min_digit'];
$new_count = $form->submission_count == 0 ? $start_no : ($form->submission_count + $start_no);
$number = str_pad($new_count, $min_digit, '0', STR_PAD_LEFT);
$form_data['submission_ref'] = $number;
if (isset($form->schema['settings']['form_submision_ref']['prefix'])) {
$form_data['submission_ref'] = $form->schema['settings']['form_submision_ref']['prefix'] . $form_data['submission_ref'];
}
if (isset($form->schema['settings']['form_submision_ref']['suffix'])) {
$form_data['submission_ref'] = $form_data['submission_ref'] . $form->schema['settings']['form_submision_ref']['suffix'];
}
$form_data['data']['submission_ref'] = $form_data['submission_ref'];
}
//if token exist update the submitted data
if (!empty($request->get('token'))) {
$submission = FormData::where('form_id', $form->id)
->when(
$request->get('token') === 'null',
function (Builder $builder) {
$builder->whereNull('token');
}
)
->when(
$request->get('token') !== 'null',
function (Builder $builder) use ($request) {
$builder->where('token', $request->get('token'));
}
)
->findOrFail($request->get('form_data_id'));
$submission->data = $form_data['data'];
$submission->submission_ref = $form_data['submission_ref'];
$submission->source = $form_data['source'];
$submission->status = $form_data['status'];
$submission->token = $form_data['token'];
$submission->save();
} else {
//store form data.
$submission = $form->data()->create($form_data);
}
//if submission is draft(incomplete) then return form_data_id & saved draft msg
if (isset($form_data['data']['status']) && $form_data['data']['status'] == 'incomplete') {
$post_submit_action['notification']['form_data_id'] = !empty($request->get('form_data_id')) ? $request->get('form_data_id') : $submission->id;
$post_submit_action['notification']['success_msg'] = __('messages.draft_saved');
}
//update form submission count if submission is complete
if ((isset($form->schema['settings']['form_submision_ref']['is_enabled']) && $form->schema['settings']['form_submision_ref']['is_enabled']) && $form_data['status'] == 'complete') {
$form->submission_count += 1;
$form->save();
}
//check for demo environment & form is complete or not
if (!$this->isDemo() && $form_data['status'] == 'complete') {
//Send notification for form
$emailConfig = $form->schema['emailConfig'];
//check if attachment is enabled
$attachments = [];
$signature_attachments = [];
foreach ($form->schema['form'] as $element) {
if ($element['type'] == 'file_upload' && $element['send_as_email_attachment'] == 1) {
$attachments[] = $element['name'];
}
//check if any signature field available to attach
if ($element['type'] == 'signature') {
$signature_attachments[] = [
'field_name' => $element['name'],
'label' => $element['label'],
'base_64_uri' => '',
];
}
}
//get signature attachments
if (!empty($signature_attachments)) {
foreach ($signature_attachments as $index => $signature) {
$signature_attachments[$index]['base_64_uri'] = $form_data['data'][$signature['field_name']];
}
$emailConfig['email']['signature_attachments'] = $signature_attachments;
}
if ((isset($emailConfig['email']['attach_pdf']) && $emailConfig['email']['attach_pdf']) || (isset($emailConfig['auto_response']['attach_pdf']) && $emailConfig['auto_response']['attach_pdf'])) {
$id = $submission->id;
$pdf = $this->__generatePdf($id);
$pdf_name = Str::slug($form->name, '-') . '.pdf';
}
//Set user defined SMTP : use_system_smtp = User SMTP
if (!empty($emailConfig['email']['enable']) && $emailConfig['smtp']['use_system_smtp']) {
//User SMTP
$form = Form::with('createdBy')->findOrFail($form_id);
$smtp = $form->createdBy->settings['smtp'];
config([
'mail.mailers.smtp.host' => $smtp['MAIL_HOST'],
'mail.mailers.smtp.port' => $smtp['MAIL_PORT'],
'mail.from.address' => $smtp['MAIL_FROM_ADDRESS'],
'mail.from.name' => $smtp['MAIL_FROM_NAME'],
'mail.mailers.smtp.encryption' => $smtp['MAIL_ENCRYPTION'],
'mail.mailers.smtp.username' => $smtp['MAIL_USERNAME'],
'mail.mailers.smtp.password' => $smtp['MAIL_PASSWORD']
]);
} else {
config([
'mail.mailers.smtp.host' => $emailConfig['smtp']['host'],
'mail.mailers.smtp.port' => $emailConfig['smtp']['port'],
'mail.from.address' => $emailConfig['smtp']['from_address'],
'mail.from.name' => $emailConfig['smtp']['from_name'],
'mail.mailers.smtp.encryption' => $emailConfig['smtp']['encryption'],
'mail.mailers.smtp.username' => $emailConfig['smtp']['username'],
'mail.mailers.smtp.password' => $emailConfig['smtp']['password']
]);
}
//Form submission Notification
if (!empty($emailConfig['email']['enable'])) {
//Replace the tags with values.
$temp = $this->_replaceTags(
$form_data['data'],
['subject' => $emailConfig['email']['subject'],
'body' => $emailConfig['email']['body'],],
$form['schema']['form']
);
$emailConfig['email']['subject'] = $temp['subject'];
$emailConfig['email']['body'] = $temp['body'];
//Attachments
if (!empty($attachments)) {
$emailConfig['email']['attachment'] = $this->getAttachments($attachments, $form_data['data']);
}
if ((isset($emailConfig['email']['attach_pdf']) && $emailConfig['email']['attach_pdf'])) {
$emailConfig['email']['pdf_attachment'] = $pdf;
$emailConfig['email']['pdf_name'] = $pdf_name;
}
if (
isset($emailConfig['email']['reply_to_email']) &&
!empty($emailConfig['email']['reply_to_email']) &&
!empty($form_data['data'][$emailConfig['email']['reply_to_email']])
) {
$emailConfig['email']['reply_to'] = $form_data['data'][$emailConfig['email']['reply_to_email']];
}
//get barcode & qr code attachment
if (
isset($form_data['data']['submission_ref']) &&
!empty($form_data['data']['submission_ref'])
) {
$ref_num = $form_data['data']['submission_ref'];
$emailConfig['email']['barcode'][$ref_num . '_barcode.png'] = $this->_generateSubmissionRefBarCode($form->schema, $form_data['data'], 'bar_code');
$emailConfig['email']['barcode'][$ref_num . '_qrcode.png'] = $this->_generateSubmissionRefBarCode($form->schema, $form_data['data'], 'qr_code');
}
Mail::send(new FormSubmitted($emailConfig['email']));
}
//Auto-Response
if ($emailConfig['auto_response']['is_enable']) {
//Replace the tags with values.
$temp = $this->_replaceTags(
$form_data['data'],
['subject' => $emailConfig['auto_response']['subject'],
'body' => $emailConfig['auto_response']['body'],],
$form['schema']['form']
);
$emailConfig['auto_response']['subject'] = $temp['subject'];
$emailConfig['auto_response']['body'] = $temp['body'];
//"TO" field is dynamic input value.
$emailConfig['auto_response']['to'] = isset($form_data['data'][$emailConfig['auto_response']['to']]) ? $form_data['data'][$emailConfig['auto_response']['to']] : null;
if (!empty($attachments)) {
$emailConfig['auto_response']['attachment'] = $this->getAttachments($attachments, $form_data['data']);
}
//get signature attachments
if (!empty($signature_attachments)) {
foreach ($signature_attachments as $index => $signature) {
$signature_attachments[$index]['base_64_uri'] = $form_data['data'][$signature['field_name']];
}
$emailConfig['auto_response']['signature_attachments'] = $signature_attachments;
}
if ((isset($emailConfig['auto_response']['attach_pdf']) && $emailConfig['auto_response']['attach_pdf'])) {
$emailConfig['auto_response']['pdf_attachment'] = $pdf;
$emailConfig['auto_response']['pdf_name'] = $pdf_name;
}
//get barcode & qr code attachment for response
if (
isset($form_data['data']['submission_ref']) &&
!empty($form_data['data']['submission_ref'])
) {
$ref_num = $form_data['data']['submission_ref'];
$emailConfig['auto_response']['barcode'][$ref_num . '_barcode.png'] = $this->_generateSubmissionRefBarCode($form->schema, $form_data['data'], 'bar_code');
$emailConfig['auto_response']['barcode'][$ref_num . '_qrcode.png'] = $this->_generateSubmissionRefBarCode($form->schema, $form_data['data'], 'qr_code');
}
if (!empty($emailConfig['auto_response']['to'])) {
Mail::send(new FormSubmitted($emailConfig['auto_response']));
}
}
//Send data to mailchimp if enabled.
if (!empty($form->mailchimp_details['is_enable']) && $form->mailchimp_details['is_enable'] == 1) {
//Set config details.
config(['newsletter.apiKey' => $form->mailchimp_details['api_key']]);
config(['newsletter.lists.subscribers.id' => $form->mailchimp_details['list_id']]);
//Subscribe if email is set.
if (isset($form_data['data'][$form->mailchimp_details['email_field']]) && !empty($form_data['data'][$form->mailchimp_details['email_field']])) {
//Get dynamic field from form input.
$email = $form_data['data'][$form->mailchimp_details['email_field']];
//explode name to get first & last name
$name = explode(' ', $form_data['data'][$form->mailchimp_details['name_field']], 2);
$fname = $name[0];
$lname = !empty($name[1]) ? $name[1] : '';
if ($form->mailchimp_details['status'] == 'subscribe') {
Newsletter::subscribe($email, ['FNAME' => $fname, 'LNAME' => $lname]);
} elseif ($form->mailchimp_details['status'] == 'subscribe_pending') {
Newsletter::subscribePending($email, ['FNAME' => $fname, 'LNAME' => $lname]);
}
}
}
$this->__addSubscriberInAcelleMail($form, $form_data['data']);
}
$this->_sendWebhook($submission);
//get qr & bar code for submission ref
$data['sub_ref_qr_code'] = $this->_generateSubmissionRefBarCode($form->schema, $form_data['data'], 'qr_code');
$data['sub_ref_bar_code'] = $this->_generateSubmissionRefBarCode($form->schema, $form_data['data'], 'bar_code');
$post_submit_action['notification']['submission_msg'] = $this->_generateSubmissionMsgHtml($form, $data);
return $this->respondSuccess($message = null, $post_submit_action);
} catch (\Exception $e) {
return $this->respondWentWrong($e);
}
}
private function __addSubscriberInAcelleMail($form, $data)
{
$acelle_mail_info = $form->acelle_mail_info;
if (
!empty($acelle_mail_info) &&
$acelle_mail_info['is_enable'] &&
!empty($acelle_mail_info['api_token']) &&
!empty($acelle_mail_info['list_id']) &&
!empty($acelle_mail_info['campaign_fields'])
) {
$args = [
'api_token' => trim($acelle_mail_info['api_token']),
'list_uid' => $acelle_mail_info['list_id'],
];
foreach ($acelle_mail_info['campaign_fields'] as $field) {
if (!empty($field['key']) && !empty($field['param_field_name']) && !empty($data[$field['param_field_name']])) {
$args[$field['key']] = is_array($data[$field['param_field_name']]) ? implode(', ', $data[$field['param_field_name']]) : strip_tags($data[$field['param_field_name']]);
}
}
$request_uri = config('constants.ACELLE_MAIL_API') . '/subscribers' . '?' . http_build_query($args);
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $request_uri);
curl_setopt($curl, CURLOPT_POST, true);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
$result = curl_exec($curl);
curl_close($curl);
}
}
protected function _sendWebhook($submission)
{
$webhook = $submission->form->webhook_info;
if (
isset($webhook['is_enable']) &&
$webhook['is_enable'] &&
!empty($webhook['url']) &&
!empty($webhook['secret_key'])
) {
//get playload to send
$payload = $submission->toArray();
//unset unused data
unset($payload['submitted_by'], $payload['form'], $payload['token']);
WebhookCall::create()
->url($webhook['url'])
->useSecret($webhook['secret_key'])
->timeoutInSeconds(5)
->maximumTries(5)
->doNotVerifySsl()
->payload($payload)
->dispatchSync();
}
}
protected function _getBarCodeForRefNum($ref_num = '', $type = 'qr_code', $is_img_format = false)
{
if (!empty($ref_num) && in_array($type, ['bar_code'])) {
$bar_code = (string)Image::canvas(305, 150, "#fff")->insert(base64_decode(DNS1D::getBarcodePNG($ref_num, 'C128', 2.5, 100, array(27, 41, 75), true)), 'center')->encode('data-url');
return $is_img_format ? '<img src="' . $bar_code . '" alt="barcode"/>' : $bar_code;
}
if (!empty($ref_num) && in_array($type, ['qr_code'])) {
$qr_code = (string)Image::canvas(300, 300, "#fff")->insert(base64_decode(DNS2D::getBarcodePNG($ref_num, 'QRCODE', 12, 12, array(27, 41, 75), true)), 'center')->encode('data-url');
return $is_img_format ? '<img src="' . $qr_code . '" alt="qrcode"/>' : $qr_code;
}
return '';
}
protected function _replaceTags($form_data, $strings, $form_schema)
{
$ref_num = $form_data['submission_ref'] ?? '';
$ref_num_qr_code = $this->_getBarCodeForRefNum($ref_num, 'qr_code', true);
$ref_num_bar_code = $this->_getBarCodeForRefNum($ref_num, 'bar_code', true);
foreach ($form_data as $name => $value) {
foreach ($strings as $key => $string) {
//If value is array(like for multiselect or checkbox) then implode it.
$value = is_array($value) ? implode(',', $value) : $value;
$string = str_replace('__' . $name . '__', $value, $string);
//replace qr/bar code
$string = str_replace('__submission_ref_qr_code__', $ref_num_qr_code, $string);
$string = str_replace('__submission_ref_bar_code__', $ref_num_bar_code, $string);
$strings[$key] = $string;
}
}
return $strings;
}
public function show($form_id, Request $request)
{
$user_id = $request->user()->id;
$form = Form::findOrFail($form_id);
$data = FormData::query()
->where('form_id', $form_id)
->orderBy('created_at', 'desc')
->get()
->filter(function (FormData $formData) use ($request) {
$date = null;
if (is_array($formData->data)) {
$dates = array_filter($formData->data, function ($item) {
return is_string($item) && strtotime($item);
});
if (!empty($dates)) {
$firstDate = reset($dates);
$date = strtotime($firstDate);
}
}
if (!$date) {
return false; // Skip entries without valid dates
}
$dateStr = Carbon::createFromTimestamp($date)->toDateString();
$isValidStartDate = $request->filled('start_date')
? $request->get('start_date') <= $dateStr
: Carbon::now()->subDays(7)->toDateString() <= $dateStr;
$isValidEndDate = $request->filled('end_date')
? $request->get('end_date') >= $dateStr
: Carbon::now()->toDateString() >= $dateStr;
return $isValidStartDate && $isValidEndDate;
});
// ->when(
// $request->filled('start_date'),
// function (Builder $builder) use ($request) {
// $startDate = Carbon::createFromFormat('Y-m-d', $request->get('start_date'));
//
// $builder->where('created_at', '>=', $startDate->toDateTimeString());
// }
// )
// ->when(
// $request->filled('end_date'),
// function (Builder $builder) use ($request) {
// $endDate = Carbon::createFromFormat('Y-m-d', $request->get('end_date'));
//
// $builder->where('created_at', '<=', $endDate->toDateTimeString());
// }
// )
// ->when(
// !$request->filled('start_date'),
// function (Builder $builder) {
// $builder->where('created_at', '>=', Carbon::now()->subDays(7)->toDateTimeString());
// }
// )
// ->when(
// !$request->filled('end_date'),
// function (Builder $builder) {
// $builder->where('created_at', '<=', Carbon::now()->toDateTimeString());
// }
// );
//check permission if user is not a creator
$has_permission = ($form->created_by != $user_id) ? $this->doUserHavePermission($form->id, 'can_view_data') : true;
if (!$form->created_by !== $user_id && !auth()->user()->hasRole([RoleEnum::SUPERVISOR->value, RoleEnum::ADMIN->value]) && !$has_permission) {
abort(404);
}
return view('form_data.show')
->with(compact('form', 'data', 'has_permission')); // Добавьте has_permission
}
public function viewData($id)
{
if (request()->ajax()) {
$form_data = FormData::with(['form', 'submittedBy',
'comments' => function ($q) {
$q->latest();
},
'comments.commentedBy',])->findOrFail($id);
return view('form_data.view_form_data')
->with(compact('form_data'));
}
}
public function destroy($id)
{
try {
if (request()->ajax()) {
$form_data = FormData::findOrFail($id);
$form_data->delete();
$form_data->comments()->delete();
}
return $this->respondSuccess(__('messages.deleted_successfully'));
} catch (Exception $e) {
return $this->respondWentWrong($e);
}
}
public function getAttachments($attachment_fields, $form_data)
{
$attachments = [];
foreach ($attachment_fields as $attachment_field) {
foreach ($form_data as $key => $values) {
if ($attachment_field == $key) {
foreach ($values as $key => $value) {
$uploaded_file = explode(',', $value);
}
$attachments = array_merge($attachments, $uploaded_file);
}
}
}
return $attachments;
}
/**
* return the data to display
* a report
*/
public function getReport($id)
{
$user = request()->user();
$form = Form::with('data')
->findOrFail($id);
//check permission if user is not a creator
$has_permission = ($form->created_by != $user->id) ? $this->doUserHavePermission($form->id, 'can_view_data') : true;
if (!$has_permission) {
abort(404);
}
$charts = [];
if (isset($form->schema['form'])) {
foreach ($form->schema['form'] as $element) {
if (in_array($element['type'], ['dropdown', 'radio', 'checkbox', 'rating'])) {
//chart title
$charts[$element['name']]['name'] = $element['label'];
//getting given option
if ($element['type'] == 'rating') {
$all_rating = implode(',', range($element['min_rating'], $element['max_rating'], $element['increment']));
$options = explode(',', $all_rating);
} else {
$options = explode(PHP_EOL, $element['options']);
}
$dropdowns = [];
foreach ($options as $key => $value) {
$dropdowns[$value] = 0;
}
//generating report
foreach ($options as $option) {
foreach ($form->data as $submitted_data) {
if (isset($submitted_data['data'][$element['name']]) && ($submitted_data['data'][$element['name']] == $option)) {
$dropdowns[$option] += 1;
} elseif (isset($submitted_data['data'][$element['name']]) && is_array($submitted_data['data'][$element['name']]) && in_array($option, $submitted_data['data'][$element['name']])) {
$dropdowns[$option] += 1;
}
}
}
//storing report
$charts[$element['name']]['values'] = $dropdowns;
}
}
}
$visitors_chart = $this->_generateVisiorsLineChartData($form->id);
$referrers_chart = $this->_generateVisiorsPieChartData($form->id);
return view('form_data.report')
->with(compact('charts', 'form', 'visitors_chart', 'referrers_chart'));
}
private function __getVisitorsReport($form_id, $chart_type)
{
$query = Visitor::where('form_id', $form_id)
->whereBetween(DB::raw('date(created_at)'), [Carbon::now()->subDays(30), Carbon::now()])
->select(
DB::raw('count(form_id) as total_visits'),
DB::raw('SUM(IF(is_unique = 1,1,0)) as unique_visits')
);
if ($chart_type == 'line') {
$query->addSelect(DB::raw('Date(created_at) as date'))
->groupBy(DB::raw('Date(created_at)'));
} elseif ($chart_type == 'pie') {
$query->addSelect('referrer')
->groupBy('referrer');
}
$visitors = $query->get();
return $visitors;
}
protected function _generateVisiorsLineChartData($form_id)
{
$visitors = $this->__getVisitorsReport($form_id, 'line');
//generate all the labels between 30 days
$dates = [];
$labels = [];
for ($i = 29; $i >= 0; $i--) {
$date = Carbon::now()->subDays($i)->format('Y-m-d');
$dates[] = $date;
$labels[] = date('j M Y', strtotime($date));
}
//get total & unique visits for last 30 days
$total_visits_in_last_30_days = [];
$unique_visits_in_last_30_days = [];
foreach ($dates as $key => $date) {
$visitor_date = null;
$total_visits = 0;
$unique_visits = 0;
foreach ($visitors as $key => $visitor) {
if ($visitor['date'] == $date) {
$visitor_date = $visitor['date'];
$total_visits = $visitor['total_visits'];
$unique_visits = $visitor['unique_visits'];
break;
}
}
//if date match store values
if ($visitor_date == $date) {
$total_visits_in_last_30_days[] = (float)$total_visits;
$unique_visits_in_last_30_days[] = (float)$unique_visits;
} else {
$total_visits_in_last_30_days[] = 0;
$unique_visits_in_last_30_days[] = 0;
}
}
$charts_data = [
'title' => __('messages.total_visitors_in_last_30_days'),
'labels' => $labels,
'total_visits_label' => __('messages.total_visits'),
'total_visits' => $total_visits_in_last_30_days,
'unique_visits_label' => __('messages.unique_visits'),
'unique_visits' => $unique_visits_in_last_30_days,
];
return $charts_data;
}
public function _generateVisiorsPieChartData($form_id)
{
$visitors = $this->__getVisitorsReport($form_id, 'pie');
//get referrer as key value
$referrers = [];
foreach ($visitors as $key => $visitor) {
if (!empty($visitor->referrer)) {
$referrers[] = ['name' => $visitor->referrer, 'y' => $visitor->total_visits];
} else {
$referrers[] = ['name' => __('messages.direct_visits'), 'y' => $visitor->total_visits];
}
}
$charts_data = [
'name' => __('messages.referrers_in_last_30_days'),
'values' => $referrers,
];
return $charts_data;
}
private function __generatePdf($id)
{
$form_data = FormData::with(['form', 'submittedBy'])->findOrFail($id);
$pdf_header = $this->__replacePdfTags($form_data);
$pdf = PDF::loadView('form_data.partials.form_data_pdf',
['form_data' => $form_data, 'pdf_header' => $pdf_header]
);
return $pdf;
}
public function downloadPdf($id)
{
$formData = FormData::with(['form'])->find($id);
if (!(auth()->user()->can('superadmin') || $this->checkIfUserIsCreatorOfGivenForm($formData->form->id) || $this->doUserHavePermission($formData->form->id, 'can_view_data'))) {
abort(403, 'Unauthorized action.');
}
$pdf = $this->__generatePdf($id);
return $pdf->stream('document.pdf');
}
public function getEditformData($id_or_slug, $data_id)
{
$form = Form::where('id', $id_or_slug)
->orWhere('slug', $id_or_slug)
->first();
if (empty($form)) {
abort(404);
}
//if submitted data id available get submitted data
if (!empty($data_id)) {
$form->load(['data' => function ($query) use ($data_id, $form) {
$query->where('id', $data_id)
->where('form_id', $form->id);
}]);
}
$nav = false;
$action_by = auth()->user()->roles->first()->name;
return view('form.show')
->with(compact('form', 'nav', 'action_by'));
}
public function postEditformData($form_id, $data_id)
{
try {
$form = Form::findOrFail($form_id);
$form_data = [];
parse_str(request()->get('form_data'), $form_data['data']);
if (!empty($data_id)) {
$existing_data = FormData::where('form_id', $form->id)
->findOrFail($data_id);
$existing_data->data = $form_data['data'];
$existing_data->save();
}
$post_submit_action['notification'] = $form->schema['settings']['notification'];
$post_submit_action['notification']['success_msg'] = __('messages.success');
$post_submit_action['notification']['post_submit_action'] = 'redirect';
$post_submit_action['notification']['redirect_url'] = action([\App\Http\Controllers\FormDataController::class, 'show'], ['id' => $form->id]);
return $this->respondSuccess(null, $post_submit_action);
} catch (Exception $e) {
return $this->respondWentWrong($e);
}
}
private function __replacePdfTags($form_data)
{
$header = '';
if (
isset($form_data->form['schema']['settings']['pdf_design']) &&
!empty($form_data->form['schema']['settings']['pdf_design']['header'])
) {
$form_name = $form_data->form->name;
$submission_date = Carbon::parse($form_data->created_at)->format(config('constants.APP_DATE_FORMAT', 'Y-m-d'));
$replacable_tags = $form_data->form['schema']['settings']['pdf_design']['header'];
$header = preg_replace(['/{form_name}/', '/{submission_date}/'], [$form_name, $submission_date], $replacable_tags);
}
return $header;
}
protected function _generateSubmissionMsgHtml($form, $data)
{
$submission_msg = View::make('form_data.partials.submission_msg')
->with(compact('form', 'data'))
->render();
return $submission_msg;
}
protected function _generateQrCodeText($schema, $form_data)
{
$string = '';
$array = [];
if (
!in_array($schema['settings']['notification']['post_submit_action'], ['redirect']) &&
isset($schema['settings']['is_qr_code_enabled']) &&
$schema['settings']['is_qr_code_enabled']
) {
foreach ($schema['form'] as $key => $element) {
if (
!in_array($element['type'], ['heading', 'html_text', 'hr', 'signature', 'file_upload'])
) {
if (
isset($form_data[$element['name']]) &&
!is_array($form_data[$element['name']]) &&
!empty($form_data[$element['name']])
) {
//check data format & set data
if (
isset($schema['settings']['qr_code_data_format']) &&
in_array($schema['settings']['qr_code_data_format'], ['json'])
) {
$array[$element['label']] = strip_tags($form_data[$element['name']]);
} else {
$string .= $element['label'] . ': ' . strip_tags($form_data[$element['name']]) . ', ';
}
} elseif (
isset($form_data[$element['name']]) &&
is_array($form_data[$element['name']]) &&
!empty($form_data[$element['name']])
) {
//check data format & set data
if (
isset($schema['settings']['qr_code_data_format']) &&
in_array($schema['settings']['qr_code_data_format'], ['json'])
) {
$array[$element['label']] = implode(', ', $form_data[$element['name']]);
} else {
$string .= $element['label'] . ': ' . implode(', ', $form_data[$element['name']]) . ', ';
}
}
}
if (
in_array($element['type'], ['file_upload']) &&
isset($form_data[$element['name']]) &&
!empty($form_data[$element['name']])
) {
//convert file name to an array
$file = implode($form_data[$element['name']]);
$files = explode(',', $file);
$file_urls = [];
foreach ($files as $key => $value) {
$file_urls[] = \Storage::url(config('constants.doc_path') . '/' . $value);
}
//check data format & set data
if (
isset($schema['settings']['qr_code_data_format']) &&
in_array($schema['settings']['qr_code_data_format'], ['json'])
) {
$array[$element['label']] = implode(', ', $file_urls);
} else {
$string .= $element['label'] . ': ' . implode(', ', $file_urls) . ', ';
}
}
}
}
if (
isset($schema['settings']['qr_code_data_format']) &&
in_array($schema['settings']['qr_code_data_format'], ['json'])
) {
return json_encode($array);
} else {
return $string;
}
}
protected function _generateSubmissionRefBarCode($schema, $data, $type = 'bar_code')
{
if (
in_array($type, ['bar_code']) &&
isset($schema['settings']['is_ref_num_bar_code_enabled']) &&
$schema['settings']['is_ref_num_bar_code_enabled']
) {
$ref_num = $data['submission_ref'] ?? '';
return $this->_getBarCodeForRefNum($ref_num, $type);
}
if (
in_array($type, ['qr_code']) &&
isset($schema['settings']['is_ref_num_qr_code_enabled']) &&
$schema['settings']['is_ref_num_qr_code_enabled']
) {
$ref_num = $data['submission_ref'] ?? '';
return $this->_getBarCodeForRefNum($ref_num, $type);
}
}
}

View File

@@ -0,0 +1,200 @@
<?php
namespace App\Http\Controllers;
class FormPhpGenerator
{
public function __construct()
{
}
public function phpStart()
{
$php = '<?php '.PHP_EOL;
//Check if the form is submitted or not.
$php .=
'
if (empty($_POST)) {
die("Permission denied.");
}
include_once("library/library.php");
ini_set("display_errors", "0");
'.PHP_EOL.PHP_EOL;
return $php;
}
//TODO:Add validation as per form data.
public function renderFormPhp()
{
}
public function reCaptcha($secret)
{
$php =
'
$is_error = verifyReCaptcha(\''.$secret.'\', $_POST["g-recaptcha-response"]);
if(!empty($is_error)){
echo json_encode($is_error);
exit;
}'.PHP_EOL.PHP_EOL;
return $php;
}
public function attachmentFields($elements)
{
$fields = [];
if (! empty($elements)) {
foreach ($elements as $element) {
if ($element['type'] == 'file_upload' && $element['send_as_email_attachment'] == 1) {
$fields[] = $element['name'];
}
}
}
$php = '//check for send as attachment'.PHP_EOL;
$php .= '
$attachment_fields = json_decode(\''.json_encode($fields).'\', true);'.PHP_EOL;
$php .= '$attachments = getAttachments($attachment_fields);'.PHP_EOL;
return $php;
}
public function email($email_details, $smtp, $response_variable, $is_autoresponse = false, $signature_fields = [])
{
$php = '//Sending email Start';
//Initialise variables. TODO: Make variable names as dynamic.
$php .= '
$smtp = json_decode(\''.json_encode($smtp).'\', true);
$email_details = json_decode(\''.json_encode($email_details).'\', true);'.PHP_EOL.PHP_EOL;
//If auto-response then Get email "TO" value from users input
if ($is_autoresponse) {
$php .= '
$email_details["to"] = !empty($_POST["fields"][$email_details["to"]]) ? $_POST["fields"][$email_details["to"]] : $email_details["from"];'.PHP_EOL.PHP_EOL;
}
//Replace tags from subject, body
$php .=
'$temp = replaceTags($_POST["fields"], array("subject" => $email_details["subject"], "body" => $email_details["body"]), json_decode(\''.json_encode($signature_fields).'\', true));
$email_details["subject"] = $temp["subject"];
$email_details["body"] = $temp["body"];
'.PHP_EOL.PHP_EOL;
//Send email.
$php .= $response_variable.' = sendEmail($email_details, $smtp, $attachments);'.PHP_EOL.PHP_EOL;
$php .= '//Sending email Start End'.PHP_EOL.PHP_EOL;
return $php;
}
public function phpEnd($notification, $email_variable)
{
$php =
'if( '.$email_variable.' ){'.PHP_EOL;
//For display message
if (empty($notification['post_submit_action']) || $notification['post_submit_action'] == 'same_page') {
$php .=
'$output = array("success" => 1,
"redirect" => 0,
"msg" => "'.$notification['success_msg'].'"
);'.PHP_EOL;
}
//For redirect
if ($notification['post_submit_action'] == 'redirect') {
$php .=
'$output = array("success" => 1,
"redirect" => 1,
"url" => "'.$notification['redirect_url'].
'");'.PHP_EOL;
}
$php .=
'echo json_encode($output); exit;
} else {'.PHP_EOL;
$php .=
'$output = array("success" => 0,
"error" => "Email error",
"msg" => "'.$notification['failed_msg'].'",
);
echo json_encode($output); exit;'.PHP_EOL;
$php .= '}
?>';
return $php;
}
public function databaseIntegration($form_data)
{
if (isset($form_data['enable_database']) && ($form_data['enable_database'] == '1')) {
// $php =
// 'if( $email_sent ){' . PHP_EOL;
$php = 'try{
require_once ("config.php");'.PHP_EOL;
$php .= '$db_data = array();'.PHP_EOL;
if (! empty($form_data['db_data'])) {
foreach ($form_data['db_data'] as $db_data) {
$column = $db_data['column'];
$col_val = $db_data['value'];
$php .= '$db_data["'.$column.'"] = "'.$col_val.'";'.PHP_EOL;
}
}
$php .=
'$insert_data = array();
foreach( $db_data as $col => $col_val){
foreach($_POST["fields"] as $key => $value){
if(is_array($value)){
$value = implode(" ,", $value);
}
$col_val = str_replace( "__" . $key . "__", htmlspecialchars( $value ) , $col_val);
}
$insert_data[$col] = $col_val;
}'.PHP_EOL;
$php .= 'if(!empty($insert_data)){
$insert_id = $db->insert ("'.$form_data['db_table'].'" , $insert_data);
if ($insert_id){
$db_msg = "Data inserted successfully";
$db_success = 1;
} else {
$db_msg = "Insert failed: " . $db->getLastError();
$db_success = 0;
}
}'.PHP_EOL;
$php .= '}catch(Exception $e){
$db_msg = "Database insert failed" . $e->getMessage();
$db_success = 0;
}'.PHP_EOL;
return $php;
} else {
return '';
}
}
public function generateDatabaseConfig($form_data)
{
if (isset($form_data['enable_database']) && ($form_data['enable_database'] == '1')) {
$php =
'<?php'.PHP_EOL;
$php .= 'require_once ("MysqliDb.php");'.PHP_EOL;
$php .= '$db = new MysqliDb ( "'.$form_data['db_host'].'", "'.$form_data['db_username'].'", "'.$form_data['db_password'].'", "'.$form_data['db_name'].'" );'.PHP_EOL;
$php .= '?>';
return $php;
} else {
return '';
}
}
}

View File

@@ -0,0 +1,329 @@
<?php
namespace App\Http\Controllers;
use App\Enums\User\RoleEnum;
use App\Form;
use App\Mail\TestEmail;
use App\PackageSubscription;
use App\UserForm;
use Carbon\Carbon;
use DB;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Mail;
use Yajra\DataTables\Facades\DataTables;
class HomeController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth');
}
/**
* Show the application dashboard.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function index()
{
$user = request()->user();
if (auth()->user()->hasRole(RoleEnum::SUPERVISOR->value, 'web')) {
$forms = Form::query()
->withCount('data')
->where('is_template', 0)
->get();
} else {
$forms = Form::query()
->withCount('data')
->where('is_template', 0)
->where('created_by', $user->id)
->groupBy('id');
}
if (request()->ajax()) {
$subscription = PackageSubscription::activeSubscription($user->id);
if (auth()->user()->hasRole(RoleEnum::SUPERVISOR->value, 'web')) {
$forms = Form::query()
->withCount('data')
->where('is_template', 0)
->get();
} else {
$forms = Form::query()
->withCount('data')
->where('is_template', 0)
->where('created_by', $user->id)
->get();
}
return DataTables::of($forms)
->addColumn(
'action',
function (Form $form) use ($subscription, $user) {
$action = '';
if (!empty($form->schema)) {
$action = '<a href="' . action([\App\Http\Controllers\FormController::class, 'show'], ['form' => $form->slug ?: $form->id]) . '"' . '
target="_blank"
class="btn btn-sm btn-info m-1" data-toggle="tooltip" title="' . __('messages.view') . '">
<i class="fa fa-eye" aria-hidden="true"></i>
</a>';
}
$action .= '<a href="' . action([\App\Http\Controllers\FormController::class, 'edit'], ['form' => $form->id]) . '"' . '
class="btn btn-sm btn-warning m-1" data-toggle="tooltip" title="' . __('messages.edit') . '">
<i class="fa fa-edit" aria-hidden="true"></i>
</a>';
if (auth()->user()->hasRole(RoleEnum::SUPERVISOR->value) || auth()->user()->id === $form->created_by) {
$action .= '<button type="button" data-href="' . action([\App\Http\Controllers\FormController::class, 'destroy'], ['form' => $form->id]) . '"' . ' class="btn btn-sm btn-danger delete_form m-1" data-toggle="tooltip"
title="' . __('messages.delete') . '">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>';
}
if (auth()->user()->hasRole(RoleEnum::SUPERVISOR->value) || auth()->user()->id === $form->created_by) {
$action .= '<button type="button" data-href="' . action([\App\Http\Controllers\FormController::class, 'copyForm'], ['id' => $form->id]) . '"' . ' class="btn btn-sm btn-primary copy_form m-1" data-toggle="tooltip"
title="' . __('messages.copy_this_form') . '">
<i class="fas fa-copy"></i>
</button>';
}
if (auth()->user()->hasRole(RoleEnum::SUPERVISOR->value) || auth()->user()->id === $form->created_by) {
$action .= '<button type="button" data-href="' . action([\App\Http\Controllers\FormController::class, 'generateWidget'], ['id' => $form->id]) . '"' . ' class="btn btn-sm btn-info generate_widget m-1" data-toggle="tooltip"
title="' . __('messages.widget') . '">
<i class="fa fa-random" aria-hidden="true"></i>
</button>';
}
$action .= '<a href="' . action([\App\Http\Controllers\FormDataController::class, 'show'], ['id' => $form->id]) . '"' . '"
class="btn btn-sm btn-success m-1" data-toggle="tooltip" title="' . __('messages.view_form_data') . '">
<i class="fa fa-list" aria-hidden="true"></i>
</a>';
if (auth()->user()->hasRole(RoleEnum::SUPERVISOR->value) || auth()->user()->id === $form->created_by) {
$action .= '<a href="' . action([\App\Http\Controllers\FormController::class, 'downloadCode'], ['id' => $form->id]) . '"' . '" class="btn btn-sm btn-dark m-1" data-toggle="tooltip"
title="' . __('messages.download_code') . '">
<i class="fas fa-download" aria-hidden="true"></i>
</a>';
}
if (auth()->user()->hasRole(RoleEnum::SUPERVISOR->value) || auth()->user()->id === $form->created_by) {
$action .= '<a data-href="' . action([\App\Http\Controllers\FormController::class, 'getCollab'], ['id' => $form->id]) . '"' . 'class="btn btn-sm btn-primary m-1 collab_btn" data-toggle="tooltip" title="' . __('messages.collaborate') . '">
<i class="fas fa-handshake text-white" aria-hidden="true"></i>
</a>';
}
return $action;
}
)
->editColumn('created_at', function ($row) {
$date_format = config('constants.APP_DATE_FORMAT');
if (config('constants.APP_TIME_FORMAT') == '12') {
$date_format .= ' h:i A';
} elseif (config('constants.APP_TIME_FORMAT') == '24') {
$date_format .= ' H:i';
} else {
$date_format = 'm/d/Y h:i A';
}
return !empty($row->created_at) ? Carbon::createFromTimestamp(strtotime($row->created_at))->format($date_format) : null;
})
->editColumn('data_count', function ($row) {
return $row->data_count;
})
->editColumn('name', function ($row) {
$html = $row->name;
if (empty($row->schema)) {
$html .= '<br><small class="text-danger">(' . (__('messages.form_is_incomplete')) . ')</small>';
}
return $html;
})
->removeColumn('id')
->rawColumns(['action', 'created_at', 'data_count', 'name'])
->make(true);
}
//Count forms
$form_count = $forms->count();
//Count templates.
$template_count = Form::where('created_by', $user->id)
->where('is_template', 1)
->count();
//Count submissions.
$submission_count = Form::join('form_data as fd', 'forms.id', '=', 'fd.form_id')
->where('is_template', 0)
->when(auth()->user()->hasRole(RoleEnum::USER->value), function (Builder $builder) use ($user) {
$builder->where('created_by', $user->id);
})
->count();
return view('home')
->with(compact('form_count', 'template_count', 'submission_count'));
}
/**
* Show Form Template
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function getTemplate()
{
if (request()->ajax()) {
$user_id = request()->user()->id;
$forms = Form::select('name', 'description', 'id', 'slug', 'is_global_template')
->where(function ($query) use ($user_id) {
$query->where('is_template', 1)
->where('created_by', $user_id)
->orWhere('is_global_template', 1);
})
->groupBy('id');
return DataTables::of($forms)
->addColumn('action', function ($row) {
$action = '<a href="' . action([\App\Http\Controllers\FormController::class, 'show'], ['form' => $row->slug ?: $row->id]) . '"' . '
target="_blank"
class="btn btn-sm btn-info m-1" data-toggle="tooltip" title="' . __('messages.view') . '">
<i class="fa fa-eye" aria-hidden="true"></i>
</a>';
if (!$row->is_global_template || auth()->user()->can('superadmin')) {
$action .= '<a href="' . action([\App\Http\Controllers\FormController::class, 'edit'], ['form' => $row->id]) . '"' . '
class="btn btn-sm btn-warning m-1" data-toggle="tooltip" title="' . __('messages.edit') . '">
<i class="fa fa-edit" aria-hidden="true"></i>
</a>
<button type="button" data-href="' . action([\App\Http\Controllers\FormController::class, 'destroy'], ['form' => $row->id]) . '"' . ' class="btn btn-sm btn-danger delete_template m-1" data-toggle="tooltip"
title="' . __('messages.delete') . '">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>';
}
return $action;
})
->editColumn('is_global_template', function ($row) {
if (auth()->user()->can('superadmin')) {
$checked = $row->is_global_template ? 'checked' : '';
$html = '<div class="form-check">
<input class="form-check-input toggle_global_template" type="checkbox" value="1" ' . $checked . ' data-form_id="' . $row->id . '">
</div>';
return $html;
}
})
->editColumn('name', function ($row) {
$name = $row->name;
if ($row->is_global_template) {
$name .= '<br><span class="badge badge-pill badge-info">' . __('messages.pre_made') . '</span>';
}
return $name;
})
->removeColumn('id')
->rawColumns(['action', 'is_global_template', 'name'])
->make(true);
}
return view('home');
}
/**
* Tests if SMTP connection details is correct or not.
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function testSMTP()
{
try {
//Set the default config.
config([
'mail.mailers.smtp.host' => request()->host,
'mail.mailers.smtp.port' => request()->port,
'mail.from.address' => request()->from_address,
'mail.from.name' => request()->from_name,
'mail.mailers.smtp.encryption' => request()->encryption,
'mail.mailers.smtp.username' => request()->username,
'mail.mailers.smtp.password' => request()->password
]);
Mail::to(request()->from_address)
->send(new TestEmail());
return $this->respondSuccess();
} catch (\Exception $e) {
return $this->respondWithError($e->getMessage());
}
}
/**
* Show assigned forms
*
* @return \Illuminate\Contracts\Support\Renderable
*/
public function getAssignedForms(Request $request)
{
if ($request->ajax()) {
$forms = UserForm::join('forms', 'user_forms.form_id', '=', 'forms.id')
->leftJoin('users', 'forms.created_by', '=', 'users.id')
->where('user_forms.assigned_to', \Auth::id())
->select('user_forms.permissions as permissions', 'forms.name as name', 'forms.description as description', 'forms.id as form_id', 'forms.created_at as created_at', 'forms.slug as slug', 'users.name as created_by');
return DataTables::of($forms)
->addColumn(
'action',
function ($row) {
$action = '';
if ((!empty($row->permissions) && in_array('can_view_form', $row->permissions)) || auth()->user()->hasRole(RoleEnum::SUPERVISOR->value)) {
$action = '<a href="' . action([\App\Http\Controllers\FormController::class, 'show'], ['form' => $row->slug ?: $row->form_id]) . '"' . '
target="_blank"
class="btn btn-sm btn-info m-1" data-toggle="tooltip" title="' . __('messages.view') . '">
<i class="fa fa-eye" aria-hidden="true"></i>
</a>';
}
if ((!empty($row->permissions) && in_array('can_design_form', $row->permissions)) || auth()->user()->hasRole(RoleEnum::SUPERVISOR->value)) {
$action .= '<a href="' . action([\App\Http\Controllers\FormController::class, 'edit'], ['form' => $row->form_id]) . '"' . '
class="btn btn-sm btn-warning m-1" data-toggle="tooltip" title="' . __('messages.edit') . '">
<i class="fa fa-edit" aria-hidden="true"></i>
</a>';
}
if ((!empty($row->permissions) && in_array('can_view_data', $row->permissions)) || auth()->user()->hasRole(RoleEnum::SUPERVISOR->value)) {
$action .= '<a href="' . action([\App\Http\Controllers\FormDataController::class, 'show'], ['id' => $row->form_id]) . '"' . '"
class="btn btn-sm btn-success m-1" data-toggle="tooltip" title="' . __('messages.view_form_data') . '">
<i class="fa fa-list" aria-hidden="true"></i>
</a>';
}
if (auth()->user()->hasRole(RoleEnum::SUPERVISOR->value)) {
$action .= '<a href="' . action([\App\Http\Controllers\FormDataController::class, 'getReport'], ['id' => $row->form_id]) . '"' . '"
target="_blank"
class="btn btn-sm btn-success m-1" data-toggle="tooltip" title="' . __('messages.report') . '">
<i class="fas fa-chart-pie" aria-hidden="true"></i>
</a>';
}
return $action;
}
)
->editColumn('created_by', function ($row) {
return ucfirst($row->created_by);
})
->removeColumn(['id', 'permissions'])
->rawColumns(['action', 'created_by'])
->make(true);
}
}
}

View File

@@ -0,0 +1,267 @@
<?php
namespace App\Http\Controllers;
use Composer\Semver\Comparator;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Artisan;
use Illuminate\Support\Facades\DB;
use Symfony\Component\Console\Output\BufferedOutput;
class IController extends Controller
{
protected $appVersion;
/**
* Constructor
*
* @return void
*/
public function __construct()
{
$this->appVersion = config('author.app_version');
}
public function index()
{
//Check for .env file
$this->isInstalled();
$this->installSettings();
return view('ic.index');
}
public function details()
{
//Check for .env file
$this->isInstalled();
$this->installSettings();
//Check if .env.example is present or not.
$env_example = base_path('.env.example');
if (! file_exists($env_example)) {
exit("<b>.env.example file not found in <code>$env_example</code></b> <br/><br/> - In the downloaded codebase you will find .env.example file, please upload it and refresh this page.");
}
return view('ic.details');
}
public function postDetails(Request $request)
{
//Check for .env file
$this->isInstalled();
$this->installSettings();
try {
ini_set('max_execution_time', 0);
ini_set('memory_limit', '512M');
$validatedData = $request->validate(
[
'APP_NAME' => 'required',
'ENVATO_PURCHASE_CODE' => 'required',
'DB_DATABASE' => 'required',
'DB_USERNAME' => 'required',
'DB_PASSWORD' => 'required',
'DB_HOST' => 'required',
'DB_PORT' => 'required',
],
[
'APP_NAME.required' => 'App Name is required',
'ENVATO_PURCHASE_CODE.required' => 'Envaot Purchase code is required',
'DB_DATABASE.required' => 'Database Name is required',
'DB_USERNAME.required' => 'Database Username is required',
'DB_PASSWORD.required' => 'Database Password is required',
'DB_HOST.required' => 'Database Host is required',
'DB_PORT.required' => 'Database port is required',
]
);
$this->outputLog = new BufferedOutput;
$input = $request->only(['APP_NAME', 'APP_TITLE', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD', 'ENVATO_PURCHASE_CODE',
'ENVATO_EMAIL', 'ENVATO_USERNAME', ]);
$input['APP_DEBUG'] = 'false';
$input['APP_URL'] = url('/');
$input['APP_ENV'] = 'live';
//Check for database details
$mysql_link = @mysqli_connect($input['DB_HOST'], $input['DB_USERNAME'], $input['DB_PASSWORD'], $input['DB_DATABASE'], $input['DB_PORT']);
if (mysqli_connect_errno()) {
$msg = '<b>ERROR</b>: Failed to connect to MySQL: '.mysqli_connect_error();
$msg .= "<br/>Provide correct details for 'Database Host', 'Database Port', 'Database Name', 'Database Username', 'Database Password'.";
return redirect()
->back()
->with('error', $msg);
}
//pos boot
$return = pos_boot($input['APP_URL'], __DIR__, $input['ENVATO_PURCHASE_CODE'], $input['ENVATO_EMAIL'], $input['ENVATO_USERNAME']);
if (! empty($return)) {
return $return;
}
//Get .env file details and write the contents in it.
$envPathExample = base_path('.env.example');
$envPath = base_path('.env');
$env_lines = file($envPathExample);
foreach ($input as $index => $value) {
foreach ($env_lines as $key => $line) {
//Check if present then replace it.
if (strpos($line, $index) !== false) {
$env_lines[$key] = $index.'="'.$value.'"'.PHP_EOL;
}
}
}
//TODO: Remove false & automate the process of creating .env file.
if (false) {
// $fp = fopen($envPath, 'w');
// fwrite($fp, implode('', $env_lines));
// fclose($fp);
// //Artisan commands
// $this->runArtisanCommands();
// return redirect()->route('install.success');
} else {
$this->deleteEnv();
//Show intermediate steps if not able to copy file.
$envContent = implode('', $env_lines);
return view('ic.envText')
->with(compact('envContent'));
}
} catch (Exception $e) {
$this->deleteEnv();
return redirect()->back()
->with('error', 'Something went wrong, please try again!!');
}
}
public function installAlternate(Request $request)
{
try {
$this->installSettings();
//Check if no .env file than redirect back.
$envPath = base_path('.env');
if (! file_exists($envPath)) {
return redirect()->route('install.details')
->with('error', 'Looks like you haven\'t created the .env file '.$envPath);
}
$this->runArtisanCommands();
return redirect()->route('install.success');
} catch (Exception $e) {
$this->deleteEnv();
return redirect()->back()
->with('error', 'Something went wrong, please try again!!');
}
}
public function success()
{
return view('ic.success');
}
public function updateConfirmation()
{
$db_version = \App\System::getVersion();
if (Comparator::greaterThan($this->appVersion, $db_version)) {
return view('ic.update_confirmation');
} else {
abort(404);
}
}
//Updating
public function update(Request $request)
{
$version = null;
try {
DB::beginTransaction();
ini_set('max_execution_time', 0);
ini_set('memory_limit', '512M');
$input = $request->only(['ENVATO_PURCHASE_CODE', 'ENVATO_USERNAME', 'ENVATO_EMAIL']);
$db_version = \App\System::getVersion();
if (Comparator::greaterThan($this->appVersion, $db_version)) {
ini_set('max_execution_time', 0);
ini_set('memory_limit', '512M');
$this->installSettings();
$return = pos_boot(config('app.url'), __DIR__, $input['ENVATO_PURCHASE_CODE'], $input['ENVATO_EMAIL'], $input['ENVATO_USERNAME'], 1);
if (! empty($return)) {
return $return;
}
DB::statement('SET default_storage_engine=INNODB;');
Artisan::call('migrate', ['--force' => true]);
\App\System::updateSystem(['version' => $this->appVersion]);
} else {
abort(404);
}
DB::commit();
$output = ['success' => 1,
'msg' => 'Updated Succesfully to version '.$this->appVersion.' !!',
];
return redirect('login')->with('status', $output);
} catch (Exception $e) {
DB::rollBack();
exit($e->getMessage());
}
}
private function isInstalled()
{
$envPath = base_path('.env');
if (file_exists($envPath)) {
abort(404);
}
}
private function installSettings()
{
config(['app.debug' => true]);
Artisan::call('config:clear');
Artisan::call('cache:clear');
}
private function deleteEnv()
{
$envPath = base_path('.env');
if ($envPath && file_exists($envPath)) {
unlink($envPath);
}
return true;
}
private function runArtisanCommands()
{
ini_set('max_execution_time', 0);
ini_set('memory_limit', '512M');
$this->installSettings();
DB::statement('SET default_storage_engine=INNODB;');
Artisan::call('migrate:fresh', ['--force' => true]);
Artisan::call('db:seed');
}
}

View File

@@ -0,0 +1,72 @@
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
class ManageProfileController extends Controller
{
/**
* Show the form for editing the profile.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function getProfile()
{
$user_id = request()->user()->id;
$user = User::find($user_id);
return view('user.profile.edit')
->with(compact('user'));
}
/**
* Update the specified profile.
*
* @param \Illuminate\Http\Request $request
* @param $id
* @return \Illuminate\Http\Response
*/
public function postProfile(Request $request, $id)
{
$validate = validator($request->all(), [
'name' => 'required',
'email' => 'required|email|unique:users,email,'.$id,
]);
if ($validate->fails()) {
return $this->respondWithError($validate->errors()->first());
}
try {
//check for demo
if ($this->isDemo()) {
return $this->respondDemo();
}
if (request()->ajax()) {
$input = request()->only('name', 'email');
$password = request('password');
if (! empty($password)) {
$input['password'] = bcrypt($password);
}
$user = User::findOrFail($id);
$user->update($input);
$dashboard_url['redirect'] = action([\App\Http\Controllers\HomeController::class, 'index']);
$output = $this->respondSuccess(__('messages.success'), $dashboard_url);
}
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace App\Http\Controllers;
use App\UserSetting;
use DateTimeZone;
use Illuminate\Http\Request;
class ManageSettingsController extends Controller
{
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function getSettings()
{
$user_id = request()->user()->id;
$settings = UserSetting::where('user_id', $user_id)
->first();
if (empty($settings)) {
$settings['language'] = config('app.locale');
$settings['timezone'] = config('app.timezone');
$settings['smtp'] = [
'MAIL_HOST' => '',
'MAIL_PORT' => '',
'MAIL_USERNAME' => '',
'MAIL_PASSWORD' => '',
'MAIL_ENCRYPTION' => '',
'MAIL_FROM_ADDRESS' => '',
'MAIL_FROM_NAME' => '',
];
}
$settings['languages'] = config('constants.langs');
$settings['timezones'] = DateTimeZone::listIdentifiers(DateTimeZone::ALL);
return view('user.settings.edit')
->with(compact('settings'));
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function postSettings(Request $request)
{
try {
if ($request->ajax()) {
$input = $request->only('language', 'timezone');
$input['smtp'] = $request->only('MAIL_HOST', 'MAIL_PORT', 'MAIL_USERNAME', 'MAIL_PASSWORD', 'MAIL_ENCRYPTION', 'MAIL_FROM_ADDRESS', 'MAIL_FROM_NAME');
$input['user_id'] = $request->user()->id;
$setting = UserSetting::where('user_id', $input['user_id'])->first();
if (empty($setting)) {
UserSetting::create($input);
} else {
$setting->smtp = $input['smtp'];
$setting->language = $input['language'];
$setting->timezone = $input['timezone'];
$setting->save();
}
$dashboard_url['redirect'] = action([\App\Http\Controllers\HomeController::class, 'index']);
$output = $this->respondSuccess(null, $dashboard_url);
}
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Http\Controllers;
use App\Package;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RegistrationController extends Controller
{
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\User
*/
public function store(Request $request)
{
$validator = $request->validate(
[
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
]
);
$user = $request->only(['name', 'email']);
if (! empty($request->input('password'))) {
$user['password'] = bcrypt($request->input('password'));
}
$package_id = $request->get('package_id', null);
$user = User::create($user);
Auth::login($user);
if (is_saas_enabled() && config('app.env') != 'demo' && ! empty($package_id)) {
$package = Package::find($package_id);
if (! empty($package)) {
return redirect()->action([\App\Http\Controllers\Superadmin\SubscriptionPaymentController::class, 'subscriptionPay'], ['package_id' => $package_id]);
}
}
return redirect()->route('home');
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace App\Http\Controllers;
use App\Package;
use App\PackageSubscription;
use Illuminate\Http\Request;
use Yajra\DataTables\Facades\DataTables;
class SubscriptionsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
$user_id = request()->user()->id;
$active_packages = Package::where('is_active', 1)
->orderBy('sort_order', 'asc')
->paginate(20);
$active_subscription = PackageSubscription::activeSubscription($user_id);
$upcoming_subscriptions = PackageSubscription::upcomingSubscriptions($user_id);
$waiting_subscriptions = PackageSubscription::waitingSubscriptions($user_id);
return view('user.subscription.index')
->with(compact('active_packages', 'active_subscription', 'upcoming_subscriptions', 'waiting_subscriptions'));
}
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function getAllSubscriptions(Request $request)
{
if ($request->ajax()) {
$package_subscription = PackageSubscription::where('user_id', $request->user()->id)
->select('package_subscriptions.package_details', 'start_date', 'end_date', 'package_price', 'paid_via', 'payment_transaction_id', 'status', 'created_at', 'id as package_subscription_id');
return Datatables::of($package_subscription)
->addColumn('action', '
<button type="button" data-href="{{action([\App\Http\Controllers\SubscriptionsController::class, "show"], [$package_subscription_id])}}" class="btn btn-icon btn-sm view_subscription text-info" data-toggle="tooltip"
title="{{ __(\'messages.view\') }}">
<i class="far fa-eye font_icon_size" aria-hidden="true"></i>
</button>
')
->editColumn('status', '
<span class="badge
@if($status == "approved")
badge-success
@elseif($status == "waiting")
badge-warning text-white
@elseif($status == "declined")
badge-danger
@endif
">
@lang("messages.".$status)
</span>
')
->addColumn('package', function ($row) {
$package_name = ! empty($row->package_details['name']) ? $row->package_details['name'] : '';
return $package_name;
})
->editColumn(
'start_date',
'@php
$date = \Carbon\Carbon::parse($start_date)->isoFormat("D/M/YY");
@endphp
{{$date}}
'
)
->editColumn('end_date', '@php
$date = \Carbon\Carbon::parse($end_date)->isoFormat("D/M/YY");
@endphp
{{$date}}
')
->editColumn('package_price', '
<span class="currency">
{{$package_price}}
</span>
')
->editColumn('created_at', '@php
$date = \Carbon\Carbon::parse($created_at)->isoFormat("D/M/YY");
@endphp
{{$date}}
')
->editColumn('paid_via', '
@if($paid_via == "offline")
@lang("messages.offline")
@else
{{ucfirst($paid_via)}}
@endif
')
->removeColumn('package_subscription_id')
->rawColumns(['action', 'package', 'status', 'start_date', 'end_date', 'paid_via', 'package_price', 'created_at'])
->make(true);
}
return view('user.subscription.index');
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
if (request()->ajax()) {
$subscription = PackageSubscription::find($id);
return view('user.subscription.show')
->with(compact('subscription'));
}
}
}

View File

@@ -0,0 +1,424 @@
<?php
namespace App\Http\Controllers\Superadmin;
use App\Enums\User\RoleEnum;
use App\Form;
use App\Http\Controllers\Controller;
use App\Notifications\UserNotification;
use App\Package;
use App\User;
use App\UserForm;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Http\Request;
use Yajra\DataTables\Facades\DataTables;
class ManageUsersController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if (!auth()->user()->hasRole([RoleEnum::ADMIN->value, RoleEnum::SUPERVISOR->value], 'web')) {
abort(403, 'Unauthorized action.');
}
if ($request->ajax()) {
$supervisor = User::role(RoleEnum::SUPERVISOR->value)->first();
$users = User::query()
->when(
!auth()->user()->hasRole(RoleEnum::SUPERVISOR->value),
function (Builder $builder) use ($supervisor) {
$builder->whereNotIn('id', [$supervisor->id]);
}
)
->select(['id', 'name', 'email', 'is_active', 'created_at']);
if (! empty($request->input('status'))) {
$is_active = ($request->input('status') == 'active') ? 1 : 0;
$users->where('is_active', $is_active);
}
return DataTables::of($users)
->addColumn(
'action',
'
@if($is_active)
<span title="@lang("messages.mark_inactive")">
<a class="btn btn-link btn-icon btn-sm text-danger toggle_is_active pointer" data-href="{{ action([\App\Http\Controllers\Superadmin\ManageUsersController::class, "toggleUserActiveStatus"], [$id])}}">
<i class="fas fa-toggle-on font_icon_size"></i>
</a>
</span>
@else
<span title="@lang("messages.mark_active")">
<a class="btn btn-link btn-icon btn-sm text-success toggle_is_active pointer" data-href="{{ action([\App\Http\Controllers\Superadmin\ManageUsersController::class, "toggleUserActiveStatus"], [$id])}}">
<i class="fas fa-toggle-off font_icon_size"></i>
</a>
</span>
@endif
<a class="btn btn-link btn-icon btn-sm text-info edit_user pointer" data-href="{{action([\App\Http\Controllers\Superadmin\ManageUsersController::class, "edit"], [$id])}}" title="@lang("messages.edit")">
<i class="fas fa-edit font_icon_size"></i>
</a>
@if(1 === 0)
<a class="btn btn-link btn-icon btn-sm text-info upgrade_account pointer" data-href="{{action([\App\Http\Controllers\Superadmin\ManageUsersController::class, "upgrade"], [$id])}}" title="@lang("messages.edit")">
<i class="fas fa-money-check font_icon_size"></i>
</a>
@endif
<a class="btn btn-link btn-icon btn-sm text-danger delete_user pointer" data-href="{{action([\App\Http\Controllers\Superadmin\ManageUsersController::class, "destroy"], [$id])}}" title="@lang("messages.delete")">
<i class="fas fa-trash-alt font_icon_size"></i>
</a>
'
)
->editColumn(
'is_active',
'
@if($is_active)
<span class="badge badge-pill badge-success">
<i class="far fa-check-circle"></i>
@lang(\'messages.active\')
</span>
@else
<span class="badge badge-pill badge-danger">
<i class="far fa-times-circle"></i>
@lang(\'messages.inactive\')
</span>
@endif
'
)
->editColumn(
'created_at',
'
@php
$date = \Carbon\Carbon::parse($created_at)->isoFormat("D/M/YY HH:mm A");
@endphp
{{$date}}
'
)
->removeColumn('id')
->rawColumns(['action', 'is_active', 'created_at'])
->make(true);
}
return view('superadmin.users.index');
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
if (request()->ajax()) {
$user = request()->user();
$forms = Form::where('is_template', 0)
->pluck('name', 'id')
->toArray();
return view('superadmin.users.create')
->with(compact('forms'));
}
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
try {
if (! empty($this->notAllowedInDemo())) {
return $this->notAllowedInDemo();
}
$input = $request->only('name', 'email', 'is_active', 'is_admin', 'can_create_form');
if (! empty($request->input('password'))) {
$input['password'] = bcrypt($request->input('password'));
}
$input['is_active'] = ! empty($input['is_active']) ? 1 : 0;
$input['can_create_form'] = ! empty($input['can_create_form']) ? 1 : 0;
$user = User::query()->create($input);
if ($request->filled('is_admin')) {
$user->assignRole(RoleEnum::ADMIN->value);
} else {
$user->assignRole(RoleEnum::USER->value);
}
//save user forms (assgined)
$permissions = $request->input('permissions');
$form_ids = $request->input('form_id');
$user_forms = [];
if (! empty($form_ids) && ! empty($permissions)) {
foreach ($form_ids as $key => $form_id) {
$user_forms[] = [
'form_id' => $form_id,
'assigned_by' => \Auth::id(),
'permissions' => $permissions,
];
}
}
$user->userForms()->createMany($user_forms);
if (! empty($request->input('send_email'))) {
$input['password'] = $request->input('password');
$user->notify(new UserNotification($input));
}
$output = $this->respondSuccess();
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
if (request()->ajax()) {
$user = User::findOrFail($id);
if (auth()->user()->hasRole([RoleEnum::SUPERVISOR->value, RoleEnum::ADMIN->value])) {
$forms = Form::where('is_template', 0)
->pluck('name', 'id')
->toArray();
$assigned_forms = UserForm::with('form')
->where('assigned_to', $id)
->get();
} else {
$logged_in_user = request()->user();
$forms = Form::where('is_template', 0)
->where('created_by', $logged_in_user->id)
->pluck('name', 'id')
->toArray();
$assigned_forms = UserForm::with('form')
->where('assigned_by', \Auth::id())
->where('assigned_to', $id)
->get();
}
return view('superadmin.users.edit')
->with(compact('user', 'forms', 'assigned_forms'));
}
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
try {
if (! empty($this->notAllowedInDemo())) {
return $this->notAllowedInDemo();
}
$input = $request->only('name', 'email', 'is_active', 'is_admin', 'can_create_form');
$input['is_active'] = ! empty($input['is_active']) ? 1 : 0;
$input['can_create_form'] = ! empty($input['can_create_form']) ? 1 : 0;
if (! empty($request->input('password'))) {
$input['password'] = bcrypt($request->input('password'));
}
$user = User::findOrFail($id);
$user->update($input);
if ($request->filled('is_admin') && $request->input('is_admin') === 'on') {
$user->assignRole(RoleEnum::ADMIN->value);
$user->removeRole(RoleEnum::USER->value);
} else {
$user->removeRole(RoleEnum::ADMIN->value);
$user->assignRole(RoleEnum::USER->value);
}
//update user forms (assgined)
$edit_permissions = $request->input('edit_permissions');
$assgined_form_ids = $request->input('edit_assigned_form_id');
if (! empty($assgined_form_ids)) {
$non_existing_ids = [];
foreach ($assgined_form_ids as $key => $id) {
if (! empty($edit_permissions[$id])) {
$user_form = UserForm::find($id);
$user_form->permissions = $edit_permissions[$id];
$user_form->save();
} else {
$non_existing_ids[] = $id;
}
}
UserForm::whereIn('id', $non_existing_ids)
->delete();
}
//save user forms (assgined)
$permissions = $request->input('permissions');
$form_ids = $request->input('form_id');
$user_forms = [];
if (! empty($form_ids) && ! empty($permissions)) {
foreach ($form_ids as $key => $form_id) {
$user_forms[] = [
'form_id' => $form_id,
'assigned_by' => \Auth::id(),
'permissions' => $permissions,
];
}
}
$user->userForms()->createMany($user_forms);
if (! empty($request->input('send_email'))) {
$input['password'] = $request->input('password');
$user->notify(new UserNotification($input));
}
$output = $this->respondSuccess();
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
try {
if (request()->ajax()) {
if (! empty($this->notAllowedInDemo())) {
return $this->notAllowedInDemo();
}
$user = User::findOrFail($id);
if (\Auth::id() != $user->id) {
$user->createdForms()->delete();
$user->userForms()->delete();
$user->delete();
$output = $this->respondSuccess();
} else {
$output = $this->respondWithError(__('messages.something_went_wrong'));
}
}
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
/**
* toggle users status(active/inactive)
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function toggleUserActiveStatus($id)
{
try {
if (request()->ajax()) {
if (! empty($this->notAllowedInDemo())) {
return $this->notAllowedInDemo();
}
$user = User::findOrFail($id);
if (\Auth::id() != $user->id) {
$user->is_active = ! $user->is_active;
$user->save();
$output = $this->respondSuccess();
} else {
$output = $this->respondWithError(__('messages.something_went_wrong'));
}
}
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
/**
* check if email exist or not
*
* @return \Illuminate\Http\Response
*/
public function checkIfEmailExist(Request $request)
{
$email = $request->input('email');
$query = User::where('email', $email);
if (! empty($request->input('user_id'))) {
$user_id = $request->input('user_id');
$query->where('id', '!=', $user_id);
}
$exists = $query->exists();
if (! $exists) {
echo 'true';
exit;
} else {
echo 'false';
exit;
}
}
/**
* Upgrade modal for upgrade the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function upgrade($id)
{
if (request()->ajax()) {
$user = User::findOrFail($id);
$active_packages = Package::where('is_active', 1)
->orderBy('sort_order', 'asc')
->paginate(20);
return view('superadmin.users.upgrade')
->with(compact('user', 'active_packages'));
}
}
}

View File

@@ -0,0 +1,173 @@
<?php
namespace App\Http\Controllers\Superadmin;
use App\Http\Controllers\Controller;
use App\Package;
use Illuminate\Http\Request;
class PackageController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
if (! auth()->user()->can('superadmin')) {
abort(403, 'Unauthorized action.');
}
$packages = Package::latest()
->paginate(20);
return view('superadmin.packages.index')
->with(compact('packages'));
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
if (! auth()->user()->can('superadmin')) {
abort(403, 'Unauthorized action.');
}
$lists = Package::list();
return view('superadmin.packages.create')
->with(compact('lists'));
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
try {
if ($request->ajax()) {
$input = $request->only('name', 'description', 'no_of_active_forms', 'price_interval', 'interval', 'price', 'sort_order', 'is_active', 'is_form_downloadable');
if (empty($input['is_active'])) {
$input['is_active'] = 0;
}
if (empty($input['is_form_downloadable'])) {
$input['is_form_downloadable'] = 0;
}
if (empty($input['no_of_active_forms'])) {
$input['no_of_active_forms'] = 0;
}
if (empty($input['price'])) {
$input['price'] = 0;
}
Package::create($input);
$package_dashboard_url['redirect'] = action([\App\Http\Controllers\Superadmin\PackageController::class, 'index']);
$output = $this->respondSuccess(__('messages.saved_successfully'), $package_dashboard_url);
}
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
if (! auth()->user()->can('superadmin')) {
abort(403, 'Unauthorized action.');
}
if (! empty($id)) {
$package = Package::find($id);
$lists = Package::list();
return view('superadmin.packages.edit')
->with(compact('package', 'lists'));
}
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
try {
if ($request->ajax()) {
$input = $request->only('name', 'description', 'no_of_active_forms', 'price_interval', 'interval', 'price', 'sort_order', 'is_active', 'is_form_downloadable');
if (empty($input['is_active'])) {
$input['is_active'] = 0;
}
if (empty($input['is_form_downloadable'])) {
$input['is_form_downloadable'] = 0;
}
if (empty($input['no_of_active_forms'])) {
$input['no_of_active_forms'] = 0;
}
if (empty($input['price'])) {
$input['price'] = 0;
}
Package::where('id', $id)
->update($input);
$package_dashboard_url['redirect'] = action([\App\Http\Controllers\Superadmin\PackageController::class, 'index']);
$output = $this->respondSuccess(__('messages.updated_successfully'), $package_dashboard_url);
}
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
try {
Package::destroy($id);
$output = $this->respondSuccess(__('messages.deleted_successfully'));
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
}

View File

@@ -0,0 +1,137 @@
<?php
namespace App\Http\Controllers\Superadmin;
use App\Http\Controllers\Controller;
use App\Package;
use App\PackageSubscription;
use Illuminate\Http\Request;
use Yajra\DataTables\Facades\DataTables;
class PackageSubscriptionsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index(Request $request)
{
if (! auth()->user()->can('superadmin')) {
abort(403, 'Unauthorized action.');
}
if ($request->ajax()) {
$package_subscription = PackageSubscription::join('users', 'package_subscriptions.user_id', 'users.id')
->select('users.name as user', 'package_subscriptions.package_details', 'start_date', 'end_date', 'package_price', 'payment_transaction_id', 'status', 'paid_via', 'package_subscriptions.id as package_subscription_id', 'users.email as user_email');
if (! empty($request->input('package_id'))) {
$package_subscription->where('package_id', $request->input('package_id'));
}
if (! empty($request->input('status'))) {
$package_subscription->where('status', $request->input('status'));
}
return Datatables::of($package_subscription)
->addColumn('action', '
<button type="button" data-href="{{action([\App\Http\Controllers\Superadmin\PackageSubscriptionsController::class, "edit"], [$package_subscription_id])}}" class="btn btn-icon btn-sm edit_subscription text-primary" data-toggle="tooltip"
title="{{ __(\'messages.edit\') }}">
<i class="far fa-edit font_icon_size" aria-hidden="true"></i>
</button>
')
->editColumn('status', '
<span class="badge
@if($status == "approved")
badge-success
@elseif($status == "waiting")
badge-warning text-white
@elseif($status == "declined")
badge-danger
@endif
">
@lang("messages.".$status)
</span>
')
->addColumn('package', function ($row) {
$package_name = ! empty($row->package_details['name']) ? $row->package_details['name'] : '';
return $package_name;
})
->editColumn(
'start_date',
'@php
$date = \Carbon\Carbon::parse($start_date)->isoFormat("D/M/YY");
@endphp
{{$date}}
'
)
->editColumn('end_date', '@php
$date = \Carbon\Carbon::parse($end_date)->isoFormat("D/M/YY");
@endphp
{{$date}}
')
->editColumn('package_price', '
<span class="currency">
{{$package_price}}
</span>
')
->editColumn('paid_via', '
@if($paid_via == "offline")
@lang("messages.offline")
@else
{{ucfirst($paid_via)}}
@endif
')
->removeColumn('package_subscription_id')
->rawColumns(['action', 'package', 'status', 'start_date', 'end_date', 'package_price', 'paid_via'])
->make(true);
}
$subscription_status = PackageSubscription::status();
$packages = Package::activePackages();
return view('superadmin.subscription.index')
->with(compact('subscription_status', 'packages'));
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
if (request()->ajax()) {
$subscription = PackageSubscription::findOrFail($id);
$status_list = PackageSubscription::status();
return view('superadmin.subscription.edit')
->with(compact('subscription', 'status_list'));
}
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
try {
$subscription_info = $request->only('start_date', 'end_date', 'status', 'payment_transaction_id');
PackageSubscription::where('id', $id)
->update($subscription_info);
$output = $this->respondSuccess(__('messages.updated_successfully'));
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
}

View File

@@ -0,0 +1,331 @@
<?php
namespace App\Http\Controllers\Superadmin;
use App\Http\Controllers\Controller;
use App\Notifications\NotificationToUserForAccountUpgrade;
use App\Notifications\SendApprovalNotificationToAdminForOfflinePayment;
use App\Package;
use App\PackageSubscription;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Notification;
use Srmklive\PayPal\Services\ExpressCheckout;
use Stripe\Checkout\Session;
use Stripe\Stripe;
class SubscriptionPaymentController extends Controller
{
/**
* Show pay form for a new package.
*
* @return Response
*/
public function subscriptionPay($package_id)
{
return $this->pay($package_id, $register_form = true);
}
/**
* Show pay form for a new package.
*
* @return Response
*/
public function pay($package_id, $register_form = null)
{
try {
$package = Package::find($package_id);
$user = request()->user();
//Check for free package & subscribe it.
if ($package->price == 0) {
DB::beginTransaction();
$paid_via = null;
$payment_transaction_id = 'FREE';
$status = 'approved';
PackageSubscription::createSubscription($user->id, $package_id, $paid_via, $payment_transaction_id, $status);
DB::commit();
if (! empty($register_form)) {
$output = [
'success' => 1,
'msg' => __('messages.registered_and_subscribed'),
];
return redirect()->action([\App\Http\Controllers\SubscriptionsController::class, 'index'])
->with('status', $output);
} else {
$output = [
'success' => 1,
'msg' => __('messages.success'),
];
return redirect()->action([\App\Http\Controllers\SubscriptionsController::class, 'index'])
->with('status', $output);
}
}
$nav = false;
$payment_gateways = $this->paymentGateways();
$stripe_payment_session = [];
if (array_key_exists('stripe', $payment_gateways)) {
Stripe::setApiKey(config('constants.STRIPE_SECRET_KEY'));
$stripe_payment_session = \Stripe\Checkout\Session::create([
'customer_email' => $user->email,
'payment_method_types' => ['card'],
'line_items' => [[
'price_data' => [
'currency' => strtolower(env('CURRENCY_CODE')),
'unit_amount' => $package->price * 100,
'product_data' => [
'name' => $package->name,
'description' => $package->description,
'images' => [],
],
],
'quantity' => 1,
]],
'mode' => 'payment',
'success_url' => action([\App\Http\Controllers\Superadmin\SubscriptionPaymentController::class, 'confirmPayment'], [$package_id]).'?paid_via=stripe&session_id={CHECKOUT_SESSION_ID}',
'cancel_url' => action([\App\Http\Controllers\Superadmin\SubscriptionPaymentController::class, 'pay'], [$package_id]),
]);
}
return view('payments.create')
->with(compact('package', 'nav', 'payment_gateways', 'stripe_payment_session'));
} catch (Exception $e) {
DB::rollBack();
$output = [
'success' => 0,
'msg' => __('messages.something_went_wrong'),
];
return redirect()->action([\App\Http\Controllers\SubscriptionsController::class, 'index'])
->with('status', $output);
}
}
/**
* confirm the payment & Save the payment details and add subscription details
*
* @return Response
*/
public function confirmPayment($package_id, Request $request)
{
try {
DB::beginTransaction();
$user = request()->user();
$paid_via = $request->get('paid_via');
//Call the payment method
$pay_function = 'pay_'.$paid_via;
$payment_transaction_id = null;
$status = 'approved';
if (method_exists($this, $pay_function)) {
$payment_transaction_id = $this->$pay_function($package_id, $request);
}
if (in_array($paid_via, ['offline'])) {
$status = 'waiting';
}
//create subscription
PackageSubscription::createSubscription($user->id, $package_id, $paid_via, $payment_transaction_id, $status);
DB::commit();
$msg = __('messages.success');
if ($request->get('paid_via') == 'offline') {
$msg = __('messages.notification_sent_for_approval');
}
$output = ['success' => 1, 'msg' => $msg];
} catch (Exception $e) {
DB::rollBack();
$output = [
'success' => 0,
'msg' => __('messages.something_went_wrong'),
];
}
return redirect()->action([\App\Http\Controllers\SubscriptionsController::class, 'index'])
->with('status', $output);
}
/**
* Offline payment method
*
* @return Response
*/
protected function pay_offline($package_id, $request)
{
$user = request()->user();
$admin_emails = explode(',', env('SUPERADMIN_EMAILS'));
$superadmins = User::whereIn('email', $admin_emails)
->get();
$package = Package::find($package_id);
$package['paid_via'] = 'Offline';
$package['package_price'] = env('CURRENCY_SYMBOL').number_format($package->price, 2);
Notification::send($superadmins, new SendApprovalNotificationToAdminForOfflinePayment($user, $package));
}
/**
* Paypal payment method
*
* @return Response
*/
protected function pay_paypal($package_id, $request)
{
$provider = new ExpressCheckout();
config(['paypal.currency' => strtoupper(env('CURRENCY_CODE'))]);
$provider = new ExpressCheckout();
$response = $provider->getExpressCheckoutDetails($request->token);
// if response ACK value is not SUCCESS or SUCCESSWITHWARNING return back with error
if (! in_array(strtoupper($response['ACK']), ['SUCCESS', 'SUCCESSWITHWARNING'])) {
return back()
->with('status', ['success' => 0, 'msg' => 'Something went wrong with paypal transaction']);
}
$invoice_id = $response['INVNUM'];
$package = Package::find($package_id);
$data = [];
$data['items'] = [
[
'name' => $package->name,
'price' => (float) $package->price,
'qty' => 1,
],
];
$data['invoice_id'] = $invoice_id;
$data['invoice_description'] = "Order #{$data['invoice_id']} Invoice";
$data['return_url'] = action([\App\Http\Controllers\Superadmin\SubscriptionPaymentController::class, 'confirmPayment'], [$package_id]);
$data['cancel_url'] = action([\App\Http\Controllers\Superadmin\SubscriptionPaymentController::class, 'pay'], [$package_id]);
$data['total'] = (float) $package->price;
$token = $request->get('token');
$PayerID = $request->get('PayerID');
// if payment is not recurring just perform transaction on PayPal and get the payment status
$payment_status = $provider->doExpressCheckoutPayment($data, $token, $PayerID);
$status = isset($payment_status['PAYMENTINFO_0_PAYMENTSTATUS']) ? $payment_status['PAYMENTINFO_0_PAYMENTSTATUS'] : null;
if (! empty($status) && $status != 'Invalid') {
return $invoice_id;
} else {
$error = 'Something went wrong with paypal transaction';
throw new \Exception($error);
}
}
/**
* Paypal payment method - redirect to paypal url for payments
*
* @return Response
*/
public function paypalExpressCheckout($package_id, Request $request)
{
$package = Package::find($package_id);
$data = [];
$data['items'] = [
[
'name' => $package->name,
'price' => (float) $package->price,
'qty' => 1,
],
];
$data['invoice_id'] = Str::random(5);
$data['invoice_description'] = "Order #{$data['invoice_id']} Invoice";
$data['return_url'] = action([\App\Http\Controllers\Superadmin\SubscriptionPaymentController::class, 'confirmPayment'], [$package_id]).'?paid_via=paypal';
$data['cancel_url'] = action([\App\Http\Controllers\Superadmin\SubscriptionPaymentController::class, 'pay'], [$package_id]);
$data['total'] = (float) $package->price;
//send request to paypal & in response get aaray of data and if it has payment link redirect on that
$provider = new ExpressCheckout();
$response = $provider->setCurrency(strtoupper(env('CURRENCY_CODE')))->setExpressCheckout($data);
// if there is no link redirect back with error message
if (! $response['paypal_link']) {
return back()
->with('status', ['success' => 0, 'msg' => 'Something went wrong with paypal transaction']);
}
return redirect($response['paypal_link']);
}
/**
* return transaction id
* after successful payment
*
* @return Response
*/
protected function pay_stripe($package_id, $request)
{
Stripe::setApiKey(config('constants.STRIPE_SECRET_KEY'));
$stripe_payment = Session::retrieve($request->session_id);
return $stripe_payment->payment_intent;
}
/**
* Confirm subscription by admin
*
* @return Response
*/
public function confirmAdminSubscription($package_id, $user_id)
{
if (request()->ajax()) {
return view('superadmin.users.confirm_upgrade')
->with(compact('package_id', 'user_id'));
}
}
/**
* Add subscription details by admin
*
* @return Response
*/
public function adminSubscription($package_id, $user_id, Request $request)
{
try {
DB::beginTransaction();
if (! empty($request->input('disable_all_packages'))) {
PackageSubscription::disableAllPackagesForUser($user_id);
}
$user = User::where('id', $user_id)->first();
$package = Package::find($package_id);
$payment_transaction_id = '';
$status = 'approved';
//create subscription
$subscription_info = PackageSubscription::createSubscription(
$user->id,
$package_id,
'admin',
$payment_transaction_id,
$status
);
Notification::send($user, new NotificationToUserForAccountUpgrade($user, $subscription_info));
DB::commit();
$output = [
'success' => 1,
'msg' => __('messages.success'),
];
} catch (Exception $e) {
DB::rollBack();
$output = [
'success' => 0,
'msg' => __('messages.something_went_wrong'),
];
}
return $output;
}
}

View File

@@ -0,0 +1,222 @@
<?php
namespace App\Http\Controllers\Superadmin;
use App\Http\Controllers\Controller;
use App\System;
use DateTimeZone;
use Illuminate\Http\Request;
class SuperadminSettingsController extends Controller
{
/**
* Display a listing of the resource.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
//
}
/**
* Show the form for creating a new resource.
*
* @return \Illuminate\Http\Response
*/
public function create()
{
if (! auth()->user()->can('superadmin')) {
abort(403, 'Unauthorized action.');
}
$languages = config('constants.langs');
$timezones = DateTimeZone::listIdentifiers(DateTimeZone::ALL);
$settings = [
'APP_NAME' => config('app.name'),
'APP_TITLE' => config('app.title'),
'CURRENCY_NAME' => env('CURRENCY_NAME'),
'CURRENCY_SYMBOL' => env('CURRENCY_SYMBOL'),
'CURRENCY_CODE' => env('CURRENCY_CODE'),
'APP_TIMEZONE' => env('APP_TIMEZONE'),
'MAIL_HOST' => config('mail.mailers.smtp.host'),
'MAIL_PORT' => config('mail.mailers.smtp.port'),
'MAIL_USERNAME' => config('mail.mailers.smtp.username'),
'MAIL_PASSWORD' => config('mail.mailers.smtp.password'),
'MAIL_ENCRYPTION' => config('mail.mailers.smtp.encryption'),
'ENABLE_REGISTRATION' => env('ENABLE_REGISTRATION'),
'ENABLE_SAAS_MODULE' => env('ENABLE_SAAS_MODULE'),
'MAIL_FROM_ADDRESS' => config('mail.from.address'),
'MAIL_FROM_NAME' => config('mail.from.name'),
'APP_LOCALE' => $languages,
'timezones' => $timezones,
'PAYPAL_MODE' => config('paypal.mode'),
'PAYPAL_SANDBOX_API_USERNAME' => config('paypal.sandbox.username'),
'PAYPAL_SANDBOX_API_PASSWORD' => config('paypal.sandbox.password'),
'PAYPAL_SANDBOX_API_SECRET' => config('paypal.sandbox.secret'),
'PAYPAL_LIVE_API_USERNAME' => config('paypal.live.username'),
'PAYPAL_LIVE_API_PASSWORD' => config('paypal.live.password'),
'PAYPAL_LIVE_API_SECRET' => config('paypal.live.secret'),
'STRIPE_PUB_KEY' => config('constants.STRIPE_PUB_KEY'),
'STRIPE_SECRET_KEY' => config('constants.STRIPE_SECRET_KEY'),
'ENABLE_OFFLINE_PAYMENT' => config('constants.ENABLE_OFFLINE_PAYMENT'),
'ACELLE_MAIL_NAME' => config('constants.ACELLE_MAIL_NAME'),
'ACELLE_MAIL_API' => config('constants.ACELLE_MAIL_API'),
];
if ($this->isDemo()) {
$settings['MAIL_USERNAME'] = '';
$settings['MAIL_PASSWORD'] = '';
}
$date_formats = System::dateFormats();
$additional_js = System::getValue('additional_js');
$additional_css = System::getValue('additional_css');
return view('superadmin.settings.create')
->with(compact('settings', 'date_formats', 'additional_js', 'additional_css'));
}
/**
* Store a newly created resource in storage.
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function store(Request $request)
{
if (! auth()->user()->can('superadmin')) {
abort(403, 'Unauthorized action.');
}
try {
//check for demo
if ($this->isDemo()) {
return redirect()
->action([\App\Http\Controllers\HomeController::class, 'index'])
->with('status', ['success' => false, 'msg' => __('messages.feature_disabled_in_demo')]);
}
//update or create system data
$systems = $request->input('system');
foreach ($systems as $key => $value) {
System::updateOrCreate(['key' => $key], ['value' => ! empty($value) ? $value : '']);
}
//update env settings
$env_settings = $request->only('APP_NAME', 'APP_TITLE', 'MAIL_HOST', 'MAIL_PORT',
'MAIL_USERNAME', 'MAIL_PASSWORD', 'MAIL_ENCRYPTION', 'APP_LOCALE',
'APP_TIMEZONE', 'CURRENCY_NAME', 'CURRENCY_SYMBOL', 'MAIL_FROM_ADDRESS',
'MAIL_FROM_NAME', 'CURRENCY_CODE', 'PAYPAL_MODE', 'PAYPAL_SANDBOX_API_USERNAME',
'PAYPAL_SANDBOX_API_PASSWORD', 'PAYPAL_SANDBOX_API_SECRET', 'PAYPAL_LIVE_API_USERNAME',
'PAYPAL_LIVE_API_PASSWORD', 'PAYPAL_LIVE_API_SECRET', 'STRIPE_PUB_KEY', 'STRIPE_SECRET_KEY',
'APP_DATE_FORMAT', 'APP_TIME_FORMAT', 'ACELLE_MAIL_NAME', 'ACELLE_MAIL_API');
//checkboxes values
$env_settings['ENABLE_REGISTRATION'] = ! empty($request->input('ENABLE_REGISTRATION')) ? $request->input('ENABLE_REGISTRATION') : 0;
$env_settings['ENABLE_SAAS_MODULE'] = ! empty($request->input('ENABLE_SAAS_MODULE')) ? $request->input('ENABLE_SAAS_MODULE') : 0;
$env_settings['ENABLE_OFFLINE_PAYMENT'] = ! empty($request->input('ENABLE_OFFLINE_PAYMENT')) ? $request->input('ENABLE_OFFLINE_PAYMENT') : 0;
$found_envs = [];
$env_path = base_path('.env');
$env_lines = file($env_path);
foreach ($env_settings as $index => $value) {
foreach ($env_lines as $key => $line) {
//Check if present then replace it.
if (strpos($line, $index) !== false) {
$env_lines[$key] = $index.'="'.$value.'"'.PHP_EOL;
$found_envs[] = $index;
}
}
}
//Add the missing env settings
$missing_envs = array_diff(array_keys($env_settings), $found_envs);
if (! empty($missing_envs)) {
$missing_envs = array_values($missing_envs);
foreach ($missing_envs as $k => $key) {
if ($k == 0) {
$env_lines[] = PHP_EOL.$key.'="'.$env_settings[$key].'"'.PHP_EOL;
} else {
$env_lines[] = $key.'="'.$env_settings[$key].'"'.PHP_EOL;
}
}
}
$env_content = implode('', $env_lines);
if (is_writable($env_path) && file_put_contents($env_path, $env_content)) {
$output = [
'success' => true,
'msg' => __('messages.saved_successfully'),
];
} else {
$output = [
'success' => false,
'msg' => __('messages.env_permission'),
];
}
return redirect()
->action([\App\Http\Controllers\HomeController::class, 'index'])
->with('status', $output);
} catch (\Exception $e) {
return redirect()
->action([\App\Http\Controllers\HomeController::class, 'index'])
->with('status',
[
'success' => false,
'msg' => __('messages.something_went_wrong'),
]
);
}
}
/**
* Display the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function show($id)
{
//
}
/**
* Show the form for editing the specified resource.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function edit($id)
{
//
}
/**
* Update the specified resource in storage.
*
* @param \Illuminate\Http\Request $request
* @param int $id
* @return \Illuminate\Http\Response
*/
public function update(Request $request, $id)
{
//
}
/**
* Remove the specified resource from storage.
*
* @param int $id
* @return \Illuminate\Http\Response
*/
public function destroy($id)
{
//
}
}

View File

@@ -0,0 +1,71 @@
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Storage;
class UploadController extends Controller
{
public function upload(Request $request)
{
//check for demo
if ($this->isDemo()) {
return $this->respondDemo();
}
try {
$file = $request->file('file');
//Generate a unique name for the file.
$file_name = time().'_'.$file->getClientOriginalName();
$path = $file->storeAs(config('constants.doc_path'), $file_name);
$output = $this->respondSuccess('Uploaded successfully', ['path' => $file_name]);
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
public function deleteFile(Request $request)
{
try {
$file_name = $request->input('file_name');
$doc_path = config('constants.doc_path');
Storage::delete($doc_path.'/'.$file_name);
$output = $this->respondSuccess('Removed');
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
public function getExistingFiles(Request $request)
{
try {
$files = $request->get('files');
$file_array = explode(',', $files);
$file_response = [];
$doc_path = config('constants.doc_path');
foreach ($file_array as $key => $file) {
if (Storage::disk('local')->exists($doc_path.'/'.$file)) {
$file_name = explode('_', $file, 2);
$file_response[$key]['uploaded_as'] = $file;
$file_response[$key]['name'] = ! empty($file_name[1]) ? $file_name[1] : $file_name[0];
$file_response[$key]['size'] = Storage::size($doc_path.'/'.$file);
$file_response[$key]['path'] = Storage::url($doc_path.'/'.$file);
}
}
$output = $this->respondSuccess('Uploaded successfully', ['files' => $file_response]);
} catch (Exception $e) {
$output = $this->respondWentWrong($e);
}
return $output;
}
}

View File

@@ -0,0 +1,46 @@
GPL Cooperation Commitment
Version 1.0
Before filing or continuing to prosecute any legal proceeding or claim
(other than a Defensive Action) arising from termination of a Covered
License, we commit to extend to the person or entity ('you') accused
of violating the Covered License the following provisions regarding
cure and reinstatement, taken from GPL version 3. As used here, the
term 'this License' refers to the specific Covered License being
enforced.
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly
and finally terminates your license, and (b) permanently, if the
copyright holder fails to notify you of the violation by some
reasonable means prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you
have received notice of violation of this License (for any work)
from that copyright holder, and you cure the violation prior to 30
days after your receipt of the notice.
We intend this Commitment to be irrevocable, and binding and
enforceable against us and assignees of or successors to our
copyrights.
Definitions
'Covered License' means the GNU General Public License, version 2
(GPLv2), the GNU Lesser General Public License, version 2.1
(LGPLv2.1), or the GNU Library General Public License, version 2
(LGPLv2), all as published by the Free Software Foundation.
'Defensive Action' means a legal proceeding or claim that We bring
against you in response to a prior proceeding or claim initiated by
you or your affiliate.
'We' means each contributor to this repository as of the date of
inclusion of this file, including subsidiaries of a corporate
contributor.
This work is available under a Creative Commons Attribution-ShareAlike
4.0 International license (https://creativecommons.org/licenses/by-sa/4.0/).

View File

@@ -0,0 +1,502 @@
GNU LESSER GENERAL PUBLIC LICENSE
Version 2.1, February 1999
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
[This is the first released version of the Lesser GPL. It also counts
as the successor of the GNU Library Public License, version 2, hence
the version number 2.1.]
Preamble
The licenses for most software are designed to take away your
freedom to share and change it. By contrast, the GNU General Public
Licenses are intended to guarantee your freedom to share and change
free software--to make sure the software is free for all its users.
This license, the Lesser General Public License, applies to some
specially designated software packages--typically libraries--of the
Free Software Foundation and other authors who decide to use it. You
can use it too, but we suggest you first think carefully about whether
this license or the ordinary General Public License is the better
strategy to use in any particular case, based on the explanations below.
When we speak of free software, we are referring to freedom of use,
not price. Our General Public Licenses are designed to make sure that
you have the freedom to distribute copies of free software (and charge
for this service if you wish); that you receive source code or can get
it if you want it; that you can change the software and use pieces of
it in new free programs; and that you are informed that you can do
these things.
To protect your rights, we need to make restrictions that forbid
distributors to deny you these rights or to ask you to surrender these
rights. These restrictions translate to certain responsibilities for
you if you distribute copies of the library or if you modify it.
For example, if you distribute copies of the library, whether gratis
or for a fee, you must give the recipients all the rights that we gave
you. You must make sure that they, too, receive or can get the source
code. If you link other code with the library, you must provide
complete object files to the recipients, so that they can relink them
with the library after making changes to the library and recompiling
it. And you must show them these terms so they know their rights.
We protect your rights with a two-step method: (1) we copyright the
library, and (2) we offer you this license, which gives you legal
permission to copy, distribute and/or modify the library.
To protect each distributor, we want to make it very clear that
there is no warranty for the free library. Also, if the library is
modified by someone else and passed on, the recipients should know
that what they have is not the original version, so that the original
author's reputation will not be affected by problems that might be
introduced by others.
Finally, software patents pose a constant threat to the existence of
any free program. We wish to make sure that a company cannot
effectively restrict the users of a free program by obtaining a
restrictive license from a patent holder. Therefore, we insist that
any patent license obtained for a version of the library must be
consistent with the full freedom of use specified in this license.
Most GNU software, including some libraries, is covered by the
ordinary GNU General Public License. This license, the GNU Lesser
General Public License, applies to certain designated libraries, and
is quite different from the ordinary General Public License. We use
this license for certain libraries in order to permit linking those
libraries into non-free programs.
When a program is linked with a library, whether statically or using
a shared library, the combination of the two is legally speaking a
combined work, a derivative of the original library. The ordinary
General Public License therefore permits such linking only if the
entire combination fits its criteria of freedom. The Lesser General
Public License permits more lax criteria for linking other code with
the library.
We call this license the "Lesser" General Public License because it
does Less to protect the user's freedom than the ordinary General
Public License. It also provides other free software developers Less
of an advantage over competing non-free programs. These disadvantages
are the reason we use the ordinary General Public License for many
libraries. However, the Lesser license provides advantages in certain
special circumstances.
For example, on rare occasions, there may be a special need to
encourage the widest possible use of a certain library, so that it becomes
a de-facto standard. To achieve this, non-free programs must be
allowed to use the library. A more frequent case is that a free
library does the same job as widely used non-free libraries. In this
case, there is little to gain by limiting the free library to free
software only, so we use the Lesser General Public License.
In other cases, permission to use a particular library in non-free
programs enables a greater number of people to use a large body of
free software. For example, permission to use the GNU C Library in
non-free programs enables many more people to use the whole GNU
operating system, as well as its variant, the GNU/Linux operating
system.
Although the Lesser General Public License is Less protective of the
users' freedom, it does ensure that the user of a program that is
linked with the Library has the freedom and the wherewithal to run
that program using a modified version of the Library.
The precise terms and conditions for copying, distribution and
modification follow. Pay close attention to the difference between a
"work based on the library" and a "work that uses the library". The
former contains code derived from the library, whereas the latter must
be combined with the library in order to run.
GNU LESSER GENERAL PUBLIC LICENSE
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
0. This License Agreement applies to any software library or other
program which contains a notice placed by the copyright holder or
other authorized party saying it may be distributed under the terms of
this Lesser General Public License (also called "this License").
Each licensee is addressed as "you".
A "library" means a collection of software functions and/or data
prepared so as to be conveniently linked with application programs
(which use some of those functions and data) to form executables.
The "Library", below, refers to any such software library or work
which has been distributed under these terms. A "work based on the
Library" means either the Library or any derivative work under
copyright law: that is to say, a work containing the Library or a
portion of it, either verbatim or with modifications and/or translated
straightforwardly into another language. (Hereinafter, translation is
included without limitation in the term "modification".)
"Source code" for a work means the preferred form of the work for
making modifications to it. For a library, complete source code means
all the source code for all modules it contains, plus any associated
interface definition files, plus the scripts used to control compilation
and installation of the library.
Activities other than copying, distribution and modification are not
covered by this License; they are outside its scope. The act of
running a program using the Library is not restricted, and output from
such a program is covered only if its contents constitute a work based
on the Library (independent of the use of the Library in a tool for
writing it). Whether that is true depends on what the Library does
and what the program that uses the Library does.
1. You may copy and distribute verbatim copies of the Library's
complete source code as you receive it, in any medium, provided that
you conspicuously and appropriately publish on each copy an
appropriate copyright notice and disclaimer of warranty; keep intact
all the notices that refer to this License and to the absence of any
warranty; and distribute a copy of this License along with the
Library.
You may charge a fee for the physical act of transferring a copy,
and you may at your option offer warranty protection in exchange for a
fee.
2. You may modify your copy or copies of the Library or any portion
of it, thus forming a work based on the Library, and copy and
distribute such modifications or work under the terms of Section 1
above, provided that you also meet all of these conditions:
a) The modified work must itself be a software library.
b) You must cause the files modified to carry prominent notices
stating that you changed the files and the date of any change.
c) You must cause the whole of the work to be licensed at no
charge to all third parties under the terms of this License.
d) If a facility in the modified Library refers to a function or a
table of data to be supplied by an application program that uses
the facility, other than as an argument passed when the facility
is invoked, then you must make a good faith effort to ensure that,
in the event an application does not supply such function or
table, the facility still operates, and performs whatever part of
its purpose remains meaningful.
(For example, a function in a library to compute square roots has
a purpose that is entirely well-defined independent of the
application. Therefore, Subsection 2d requires that any
application-supplied function or table used by this function must
be optional: if the application does not supply it, the square
root function must still compute square roots.)
These requirements apply to the modified work as a whole. If
identifiable sections of that work are not derived from the Library,
and can be reasonably considered independent and separate works in
themselves, then this License, and its terms, do not apply to those
sections when you distribute them as separate works. But when you
distribute the same sections as part of a whole which is a work based
on the Library, the distribution of the whole must be on the terms of
this License, whose permissions for other licensees extend to the
entire whole, and thus to each and every part regardless of who wrote
it.
Thus, it is not the intent of this section to claim rights or contest
your rights to work written entirely by you; rather, the intent is to
exercise the right to control the distribution of derivative or
collective works based on the Library.
In addition, mere aggregation of another work not based on the Library
with the Library (or with a work based on the Library) on a volume of
a storage or distribution medium does not bring the other work under
the scope of this License.
3. You may opt to apply the terms of the ordinary GNU General Public
License instead of this License to a given copy of the Library. To do
this, you must alter all the notices that refer to this License, so
that they refer to the ordinary GNU General Public License, version 2,
instead of to this License. (If a newer version than version 2 of the
ordinary GNU General Public License has appeared, then you can specify
that version instead if you wish.) Do not make any other change in
these notices.
Once this change is made in a given copy, it is irreversible for
that copy, so the ordinary GNU General Public License applies to all
subsequent copies and derivative works made from that copy.
This option is useful when you wish to copy part of the code of
the Library into a program that is not a library.
4. You may copy and distribute the Library (or a portion or
derivative of it, under Section 2) in object code or executable form
under the terms of Sections 1 and 2 above provided that you accompany
it with the complete corresponding machine-readable source code, which
must be distributed under the terms of Sections 1 and 2 above on a
medium customarily used for software interchange.
If distribution of object code is made by offering access to copy
from a designated place, then offering equivalent access to copy the
source code from the same place satisfies the requirement to
distribute the source code, even though third parties are not
compelled to copy the source along with the object code.
5. A program that contains no derivative of any portion of the
Library, but is designed to work with the Library by being compiled or
linked with it, is called a "work that uses the Library". Such a
work, in isolation, is not a derivative work of the Library, and
therefore falls outside the scope of this License.
However, linking a "work that uses the Library" with the Library
creates an executable that is a derivative of the Library (because it
contains portions of the Library), rather than a "work that uses the
library". The executable is therefore covered by this License.
Section 6 states terms for distribution of such executables.
When a "work that uses the Library" uses material from a header file
that is part of the Library, the object code for the work may be a
derivative work of the Library even though the source code is not.
Whether this is true is especially significant if the work can be
linked without the Library, or if the work is itself a library. The
threshold for this to be true is not precisely defined by law.
If such an object file uses only numerical parameters, data
structure layouts and accessors, and small macros and small inline
functions (ten lines or less in length), then the use of the object
file is unrestricted, regardless of whether it is legally a derivative
work. (Executables containing this object code plus portions of the
Library will still fall under Section 6.)
Otherwise, if the work is a derivative of the Library, you may
distribute the object code for the work under the terms of Section 6.
Any executables containing that work also fall under Section 6,
whether or not they are linked directly with the Library itself.
6. As an exception to the Sections above, you may also combine or
link a "work that uses the Library" with the Library to produce a
work containing portions of the Library, and distribute that work
under terms of your choice, provided that the terms permit
modification of the work for the customer's own use and reverse
engineering for debugging such modifications.
You must give prominent notice with each copy of the work that the
Library is used in it and that the Library and its use are covered by
this License. You must supply a copy of this License. If the work
during execution displays copyright notices, you must include the
copyright notice for the Library among them, as well as a reference
directing the user to the copy of this License. Also, you must do one
of these things:
a) Accompany the work with the complete corresponding
machine-readable source code for the Library including whatever
changes were used in the work (which must be distributed under
Sections 1 and 2 above); and, if the work is an executable linked
with the Library, with the complete machine-readable "work that
uses the Library", as object code and/or source code, so that the
user can modify the Library and then relink to produce a modified
executable containing the modified Library. (It is understood
that the user who changes the contents of definitions files in the
Library will not necessarily be able to recompile the application
to use the modified definitions.)
b) Use a suitable shared library mechanism for linking with the
Library. A suitable mechanism is one that (1) uses at run time a
copy of the library already present on the user's computer system,
rather than copying library functions into the executable, and (2)
will operate properly with a modified version of the library, if
the user installs one, as long as the modified version is
interface-compatible with the version that the work was made with.
c) Accompany the work with a written offer, valid for at
least three years, to give the same user the materials
specified in Subsection 6a, above, for a charge no more
than the cost of performing this distribution.
d) If distribution of the work is made by offering access to copy
from a designated place, offer equivalent access to copy the above
specified materials from the same place.
e) Verify that the user has already received a copy of these
materials or that you have already sent this user a copy.
For an executable, the required form of the "work that uses the
Library" must include any data and utility programs needed for
reproducing the executable from it. However, as a special exception,
the materials to be distributed need not include anything that is
normally distributed (in either source or binary form) with the major
components (compiler, kernel, and so on) of the operating system on
which the executable runs, unless that component itself accompanies
the executable.
It may happen that this requirement contradicts the license
restrictions of other proprietary libraries that do not normally
accompany the operating system. Such a contradiction means you cannot
use both them and the Library together in an executable that you
distribute.
7. You may place library facilities that are a work based on the
Library side-by-side in a single library together with other library
facilities not covered by this License, and distribute such a combined
library, provided that the separate distribution of the work based on
the Library and of the other library facilities is otherwise
permitted, and provided that you do these two things:
a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities. This must be distributed under the terms of the
Sections above.
b) Give prominent notice with the combined library of the fact
that part of it is a work based on the Library, and explaining
where to find the accompanying uncombined form of the same work.
8. You may not copy, modify, sublicense, link with, or distribute
the Library except as expressly provided under this License. Any
attempt otherwise to copy, modify, sublicense, link with, or
distribute the Library is void, and will automatically terminate your
rights under this License. However, parties who have received copies,
or rights, from you under this License will not have their licenses
terminated so long as such parties remain in full compliance.
9. You are not required to accept this License, since you have not
signed it. However, nothing else grants you permission to modify or
distribute the Library or its derivative works. These actions are
prohibited by law if you do not accept this License. Therefore, by
modifying or distributing the Library (or any work based on the
Library), you indicate your acceptance of this License to do so, and
all its terms and conditions for copying, distributing or modifying
the Library or works based on it.
10. Each time you redistribute the Library (or any work based on the
Library), the recipient automatically receives a license from the
original licensor to copy, distribute, link with or modify the Library
subject to these terms and conditions. You may not impose any further
restrictions on the recipients' exercise of the rights granted herein.
You are not responsible for enforcing compliance by third parties with
this License.
11. If, as a consequence of a court judgment or allegation of patent
infringement or for any other reason (not limited to patent issues),
conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot
distribute so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you
may not distribute the Library at all. For example, if a patent
license would not permit royalty-free redistribution of the Library by
all those who receive copies directly or indirectly through you, then
the only way you could satisfy both it and this License would be to
refrain entirely from distribution of the Library.
If any portion of this section is held invalid or unenforceable under any
particular circumstance, the balance of the section is intended to apply,
and the section as a whole is intended to apply in other circumstances.
It is not the purpose of this section to induce you to infringe any
patents or other property right claims or to contest validity of any
such claims; this section has the sole purpose of protecting the
integrity of the free software distribution system which is
implemented by public license practices. Many people have made
generous contributions to the wide range of software distributed
through that system in reliance on consistent application of that
system; it is up to the author/donor to decide if he or she is willing
to distribute software through any other system and a licensee cannot
impose that choice.
This section is intended to make thoroughly clear what is believed to
be a consequence of the rest of this License.
12. If the distribution and/or use of the Library is restricted in
certain countries either by patents or by copyrighted interfaces, the
original copyright holder who places the Library under this License may add
an explicit geographical distribution limitation excluding those countries,
so that distribution is permitted only in or among countries not thus
excluded. In such case, this License incorporates the limitation as if
written in the body of this License.
13. The Free Software Foundation may publish revised and/or new
versions of the Lesser General Public License from time to time.
Such new versions will be similar in spirit to the present version,
but may differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
specifies a version number of this License which applies to it and
"any later version", you have the option of following the terms and
conditions either of that version or of any later version published by
the Free Software Foundation. If the Library does not specify a
license version number, you may choose any version ever published by
the Free Software Foundation.
14. If you wish to incorporate parts of the Library into other free
programs whose distribution conditions are incompatible with these,
write to the author to ask for permission. For software which is
copyrighted by the Free Software Foundation, write to the Free
Software Foundation; we sometimes make exceptions for this. Our
decision will be guided by the two goals of preserving the free status
of all derivatives of our free software and of promoting the sharing
and reuse of software generally.
NO WARRANTY
15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGES.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Libraries
If you develop a new library, and you want it to be of the greatest
possible use to the public, we recommend making it free software that
everyone can redistribute and change. You can do so by permitting
redistribution under these terms (or, alternatively, under the terms of the
ordinary General Public License).
To apply these terms, attach the following notices to the library. It is
safest to attach them to the start of each source file to most effectively
convey the exclusion of warranty; and each file should have at least the
"copyright" line and a pointer to where the full notice is found.
<one line to give the library's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
Also add information on how to contact you by electronic and paper mail.
You should also get your employer (if you work as a programmer) or your
school, if any, to sign a "copyright disclaimer" for the library, if
necessary. Here is a sample; alter the names:
Yoyodyne, Inc., hereby disclaims all copyright interest in the
library `Frob' (a library for tweaking knobs) written by James Random Hacker.
<signature of Ty Coon>, 1 April 1990
Ty Coon, President of Vice
That's all there is to it!

View File

@@ -0,0 +1,220 @@
![PHPMailer](https://raw.github.com/PHPMailer/PHPMailer/master/examples/images/phpmailer.png)
# PHPMailer - A full-featured email creation and transfer class for PHP
Build status: [![Build Status](https://travis-ci.org/PHPMailer/PHPMailer.svg)](https://travis-ci.org/PHPMailer/PHPMailer)
[![Scrutinizer Quality Score](https://scrutinizer-ci.com/g/PHPMailer/PHPMailer/badges/quality-score.png?s=3758e21d279becdf847a557a56a3ed16dfec9d5d)](https://scrutinizer-ci.com/g/PHPMailer/PHPMailer/)
[![Code Coverage](https://scrutinizer-ci.com/g/PHPMailer/PHPMailer/badges/coverage.png?s=3fe6ca5fe8cd2cdf96285756e42932f7ca256962)](https://scrutinizer-ci.com/g/PHPMailer/PHPMailer/)
[![Latest Stable Version](https://poser.pugx.org/phpmailer/phpmailer/v/stable.svg)](https://packagist.org/packages/phpmailer/phpmailer) [![Total Downloads](https://poser.pugx.org/phpmailer/phpmailer/downloads)](https://packagist.org/packages/phpmailer/phpmailer) [![Latest Unstable Version](https://poser.pugx.org/phpmailer/phpmailer/v/unstable.svg)](https://packagist.org/packages/phpmailer/phpmailer) [![License](https://poser.pugx.org/phpmailer/phpmailer/license.svg)](https://packagist.org/packages/phpmailer/phpmailer)
## Class Features
- Probably the world's most popular code for sending email from PHP!
- Used by many open-source projects: WordPress, Drupal, 1CRM, SugarCRM, Yii, Joomla! and many more
- Integrated SMTP support - send without a local mail server
- Send emails with multiple To, CC, BCC and Reply-to addresses
- Multipart/alternative emails for mail clients that do not read HTML email
- Add attachments, including inline
- Support for UTF-8 content and 8bit, base64, binary, and quoted-printable encodings
- SMTP authentication with LOGIN, PLAIN, CRAM-MD5 and XOAUTH2 mechanisms over SSL and SMTP+STARTTLS transports
- Validates email addresses automatically
- Protect against header injection attacks
- Error messages in over 50 languages!
- DKIM and S/MIME signing support
- Compatible with PHP 5.5 and later
- Namespaced to prevent name clashes
- Much more!
## Why you might need it
Many PHP developers utilize email in their code. The only PHP function that supports this is the `mail()` function. However, it does not provide any assistance for making use of popular features such as HTML-based emails and attachments.
Formatting email correctly is surprisingly difficult. There are myriad overlapping RFCs, requiring tight adherence to horribly complicated formatting and encoding rules - the vast majority of code that you'll find online that uses the `mail()` function directly is just plain wrong!
*Please* don't be tempted to do it yourself - if you don't use PHPMailer, there are many other excellent libraries that you should look at before rolling your own - try [SwiftMailer](https://swiftmailer.symfony.com/), [Zend/Mail](https://zendframework.github.io/zend-mail/), [eZcomponents](https://github.com/zetacomponents/Mail) etc.
The PHP `mail()` function usually sends via a local mail server, typically fronted by a `sendmail` binary on Linux, BSD and OS X platforms, however, Windows usually doesn't include a local mail server; PHPMailer's integrated SMTP implementation allows email sending on Windows platforms without a local mail server.
## License
This software is distributed under the [LGPL 2.1](http://www.gnu.org/licenses/lgpl-2.1.html) license, along with the [GPL Cooperation Commitment](https://gplcc.github.io/gplcc/). Please read LICENSE for information on the software availability and distribution.
## Installation & loading
PHPMailer is available on [Packagist](https://packagist.org/packages/phpmailer/phpmailer) (using semantic versioning), and installation via [Composer](https://getcomposer.org) is the recommended way to install PHPMailer. Just add this line to your `composer.json` file:
```json
"phpmailer/phpmailer": "~6.0"
```
or run
```sh
composer require phpmailer/phpmailer
```
Note that the `vendor` folder and the `vendor/autoload.php` script are generated by Composer; they are not part of PHPMailer.
If you want to use the Gmail XOAUTH2 authentication class, you will also need to add a dependency on the `league/oauth2-client` package in your `composer.json`.
Alternatively, if you're not using Composer, copy the contents of the PHPMailer folder into one of the `include_path` directories specified in your PHP configuration and load each class file manually:
```php
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
require 'path/to/PHPMailer/src/Exception.php';
require 'path/to/PHPMailer/src/PHPMailer.php';
require 'path/to/PHPMailer/src/SMTP.php';
```
If you're not using the `SMTP` class explicitly (you're probably not), you don't need a `use` line for the SMTP class.
If you don't speak git or just want a tarball, click the 'zip' button on the right of the project page in GitHub, though note that docs and examples are not included in the tarball.
## Legacy versions
PHPMailer 5.2 (which is compatible with PHP 5.0 - 7.0) is no longer being supported for feature updates, and will only be receiving security updates from now on. You will find the latest version of 5.2 in the [5.2-stable branch](https://github.com/PHPMailer/PHPMailer/tree/5.2-stable), and future versions of 5.2 will be tagged with 5.2.x version numbers, so existing Composer configs should remain working. If you're using PHP 5.5 or later, we recommend you make the necessary changes to switch to the 6.0 release.
The 5.2 branch will not receive security updates after December 31st 2018.
## Upgrading from 5.2
The biggest changes are that source files are now in the `src/` folder, and PHPMailer now declares the namespace `PHPMailer\PHPMailer`. This has several important effects [read the upgrade guide](https://github.com/PHPMailer/PHPMailer/tree/master/UPGRADING.md) for more details.
### Minimal installation
While installing the entire package manually or with Composer is simple, convenient and reliable, you may want to include only vital files in your project. At the very least you will need [src/PHPMailer.php](https://github.com/PHPMailer/PHPMailer/tree/master/src/PHPMailer.php). If you're using SMTP, you'll need [src/SMTP.php](https://github.com/PHPMailer/PHPMailer/tree/master/src/SMTP.php), and if you're using POP-before SMTP, you'll need [src/POP3.php](https://github.com/PHPMailer/PHPMailer/tree/master/src/POP3.php). You can skip the [language](https://github.com/PHPMailer/PHPMailer/tree/master/language/) folder if you're not showing errors to users and can make do with English-only errors. If you're using XOAUTH2 you will need [src/OAuth.php](https://github.com/PHPMailer/PHPMailer/tree/master/src/OAuth.php) as well as the Composer dependencies for the services you wish to authenticate with. Really, it's much easier to use Composer!
## A Simple Example
```php
<?php
// Import PHPMailer classes into the global namespace
// These must be at the top of your script, not inside a function
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
// Load Composer's autoloader
require 'vendor/autoload.php';
// Instantiation and passing `true` enables exceptions
$mail = new PHPMailer(true);
try {
//Server settings
$mail->SMTPDebug = 2; // Enable verbose debug output
$mail->isSMTP(); // Set mailer to use SMTP
$mail->Host = 'smtp1.example.com;smtp2.example.com'; // Specify main and backup SMTP servers
$mail->SMTPAuth = true; // Enable SMTP authentication
$mail->Username = 'user@example.com'; // SMTP username
$mail->Password = 'secret'; // SMTP password
$mail->SMTPSecure = 'tls'; // Enable TLS encryption, `ssl` also accepted
$mail->Port = 587; // TCP port to connect to
//Recipients
$mail->setFrom('from@example.com', 'Mailer');
$mail->addAddress('joe@example.net', 'Joe User'); // Add a recipient
$mail->addAddress('ellen@example.com'); // Name is optional
$mail->addReplyTo('info@example.com', 'Information');
$mail->addCC('cc@example.com');
$mail->addBCC('bcc@example.com');
// Attachments
$mail->addAttachment('/var/tmp/file.tar.gz'); // Add attachments
$mail->addAttachment('/tmp/image.jpg', 'new.jpg'); // Optional name
// Content
$mail->isHTML(true); // Set email format to HTML
$mail->Subject = 'Here is the subject';
$mail->Body = 'This is the HTML message body <b>in bold!</b>';
$mail->AltBody = 'This is the body in plain text for non-HTML mail clients';
$mail->send();
echo 'Message has been sent';
} catch (Exception $e) {
echo "Message could not be sent. Mailer Error: {$mail->ErrorInfo}";
}
```
You'll find plenty more to play with in the [examples](https://github.com/PHPMailer/PHPMailer/tree/master/examples) folder.
That's it. You should now be ready to use PHPMailer!
## Localization
PHPMailer defaults to English, but in the [language](https://github.com/PHPMailer/PHPMailer/tree/master/language/) folder you'll find numerous (48 at the time of writing!) translations for PHPMailer error messages that you may encounter. Their filenames contain [ISO 639-1](http://en.wikipedia.org/wiki/ISO_639-1) language code for the translations, for example `fr` for French. To specify a language, you need to tell PHPMailer which one to use, like this:
```php
// To load the French version
$mail->setLanguage('fr', '/optional/path/to/language/directory/');
```
We welcome corrections and new languages - if you're looking for corrections to do, run the [PHPMailerLangTest.php](https://github.com/PHPMailer/PHPMailer/tree/master/test/PHPMailerLangTest.php) script in the tests folder and it will show any missing translations.
## Documentation
Start reading at the [GitHub wiki](https://github.com/PHPMailer/PHPMailer/wiki). If you're having trouble, this should be the first place you look as it's the most frequently updated.
Examples of how to use PHPMailer for common scenarios can be found in the [examples](https://github.com/PHPMailer/PHPMailer/tree/master/examples) folder. If you're looking for a good starting point, we recommend you start with [the Gmail example](https://github.com/PHPMailer/PHPMailer/tree/master/examples/gmail.phps).
Note that in order to reduce PHPMailer's deployed code footprint, the examples are no longer included if you load PHPMailer via Composer or via [GitHub's zip file download](https://github.com/PHPMailer/PHPMailer/archive/master.zip), so you'll need to either clone the git repository or use the above links to get to the examples directly.
Complete generated API documentation is [available online](http://phpmailer.github.io/PHPMailer/).
You can generate complete API-level documentation by running `phpdoc` in the top-level folder, and documentation will appear in the `docs` folder, though you'll need to have [PHPDocumentor](http://www.phpdoc.org) installed. You may find [the unit tests](https://github.com/PHPMailer/PHPMailer/tree/master/test/phpmailerTest.php) a good source of how to do various operations such as encryption.
If the documentation doesn't cover what you need, search the [many questions on Stack Overflow](http://stackoverflow.com/questions/tagged/phpmailer), and before you ask a question about "SMTP Error: Could not connect to SMTP host.", [read the troubleshooting guide](https://github.com/PHPMailer/PHPMailer/wiki/Troubleshooting).
## Tests
There is a PHPUnit test script in the [test](https://github.com/PHPMailer/PHPMailer/tree/master/test/) folder. PHPMailer uses PHPUnit 4.8 - we would use 5.x but we need to run on PHP 5.5.
Build status: [![Build Status](https://travis-ci.org/PHPMailer/PHPMailer.svg)](https://travis-ci.org/PHPMailer/PHPMailer)
If this isn't passing, is there something you can do to help?
## Security
Please disclose any vulnerabilities found responsibly - report any security problems found to the maintainers privately.
PHPMailer versions prior to 5.2.22 (released January 9th 2017) have a local file disclosure vulnerability, [CVE-2017-5223](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-5223). If content passed into `msgHTML()` is sourced from unfiltered user input, relative paths can map to absolute local file paths and added as attachments. Also note that `addAttachment` (just like `file_get_contents`, `passthru`, `unlink`, etc) should not be passed user-sourced params either! Reported by Yongxiang Li of Asiasecurity.
PHPMailer versions prior to 5.2.20 (released December 28th 2016) are vulnerable to [CVE-2016-10045](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10045) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10045-Vuln-Patch-Bypass.html), and patched by Paul Buonopane (@Zenexer).
PHPMailer versions prior to 5.2.18 (released December 2016) are vulnerable to [CVE-2016-10033](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10033) a critical remote code execution vulnerability, responsibly reported by [Dawid Golunski](http://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10033-Vuln.html).
See [SECURITY](https://github.com/PHPMailer/PHPMailer/tree/master/SECURITY.md) for more detail on security issues.
## Contributing
Please submit bug reports, suggestions and pull requests to the [GitHub issue tracker](https://github.com/PHPMailer/PHPMailer/issues).
We're particularly interested in fixing edge-cases, expanding test coverage and updating translations.
If you found a mistake in the docs, or want to add something, go ahead and amend the wiki - anyone can edit it.
If you have git clones from prior to the move to the PHPMailer GitHub organisation, you'll need to update any remote URLs referencing the old GitHub location with a command like this from within your clone:
```sh
git remote set-url upstream https://github.com/PHPMailer/PHPMailer.git
```
Please *don't* use the SourceForge or Google Code projects any more; they are obsolete and no longer maintained.
## Sponsorship
Development time and resources for PHPMailer are provided by [Smartmessages.net](https://info.smartmessages.net/), a powerful email marketing system.
<a href="https://info.smartmessages.net/"><img src="https://www.smartmessages.net/img/smartmessages-logo.svg" width="250" height="28" alt="Smartmessages email marketing"></a>
Other contributions are gladly received, whether in beer 🍺, T-shirts 👕, Amazon wishlist raids, or cold, hard cash 💰. If you'd like to donate to say "thank you" to maintainers or contributors, please contact them through individual profile pages via [the contributors page](https://github.com/PHPMailer/PHPMailer/graphs/contributors).
## Changelog
See [changelog](changelog.md).
## History
- PHPMailer was originally written in 2001 by Brent R. Matzelle as a [SourceForge project](http://sourceforge.net/projects/phpmailer/).
- Marcus Bointon (coolbru on SF) and Andy Prevost (codeworxtech) took over the project in 2004.
- Became an Apache incubator project on Google Code in 2010, managed by Jim Jagielski.
- Marcus created his fork on [GitHub](https://github.com/Synchro/PHPMailer) in 2008.
- Jim and Marcus decide to join forces and use GitHub as the canonical and official repo for PHPMailer in 2013.
- PHPMailer moves to the [PHPMailer organisation](https://github.com/PHPMailer) on GitHub in 2013.
### What's changed since moving from SourceForge?
- Official successor to the SourceForge and Google Code projects.
- Test suite.
- Continuous integration with Travis-CI.
- Composer support.
- Public development.
- Additional languages and language strings.
- CRAM-MD5 authentication support.
- Preserves full repo history of authors, commits and branches from the original SourceForge project.

View File

@@ -0,0 +1,28 @@
# Security notices relating to PHPMailer
Please disclose any vulnerabilities found responsibly - report any security problems found to the maintainers privately.
PHPMailer versions prior to 6.0.6 and 5.2.27 are vulnerable to an object injection attack by passing `phar://` paths into `addAttachment()` and other functions that may receive unfiltered local paths, possibly leading to RCE. Recorded as [CVE-2018-19296](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2018-19296). See [this article](https://knasmueller.net/5-answers-about-php-phar-exploitation) for more info on this type of vulnerability. Mitigated by blocking the use of paths containing URL-protocol style prefixes such as `phar://`. Reported by Sehun Oh of cyberone.kr.
PHPMailer versions prior to 5.2.24 (released July 26th 2017) have an XSS vulnerability in one of the code examples, [CVE-2017-11503](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-11503). The `code_generator.phps` example did not filter user input prior to output. This file is distributed with a `.phps` extension, so it it not normally executable unless it is explicitly renamed, and the file is not included when PHPMailer is loaded through composer, so it is safe by default. There was also an undisclosed potential XSS vulnerability in the default exception handler (unused by default). Patches for both issues kindly provided by Patrick Monnerat of the Fedora Project.
PHPMailer versions prior to 5.2.22 (released January 9th 2017) have a local file disclosure vulnerability, [CVE-2017-5223](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2017-5223). If content passed into `msgHTML()` is sourced from unfiltered user input, relative paths can map to absolute local file paths and added as attachments. Also note that `addAttachment` (just like `file_get_contents`, `passthru`, `unlink`, etc) should not be passed user-sourced params either! Reported by Yongxiang Li of Asiasecurity.
PHPMailer versions prior to 5.2.20 (released December 28th 2016) are vulnerable to [CVE-2016-10045](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10045) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](https://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10045-Vuln-Patch-Bypass.html), and patched by Paul Buonopane (@Zenexer).
PHPMailer versions prior to 5.2.18 (released December 2016) are vulnerable to [CVE-2016-10033](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2016-10033) a remote code execution vulnerability, responsibly reported by [Dawid Golunski](http://legalhackers.com/advisories/PHPMailer-Exploit-Remote-Code-Exec-CVE-2016-10033-Vuln.html).
PHPMailer versions prior to 5.2.14 (released November 2015) are vulnerable to [CVE-2015-8476](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2015-8476) an SMTP CRLF injection bug permitting arbitrary message sending.
PHPMailer versions prior to 5.2.10 (released May 2015) are vulnerable to [CVE-2008-5619](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2008-5619), a remote code execution vulnerability in the bundled html2text library. This file was removed in 5.2.10, so if you are using a version prior to that and make use of the html2text function, it's vitally important that you upgrade and remove this file.
PHPMailer versions prior to 2.0.7 and 2.2.1 are vulnerable to [CVE-2012-0796](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2012-0796), an email header injection attack.
Joomla 1.6.0 uses PHPMailer in an unsafe way, allowing it to reveal local file paths, reported in [CVE-2011-3747](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2011-3747).
PHPMailer didn't sanitise the `$lang_path` parameter in `SetLanguage`. This wasn't a problem in itself, but some apps (PHPClassifieds, ATutor) also failed to sanitise user-provided parameters passed to it, permitting semi-arbitrary local file inclusion, reported in [CVE-2010-4914](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2010-4914), [CVE-2007-2021](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2007-2021) and [CVE-2006-5734](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2006-5734).
PHPMailer 1.7.2 and earlier contained a possible DDoS vulnerability reported in [CVE-2005-1807](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2005-1807).
PHPMailer 1.7 and earlier (June 2003) have a possible vulnerability in the `SendmailSend` method where shell commands may not be sanitised. Reported in [CVE-2007-3215](https://web.nvd.nist.gov/view/vuln/detail?vulnId=CVE-2007-3215).

View File

@@ -0,0 +1 @@
6.0.7

View File

@@ -0,0 +1,55 @@
{
"name": "phpmailer/phpmailer",
"type": "library",
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
"authors": [
{
"name": "Marcus Bointon",
"email": "phpmailer@synchromedia.co.uk"
},
{
"name": "Jim Jagielski",
"email": "jimjag@gmail.com"
},
{
"name": "Andy Prevost",
"email": "codeworxtech@users.sourceforge.net"
},
{
"name": "Brent R. Matzelle"
}
],
"require": {
"php": ">=5.5.0",
"ext-ctype": "*",
"ext-filter": "*"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.2",
"phpdocumentor/phpdocumentor": "2.*",
"phpunit/phpunit": "^4.8 || ^5.7",
"zendframework/zend-serializer": "2.7.*",
"doctrine/annotations": "1.2.*",
"zendframework/zend-eventmanager": "3.0.*",
"zendframework/zend-i18n": "2.7.3"
},
"suggest": {
"psr/log": "For optional PSR-3 debug logging",
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
"stevenmaguire/oauth2-microsoft": "Needed for Microsoft XOAUTH2 authentication",
"ext-mbstring": "Needed to send email in multibyte encoding charset",
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)"
},
"autoload": {
"psr-4": {
"PHPMailer\\PHPMailer\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"PHPMailer\\Test\\": "test/"
}
},
"license": "LGPL-2.1-only"
}

View File

@@ -0,0 +1,146 @@
<?php
/**
* PHPMailer - PHP email creation and transport class.
* PHP Version 5.5
*
* @see https://github.com/PHPMailer/PHPMailer/ The PHPMailer GitHub project
*
* @author Marcus Bointon (Synchro/coolbru) <phpmailer@synchromedia.co.uk>
* @author Jim Jagielski (jimjag) <jimjag@gmail.com>
* @author Andy Prevost (codeworxtech) <codeworxtech@users.sourceforge.net>
* @author Brent R. Matzelle (original founder)
* @copyright 2012 - 2017 Marcus Bointon
* @copyright 2010 - 2012 Jim Jagielski
* @copyright 2004 - 2009 Andy Prevost
* @license http://www.gnu.org/copyleft/lesser.html GNU Lesser General Public License
* @note This program is distributed in the hope that it will be useful - WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE.
*/
/**
* Get an OAuth2 token from an OAuth2 provider.
* * Install this script on your server so that it's accessible
* as [https/http]://<yourdomain>/<folder>/get_oauth_token.php
* e.g.: http://localhost/phpmailer/get_oauth_token.php
* * Ensure dependencies are installed with 'composer install'
* * Set up an app in your Google/Yahoo/Microsoft account
* * Set the script address as the app's redirect URL
* If no refresh token is obtained when running this file,
* revoke access to your app and run the script again.
*/
namespace PHPMailer\PHPMailer;
/**
* Aliases for League Provider Classes
* Make sure you have added these to your composer.json and run `composer install`
* Plenty to choose from here:
*
* @see http://oauth2-client.thephpleague.com/providers/thirdparty/
*/
// @see https://github.com/thephpleague/oauth2-google
use Hayageek\OAuth2\Client\Provider\Yahoo;
// @see https://packagist.org/packages/hayageek/oauth2-yahoo
use League\OAuth2\Client\Provider\Google;
// @see https://github.com/stevenmaguire/oauth2-microsoft
use Stevenmaguire\OAuth2\Client\Provider\Microsoft;
if (! isset($_GET['code']) && ! isset($_GET['provider'])) {
?>
<html>
<body>Select Provider:<br/>
<a href='?provider=Google'>Google</a><br/>
<a href='?provider=Yahoo'>Yahoo</a><br/>
<a href='?provider=Microsoft'>Microsoft/Outlook/Hotmail/Live/Office365</a><br/>
</body>
</html>
<?php
exit;
}
require 'vendor/autoload.php';
session_start();
$providerName = '';
if (array_key_exists('provider', $_GET)) {
$providerName = $_GET['provider'];
$_SESSION['provider'] = $providerName;
} elseif (array_key_exists('provider', $_SESSION)) {
$providerName = $_SESSION['provider'];
}
if (! in_array($providerName, ['Google', 'Microsoft', 'Yahoo'])) {
exit('Only Google, Microsoft and Yahoo OAuth2 providers are currently supported in this script.');
}
//These details are obtained by setting up an app in the Google developer console,
//or whichever provider you're using.
$clientId = 'RANDOMCHARS-----duv1n2.apps.googleusercontent.com';
$clientSecret = 'RANDOMCHARS-----lGyjPcRtvP';
//If this automatic URL doesn't work, set it yourself manually to the URL of this script
$redirectUri = (isset($_SERVER['HTTPS']) ? 'https://' : 'http://').$_SERVER['HTTP_HOST'].$_SERVER['PHP_SELF'];
//$redirectUri = 'http://localhost/PHPMailer/redirect';
$params = [
'clientId' => $clientId,
'clientSecret' => $clientSecret,
'redirectUri' => $redirectUri,
'accessType' => 'offline',
];
$options = [];
$provider = null;
switch ($providerName) {
case 'Google':
$provider = new Google($params);
$options = [
'scope' => [
'https://mail.google.com/',
],
];
break;
case 'Yahoo':
$provider = new Yahoo($params);
break;
case 'Microsoft':
$provider = new Microsoft($params);
$options = [
'scope' => [
'wl.imap',
'wl.offline_access',
],
];
break;
}
if (null === $provider) {
exit('Provider missing');
}
if (! isset($_GET['code'])) {
// If we don't have an authorization code then get one
$authUrl = $provider->getAuthorizationUrl($options);
$_SESSION['oauth2state'] = $provider->getState();
header('Location: '.$authUrl);
exit;
// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
unset($_SESSION['oauth2state']);
unset($_SESSION['provider']);
exit('Invalid state');
} else {
unset($_SESSION['provider']);
// Try to get an access token (using the authorization code grant)
$token = $provider->getAccessToken(
'authorization_code',
[
'code' => $_GET['code'],
]
);
// Use this to interact with an API on the users behalf
// Use this to get a new access token if the old one expires
echo 'Refresh Token: ', $token->getRefreshToken();
}

View File

@@ -0,0 +1,25 @@
<?php
/**
* Armenian PHPMailer language file: refer to English translation for definitive list
*
* @author Hrayr Grigoryan <hrayr@bits.am>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP -ի սխալ: չհաջողվեց ստուգել իսկությունը.';
$PHPMAILER_LANG['connect_host'] = 'SMTP -ի սխալ: չհաջողվեց կապ հաստատել SMTP սերվերի հետ.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP -ի սխալ: տվյալները ընդունված չեն.';
$PHPMAILER_LANG['empty_message'] = 'Հաղորդագրությունը դատարկ է';
$PHPMAILER_LANG['encoding'] = 'Կոդավորման անհայտ տեսակ: ';
$PHPMAILER_LANG['execute'] = 'Չհաջողվեց իրականացնել հրամանը: ';
$PHPMAILER_LANG['file_access'] = 'Ֆայլը հասանելի չէ: ';
$PHPMAILER_LANG['file_open'] = 'Ֆայլի սխալ: ֆայլը չհաջողվեց բացել: ';
$PHPMAILER_LANG['from_failed'] = 'Ուղարկողի հետևյալ հասցեն սխալ է: ';
$PHPMAILER_LANG['instantiate'] = 'Հնարավոր չէ կանչել mail ֆունկցիան.';
$PHPMAILER_LANG['invalid_address'] = 'Հասցեն սխալ է: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' փոստային սերվերի հետ չի աշխատում.';
$PHPMAILER_LANG['provide_address'] = 'Անհրաժեշտ է տրամադրել գոնե մեկ ստացողի e-mail հասցե.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP -ի սխալ: չի հաջողվել ուղարկել հետևյալ ստացողների հասցեներին: ';
$PHPMAILER_LANG['signing'] = 'Ստորագրման սխալ: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP -ի connect() ֆունկցիան չի հաջողվել';
$PHPMAILER_LANG['smtp_error'] = 'SMTP սերվերի սխալ: ';
$PHPMAILER_LANG['variable_set'] = 'Չի հաջողվում ստեղծել կամ վերափոխել փոփոխականը: ';
$PHPMAILER_LANG['extension_missing'] = 'Հավելվածը բացակայում է: ';

View File

@@ -0,0 +1,26 @@
<?php
/**
* Arabic PHPMailer language file: refer to English translation for definitive list
*
* @author bahjat al mostafa <bahjat983@hotmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'خطأ SMTP : لا يمكن تأكيد الهوية.';
$PHPMAILER_LANG['connect_host'] = 'خطأ SMTP: لا يمكن الاتصال بالخادم SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'خطأ SMTP: لم يتم قبول المعلومات .';
$PHPMAILER_LANG['empty_message'] = 'نص الرسالة فارغ';
$PHPMAILER_LANG['encoding'] = 'ترميز غير معروف: ';
$PHPMAILER_LANG['execute'] = 'لا يمكن تنفيذ : ';
$PHPMAILER_LANG['file_access'] = 'لا يمكن الوصول للملف: ';
$PHPMAILER_LANG['file_open'] = 'خطأ في الملف: لا يمكن فتحه: ';
$PHPMAILER_LANG['from_failed'] = 'خطأ على مستوى عنوان المرسل : ';
$PHPMAILER_LANG['instantiate'] = 'لا يمكن توفير خدمة البريد.';
$PHPMAILER_LANG['invalid_address'] = 'الإرسال غير ممكن لأن عنوان البريد الإلكتروني غير صالح: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' برنامج الإرسال غير مدعوم.';
$PHPMAILER_LANG['provide_address'] = 'يجب توفير عنوان البريد الإلكتروني لمستلم واحد على الأقل.';
$PHPMAILER_LANG['recipients_failed'] = 'خطأ SMTP: الأخطاء التالية '.
'فشل في الارسال لكل من : ';
$PHPMAILER_LANG['signing'] = 'خطأ في التوقيع: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() غير ممكن.';
$PHPMAILER_LANG['smtp_error'] = 'خطأ على مستوى الخادم SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'لا يمكن تعيين أو إعادة تعيين متغير: ';
$PHPMAILER_LANG['extension_missing'] = 'الإضافة غير موجودة: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Azerbaijani PHPMailer language file: refer to English translation for definitive list
*
* @author @mirjalal
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP xətası: Giriş uğursuz oldu.';
$PHPMAILER_LANG['connect_host'] = 'SMTP xətası: SMTP serverinə qoşulma uğursuz oldu.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP xətası: Verilənlər qəbul edilməyib.';
$PHPMAILER_LANG['empty_message'] = 'Boş mesaj göndərilə bilməz.';
$PHPMAILER_LANG['encoding'] = 'Qeyri-müəyyən kodlaşdırma: ';
$PHPMAILER_LANG['execute'] = 'Əmr yerinə yetirilmədi: ';
$PHPMAILER_LANG['file_access'] = 'Fayla giriş yoxdur: ';
$PHPMAILER_LANG['file_open'] = 'Fayl xətası: Fayl açıla bilmədi: ';
$PHPMAILER_LANG['from_failed'] = 'Göstərilən poçtlara göndərmə uğursuz oldu: ';
$PHPMAILER_LANG['instantiate'] = 'Mail funksiyası işə salına bilmədi.';
$PHPMAILER_LANG['invalid_address'] = 'Düzgün olmayan e-mail adresi: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' - e-mail kitabxanası dəstəklənmir.';
$PHPMAILER_LANG['provide_address'] = 'Ən azı bir e-mail adresi daxil edilməlidir.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP xətası: Aşağıdakı ünvanlar üzrə alıcılara göndərmə uğursuzdur: ';
$PHPMAILER_LANG['signing'] = 'İmzalama xətası: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP serverinə qoşulma uğursuz oldu.';
$PHPMAILER_LANG['smtp_error'] = 'SMTP serveri xətası: ';
$PHPMAILER_LANG['variable_set'] = 'Dəyişənin quraşdırılması uğursuz oldu: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Bosnian PHPMailer language file: refer to English translation for definitive list
*
* @author Ermin Islamagić <ermin@islamagic.com>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela prijava.';
$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Nije moguće spojiti se sa SMTP serverom.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.';
$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.';
$PHPMAILER_LANG['encoding'] = 'Nepoznata kriptografija: ';
$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: ';
$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: ';
$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: ';
$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje sa navedenih e-mail adresa nije uspjelo: ';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedene e-mail adrese nije uspjelo: ';
$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.';
$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.';
$PHPMAILER_LANG['provide_address'] = 'Definišite barem jednu adresu primaoca.';
$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP server nije uspjelo.';
$PHPMAILER_LANG['smtp_error'] = 'SMTP greška: ';
$PHPMAILER_LANG['variable_set'] = 'Nije moguće postaviti varijablu ili je vratiti nazad: ';
$PHPMAILER_LANG['extension_missing'] = 'Nedostaje ekstenzija: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Belarusian PHPMailer language file: refer to English translation for definitive list
*
* @author Aleksander Maksymiuk <info@setpro.pl>
*/
$PHPMAILER_LANG['authenticate'] = 'Памылка SMTP: памылка ідэнтыфікацыі.';
$PHPMAILER_LANG['connect_host'] = 'Памылка SMTP: нельга ўстанавіць сувязь з SMTP-серверам.';
$PHPMAILER_LANG['data_not_accepted'] = 'Памылка SMTP: звесткі непрынятыя.';
$PHPMAILER_LANG['empty_message'] = 'Пустое паведамленне.';
$PHPMAILER_LANG['encoding'] = 'Невядомая кадыроўка тэксту: ';
$PHPMAILER_LANG['execute'] = 'Нельга выканаць каманду: ';
$PHPMAILER_LANG['file_access'] = 'Няма доступу да файла: ';
$PHPMAILER_LANG['file_open'] = 'Нельга адкрыць файл: ';
$PHPMAILER_LANG['from_failed'] = 'Няправільны адрас адпраўніка: ';
$PHPMAILER_LANG['instantiate'] = 'Нельга прымяніць функцыю mail().';
$PHPMAILER_LANG['invalid_address'] = 'Нельга даслаць паведамленне, няправільны email атрымальніка: ';
$PHPMAILER_LANG['provide_address'] = 'Запоўніце, калі ласка, правільны email атрымальніка.';
$PHPMAILER_LANG['mailer_not_supported'] = ' - паштовы сервер не падтрымліваецца.';
$PHPMAILER_LANG['recipients_failed'] = 'Памылка SMTP: няправільныя атрымальнікі: ';
$PHPMAILER_LANG['signing'] = 'Памылка подпісу паведамлення: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Памылка сувязі з SMTP-серверам.';
$PHPMAILER_LANG['smtp_error'] = 'Памылка SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'Нельга ўстанавіць або перамяніць значэнне пераменнай: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Bulgarian PHPMailer language file: refer to English translation for definitive list
*
* @author Mikhail Kyosev <mialygk@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP грешка: Не може да се удостовери пред сървъра.';
$PHPMAILER_LANG['connect_host'] = 'SMTP грешка: Не може да се свърже с SMTP хоста.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP грешка: данните не са приети.';
$PHPMAILER_LANG['empty_message'] = 'Съдържанието на съобщението е празно';
$PHPMAILER_LANG['encoding'] = 'Неизвестно кодиране: ';
$PHPMAILER_LANG['execute'] = 'Не може да се изпълни: ';
$PHPMAILER_LANG['file_access'] = 'Няма достъп до файл: ';
$PHPMAILER_LANG['file_open'] = 'Файлова грешка: Не може да се отвори файл: ';
$PHPMAILER_LANG['from_failed'] = 'Следните адреси за подател са невалидни: ';
$PHPMAILER_LANG['instantiate'] = 'Не може да се инстанцира функцията mail.';
$PHPMAILER_LANG['invalid_address'] = 'Невалиден адрес: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' - пощенски сървър не се поддържа.';
$PHPMAILER_LANG['provide_address'] = 'Трябва да предоставите поне един email адрес за получател.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP грешка: Следните адреси за Получател са невалидни: ';
$PHPMAILER_LANG['signing'] = 'Грешка при подписване: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP провален connect().';
$PHPMAILER_LANG['smtp_error'] = 'SMTP сървърна грешка: ';
$PHPMAILER_LANG['variable_set'] = 'Не може да се установи или възстанови променлива: ';
$PHPMAILER_LANG['extension_missing'] = 'Липсва разширение: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Catalan PHPMailer language file: refer to English translation for definitive list
*
* @author Ivan <web AT microstudi DOT com>
*/
$PHPMAILER_LANG['authenticate'] = 'Error SMTP: No sha pogut autenticar.';
$PHPMAILER_LANG['connect_host'] = 'Error SMTP: No es pot connectar al servidor SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Dades no acceptades.';
$PHPMAILER_LANG['empty_message'] = 'El cos del missatge està buit.';
$PHPMAILER_LANG['encoding'] = 'Codificació desconeguda: ';
$PHPMAILER_LANG['execute'] = 'No es pot executar: ';
$PHPMAILER_LANG['file_access'] = 'No es pot accedir a larxiu: ';
$PHPMAILER_LANG['file_open'] = 'Error dArxiu: No es pot obrir larxiu: ';
$PHPMAILER_LANG['from_failed'] = 'La(s) següent(s) adreces de remitent han fallat: ';
$PHPMAILER_LANG['instantiate'] = 'No sha pogut crear una instància de la funció Mail.';
$PHPMAILER_LANG['invalid_address'] = 'Adreça demail invalida: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no està suportat';
$PHPMAILER_LANG['provide_address'] = 'Sha de proveir almenys una adreça demail com a destinatari.';
$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Els següents destinataris han fallat: ';
$PHPMAILER_LANG['signing'] = 'Error al signar: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Ha fallat el SMTP Connect().';
$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'No sha pogut establir o restablir la variable: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Chinese PHPMailer language file: refer to English translation for definitive list
*
* @author LiuXin <http://www.80x86.cn/blog/>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP 错误:身份验证失败。';
$PHPMAILER_LANG['connect_host'] = 'SMTP 错误: 不能连接SMTP主机。';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 错误: 数据不可接受。';
//$PHPMAILER_LANG['empty_message'] = 'Message body empty';
$PHPMAILER_LANG['encoding'] = '未知编码:';
$PHPMAILER_LANG['execute'] = '不能执行: ';
$PHPMAILER_LANG['file_access'] = '不能访问文件:';
$PHPMAILER_LANG['file_open'] = '文件错误:不能打开文件:';
$PHPMAILER_LANG['from_failed'] = '下面的发送地址邮件发送失败了: ';
$PHPMAILER_LANG['instantiate'] = '不能实现mail方法。';
//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' 您所选择的发送邮件的方法并不支持。';
$PHPMAILER_LANG['provide_address'] = '您必须提供至少一个 收信人的email地址。';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP 错误: 下面的 收件人失败了: ';
//$PHPMAILER_LANG['signing'] = 'Signing Error: ';
//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: ';
//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,23 @@
<?php
/**
* Czech PHPMailer language file: refer to English translation for definitive list
*/
$PHPMAILER_LANG['authenticate'] = 'Chyba SMTP: Autentizace selhala.';
$PHPMAILER_LANG['connect_host'] = 'Chyba SMTP: Nelze navázat spojení se SMTP serverem.';
$PHPMAILER_LANG['data_not_accepted'] = 'Chyba SMTP: Data nebyla přijata.';
$PHPMAILER_LANG['empty_message'] = 'Prázdné tělo zprávy';
$PHPMAILER_LANG['encoding'] = 'Neznámé kódování: ';
$PHPMAILER_LANG['execute'] = 'Nelze provést: ';
$PHPMAILER_LANG['file_access'] = 'Nelze získat přístup k souboru: ';
$PHPMAILER_LANG['file_open'] = 'Chyba souboru: Nelze otevřít soubor pro čtení: ';
$PHPMAILER_LANG['from_failed'] = 'Následující adresa odesílatele je nesprávná: ';
$PHPMAILER_LANG['instantiate'] = 'Nelze vytvořit instanci emailové funkce.';
$PHPMAILER_LANG['invalid_address'] = 'Neplatná adresa: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer není podporován.';
$PHPMAILER_LANG['provide_address'] = 'Musíte zadat alespoň jednu emailovou adresu příjemce.';
$PHPMAILER_LANG['recipients_failed'] = 'Chyba SMTP: Následující adresy příjemců nejsou správně: ';
$PHPMAILER_LANG['signing'] = 'Chyba přihlašování: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() selhal.';
$PHPMAILER_LANG['smtp_error'] = 'Chyba SMTP serveru: ';
$PHPMAILER_LANG['variable_set'] = 'Nelze nastavit nebo změnit proměnnou: ';
$PHPMAILER_LANG['extension_missing'] = 'Chybí rozšíření: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Danish PHPMailer language file: refer to English translation for definitive list
*
* @author Mikael Stokkebro <info@stokkebro.dk>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP fejl: Kunne ikke logge på.';
$PHPMAILER_LANG['connect_host'] = 'SMTP fejl: Kunne ikke tilslutte SMTP serveren.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP fejl: Data kunne ikke accepteres.';
//$PHPMAILER_LANG['empty_message'] = 'Message body empty';
$PHPMAILER_LANG['encoding'] = 'Ukendt encode-format: ';
$PHPMAILER_LANG['execute'] = 'Kunne ikke køre: ';
$PHPMAILER_LANG['file_access'] = 'Ingen adgang til fil: ';
$PHPMAILER_LANG['file_open'] = 'Fil fejl: Kunne ikke åbne filen: ';
$PHPMAILER_LANG['from_failed'] = 'Følgende afsenderadresse er forkert: ';
$PHPMAILER_LANG['instantiate'] = 'Kunne ikke initialisere email funktionen.';
//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer understøttes ikke.';
$PHPMAILER_LANG['provide_address'] = 'Du skal indtaste mindst en modtagers emailadresse.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP fejl: Følgende modtagere er forkerte: ';
//$PHPMAILER_LANG['signing'] = 'Signing Error: ';
//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: ';
//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,23 @@
<?php
/**
* German PHPMailer language file: refer to English translation for definitive list
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP-Fehler: Authentifizierung fehlgeschlagen.';
$PHPMAILER_LANG['connect_host'] = 'SMTP-Fehler: Konnte keine Verbindung zum SMTP-Host herstellen.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-Fehler: Daten werden nicht akzeptiert.';
$PHPMAILER_LANG['empty_message'] = 'E-Mail-Inhalt ist leer.';
$PHPMAILER_LANG['encoding'] = 'Unbekannte Kodierung: ';
$PHPMAILER_LANG['execute'] = 'Konnte folgenden Befehl nicht ausführen: ';
$PHPMAILER_LANG['file_access'] = 'Zugriff auf folgende Datei fehlgeschlagen: ';
$PHPMAILER_LANG['file_open'] = 'Dateifehler: Konnte folgende Datei nicht öffnen: ';
$PHPMAILER_LANG['from_failed'] = 'Die folgende Absenderadresse ist nicht korrekt: ';
$PHPMAILER_LANG['instantiate'] = 'Mail-Funktion konnte nicht initialisiert werden.';
$PHPMAILER_LANG['invalid_address'] = 'Die Adresse ist ungültig: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer wird nicht unterstützt.';
$PHPMAILER_LANG['provide_address'] = 'Bitte geben Sie mindestens eine Empfängeradresse an.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP-Fehler: Die folgenden Empfänger sind nicht korrekt: ';
$PHPMAILER_LANG['signing'] = 'Fehler beim Signieren: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Verbindung zum SMTP-Server fehlgeschlagen.';
$PHPMAILER_LANG['smtp_error'] = 'Fehler vom SMTP-Server: ';
$PHPMAILER_LANG['variable_set'] = 'Kann Variable nicht setzen oder zurücksetzen: ';
$PHPMAILER_LANG['extension_missing'] = 'Fehlende Erweiterung: ';

View File

@@ -0,0 +1,23 @@
<?php
/**
* Greek PHPMailer language file: refer to English translation for definitive list
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP Σφάλμα: Αδυναμία πιστοποίησης (authentication).';
$PHPMAILER_LANG['connect_host'] = 'SMTP Σφάλμα: Αδυναμία σύνδεσης στον SMTP-Host.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Σφάλμα: Τα δεδομένα δεν έγιναν αποδεκτά.';
$PHPMAILER_LANG['empty_message'] = 'Το E-Mail δεν έχει περιεχόμενο .';
$PHPMAILER_LANG['encoding'] = 'Αγνωστο Encoding-Format: ';
$PHPMAILER_LANG['execute'] = 'Αδυναμία εκτέλεσης ακόλουθης εντολής: ';
$PHPMAILER_LANG['file_access'] = 'Αδυναμία προσπέλασης του αρχείου: ';
$PHPMAILER_LANG['file_open'] = 'Σφάλμα Αρχείου: Δεν είναι δυνατό το άνοιγμα του ακόλουθου αρχείου: ';
$PHPMAILER_LANG['from_failed'] = 'Η παρακάτω διεύθυνση αποστολέα δεν είναι σωστή: ';
$PHPMAILER_LANG['instantiate'] = 'Αδυναμία εκκίνησης Mail function.';
$PHPMAILER_LANG['invalid_address'] = 'Το μήνυμα δεν εστάλη, η διεύθυνση δεν είναι έγκυρη: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer δεν υποστηρίζεται.';
$PHPMAILER_LANG['provide_address'] = 'Παρακαλούμε δώστε τουλάχιστον μια e-mail διεύθυνση παραλήπτη.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP Σφάλμα: Οι παρακάτω διευθύνσεις παραλήπτη δεν είναι έγκυρες: ';
$PHPMAILER_LANG['signing'] = 'Σφάλμα υπογραφής: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Αποτυχία σύνδεσης στον SMTP Server.';
$PHPMAILER_LANG['smtp_error'] = 'Σφάλμα από τον SMTP Server: ';
$PHPMAILER_LANG['variable_set'] = 'Αδυναμία ορισμού ή αρχικοποίησης μεταβλητής: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,23 @@
<?php
/**
* Esperanto PHPMailer language file: refer to English translation for definitive list
*/
$PHPMAILER_LANG['authenticate'] = 'Eraro de servilo SMTP : aŭtentigo malsukcesis.';
$PHPMAILER_LANG['connect_host'] = 'Eraro de servilo SMTP : konektado al servilo malsukcesis.';
$PHPMAILER_LANG['data_not_accepted'] = 'Eraro de servilo SMTP : neĝustaj datumoj.';
$PHPMAILER_LANG['empty_message'] = 'Teksto de mesaĝo mankas.';
$PHPMAILER_LANG['encoding'] = 'Nekonata kodoprezento: ';
$PHPMAILER_LANG['execute'] = 'Lanĉi rulumadon ne eblis: ';
$PHPMAILER_LANG['file_access'] = 'Aliro al dosiero ne sukcesis: ';
$PHPMAILER_LANG['file_open'] = 'Eraro de dosiero: malfermo neeblas: ';
$PHPMAILER_LANG['from_failed'] = 'Jena adreso de sendinto malsukcesis: ';
$PHPMAILER_LANG['instantiate'] = 'Genero de retmesaĝa funkcio neeblis.';
$PHPMAILER_LANG['invalid_address'] = 'Retadreso ne validas: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mesaĝilo ne subtenata.';
$PHPMAILER_LANG['provide_address'] = 'Vi devas tajpi almenaŭ unu recevontan retadreson.';
$PHPMAILER_LANG['recipients_failed'] = 'Eraro de servilo SMTP : la jenaj poŝtrecivuloj kaŭzis eraron: ';
$PHPMAILER_LANG['signing'] = 'Eraro de subskribo: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP konektado malsukcesis.';
$PHPMAILER_LANG['smtp_error'] = 'Eraro de servilo SMTP : ';
$PHPMAILER_LANG['variable_set'] = 'Variablo ne pravalorizeblas aŭ ne repravalorizeblas: ';
$PHPMAILER_LANG['extension_missing'] = 'Mankas etendo: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Spanish PHPMailer language file: refer to English translation for definitive list
*
* @author Matt Sturdy <matt.sturdy@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'Error SMTP: Imposible autentificar.';
$PHPMAILER_LANG['connect_host'] = 'Error SMTP: Imposible conectar al servidor SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Error SMTP: Datos no aceptados.';
$PHPMAILER_LANG['empty_message'] = 'El cuerpo del mensaje está vacío.';
$PHPMAILER_LANG['encoding'] = 'Codificación desconocida: ';
$PHPMAILER_LANG['execute'] = 'Imposible ejecutar: ';
$PHPMAILER_LANG['file_access'] = 'Imposible acceder al archivo: ';
$PHPMAILER_LANG['file_open'] = 'Error de Archivo: Imposible abrir el archivo: ';
$PHPMAILER_LANG['from_failed'] = 'La(s) siguiente(s) direcciones de remitente fallaron: ';
$PHPMAILER_LANG['instantiate'] = 'Imposible crear una instancia de la función Mail.';
$PHPMAILER_LANG['invalid_address'] = 'Imposible enviar: dirección de email inválido: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer no está soportado.';
$PHPMAILER_LANG['provide_address'] = 'Debe proporcionar al menos una dirección de email de destino.';
$PHPMAILER_LANG['recipients_failed'] = 'Error SMTP: Los siguientes destinos fallaron: ';
$PHPMAILER_LANG['signing'] = 'Error al firmar: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() falló.';
$PHPMAILER_LANG['smtp_error'] = 'Error del servidor SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'No se pudo configurar la variable: ';
$PHPMAILER_LANG['extension_missing'] = 'Extensión faltante: ';

View File

@@ -0,0 +1,26 @@
<?php
/**
* Estonian PHPMailer language file: refer to English translation for definitive list
*
* @author Indrek Päri
* @author Elan Ruusamäe <glen@delfi.ee>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP Viga: Autoriseerimise viga.';
$PHPMAILER_LANG['connect_host'] = 'SMTP Viga: Ei õnnestunud luua ühendust SMTP serveriga.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Viga: Vigased andmed.';
$PHPMAILER_LANG['empty_message'] = 'Tühi kirja sisu';
$PHPMAILER_LANG['encoding'] = 'Tundmatu kodeering: ';
$PHPMAILER_LANG['execute'] = 'Tegevus ebaõnnestus: ';
$PHPMAILER_LANG['file_access'] = 'Pole piisavalt õiguseid järgneva faili avamiseks: ';
$PHPMAILER_LANG['file_open'] = 'Faili Viga: Faili avamine ebaõnnestus: ';
$PHPMAILER_LANG['from_failed'] = 'Järgnev saatja e-posti aadress on vigane: ';
$PHPMAILER_LANG['instantiate'] = 'mail funktiooni käivitamine ebaõnnestus.';
$PHPMAILER_LANG['invalid_address'] = 'Saatmine peatatud, e-posti address vigane: ';
$PHPMAILER_LANG['provide_address'] = 'Te peate määrama vähemalt ühe saaja e-posti aadressi.';
$PHPMAILER_LANG['mailer_not_supported'] = ' maileri tugi puudub.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP Viga: Järgnevate saajate e-posti aadressid on vigased: ';
$PHPMAILER_LANG['signing'] = 'Viga allkirjastamisel: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() ebaõnnestus.';
$PHPMAILER_LANG['smtp_error'] = 'SMTP serveri viga: ';
$PHPMAILER_LANG['variable_set'] = 'Ei õnnestunud määrata või lähtestada muutujat: ';
$PHPMAILER_LANG['extension_missing'] = 'Nõutud laiendus on puudu: ';

View File

@@ -0,0 +1,26 @@
<?php
/**
* Persian/Farsi PHPMailer language file: refer to English translation for definitive list
*
* @author Ali Jazayeri <jaza.ali@gmail.com>
* @author Mohammad Hossein Mojtahedi <mhm5000@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'خطای SMTP: احراز هویت با شکست مواجه شد.';
$PHPMAILER_LANG['connect_host'] = 'خطای SMTP: اتصال به سرور SMTP برقرار نشد.';
$PHPMAILER_LANG['data_not_accepted'] = 'خطای SMTP: داده‌ها نا‌درست هستند.';
$PHPMAILER_LANG['empty_message'] = 'بخش متن پیام خالی است.';
$PHPMAILER_LANG['encoding'] = 'کد‌گذاری نا‌شناخته: ';
$PHPMAILER_LANG['execute'] = 'امکان اجرا وجود ندارد: ';
$PHPMAILER_LANG['file_access'] = 'امکان دسترسی به فایل وجود ندارد: ';
$PHPMAILER_LANG['file_open'] = 'خطای File: امکان بازکردن فایل وجود ندارد: ';
$PHPMAILER_LANG['from_failed'] = 'آدرس فرستنده اشتباه است: ';
$PHPMAILER_LANG['instantiate'] = 'امکان معرفی تابع ایمیل وجود ندارد.';
$PHPMAILER_LANG['invalid_address'] = 'آدرس ایمیل معتبر نیست: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer پشتیبانی نمی‌شود.';
$PHPMAILER_LANG['provide_address'] = 'باید حداقل یک آدرس گیرنده وارد کنید.';
$PHPMAILER_LANG['recipients_failed'] = 'خطای SMTP: ارسال به آدرس گیرنده با خطا مواجه شد: ';
$PHPMAILER_LANG['signing'] = 'خطا در امضا: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'خطا در اتصال به SMTP.';
$PHPMAILER_LANG['smtp_error'] = 'خطا در SMTP Server: ';
$PHPMAILER_LANG['variable_set'] = 'امکان ارسال یا ارسال مجدد متغیر‌ها وجود ندارد: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,26 @@
<?php
/**
* Finnish PHPMailer language file: refer to English translation for definitive list
*
* @author Jyry Kuukanen
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP-virhe: käyttäjätunnistus epäonnistui.';
$PHPMAILER_LANG['connect_host'] = 'SMTP-virhe: yhteys palvelimeen ei onnistu.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP-virhe: data on virheellinen.';
//$PHPMAILER_LANG['empty_message'] = 'Message body empty';
$PHPMAILER_LANG['encoding'] = 'Tuntematon koodaustyyppi: ';
$PHPMAILER_LANG['execute'] = 'Suoritus epäonnistui: ';
$PHPMAILER_LANG['file_access'] = 'Seuraavaan tiedostoon ei ole oikeuksia: ';
$PHPMAILER_LANG['file_open'] = 'Tiedostovirhe: Ei voida avata tiedostoa: ';
$PHPMAILER_LANG['from_failed'] = 'Seuraava lähettäjän osoite on virheellinen: ';
$PHPMAILER_LANG['instantiate'] = 'mail-funktion luonti epäonnistui.';
//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: ';
$PHPMAILER_LANG['mailer_not_supported'] = 'postivälitintyyppiä ei tueta.';
$PHPMAILER_LANG['provide_address'] = 'Aseta vähintään yksi vastaanottajan sähk&ouml;postiosoite.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP-virhe: seuraava vastaanottaja osoite on virheellinen.';
$PHPMAILER_LANG['encoding'] = 'Tuntematon koodaustyyppi: ';
//$PHPMAILER_LANG['signing'] = 'Signing Error: ';
//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: ';
//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Faroese PHPMailer language file: refer to English translation for definitive list
*
* @author Dávur Sørensen <http://www.profo-webdesign.dk>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP feilur: Kundi ikki góðkenna.';
$PHPMAILER_LANG['connect_host'] = 'SMTP feilur: Kundi ikki knýta samband við SMTP vert.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP feilur: Data ikki góðkent.';
//$PHPMAILER_LANG['empty_message'] = 'Message body empty';
$PHPMAILER_LANG['encoding'] = 'Ókend encoding: ';
$PHPMAILER_LANG['execute'] = 'Kundi ikki útføra: ';
$PHPMAILER_LANG['file_access'] = 'Kundi ikki tilganga fílu: ';
$PHPMAILER_LANG['file_open'] = 'Fílu feilur: Kundi ikki opna fílu: ';
$PHPMAILER_LANG['from_failed'] = 'fylgjandi Frá/From adressa miseydnaðist: ';
$PHPMAILER_LANG['instantiate'] = 'Kuni ikki instantiera mail funktión.';
//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' er ikki supporterað.';
$PHPMAILER_LANG['provide_address'] = 'Tú skal uppgeva minst móttakara-emailadressu(r).';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP Feilur: Fylgjandi móttakarar miseydnaðust: ';
//$PHPMAILER_LANG['signing'] = 'Signing Error: ';
//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: ';
//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* French PHPMailer language file: refer to English translation for definitive list
*
* @see http://unicode.org/udhr/n/notes_fra.html
*/
$PHPMAILER_LANG['authenticate'] = 'Erreur SMTP: échec de l\'authentification.';
$PHPMAILER_LANG['connect_host'] = 'Erreur SMTP: impossible de se connecter au serveur SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Erreur SMTP: données incorrectes.';
$PHPMAILER_LANG['empty_message'] = 'Corps du message vide.';
$PHPMAILER_LANG['encoding'] = 'Encodage inconnu: ';
$PHPMAILER_LANG['execute'] = 'Impossible de lancer l\'exécution: ';
$PHPMAILER_LANG['file_access'] = 'Impossible d\'accéder au fichier: ';
$PHPMAILER_LANG['file_open'] = 'Ouverture du fichier impossible: ';
$PHPMAILER_LANG['from_failed'] = 'L\'adresse d\'expéditeur suivante a échoué: ';
$PHPMAILER_LANG['instantiate'] = 'Impossible d\'instancier la fonction mail.';
$PHPMAILER_LANG['invalid_address'] = 'L\'adresse courriel n\'est pas valide: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' client de messagerie non supporté.';
$PHPMAILER_LANG['provide_address'] = 'Vous devez fournir au moins une adresse de destinataire.';
$PHPMAILER_LANG['recipients_failed'] = 'Erreur SMTP: les destinataires suivants sont en erreur: ';
$PHPMAILER_LANG['signing'] = 'Erreur de signature: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Échec de la connexion SMTP.';
$PHPMAILER_LANG['smtp_error'] = 'Erreur du serveur SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'Impossible d\'initialiser ou de réinitialiser une variable: ';
$PHPMAILER_LANG['extension_missing'] = 'Extension manquante: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Galician PHPMailer language file: refer to English translation for definitive list
*
* @author by Donato Rouco <donatorouco@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'Erro SMTP: Non puido ser autentificado.';
$PHPMAILER_LANG['connect_host'] = 'Erro SMTP: Non puido conectar co servidor SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Erro SMTP: Datos non aceptados.';
$PHPMAILER_LANG['empty_message'] = 'Corpo da mensaxe vacía';
$PHPMAILER_LANG['encoding'] = 'Codificación descoñecida: ';
$PHPMAILER_LANG['execute'] = 'Non puido ser executado: ';
$PHPMAILER_LANG['file_access'] = 'Nob puido acceder ó arquivo: ';
$PHPMAILER_LANG['file_open'] = 'Erro de Arquivo: No puido abrir o arquivo: ';
$PHPMAILER_LANG['from_failed'] = 'A(s) seguinte(s) dirección(s) de remitente(s) deron erro: ';
$PHPMAILER_LANG['instantiate'] = 'Non puido crear unha instancia da función Mail.';
$PHPMAILER_LANG['invalid_address'] = 'Non puido envia-lo correo: dirección de email inválida: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer non está soportado.';
$PHPMAILER_LANG['provide_address'] = 'Debe engadir polo menos unha dirección de email coma destino.';
$PHPMAILER_LANG['recipients_failed'] = 'Erro SMTP: Os seguintes destinos fallaron: ';
$PHPMAILER_LANG['signing'] = 'Erro ó firmar: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallou.';
$PHPMAILER_LANG['smtp_error'] = 'Erro do servidor SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'Non puidemos axustar ou reaxustar a variábel: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Hebrew PHPMailer language file: refer to English translation for definitive list
*
* @author Ronny Sherer <ronny@hoojima.com>
*/
$PHPMAILER_LANG['authenticate'] = 'שגיאת SMTP: פעולת האימות נכשלה.';
$PHPMAILER_LANG['connect_host'] = 'שגיאת SMTP: לא הצלחתי להתחבר לשרת SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'שגיאת SMTP: מידע לא התקבל.';
$PHPMAILER_LANG['empty_message'] = 'גוף ההודעה ריק';
$PHPMAILER_LANG['invalid_address'] = 'כתובת שגויה: ';
$PHPMAILER_LANG['encoding'] = 'קידוד לא מוכר: ';
$PHPMAILER_LANG['execute'] = 'לא הצלחתי להפעיל את: ';
$PHPMAILER_LANG['file_access'] = 'לא ניתן לגשת לקובץ: ';
$PHPMAILER_LANG['file_open'] = 'שגיאת קובץ: לא ניתן לגשת לקובץ: ';
$PHPMAILER_LANG['from_failed'] = 'כתובות הנמענים הבאות נכשלו: ';
$PHPMAILER_LANG['instantiate'] = 'לא הצלחתי להפעיל את פונקציית המייל.';
$PHPMAILER_LANG['mailer_not_supported'] = ' אינה נתמכת.';
$PHPMAILER_LANG['provide_address'] = 'חובה לספק לפחות כתובת אחת של מקבל המייל.';
$PHPMAILER_LANG['recipients_failed'] = 'שגיאת SMTP: הנמענים הבאים נכשלו: ';
$PHPMAILER_LANG['signing'] = 'שגיאת חתימה: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
$PHPMAILER_LANG['smtp_error'] = 'שגיאת שרת SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'לא ניתן לקבוע או לשנות את המשתנה: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Hindi PHPMailer language file: refer to English translation for definitive list
*
* @author Yash Karanke <mr.karanke@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP त्रुटि: प्रामाणिकता की जांच नहीं हो सका। ';
$PHPMAILER_LANG['connect_host'] = 'SMTP त्रुटि: SMTP सर्वर से कनेक्ट नहीं हो सका। ';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP त्रुटि: डेटा स्वीकार नहीं किया जाता है। ';
$PHPMAILER_LANG['empty_message'] = 'संदेश खाली है। ';
$PHPMAILER_LANG['encoding'] = 'अज्ञात एन्कोडिंग प्रकार। ';
$PHPMAILER_LANG['execute'] = 'आदेश को निष्पादित करने में विफल। ';
$PHPMAILER_LANG['file_access'] = 'फ़ाइल उपलब्ध नहीं है। ';
$PHPMAILER_LANG['file_open'] = 'फ़ाइल त्रुटि: फाइल को खोला नहीं जा सका। ';
$PHPMAILER_LANG['from_failed'] = 'प्रेषक का पता गलत है। ';
$PHPMAILER_LANG['instantiate'] = 'मेल फ़ंक्शन कॉल नहीं कर सकता है।';
$PHPMAILER_LANG['invalid_address'] = 'पता गलत है। ';
$PHPMAILER_LANG['mailer_not_supported'] = 'मेल सर्वर के साथ काम नहीं करता है। ';
$PHPMAILER_LANG['provide_address'] = 'आपको कम से कम एक प्राप्तकर्ता का ई-मेल पता प्रदान करना होगा।';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP त्रुटि: निम्न प्राप्तकर्ताओं को पते भेजने में विफल। ';
$PHPMAILER_LANG['signing'] = 'साइनअप त्रुटि:। ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP का connect () फ़ंक्शन विफल हुआ। ';
$PHPMAILER_LANG['smtp_error'] = 'SMTP सर्वर त्रुटि। ';
$PHPMAILER_LANG['variable_set'] = 'चर को बना या संशोधित नहीं किया जा सकता। ';
$PHPMAILER_LANG['extension_missing'] = 'एक्सटेन्षन गायब है: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Croatian PHPMailer language file: refer to English translation for definitive list
*
* @author Hrvoj3e <hrvoj3e@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP Greška: Neuspjela autentikacija.';
$PHPMAILER_LANG['connect_host'] = 'SMTP Greška: Ne mogu se spojiti na SMTP poslužitelj.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Greška: Podatci nisu prihvaćeni.';
$PHPMAILER_LANG['empty_message'] = 'Sadržaj poruke je prazan.';
$PHPMAILER_LANG['encoding'] = 'Nepoznati encoding: ';
$PHPMAILER_LANG['execute'] = 'Nije moguće izvršiti naredbu: ';
$PHPMAILER_LANG['file_access'] = 'Nije moguće pristupiti datoteci: ';
$PHPMAILER_LANG['file_open'] = 'Nije moguće otvoriti datoteku: ';
$PHPMAILER_LANG['from_failed'] = 'SMTP Greška: Slanje s navedenih e-mail adresa nije uspjelo: ';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP Greška: Slanje na navedenih e-mail adresa nije uspjelo: ';
$PHPMAILER_LANG['instantiate'] = 'Ne mogu pokrenuti mail funkcionalnost.';
$PHPMAILER_LANG['invalid_address'] = 'E-mail nije poslan. Neispravna e-mail adresa: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer nije podržan.';
$PHPMAILER_LANG['provide_address'] = 'Definirajte barem jednu adresu primatelja.';
$PHPMAILER_LANG['signing'] = 'Greška prilikom prijave: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Spajanje na SMTP poslužitelj nije uspjelo.';
$PHPMAILER_LANG['smtp_error'] = 'Greška SMTP poslužitelja: ';
$PHPMAILER_LANG['variable_set'] = 'Ne mogu postaviti varijablu niti ju vratiti nazad: ';
$PHPMAILER_LANG['extension_missing'] = 'Nedostaje proširenje: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Hungarian PHPMailer language file: refer to English translation for definitive list
*
* @author @dominicus-75
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP hiba: az azonosítás sikertelen.';
$PHPMAILER_LANG['connect_host'] = 'SMTP hiba: nem lehet kapcsolódni az SMTP-szerverhez.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP hiba: adatok visszautasítva.';
$PHPMAILER_LANG['empty_message'] = 'Üres az üzenettörzs.';
$PHPMAILER_LANG['encoding'] = 'Ismeretlen kódolás: ';
$PHPMAILER_LANG['execute'] = 'Nem lehet végrehajtani: ';
$PHPMAILER_LANG['file_access'] = 'A következő fájl nem elérhető: ';
$PHPMAILER_LANG['file_open'] = 'Fájl hiba: a következő fájlt nem lehet megnyitni: ';
$PHPMAILER_LANG['from_failed'] = 'A feladóként megadott következő cím hibás: ';
$PHPMAILER_LANG['instantiate'] = 'A PHP mail() függvényt nem sikerült végrehajtani.';
$PHPMAILER_LANG['invalid_address'] = 'Érvénytelen cím: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' a mailer-osztály nem támogatott.';
$PHPMAILER_LANG['provide_address'] = 'Legalább egy címzettet fel kell tüntetni.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP hiba: a címzettként megadott következő címek hibásak: ';
$PHPMAILER_LANG['signing'] = 'Hibás aláírás: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'Hiba az SMTP-kapcsolatban.';
$PHPMAILER_LANG['smtp_error'] = 'SMTP-szerver hiba: ';
$PHPMAILER_LANG['variable_set'] = 'A következő változók beállítása nem sikerült: ';
$PHPMAILER_LANG['extension_missing'] = 'Bővítmény hiányzik: ';

View File

@@ -0,0 +1,26 @@
<?php
/**
* Indonesian PHPMailer language file: refer to English translation for definitive list
*
* @author Cecep Prawiro <cecep.prawiro@gmail.com>
* @author @januridp
*/
$PHPMAILER_LANG['authenticate'] = 'Kesalahan SMTP: Tidak dapat mengotentikasi.';
$PHPMAILER_LANG['connect_host'] = 'Kesalahan SMTP: Tidak dapat terhubung ke host SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'Kesalahan SMTP: Data tidak diterima.';
$PHPMAILER_LANG['empty_message'] = 'Isi pesan kosong';
$PHPMAILER_LANG['encoding'] = 'Pengkodean karakter tidak dikenali: ';
$PHPMAILER_LANG['execute'] = 'Tidak dapat menjalankan proses : ';
$PHPMAILER_LANG['file_access'] = 'Tidak dapat mengakses berkas : ';
$PHPMAILER_LANG['file_open'] = 'Kesalahan File: Berkas tidak dapat dibuka : ';
$PHPMAILER_LANG['from_failed'] = 'Alamat pengirim berikut mengakibatkan kesalahan : ';
$PHPMAILER_LANG['instantiate'] = 'Tidak dapat menginisialisasi fungsi surel';
$PHPMAILER_LANG['invalid_address'] = 'Gagal terkirim, alamat surel tidak benar : ';
$PHPMAILER_LANG['provide_address'] = 'Harus disediakan minimal satu alamat tujuan';
$PHPMAILER_LANG['mailer_not_supported'] = ' mailer tidak didukung';
$PHPMAILER_LANG['recipients_failed'] = 'Kesalahan SMTP: Alamat tujuan berikut menghasilkan kesalahan : ';
$PHPMAILER_LANG['signing'] = 'Kesalahan dalam tanda tangan : ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() gagal.';
$PHPMAILER_LANG['smtp_error'] = 'Kesalahan pada pelayan SMTP : ';
$PHPMAILER_LANG['variable_set'] = 'Tidak dapat mengatur atau mengatur ulang variable : ';
$PHPMAILER_LANG['extension_missing'] = 'Ekstensi hilang: ';

View File

@@ -0,0 +1,26 @@
<?php
/**
* Italian PHPMailer language file: refer to English translation for definitive list
*
* @author Ilias Bartolini <brain79@inwind.it>
* @author Stefano Sabatini <sabas88@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP Error: Impossibile autenticarsi.';
$PHPMAILER_LANG['connect_host'] = 'SMTP Error: Impossibile connettersi all\'host SMTP.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP Error: Dati non accettati dal server.';
$PHPMAILER_LANG['empty_message'] = 'Il corpo del messaggio è vuoto';
$PHPMAILER_LANG['encoding'] = 'Codifica dei caratteri sconosciuta: ';
$PHPMAILER_LANG['execute'] = 'Impossibile eseguire l\'operazione: ';
$PHPMAILER_LANG['file_access'] = 'Impossibile accedere al file: ';
$PHPMAILER_LANG['file_open'] = 'File Error: Impossibile aprire il file: ';
$PHPMAILER_LANG['from_failed'] = 'I seguenti indirizzi mittenti hanno generato errore: ';
$PHPMAILER_LANG['instantiate'] = 'Impossibile istanziare la funzione mail';
$PHPMAILER_LANG['invalid_address'] = 'Impossibile inviare, l\'indirizzo email non è valido: ';
$PHPMAILER_LANG['provide_address'] = 'Deve essere fornito almeno un indirizzo ricevente';
$PHPMAILER_LANG['mailer_not_supported'] = 'Mailer non supportato';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP Error: I seguenti indirizzi destinatari hanno generato un errore: ';
$PHPMAILER_LANG['signing'] = 'Errore nella firma: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() fallita.';
$PHPMAILER_LANG['smtp_error'] = 'Errore del server SMTP: ';
$PHPMAILER_LANG['variable_set'] = 'Impossibile impostare o resettare la variabile: ';
$PHPMAILER_LANG['extension_missing'] = 'Estensione mancante: ';

View File

@@ -0,0 +1,26 @@
<?php
/**
* Japanese PHPMailer language file: refer to English translation for definitive list
*
* @author Mitsuhiro Yoshida <http://mitstek.com/>
* @author Yoshi Sakai <http://bluemooninc.jp/>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTPエラー: 認証できませんでした。';
$PHPMAILER_LANG['connect_host'] = 'SMTPエラー: SMTPホストに接続できませんでした。';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTPエラー: データが受け付けられませんでした。';
//$PHPMAILER_LANG['empty_message'] = 'Message body empty';
$PHPMAILER_LANG['encoding'] = '不明なエンコーディング: ';
$PHPMAILER_LANG['execute'] = '実行できませんでした: ';
$PHPMAILER_LANG['file_access'] = 'ファイルにアクセスできません: ';
$PHPMAILER_LANG['file_open'] = 'ファイルエラー: ファイルを開けません: ';
$PHPMAILER_LANG['from_failed'] = 'Fromアドレスを登録する際にエラーが発生しました: ';
$PHPMAILER_LANG['instantiate'] = 'メール関数が正常に動作しませんでした。';
//$PHPMAILER_LANG['invalid_address'] = 'Invalid address: ';
$PHPMAILER_LANG['provide_address'] = '少なくとも1つメールアドレスを 指定する必要があります。';
$PHPMAILER_LANG['mailer_not_supported'] = ' メーラーがサポートされていません。';
$PHPMAILER_LANG['recipients_failed'] = 'SMTPエラー: 次の受信者アドレスに 間違いがあります: ';
//$PHPMAILER_LANG['signing'] = 'Signing Error: ';
//$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP Connect() failed.';
//$PHPMAILER_LANG['smtp_error'] = 'SMTP server error: ';
//$PHPMAILER_LANG['variable_set'] = 'Cannot set or reset variable: ';
//$PHPMAILER_LANG['extension_missing'] = 'Extension missing: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Georgian PHPMailer language file: refer to English translation for definitive list
*
* @author Avtandil Kikabidze aka LONGMAN <akalongman@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP შეცდომა: ავტორიზაცია შეუძლებელია.';
$PHPMAILER_LANG['connect_host'] = 'SMTP შეცდომა: SMTP სერვერთან დაკავშირება შეუძლებელია.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP შეცდომა: მონაცემები არ იქნა მიღებული.';
$PHPMAILER_LANG['encoding'] = 'კოდირების უცნობი ტიპი: ';
$PHPMAILER_LANG['execute'] = 'შეუძლებელია შემდეგი ბრძანების შესრულება: ';
$PHPMAILER_LANG['file_access'] = 'შეუძლებელია წვდომა ფაილთან: ';
$PHPMAILER_LANG['file_open'] = 'ფაილური სისტემის შეცდომა: არ იხსნება ფაილი: ';
$PHPMAILER_LANG['from_failed'] = 'გამგზავნის არასწორი მისამართი: ';
$PHPMAILER_LANG['instantiate'] = 'mail ფუნქციის გაშვება ვერ ხერხდება.';
$PHPMAILER_LANG['provide_address'] = 'გთხოვთ მიუთითოთ ერთი ადრესატის e-mail მისამართი მაინც.';
$PHPMAILER_LANG['mailer_not_supported'] = ' - საფოსტო სერვერის მხარდაჭერა არ არის.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP შეცდომა: შემდეგ მისამართებზე გაგზავნა ვერ მოხერხდა: ';
$PHPMAILER_LANG['empty_message'] = 'შეტყობინება ცარიელია';
$PHPMAILER_LANG['invalid_address'] = 'არ გაიგზავნა, e-mail მისამართის არასწორი ფორმატი: ';
$PHPMAILER_LANG['signing'] = 'ხელმოწერის შეცდომა: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'შეცდომა SMTP სერვერთან დაკავშირებისას';
$PHPMAILER_LANG['smtp_error'] = 'SMTP სერვერის შეცდომა: ';
$PHPMAILER_LANG['variable_set'] = 'შეუძლებელია შემდეგი ცვლადის შექმნა ან შეცვლა: ';
$PHPMAILER_LANG['extension_missing'] = 'ბიბლიოთეკა არ არსებობს: ';

View File

@@ -0,0 +1,25 @@
<?php
/**
* Korean PHPMailer language file: refer to English translation for definitive list
*
* @author ChalkPE <amato0617@gmail.com>
*/
$PHPMAILER_LANG['authenticate'] = 'SMTP 오류: 인증할 수 없습니다.';
$PHPMAILER_LANG['connect_host'] = 'SMTP 오류: SMTP 호스트에 접속할 수 없습니다.';
$PHPMAILER_LANG['data_not_accepted'] = 'SMTP 오류: 데이터가 받아들여지지 않았습니다.';
$PHPMAILER_LANG['empty_message'] = '메세지 내용이 없습니다';
$PHPMAILER_LANG['encoding'] = '알 수 없는 인코딩: ';
$PHPMAILER_LANG['execute'] = '실행 불가: ';
$PHPMAILER_LANG['file_access'] = '파일 접근 불가: ';
$PHPMAILER_LANG['file_open'] = '파일 오류: 파일을 열 수 없습니다: ';
$PHPMAILER_LANG['from_failed'] = '다음 From 주소에서 오류가 발생했습니다: ';
$PHPMAILER_LANG['instantiate'] = 'mail 함수를 인스턴스화할 수 없습니다';
$PHPMAILER_LANG['invalid_address'] = '잘못된 주소: ';
$PHPMAILER_LANG['mailer_not_supported'] = ' 메일러는 지원되지 않습니다.';
$PHPMAILER_LANG['provide_address'] = '적어도 한 개 이상의 수신자 메일 주소를 제공해야 합니다.';
$PHPMAILER_LANG['recipients_failed'] = 'SMTP 오류: 다음 수신자에서 오류가 발생했습니다: ';
$PHPMAILER_LANG['signing'] = '서명 오류: ';
$PHPMAILER_LANG['smtp_connect_failed'] = 'SMTP 연결을 실패하였습니다.';
$PHPMAILER_LANG['smtp_error'] = 'SMTP 서버 오류: ';
$PHPMAILER_LANG['variable_set'] = '변수 설정 및 초기화 불가: ';
$PHPMAILER_LANG['extension_missing'] = '확장자 없음: ';

Some files were not shown because too many files have changed in this diff Show More