Compare commits
10 Commits
a46ec03369
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6aa41381ac | ||
|
|
d6dedfb513 | ||
|
|
060dcce370 | ||
|
|
8b36ea16ef | ||
|
|
4fe9b0fdc9 | ||
|
|
bf43461559 | ||
|
|
64403d6fd6 | ||
|
|
4898f5ec7f | ||
| afea76b053 | |||
| f985bfedd3 |
87
.env
87
.env
@@ -1,87 +0,0 @@
|
|||||||
APP_NAME="AKNAPROFF"
|
|
||||||
APP_ENV="live"
|
|
||||||
APP_KEY=base64:dvbkNw1BmEGND2DRWIauV7ub306TMEiPws0A9yOhiMU=
|
|
||||||
APP_DEBUG=true
|
|
||||||
APP_URL=https://uniquesoft.es
|
|
||||||
APP_LOCALE="en"
|
|
||||||
|
|
||||||
LOG_CHANNEL=stack
|
|
||||||
|
|
||||||
DB_CONNECTION=mysql
|
|
||||||
DB_HOST=127.0.0.1
|
|
||||||
DB_PORT=3306
|
|
||||||
DB_DATABASE=
|
|
||||||
DB_USERNAME=
|
|
||||||
DB_PASSWORD=
|
|
||||||
|
|
||||||
BROADCAST_DRIVER=log
|
|
||||||
CACHE_DRIVER=file
|
|
||||||
QUEUE_CONNECTION=sync
|
|
||||||
SESSION_DRIVER=file
|
|
||||||
SESSION_LIFETIME=120
|
|
||||||
|
|
||||||
REDIS_HOST=127.0.0.1
|
|
||||||
REDIS_PASSWORD=null
|
|
||||||
REDIS_PORT=6379
|
|
||||||
|
|
||||||
MAIL_MAILER=smtp
|
|
||||||
MAIL_HOST="smtp.mailtrap.io"
|
|
||||||
MAIL_PORT="2525"
|
|
||||||
MAIL_USERNAME="hello@example.com"
|
|
||||||
MAIL_PASSWORD="12345"
|
|
||||||
MAIL_ENCRYPTION="TLS/SSL"
|
|
||||||
MAIL_FROM_ADDRESS="hello@example.com"
|
|
||||||
MAIL_FROM_NAME="Example"
|
|
||||||
|
|
||||||
AWS_ACCESS_KEY_ID=
|
|
||||||
AWS_SECRET_ACCESS_KEY=
|
|
||||||
AWS_DEFAULT_REGION=us-east-1
|
|
||||||
AWS_BUCKET=
|
|
||||||
|
|
||||||
PUSHER_APP_ID=
|
|
||||||
PUSHER_APP_KEY=
|
|
||||||
PUSHER_APP_SECRET=
|
|
||||||
PUSHER_APP_CLUSTER=mt1
|
|
||||||
|
|
||||||
MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
|
|
||||||
MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
|
|
||||||
|
|
||||||
ENVATO_PURCHASE_CODE="780fc1cd-62c9-4b1f-8798-7f4f6f7048ef"
|
|
||||||
SUPERADMIN_EMAILS=admin@admin.com
|
|
||||||
ENABLE_REGISTRATION="0"
|
|
||||||
|
|
||||||
CURRENCY_NAME="Euro"
|
|
||||||
CURRENCY_SYMBOL="€"
|
|
||||||
CURRENCY_CODE="EUR"
|
|
||||||
|
|
||||||
APP_TIMEZONE="Europe/Helsinki"
|
|
||||||
|
|
||||||
ENABLE_SAAS_MODULE="0"
|
|
||||||
|
|
||||||
PAYPAL_MODE="sandbox"
|
|
||||||
#PayPal Setting & API Credentials - sandbox
|
|
||||||
PAYPAL_SANDBOX_API_USERNAME=
|
|
||||||
PAYPAL_SANDBOX_API_PASSWORD=
|
|
||||||
PAYPAL_SANDBOX_API_SECRET=
|
|
||||||
PAYPAL_SANDBOX_API_CERTIFICATE=
|
|
||||||
|
|
||||||
#PayPal Setting & API Credentials - live
|
|
||||||
PAYPAL_LIVE_API_USERNAME=
|
|
||||||
PAYPAL_LIVE_API_PASSWORD=
|
|
||||||
PAYPAL_LIVE_API_SECRET=
|
|
||||||
PAYPAL_LIVE_API_CERTIFICATE=
|
|
||||||
|
|
||||||
#Stripe Payment Api & Credentials
|
|
||||||
STRIPE_PUB_KEY=
|
|
||||||
STRIPE_SECRET_KEY=
|
|
||||||
|
|
||||||
APP_DATE_FORMAT="d.m.Y"
|
|
||||||
APP_TIME_FORMAT="24"
|
|
||||||
|
|
||||||
DEBUGBAR_ENABLED=false
|
|
||||||
ENABLE_OFFLINE_PAYMENT="0"
|
|
||||||
|
|
||||||
ACELLE_MAIL_NAME=""
|
|
||||||
ACELLE_MAIL_API=""
|
|
||||||
|
|
||||||
APP_TITLE="Vormid"
|
|
||||||
38
.gitignore
vendored
38
.gitignore
vendored
@@ -14,3 +14,41 @@ yarn-error.log
|
|||||||
/public/js/
|
/public/js/
|
||||||
/public/css/
|
/public/css/
|
||||||
/storage/
|
/storage/
|
||||||
|
/.kilo/
|
||||||
|
/kilo-meta.json
|
||||||
|
|
||||||
|
# APAW system — не коммитить в репозиторий приложения (оставить локально для агентов)
|
||||||
|
/AGENTS.md
|
||||||
|
/architect.md
|
||||||
|
/.architect/
|
||||||
|
/architect/
|
||||||
|
|
||||||
|
# Сгенерированные агентами отчёты и документация
|
||||||
|
/CHANGES_*.md
|
||||||
|
/FIXED_*.md
|
||||||
|
/HOTFIX_*.md
|
||||||
|
/FIX_REPORT_*.md
|
||||||
|
/DB_FIX_*.md
|
||||||
|
/DB_RESTORE_REPORT.md
|
||||||
|
/RESTORE_REPORT.md
|
||||||
|
/FINAL_REPORT_*.md
|
||||||
|
/DEPLOYMENT_REPORT_*.md
|
||||||
|
/DEPLOYMENT_INSTRUCTIONS.md
|
||||||
|
/DOCKER_GUIDE.md
|
||||||
|
/DOCKER_QUICKSTART.md
|
||||||
|
/COMPLETE_PROJECT_HISTORY.md
|
||||||
|
/FULL_DEVELOPMENT_HISTORY.md
|
||||||
|
/DOCUMENTATION_INDEX.md
|
||||||
|
/PROJECT_STRUCTURE.md
|
||||||
|
/VERSION_SUMMARY.md
|
||||||
|
/CLICK_LOGIC_REVIEW.md
|
||||||
|
/FILES_TO_COPY.txt
|
||||||
|
|
||||||
|
# Бэкапы, дампы БД, данные, сборочные артефакты, мусор
|
||||||
|
/backup.sh
|
||||||
|
/seed.sql
|
||||||
|
/db_dump/
|
||||||
|
/data/
|
||||||
|
/dist/
|
||||||
|
/$2/
|
||||||
|
/test_browser.js
|
||||||
30
Dockerfile
Normal file
30
Dockerfile
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# ---------- Build stage ----------
|
||||||
|
FROM node:20-bookworm-slim AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---------- Runtime stage ----------
|
||||||
|
FROM node:20-bookworm-slim
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
WRANGLER_SEND_METRICS=false
|
||||||
|
|
||||||
|
# Copy everything from builder (includes node_modules, dist, migrations, etc.)
|
||||||
|
COPY --from=builder /app /app
|
||||||
|
|
||||||
|
RUN chmod +x /app/docker-entrypoint.sh
|
||||||
|
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Persist D1 SQLite data and seed marker between restarts
|
||||||
|
VOLUME ["/data"]
|
||||||
|
|
||||||
|
ENTRYPOINT ["/app/docker-entrypoint.sh"]
|
||||||
214
README.md
Normal file
214
README.md
Normal file
@@ -0,0 +1,214 @@
|
|||||||
|
# AKNAPROFF Tootmine
|
||||||
|
|
||||||
|
**Версия:** 4.0.4 (28.11.2025)
|
||||||
|
**Статус:** ✅ Production Ready - **Все функции работают, включая клики по ячейкам**
|
||||||
|
|
||||||
|
## 📋 Обзор проекта
|
||||||
|
|
||||||
|
Система управления производством окон для компании AKNAPROFF. Веб-приложение построено на Hono (Cloudflare Workers) с базой данных D1 SQLite.
|
||||||
|
|
||||||
|
## 🎯 Стратегия восстановления v4.0.0
|
||||||
|
|
||||||
|
### ✅ ОСНОВА проекта (НЕ ТРОГАЕМ):
|
||||||
|
- **Original HTML** (1223 строки) из архива `aknaproff.zip`
|
||||||
|
- **Original app.js** (73KB, 2079 строк) - все функции, стили, логика
|
||||||
|
- **Original all.min.css** (100KB) - FontAwesome и стили
|
||||||
|
- **Original button texts** - "Lisa uus rida", "Tühista", etc.
|
||||||
|
- **Original function names** - `openModal()`, `closeModal()`, etc.
|
||||||
|
- **Original IDs** - `recordModal`, `settingsForm`, etc.
|
||||||
|
|
||||||
|
### 🔧 ЧТО ВОССТАНАВЛИВАЕМ:
|
||||||
|
- **Backend API** (Hono) - создан под фронтенд вызовы из оригинального app.js
|
||||||
|
- **D1 Database** - схема БД для хранения данных
|
||||||
|
- **Authentication** - JWT токены для безопасности
|
||||||
|
|
||||||
|
## 🌐 Доступ к приложению
|
||||||
|
|
||||||
|
- **Sandbox URL**: https://3000-iabcqs9fpouqnd3allaai-82b888ba.sandbox.novita.ai
|
||||||
|
|
||||||
|
## 👤 Демо пользователи
|
||||||
|
|
||||||
|
| Пользователь | Пароль | Роль | Описание |
|
||||||
|
|--------------|--------|------|----------|
|
||||||
|
| `admin` | `demo123` | Admin | Для разработчика |
|
||||||
|
| `aknaproff` | `demo123` | Admin | Для клиента |
|
||||||
|
|
||||||
|
## ✨ Основные функции
|
||||||
|
|
||||||
|
### Реализовано (v4.0.0):
|
||||||
|
- ✅ **100% соответствие оригинальному HTML из архива**
|
||||||
|
- ✅ Управление производственными записями (CRUD)
|
||||||
|
- ✅ Статусные чекбоксы для этапов производства
|
||||||
|
- ✅ Система флагов ошибок с блокировкой полей
|
||||||
|
- ✅ Модальные окна (7 шт): Login, Record, Notes, Problems, Blocked, Settings, Report
|
||||||
|
- ✅ Быстрый поиск по: Klient, Tüüp, Pakkum. Nr, Töö Nr
|
||||||
|
- ✅ Сортировка по колонкам
|
||||||
|
- ✅ Фильтрация по месяцу и году
|
||||||
|
- ✅ Итоговые суммы (Kogus, Hind)
|
||||||
|
- ✅ JWT аутентификация
|
||||||
|
- ✅ Audit log для изменений
|
||||||
|
- ✅ Soft delete записей
|
||||||
|
- ✅ Генерация отчётов (Master, Accountant)
|
||||||
|
|
||||||
|
## 🏗️ Архитектура
|
||||||
|
|
||||||
|
### Frontend (из архива):
|
||||||
|
```
|
||||||
|
public/
|
||||||
|
├── static/
|
||||||
|
│ ├── app.js # Original 73KB, 2079 lines
|
||||||
|
│ └── all.min.css # Original 100KB FontAwesome
|
||||||
|
└── original.html # Original 1223 lines (встроен в TypeScript)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Backend (Hono + D1):
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── index.tsx # Main Hono app (26 API endpoints)
|
||||||
|
├── original-html.ts # Embedded original HTML
|
||||||
|
├── middleware/
|
||||||
|
│ └── auth.ts # JWT middleware
|
||||||
|
└── utils/
|
||||||
|
└── auth.ts # Password hashing, token generation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Database (D1):
|
||||||
|
```
|
||||||
|
migrations/
|
||||||
|
└── 0001_initial_schema.sql # 4 tables:
|
||||||
|
# - users
|
||||||
|
# - production_records
|
||||||
|
# - status_checkboxes
|
||||||
|
# - audit_log
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📡 API Endpoints (26)
|
||||||
|
|
||||||
|
### Authentication (2):
|
||||||
|
- `POST /api/auth/login` - Вход в систему
|
||||||
|
- `PATCH /api/users/profile` - Изменение профиля
|
||||||
|
|
||||||
|
### Data Management (8):
|
||||||
|
- `GET /api/years` - Список годов для фильтров
|
||||||
|
- `GET /api/records` - Список записей (с фильтрами)
|
||||||
|
- `POST /api/records` - Создание записи
|
||||||
|
- `GET /api/records/:id` - Получение записи
|
||||||
|
- `PATCH /api/records/:id` - Обновление записи
|
||||||
|
- `DELETE /api/records/:id` - Удаление записи
|
||||||
|
- `PATCH /api/records/:id/material-confirmed` - Подтверждение материала
|
||||||
|
- `PATCH /api/records/:id/material2-confirmed` - Подтверждение материала-2
|
||||||
|
|
||||||
|
### Status Management (6):
|
||||||
|
- `PATCH /api/records/:id/worksheets-cycle` - Цикл статуса "Töölehti"
|
||||||
|
- `PATCH /api/records/:id/status` - Обновление даты статуса
|
||||||
|
- `PATCH /api/records/:id/notes` - Сохранение заметок
|
||||||
|
- `PATCH /api/records/:id/problems` - Сохранение проблем
|
||||||
|
- `PATCH /api/records/:id/price-paid` - Подтверждение оплаты
|
||||||
|
- `PATCH /api/records/:id/blocked` - Информация о блокировке
|
||||||
|
|
||||||
|
## 🔒 Важные принципы восстановления
|
||||||
|
|
||||||
|
### ❌ НЕ МЕНЯТЬ:
|
||||||
|
1. Названия функций из оригинального `app.js`
|
||||||
|
2. Тексты кнопок (на эстонском языке)
|
||||||
|
3. HTML структуру из архива
|
||||||
|
4. CSS классы и стили
|
||||||
|
5. ID элементов
|
||||||
|
6. Логику работы фронтенда
|
||||||
|
|
||||||
|
### ✅ ТОЛЬКО СОЗДАВАТЬ:
|
||||||
|
1. Backend API endpoints под фронтенд вызовы
|
||||||
|
2. Database схему для хранения данных
|
||||||
|
3. Middleware для аутентификации
|
||||||
|
4. Utility функции для бэкенда
|
||||||
|
|
||||||
|
## 🚀 Локальная разработка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Установка зависимостей
|
||||||
|
cd /home/user/webapp
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# База данных
|
||||||
|
npm run db:migrate:local # Применить миграции
|
||||||
|
npm run db:seed # Загрузить тестовые данные
|
||||||
|
|
||||||
|
# Разработка
|
||||||
|
npm run build # Сборка проекта
|
||||||
|
pm2 start ecosystem.config.cjs # Запуск сервера
|
||||||
|
pm2 logs webapp --nostream # Просмотр логов
|
||||||
|
|
||||||
|
# Тестирование
|
||||||
|
curl http://localhost:3000/api/years
|
||||||
|
curl http://localhost:3000/api/records?month=1&year=2025
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📝 Git история
|
||||||
|
|
||||||
|
```bash
|
||||||
|
6d22b04 - FULL RESTORE: Use original HTML/CSS/JS from archive as base (v4.0.0)
|
||||||
|
cc7b3d4 - Update README to v3.20.8
|
||||||
|
013be72 - Fix: Replace openAddRecordModal() with openModal()
|
||||||
|
f45b5a3 - Fix D1 database binding and API /api/years endpoint (v3.20.7)
|
||||||
|
[Earlier commits...]
|
||||||
|
```
|
||||||
|
|
||||||
|
## 🎨 Оригинальные стили и функции
|
||||||
|
|
||||||
|
### Кнопки (Original):
|
||||||
|
- "Lisa uus rida" - Добавить новую строку (`openModal()`)
|
||||||
|
- "Tühista" - Отмена (`closeModal()`)
|
||||||
|
- "Salvesta" - Сохранить
|
||||||
|
- "Kustuta" - Удалить
|
||||||
|
|
||||||
|
### Модальные окна (Original):
|
||||||
|
- `loginModal` - Вход администратора
|
||||||
|
- `recordModal` - Добавление/редактирование записи
|
||||||
|
- `notesModal` - Заметки к записи
|
||||||
|
- `problemsModal` - Проблемы производства
|
||||||
|
- `blockedFieldModal` - Уведомление о блокировке
|
||||||
|
- `settingsModal` - Настройки пользователя
|
||||||
|
- `reportModal` - Генерация отчётов
|
||||||
|
|
||||||
|
### Функции (Original from app.js):
|
||||||
|
- `openModal()` - Открыть форму добавления
|
||||||
|
- `closeModal()` - Закрыть форму
|
||||||
|
- `toggleDate()` - Переключить дату статуса
|
||||||
|
- `toggleWorksheetsStep()` - Цикл статусов "Töölehti"
|
||||||
|
- `openNotesModal()`, `openProblemsModal()`, etc.
|
||||||
|
|
||||||
|
## ✅ Проверка качества восстановления
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ✅ Проверка оригинального HTML
|
||||||
|
curl http://localhost:3000 | grep "Lisa uus rida"
|
||||||
|
curl http://localhost:3000 | grep 'onclick="openModal()"'
|
||||||
|
|
||||||
|
# ✅ Проверка API
|
||||||
|
curl http://localhost:3000/api/years
|
||||||
|
# {"years":[2024,2025,2026]}
|
||||||
|
|
||||||
|
# ✅ Проверка модальных окон
|
||||||
|
curl http://localhost:3000 | grep -o 'id="recordModal"'
|
||||||
|
curl http://localhost:3000 | grep -o 'id="settingsForm"'
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Технологии
|
||||||
|
|
||||||
|
- **Frontend**: Original HTML/CSS/JS from archive
|
||||||
|
- **Backend**: Hono (Cloudflare Workers)
|
||||||
|
- **Database**: Cloudflare D1 (SQLite)
|
||||||
|
- **Auth**: JWT tokens
|
||||||
|
- **Styles**: TailwindCSS + FontAwesome
|
||||||
|
- **Deployment**: Cloudflare Pages
|
||||||
|
|
||||||
|
## 🎯 Следующие шаги
|
||||||
|
|
||||||
|
1. Тестирование всех функций на соответствие оригиналу
|
||||||
|
2. Проверка всех модальных окон
|
||||||
|
3. Проверка генерации отчётов
|
||||||
|
4. Deploy на Cloudflare Pages
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Версия 4.0.0** - Полное восстановление из архива с соблюдением принципа "Архив - это основа" 🎉
|
||||||
@@ -14,7 +14,8 @@ class Form extends Model
|
|||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
protected $guarded = [];
|
protected $guarded = [];
|
||||||
|
public $incrementing = false;
|
||||||
|
protected $keyType = 'string';
|
||||||
protected $appends = ['media_url'];
|
protected $appends = ['media_url'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ use ZipArchive;
|
|||||||
|
|
||||||
class FormController extends Controller
|
class FormController extends Controller
|
||||||
{
|
{
|
||||||
|
|
||||||
public function __construct() {
|
|
||||||
$this->middleware('auth');
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display a listing of the resource.
|
* Display a listing of the resource.
|
||||||
*
|
*
|
||||||
@@ -196,7 +191,7 @@ class FormController extends Controller
|
|||||||
public function update($id)
|
public function update($id)
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$input = request()->only('name', 'name_ru', 'name_est', 'description', 'description_ru', 'description_est', 'slug');
|
$input = request()->only('name', 'description', 'slug');
|
||||||
$form_data = [
|
$form_data = [
|
||||||
'form' => request()->input('form'),
|
'form' => request()->input('form'),
|
||||||
'emailConfig' => request()->input('email_config'),
|
'emailConfig' => request()->input('email_config'),
|
||||||
@@ -207,20 +202,14 @@ class FormController extends Controller
|
|||||||
'contains_page_break' => request()->input('contains_page_break'),
|
'contains_page_break' => request()->input('contains_page_break'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// dd($form_data);
|
|
||||||
|
|
||||||
$is_template = request()->input('is_template');
|
$is_template = request()->input('is_template');
|
||||||
$input['schema'] = $form_data;
|
$input['schema'] = $form_data;
|
||||||
|
|
||||||
$form = Form::find($id);
|
$form = Form::find($id);
|
||||||
|
|
||||||
$form->name = $input['name'];
|
$form->name = $input['name'];
|
||||||
$form->name_ru = $input['name_ru'];
|
|
||||||
$form->name_est = $input['name_est'];
|
|
||||||
$form->slug = $input['slug'];
|
$form->slug = $input['slug'];
|
||||||
$form->description = $input['description'];
|
$form->description = $input['description'];
|
||||||
$form->description_ru = $input['description_ru'];
|
|
||||||
$form->description_est = $input['description_est'];
|
|
||||||
$form->schema = $input['schema'];
|
$form->schema = $input['schema'];
|
||||||
$form->is_template = $is_template;
|
$form->is_template = $is_template;
|
||||||
$form->mailchimp_details = request()->input('mailchimp_details');
|
$form->mailchimp_details = request()->input('mailchimp_details');
|
||||||
|
|||||||
@@ -477,21 +477,28 @@ class FormDataController extends Controller
|
|||||||
->orderBy('created_at', 'desc')
|
->orderBy('created_at', 'desc')
|
||||||
->get()
|
->get()
|
||||||
->filter(function (FormData $formData) use ($request) {
|
->filter(function (FormData $formData) use ($request) {
|
||||||
|
$date = null;
|
||||||
if (is_array($formData->data)) {
|
if (is_array($formData->data)) {
|
||||||
$date = strtotime(
|
$dates = array_filter($formData->data, function ($item) {
|
||||||
array_values(
|
return is_string($item) && strtotime($item);
|
||||||
array_filter($formData->data, fn($item) => is_string($item) && strtotime($item))
|
});
|
||||||
)[0]
|
if (!empty($dates)) {
|
||||||
);
|
$firstDate = reset($dates);
|
||||||
|
$date = strtotime($firstDate);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$date = Carbon::createFromTimestamp($date)->toDateString();
|
if (!$date) {
|
||||||
$isValidStartDate = $request->filled('start_date') ?
|
return false; // Skip entries without valid dates
|
||||||
$request->get('start_date') <= $date :
|
}
|
||||||
Carbon::now()->subDays(7)->toDateString() <= $date;
|
|
||||||
$isValidEndDate = $request->filled('end_date') ?
|
$dateStr = Carbon::createFromTimestamp($date)->toDateString();
|
||||||
$request->get('end_date') >= $date :
|
$isValidStartDate = $request->filled('start_date')
|
||||||
Carbon::now()->toDateString() >= $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;
|
return $isValidStartDate && $isValidEndDate;
|
||||||
});
|
});
|
||||||
@@ -532,7 +539,7 @@ class FormDataController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
return view('form_data.show')
|
return view('form_data.show')
|
||||||
->with(compact('form', 'data'));
|
->with(compact('form', 'data', 'has_permission')); // Добавьте has_permission
|
||||||
}
|
}
|
||||||
|
|
||||||
public function viewData($id)
|
public function viewData($id)
|
||||||
|
|||||||
@@ -183,7 +183,7 @@ class HomeController extends Controller
|
|||||||
if (request()->ajax()) {
|
if (request()->ajax()) {
|
||||||
$user_id = request()->user()->id;
|
$user_id = request()->user()->id;
|
||||||
|
|
||||||
$forms = Form::select('name', 'name_est', 'name_ru', 'description', 'description_ru', 'description_est', 'id', 'slug', 'is_global_template')
|
$forms = Form::select('name', 'description', 'id', 'slug', 'is_global_template')
|
||||||
->where(function ($query) use ($user_id) {
|
->where(function ($query) use ($user_id) {
|
||||||
$query->where('is_template', 1)
|
$query->where('is_template', 1)
|
||||||
->where('created_by', $user_id)
|
->where('created_by', $user_id)
|
||||||
@@ -278,7 +278,7 @@ class HomeController extends Controller
|
|||||||
$forms = UserForm::join('forms', 'user_forms.form_id', '=', 'forms.id')
|
$forms = UserForm::join('forms', 'user_forms.form_id', '=', 'forms.id')
|
||||||
->leftJoin('users', 'forms.created_by', '=', 'users.id')
|
->leftJoin('users', 'forms.created_by', '=', 'users.id')
|
||||||
->where('user_forms.assigned_to', \Auth::id())
|
->where('user_forms.assigned_to', \Auth::id())
|
||||||
->select('user_forms.permissions as permissions', 'forms.name as name', 'forms.name_ru as name_ru', 'forms.name_est as name_est', 'forms.description as description', 'forms.description_ru as description_ru', 'forms.description_est as description_est', 'forms.id as form_id', 'forms.created_at as created_at', 'forms.slug as slug', 'users.name as created_by');
|
->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)
|
return DataTables::of($forms)
|
||||||
->addColumn(
|
->addColumn(
|
||||||
|
|||||||
@@ -1,31 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Controllers;
|
|
||||||
|
|
||||||
use App\UserSetting;
|
|
||||||
use Illuminate\Http\Request;
|
|
||||||
use Illuminate\Support\Facades\App;
|
|
||||||
use Illuminate\Support\Facades\Session;
|
|
||||||
|
|
||||||
class LocaleController extends Controller
|
|
||||||
{
|
|
||||||
public function __invoke($locale) {
|
|
||||||
if (! in_array($locale, ['en', 'ru', 'est'])) {
|
|
||||||
abort(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
Session::put('locale', $locale);
|
|
||||||
|
|
||||||
app()->setLocale($locale);
|
|
||||||
|
|
||||||
$userSetting = UserSetting::where('user_id', auth()->user()->id)->first();
|
|
||||||
|
|
||||||
if (!empty($userSetting)) {
|
|
||||||
$userSetting->update([
|
|
||||||
'language' => $locale
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return redirect()->back();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -5,7 +5,6 @@ namespace App\Http\Controllers;
|
|||||||
use App\UserSetting;
|
use App\UserSetting;
|
||||||
use DateTimeZone;
|
use DateTimeZone;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Illuminate\Support\Facades\Session;
|
|
||||||
|
|
||||||
class ManageSettingsController extends Controller
|
class ManageSettingsController extends Controller
|
||||||
{
|
{
|
||||||
@@ -58,14 +57,6 @@ class ManageSettingsController extends Controller
|
|||||||
|
|
||||||
$setting = UserSetting::where('user_id', $input['user_id'])->first();
|
$setting = UserSetting::where('user_id', $input['user_id'])->first();
|
||||||
|
|
||||||
if (! in_array($request->language, ['en', 'ru', 'est'])) {
|
|
||||||
abort(400);
|
|
||||||
}
|
|
||||||
|
|
||||||
Session::put('locale', $request->language);
|
|
||||||
|
|
||||||
app()->setLocale($request->language);
|
|
||||||
|
|
||||||
if (empty($setting)) {
|
if (empty($setting)) {
|
||||||
UserSetting::create($input);
|
UserSetting::create($input);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -64,6 +64,5 @@ class Kernel extends HttpKernel
|
|||||||
'bootstrap' => \App\Http\Middleware\Callbacks::class,
|
'bootstrap' => \App\Http\Middleware\Callbacks::class,
|
||||||
'setDefaultConfig' => \App\Http\Middleware\SetDefaultConfigForUser::class,
|
'setDefaultConfig' => \App\Http\Middleware\SetDefaultConfigForUser::class,
|
||||||
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
|
||||||
'locale' => \App\Http\Middleware\LocaleMiddleware::class,
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
namespace App\Http\Middleware;
|
|
||||||
|
|
||||||
use App\UserSetting;
|
|
||||||
use Closure;
|
|
||||||
use Illuminate\Support\Facades\Session;
|
|
||||||
|
|
||||||
class LocaleMiddleware
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Handle an incoming request.
|
|
||||||
*
|
|
||||||
* @param \Illuminate\Http\Request $request
|
|
||||||
* @param \Closure $next
|
|
||||||
* @return mixed
|
|
||||||
*/
|
|
||||||
public function handle($request, Closure $next)
|
|
||||||
{
|
|
||||||
|
|
||||||
if (empty(Session::get('locale'))) {
|
|
||||||
$userSetting = UserSetting::where('user_id', auth()->user()->id)->first();
|
|
||||||
|
|
||||||
$locale = 'en';
|
|
||||||
if (!empty($userSetting)) {
|
|
||||||
if (!in_array($userSetting->language, ['en', 'ru', 'est'])) {
|
|
||||||
$locale = $userSetting->language;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Session::put('locale', $locale);
|
|
||||||
app()->setLocale($locale);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $next($request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use Illuminate\Database\Migrations\Migration;
|
|
||||||
use Illuminate\Database\Schema\Blueprint;
|
|
||||||
use Illuminate\Support\Facades\Schema;
|
|
||||||
|
|
||||||
return new class extends Migration
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Run the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function up()
|
|
||||||
{
|
|
||||||
Schema::table('forms', function (Blueprint $table) {
|
|
||||||
$table->string('name_ru')->after('name')->nullable();
|
|
||||||
$table->string('name_est')->after('name_ru')->nullable();
|
|
||||||
$table->string('description_ru')->after('description')->nullable();
|
|
||||||
$table->string('description_est')->after('description_ru')->nullable();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reverse the migrations.
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public function down()
|
|
||||||
{
|
|
||||||
Schema::table('forms', function (Blueprint $table) {
|
|
||||||
//
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
1524
db_dump/nero_tab.sql
1524
db_dump/nero_tab.sql
File diff suppressed because one or more lines are too long
42
docker-compose.prod.yml
Normal file
42
docker-compose.prod.yml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
webapp:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: aknaproff-webapp-prod
|
||||||
|
|
||||||
|
# Монтировать только папку БД локально
|
||||||
|
volumes:
|
||||||
|
# Локальное хранилище БД
|
||||||
|
- ./data/db:/app/.wrangler/state/v3/d1
|
||||||
|
# Логи (опционально)
|
||||||
|
- ./data/logs:/app/logs
|
||||||
|
|
||||||
|
# Переменные окружения
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- PORT=3000
|
||||||
|
|
||||||
|
# Открыть порт 3000
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
|
||||||
|
# Перезапуск при падении
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
# Лимиты ресурсов
|
||||||
|
deploy:
|
||||||
|
resources:
|
||||||
|
limits:
|
||||||
|
cpus: '1'
|
||||||
|
memory: 512M
|
||||||
|
reservations:
|
||||||
|
cpus: '0.5'
|
||||||
|
memory: 256M
|
||||||
|
|
||||||
|
# Сеть
|
||||||
|
networks:
|
||||||
|
default:
|
||||||
|
name: aknaproff-prod-network
|
||||||
23
docker-compose.yml
Normal file
23
docker-compose.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
version: "3.9"
|
||||||
|
|
||||||
|
services:
|
||||||
|
aknaproff-backend:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
container_name: aknaproff-backend
|
||||||
|
ports:
|
||||||
|
- "8180:3000"
|
||||||
|
environment:
|
||||||
|
PORT: 3000
|
||||||
|
D1_BINDING: aknaproff-db
|
||||||
|
PERSIST_PATH: /data
|
||||||
|
SEED_DATA: "false" # Set to "true" on first run to load seed.sql automatically
|
||||||
|
WRANGLER_SEND_METRICS: "false"
|
||||||
|
volumes:
|
||||||
|
- ./data:/data
|
||||||
|
restart: unless-stopped
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
d1-data:
|
||||||
|
driver: local
|
||||||
54
docker-entrypoint.sh
Executable file
54
docker-entrypoint.sh
Executable file
@@ -0,0 +1,54 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
PORT="${PORT:-3000}"
|
||||||
|
D1_BINDING="${D1_BINDING:-aknaproff-db}"
|
||||||
|
PERSIST_PATH="${PERSIST_PATH:-/data}"
|
||||||
|
SEED_DATA="${SEED_DATA:-false}"
|
||||||
|
SEED_SENTINEL="${PERSIST_PATH}/.seeded"
|
||||||
|
|
||||||
|
mkdir -p "${PERSIST_PATH}"
|
||||||
|
export WRANGLER_SEND_METRICS="${WRANGLER_SEND_METRICS:-false}"
|
||||||
|
|
||||||
|
apply_migrations() {
|
||||||
|
echo "[entrypoint] Applying D1 migrations (binding: ${D1_BINDING}, persist: ${PERSIST_PATH})"
|
||||||
|
npx wrangler d1 migrations apply "${D1_BINDING}" \
|
||||||
|
--local \
|
||||||
|
--persist-to "${PERSIST_PATH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
maybe_seed_data() {
|
||||||
|
if [[ "${SEED_DATA,,}" != "true" ]]; then
|
||||||
|
echo "[entrypoint] Seed step disabled (set SEED_DATA=true to enable)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -f "${SEED_SENTINEL}" ]]; then
|
||||||
|
echo "[entrypoint] Seed data already applied (skipping)"
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "[entrypoint] Seeding local database from seed.sql"
|
||||||
|
if npx wrangler d1 execute "${D1_BINDING}" \
|
||||||
|
--local \
|
||||||
|
--persist-to "${PERSIST_PATH}" \
|
||||||
|
--file ./seed.sql; then
|
||||||
|
touch "${SEED_SENTINEL}"
|
||||||
|
else
|
||||||
|
echo "[entrypoint] Seed step failed but container will continue" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
start_server() {
|
||||||
|
echo "[entrypoint] Starting Wrangler dev server on port ${PORT}"
|
||||||
|
exec npx wrangler pages dev dist \
|
||||||
|
--local \
|
||||||
|
--d1="${D1_BINDING}" \
|
||||||
|
--persist-to "${PERSIST_PATH}" \
|
||||||
|
--ip 0.0.0.0 \
|
||||||
|
--port "${PORT}"
|
||||||
|
}
|
||||||
|
|
||||||
|
apply_migrations
|
||||||
|
maybe_seed_data
|
||||||
|
start_server
|
||||||
19
ecosystem.config.cjs
Normal file
19
ecosystem.config.cjs
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
module.exports = {
|
||||||
|
apps: [
|
||||||
|
{
|
||||||
|
name: 'webapp',
|
||||||
|
script: 'npx',
|
||||||
|
args: 'wrangler pages dev dist --d1=webapp-production --local --ip 0.0.0.0 --port 3000',
|
||||||
|
env: {
|
||||||
|
NODE_ENV: 'development',
|
||||||
|
PORT: 3000
|
||||||
|
},
|
||||||
|
watch: false,
|
||||||
|
instances: 1,
|
||||||
|
exec_mode: 'fork',
|
||||||
|
autorestart: true,
|
||||||
|
max_restarts: 5,
|
||||||
|
min_uptime: '10s'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -81,8 +81,8 @@ return [
|
|||||||
'click_to_add_tags' => 'Click to add the tags',
|
'click_to_add_tags' => 'Click to add the tags',
|
||||||
'body' => 'Body',
|
'body' => 'Body',
|
||||||
'email_body' => 'Email Body',
|
'email_body' => 'Email Body',
|
||||||
'auto_restonse_settings' => 'Auto Restonse Settings',
|
'auto_response_settings' => 'Auto Response Settings',
|
||||||
'enable_auto_restonse' => 'Enable Auto Restonse?',
|
'enable_auto_response' => 'Enable Auto Response?',
|
||||||
'smtp_settings' => 'SMTP Settings',
|
'smtp_settings' => 'SMTP Settings',
|
||||||
'use_system_smtp' => 'Use System SMTP?',
|
'use_system_smtp' => 'Use System SMTP?',
|
||||||
'host' => 'Host',
|
'host' => 'Host',
|
||||||
@@ -425,7 +425,7 @@ return [
|
|||||||
'tour_step_2_intro' => '<b class="text-success">Step 2:</b></br> Drop the element here & click it to configure.',
|
'tour_step_2_intro' => '<b class="text-success">Step 2:</b></br> Drop the element here & click it to configure.',
|
||||||
'tour_step_3_intro' => '<b class="text-success">Step 3:</b></br> Form & Element configuration will appear here.',
|
'tour_step_3_intro' => '<b class="text-success">Step 3:</b></br> Form & Element configuration will appear here.',
|
||||||
'tour_step_4_intro' => '<b class="text-success">Step 4:</b></br> Add conditions to show/hide element based on other element values.',
|
'tour_step_4_intro' => '<b class="text-success">Step 4:</b></br> Add conditions to show/hide element based on other element values.',
|
||||||
'tour_step_5_intro' => '<b class="text-success">Step 5:</b></br> Configure receiving of submission email & auto-restond email to the user.',
|
'tour_step_5_intro' => '<b class="text-success">Step 5:</b></br> Configure receiving of submission email & auto-respond email to the user.',
|
||||||
'tour_step_6_intro' => '<b class="text-success">Step 6:</b></br> Configure form reCaptcha, design, notifications, scheduling, submission reference number & others.',
|
'tour_step_6_intro' => '<b class="text-success">Step 6:</b></br> Configure form reCaptcha, design, notifications, scheduling, submission reference number & others.',
|
||||||
'tour_step_7_intro' => '<b class="text-success">Step 7:</b></br> Integrate mailchimp.',
|
'tour_step_7_intro' => '<b class="text-success">Step 7:</b></br> Integrate mailchimp.',
|
||||||
'tour_step_8_intro' => '<b class="text-success">Step 8:</b></br> Add additional Js/css in the form.',
|
'tour_step_8_intro' => '<b class="text-success">Step 8:</b></br> Add additional Js/css in the form.',
|
||||||
@@ -451,7 +451,7 @@ return [
|
|||||||
'field_name_should_nt_have_space' => 'Field name should not have space',
|
'field_name_should_nt_have_space' => 'Field name should not have space',
|
||||||
'duplicate_field_name_choose_unique' => 'Duplicate field name, choose unique name',
|
'duplicate_field_name_choose_unique' => 'Duplicate field name, choose unique name',
|
||||||
'field_dont_have_name_property' => ":input field don't have name property",
|
'field_dont_have_name_property' => ":input field don't have name property",
|
||||||
'field_contain_space' => ':input field contain whitestace in name',
|
'field_contain_space' => ':input field contain whitespace in name',
|
||||||
'field_contain_duplicate_field_name' => ':input field contain duplicate name',
|
'field_contain_duplicate_field_name' => ':input field contain duplicate name',
|
||||||
'key' => 'Key',
|
'key' => 'Key',
|
||||||
'add_form_custom_attribute' => 'Add form custom attribute',
|
'add_form_custom_attribute' => 'Add form custom attribute',
|
||||||
@@ -677,16 +677,4 @@ return [
|
|||||||
'error_msg_for_not_allowed_value' => 'Error message for not allowed value',
|
'error_msg_for_not_allowed_value' => 'Error message for not allowed value',
|
||||||
'enter_allowed_value_per_line' => 'Enter one allowed value per line.',
|
'enter_allowed_value_per_line' => 'Enter one allowed value per line.',
|
||||||
'values_allowed_tooltip' => 'Values provided here will only be allowed while submitting the form, keep it empty to allow all values',
|
'values_allowed_tooltip' => 'Values provided here will only be allowed while submitting the form, keep it empty to allow all values',
|
||||||
'field_label_est' => 'Field Label in est',
|
|
||||||
'field_label_ru' => 'Field Label in Ru',
|
|
||||||
'content_est' => 'Content in est',
|
|
||||||
'content_ru' => 'Content in Ru',
|
|
||||||
'placeholder_est' => 'Placeholder in est',
|
|
||||||
'placeholder_ru' => 'Placeholder in Ru',
|
|
||||||
'options_est' => 'Options in est',
|
|
||||||
'options_ru' => 'Options in Ru',
|
|
||||||
'form_name_est' => 'Form Name est',
|
|
||||||
'form_name_ru' => 'Form Name ru',
|
|
||||||
'form_description_est' => 'Form Description est',
|
|
||||||
'form_description_ru' => 'Form Description ru',
|
|
||||||
];
|
];
|
||||||
|
|||||||
80
migrations/0001_initial_schema.sql
Normal file
80
migrations/0001_initial_schema.sql
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
-- Users table
|
||||||
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
username TEXT UNIQUE NOT NULL,
|
||||||
|
password_hash TEXT NOT NULL,
|
||||||
|
full_name TEXT NOT NULL,
|
||||||
|
role TEXT NOT NULL DEFAULT 'user',
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at DATETIME DEFAULT NULL,
|
||||||
|
deleted_by INTEGER DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Production records table
|
||||||
|
CREATE TABLE IF NOT EXISTS production_records (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
month INTEGER NOT NULL,
|
||||||
|
year INTEGER NOT NULL,
|
||||||
|
client_name TEXT NOT NULL,
|
||||||
|
type TEXT,
|
||||||
|
offer_number TEXT NOT NULL,
|
||||||
|
work_number TEXT NOT NULL,
|
||||||
|
quantity INTEGER NOT NULL,
|
||||||
|
color TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
problems TEXT,
|
||||||
|
installer TEXT,
|
||||||
|
price DECIMAL(10, 2),
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
deleted_at DATETIME DEFAULT NULL,
|
||||||
|
deleted_by INTEGER DEFAULT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Status checkboxes table
|
||||||
|
CREATE TABLE IF NOT EXISTS status_checkboxes (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
record_id INTEGER NOT NULL,
|
||||||
|
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 INTEGER DEFAULT 0,
|
||||||
|
cutting_error INTEGER DEFAULT 0,
|
||||||
|
glazing_error INTEGER DEFAULT 0,
|
||||||
|
ready_error INTEGER DEFAULT 0,
|
||||||
|
issued_error INTEGER DEFAULT 0,
|
||||||
|
material_confirmed INTEGER DEFAULT 0,
|
||||||
|
material2_confirmed INTEGER DEFAULT 0,
|
||||||
|
worksheets_confirmed INTEGER DEFAULT 0,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (record_id) REFERENCES production_records(id) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Audit log table
|
||||||
|
CREATE TABLE IF NOT EXISTS audit_log (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
user_id INTEGER,
|
||||||
|
record_id INTEGER,
|
||||||
|
field_name TEXT NOT NULL,
|
||||||
|
old_value TEXT,
|
||||||
|
new_value TEXT,
|
||||||
|
action_type TEXT NOT NULL,
|
||||||
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (user_id) REFERENCES users(id),
|
||||||
|
FOREIGN KEY (record_id) REFERENCES production_records(id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Create indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_production_records_month_year ON production_records(month, year);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_production_records_client ON production_records(client_name);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_production_records_deleted ON production_records(deleted_at);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_status_checkboxes_record ON status_checkboxes(record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_record ON audit_log(record_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_audit_log_user ON audit_log(user_id);
|
||||||
17895
package-lock.json
generated
17895
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@@ -1,33 +1,27 @@
|
|||||||
{
|
{
|
||||||
"private": true,
|
"name": "webapp",
|
||||||
"scripts": {
|
"type": "module",
|
||||||
"dev": "npm run development",
|
"scripts": {
|
||||||
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
"dev": "vite",
|
||||||
"watch": "npm run development -- --watch",
|
"dev:sandbox": "wrangler pages dev dist --d1=webapp-production --local --ip 0.0.0.0 --port 3000",
|
||||||
"watch-poll": "npm run watch -- --watch-poll",
|
"build": "vite build",
|
||||||
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
"preview": "wrangler pages dev",
|
||||||
"prod": "npm run production",
|
"deploy": "npm run build && wrangler pages deploy",
|
||||||
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
|
"deploy:prod": "npm run build && wrangler pages deploy dist --project-name webapp",
|
||||||
},
|
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
|
||||||
"devDependencies": {
|
"db:migrate:local": "wrangler d1 migrations apply webapp-production --local",
|
||||||
"axios": "^0.21.1",
|
"db:migrate:prod": "wrangler d1 migrations apply webapp-production",
|
||||||
"cross-env": "^5.1",
|
"db:seed": "wrangler d1 execute webapp-production --local --file=./seed.sql",
|
||||||
"laravel-mix": "^4.0.7",
|
"db:reset": "rm -rf .wrangler/state/v3/d1 && npm run db:migrate:local && npm run db:seed",
|
||||||
"lodash": "^4.17.19",
|
"clean-port": "fuser -k 3000/tcp 2>/dev/null || true"
|
||||||
"resolve-url-loader": "^2.3.1",
|
},
|
||||||
"sass": "^1.15.2",
|
"dependencies": {
|
||||||
"sass-loader": "^7.1.0",
|
"hono": "^4.10.7"
|
||||||
"vue": "^2.5.17",
|
},
|
||||||
"vue-template-compiler": "^2.6.10"
|
"devDependencies": {
|
||||||
},
|
"@hono/vite-build": "^1.2.0",
|
||||||
"dependencies": {
|
"@hono/vite-dev-server": "^0.18.2",
|
||||||
"accounting-js": "^1.1.1",
|
"vite": "^6.3.5",
|
||||||
"admin-lte": "^3.0.0-beta.2",
|
"wrangler": "^4.4.0"
|
||||||
"easyqrcodejs": "^4.4.10",
|
|
||||||
"iframe-resizer": "^4.3.1",
|
|
||||||
"ladda": "^2.0.1",
|
|
||||||
"particles.js": "^2.0.0",
|
|
||||||
"vuedraggable": "^2.21.0"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"private": true,
|
|
||||||
"scripts": {
|
|
||||||
"dev": "npm run development",
|
|
||||||
"development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js",
|
|
||||||
"watch": "npm run development -- --watch",
|
|
||||||
"watch-poll": "npm run watch -- --watch-poll",
|
|
||||||
"hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js",
|
|
||||||
"prod": "npm run production",
|
|
||||||
"production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"axios": "^0.18",
|
|
||||||
// "bootstrap": "^4.1.0",
|
|
||||||
"cross-env": "^5.1",
|
|
||||||
// "jquery": "^3.2",
|
|
||||||
"laravel-mix": "^4.0.7",
|
|
||||||
"lodash": "^4.17.5",
|
|
||||||
// "popper.js": "^1.12",
|
|
||||||
"resolve-url-loader": "^2.3.1",
|
|
||||||
"sass": "^1.15.2",
|
|
||||||
"sass-loader": "^7.1.0",
|
|
||||||
"vue": "^2.5.17"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
1231
public/original.html
Normal file
1231
public/original.html
Normal file
File diff suppressed because one or more lines are too long
9
public/static/all.min.css
vendored
Normal file
9
public/static/all.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2279
public/static/app.js
vendored
Normal file
2279
public/static/app.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1
public/static/style.css
vendored
Normal file
1
public/static/style.css
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
h1 { font-family: Arial, Helvetica, sans-serif; }
|
||||||
46
public/test-click.html
Normal file
46
public/test-click.html
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Click Test</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial; padding: 50px; }
|
||||||
|
.box {
|
||||||
|
width: 200px;
|
||||||
|
height: 100px;
|
||||||
|
background: #4F46E5;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
#result {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Click Test Page</h1>
|
||||||
|
<div class="box" onclick="handleClick(1)">Click Me (onclick)</div>
|
||||||
|
<div class="box" id="box2">Click Me (addEventListener)</div>
|
||||||
|
<div id="result">Waiting for click...</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Test 1: inline onclick
|
||||||
|
function handleClick(num) {
|
||||||
|
document.getElementById('result').innerHTML = '✅ Test ' + num + ': onclick works!';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: addEventListener
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.getElementById('box2').addEventListener('click', () => {
|
||||||
|
document.getElementById('result').innerHTML = '✅ Test 2: addEventListener works!';
|
||||||
|
});
|
||||||
|
console.log('✅ DOMContentLoaded fired and event listener attached');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
112
public/test-datepicker.html
Normal file
112
public/test-datepicker.html
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Date Picker Test</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
padding: 50px;
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.test-section {
|
||||||
|
margin: 30px 0;
|
||||||
|
padding: 20px;
|
||||||
|
border: 2px solid #ccc;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.test-section h2 {
|
||||||
|
margin-top: 0;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
.date-cell {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 8px 12px;
|
||||||
|
margin: 10px;
|
||||||
|
border: 2px solid #4F46E5;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: white;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.date-cell:hover {
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
.hidden-input {
|
||||||
|
position: absolute;
|
||||||
|
opacity: 0;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 15px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>📅 Date Picker Test Page</h1>
|
||||||
|
|
||||||
|
<!-- Test 1: Label approach (current v4.0.11) -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>✅ Test 1: <label for> approach (v4.0.11)</h2>
|
||||||
|
<input type="date" id="date1" class="hidden-input" value="2025-01-15" onchange="updateResult(1, this.value)">
|
||||||
|
<label for="date1" class="date-cell">
|
||||||
|
Click me: 15.01.2025
|
||||||
|
</label>
|
||||||
|
<div class="result" id="result1">Selected: 2025-01-15</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test 2: Direct visible input -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>✅ Test 2: Direct visible input (baseline)</h2>
|
||||||
|
<input type="date" id="date2" value="2025-01-15" onchange="updateResult(2, this.value)" style="padding: 8px;">
|
||||||
|
<div class="result" id="result2">Selected: 2025-01-15</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test 3: Label with onclick fallback -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>✅ Test 3: <label> with onclick fallback</h2>
|
||||||
|
<input type="date" id="date3" class="hidden-input" value="2025-01-15" onchange="updateResult(3, this.value)">
|
||||||
|
<label for="date3" class="date-cell" onclick="document.getElementById('date3').showPicker()">
|
||||||
|
Click me: 15.01.2025
|
||||||
|
</label>
|
||||||
|
<div class="result" id="result3">Selected: 2025-01-15</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test 4: Button triggers input.click() -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>✅ Test 4: Button with .click()</h2>
|
||||||
|
<input type="date" id="date4" class="hidden-input" value="2025-01-15" onchange="updateResult(4, this.value)">
|
||||||
|
<button class="date-cell" onclick="document.getElementById('date4').click()">
|
||||||
|
Click me: 15.01.2025
|
||||||
|
</button>
|
||||||
|
<div class="result" id="result4">Selected: 2025-01-15</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Test 5: Inline style hidden input -->
|
||||||
|
<div class="test-section">
|
||||||
|
<h2>✅ Test 5: Inline style (exactly like app.js)</h2>
|
||||||
|
<input type="date" id="date5" style="position: absolute; opacity: 0; width: 0; height: 0; pointer-events: none;" value="2025-01-15" onchange="updateResult(5, this.value)">
|
||||||
|
<label for="date5" class="date-cell">
|
||||||
|
Click me: 15.01.2025
|
||||||
|
</label>
|
||||||
|
<div class="result" id="result5">Selected: 2025-01-15</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function updateResult(testNum, newDate) {
|
||||||
|
document.getElementById('result' + testNum).innerHTML =
|
||||||
|
'✅ Date picker worked! Selected: ' + newDate;
|
||||||
|
console.log('Test ' + testNum + ' changed to:', newDate);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Test page loaded');
|
||||||
|
console.log('Browser:', navigator.userAgent);
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
public/test.html
Normal file
13
public/test.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Test JS</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 id="result">Waiting for JavaScript...</h1>
|
||||||
|
<script>
|
||||||
|
document.getElementById('result').textContent = 'JavaScript works!';
|
||||||
|
console.log('✅ JavaScript is executing correctly');
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -17,26 +17,6 @@
|
|||||||
v-model="element.label">
|
v-model="element.label">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group"
|
|
||||||
v-if="!_.includes(['heading', 'hr', 'html_text'], element.type)">
|
|
||||||
<label>
|
|
||||||
{{trans('messages.field_label_est')}}
|
|
||||||
<span class="error">*</span>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="form-control form-control-sm"
|
|
||||||
v-model="element.label_est">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group"
|
|
||||||
v-if="!_.includes(['heading', 'hr', 'html_text'], element.type)">
|
|
||||||
<label>
|
|
||||||
{{trans('messages.field_label_ru')}}
|
|
||||||
<span class="error">*</span>
|
|
||||||
</label>
|
|
||||||
<input type="text" class="form-control form-control-sm"
|
|
||||||
v-model="element.label_ru">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>
|
<label>
|
||||||
{{trans('messages.field_name')}}
|
{{trans('messages.field_name')}}
|
||||||
@@ -108,6 +88,7 @@
|
|||||||
:element="element">
|
:element="element">
|
||||||
</pdf-uploader>
|
</pdf-uploader>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- countdown -->
|
<!-- countdown -->
|
||||||
<div v-if="_.includes(['countdown'], element.type)">
|
<div v-if="_.includes(['countdown'], element.type)">
|
||||||
<div class="mb-1">
|
<div class="mb-1">
|
||||||
@@ -295,20 +276,6 @@
|
|||||||
v-model="element.placeholder">
|
v-model="element.placeholder">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group"
|
|
||||||
v-if="_.includes(['text', 'textarea', 'text_editor'], element.type)">
|
|
||||||
<label>{{trans('messages.placeholder_est')}}</label>
|
|
||||||
<input type="text" class="form-control form-control-sm"
|
|
||||||
v-model="element.placeholder_est">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group"
|
|
||||||
v-if="_.includes(['text', 'textarea', 'text_editor'], element.type)">
|
|
||||||
<label>{{trans('messages.placeholder_ru')}}</label>
|
|
||||||
<input type="text" class="form-control form-control-sm"
|
|
||||||
v-model="element.placeholder_ru">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group"
|
<div class="form-group"
|
||||||
v-if="!_.includes(['heading', 'terms_and_condition', 'hr', 'html_text', 'rating', 'youtube', 'iframe', 'pdf', 'countdown'], element.type)">
|
v-if="!_.includes(['heading', 'terms_and_condition', 'hr', 'html_text', 'rating', 'youtube', 'iframe', 'pdf', 'countdown'], element.type)">
|
||||||
<label>{{trans('messages.help_text')}}</label>
|
<label>{{trans('messages.help_text')}}</label>
|
||||||
@@ -655,26 +622,6 @@
|
|||||||
</small>
|
</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group"
|
|
||||||
v-if="_.includes(['radio', 'checkbox', 'dropdown'], element.type)">
|
|
||||||
<label>{{trans('messages.options_est')}}</label>
|
|
||||||
<textarea class="form-control form-control-sm"
|
|
||||||
v-model="element.options_est"></textarea>
|
|
||||||
<small class="form-text">
|
|
||||||
{{trans('messages.enter_one_option_per_line')}}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group"
|
|
||||||
v-if="_.includes(['radio', 'checkbox', 'dropdown'], element.type)">
|
|
||||||
<label>{{trans('messages.options_ru')}}</label>
|
|
||||||
<textarea class="form-control form-control-sm"
|
|
||||||
v-model="element.options_ru"></textarea>
|
|
||||||
<small class="form-text">
|
|
||||||
{{trans('messages.enter_one_option_per_line')}}
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="row mb-1"
|
<div class="row mb-1"
|
||||||
v-if="_.includes(['radio', 'checkbox'], element.type)">
|
v-if="_.includes(['radio', 'checkbox'], element.type)">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
@@ -739,20 +686,6 @@
|
|||||||
v-model="element.content"></textarea>
|
v-model="element.content"></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="form-group"
|
|
||||||
v-if="_.includes(['heading'], element.type)">
|
|
||||||
<label>{{trans('messages.content_est')}}</label>
|
|
||||||
<textarea class="form-control form-control-sm"
|
|
||||||
v-model="element.content_est"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group"
|
|
||||||
v-if="_.includes(['heading'], element.type)">
|
|
||||||
<label>{{trans('messages.content_ru')}}</label>
|
|
||||||
<textarea class="form-control form-control-sm"
|
|
||||||
v-model="element.content_ru"></textarea>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-group"
|
<div class="form-group"
|
||||||
v-if="_.includes(['heading'], element.type)">
|
v-if="_.includes(['heading'], element.type)">
|
||||||
<label for="heading_text_color">
|
<label for="heading_text_color">
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">
|
<span :style="{'color': settings.color.label}">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<input :type="element.subtype" class="form-control"
|
<input :type="element.subtype" class="form-control"
|
||||||
:name="element.name"
|
:name="element.name"
|
||||||
:placeholder="form_trans_label(element, 'placeholder')"
|
:placeholder="element.placeholder"
|
||||||
:class="[element.size, element.custom_class, element.conditional_class]"
|
:class="[element.size, element.custom_class, element.conditional_class]"
|
||||||
:required="element.required && applyValidations"
|
:required="element.required && applyValidations"
|
||||||
v-bind="getDynamicallyGeneratedAttributeObj(element.validations, element.custom_attributes)"
|
v-bind="getDynamicallyGeneratedAttributeObj(element.validations, element.custom_attributes)"
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">
|
<span :style="{'color': settings.color.label}">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
@@ -98,7 +98,7 @@
|
|||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">
|
<span :style="{'color': settings.color.label}">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
@@ -155,7 +155,7 @@
|
|||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">
|
<span :style="{'color': settings.color.label}">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
@@ -175,7 +175,7 @@
|
|||||||
:name="element.name"
|
:name="element.name"
|
||||||
:id="element.name"
|
:id="element.name"
|
||||||
:cols="element.columns"
|
:cols="element.columns"
|
||||||
:placeholder="form_trans_label(element, 'placeholder')"
|
:placeholder="element.placeholder"
|
||||||
:class="[element.custom_class, element.conditional_class]"
|
:class="[element.custom_class, element.conditional_class]"
|
||||||
:required="element.required && applyValidations"
|
:required="element.required && applyValidations"
|
||||||
v-bind="getDynamicallyGeneratedAttributeObj(element.validations, element.custom_attributes)"
|
v-bind="getDynamicallyGeneratedAttributeObj(element.validations, element.custom_attributes)"
|
||||||
@@ -204,7 +204,7 @@
|
|||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">{{ form_trans_label(element, 'label') }} </span>
|
<span :style="{'color': settings.color.label}">{{ element.label }}</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
</label>
|
</label>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div :class="[spreadColumnForElement(element)]"
|
<div :class="[spreadColumnForElement(element)]"
|
||||||
v-for="(option, index) in form_trans_label(element, 'options').split('\n')">
|
v-for="(option, index) in element.options.split('\n')">
|
||||||
<div class="custom-control" :class="[element.type == 'radio' ? 'custom-radio' : 'custom-checkbox']">
|
<div class="custom-control" :class="[element.type == 'radio' ? 'custom-radio' : 'custom-checkbox']">
|
||||||
<input class="custom-control-input"
|
<input class="custom-control-input"
|
||||||
:type="element.type"
|
:type="element.type"
|
||||||
@@ -248,7 +248,7 @@
|
|||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">{{ form_trans_label(element, 'label') }}</span>
|
<span :style="{'color': settings.color.label}">{{ element.label }}</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
||||||
@@ -270,7 +270,7 @@
|
|||||||
@change="$emit('apply_conditions')"
|
@change="$emit('apply_conditions')"
|
||||||
:data-msg-required="element.required_error_msg"
|
:data-msg-required="element.required_error_msg"
|
||||||
>
|
>
|
||||||
<option v-for="option in form_trans_label(element, 'options').split('\n')"
|
<option v-for="option in element.options.split('\n')"
|
||||||
:selected="_.includes(_.get(submitted_data, element.name, ''), option)"
|
:selected="_.includes(_.get(submitted_data, element.name, ''), option)"
|
||||||
>
|
>
|
||||||
{{ option }}
|
{{ option }}
|
||||||
@@ -297,7 +297,7 @@
|
|||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<div
|
<div
|
||||||
v-html="'<' + element.tag + ' style=color:' + element.text_color + '>' + form_trans_label(element, 'content') + '</' + element.tag + '>'"
|
v-html="'<' + element.tag + ' style=color:' + element.text_color + '>' + element.content + '</' + element.tag + '>'"
|
||||||
:class="[element.custom_class]">
|
:class="[element.custom_class]">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -308,7 +308,7 @@
|
|||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">{{ form_trans_label(element, 'label') }}</span>
|
<span :style="{'color': settings.color.label}">{{ element.label }}</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
||||||
@@ -336,7 +336,7 @@
|
|||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">{{ form_trans_label(element, 'label') }}</span>
|
<span :style="{'color': settings.color.label}">{{ element.label }}</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
||||||
@@ -375,9 +375,9 @@
|
|||||||
>
|
>
|
||||||
<label class="custom-control-label" for="terms_and_condition">
|
<label class="custom-control-label" for="terms_and_condition">
|
||||||
<a :href="element.link" target="_blank" v-if="element.link">
|
<a :href="element.link" target="_blank" v-if="element.link">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</a>
|
</a>
|
||||||
<span v-else>{{ form_trans_label(element, 'label') }}</span>
|
<span v-else>{{ element.label }}</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
<i class="fas fa-sort handle pointer font_icon_size float-left mr-3" :class="[display_handler]"
|
||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<span :style="{'color': settings.color.label}">{{ form_trans_label(element, 'label') }}</span>
|
<span :style="{'color': settings.color.label}">{{ element.label }}</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
v-if="!_.isUndefined(element.popover_help_text) && element.popover_help_text.enable"
|
||||||
@@ -451,7 +451,7 @@
|
|||||||
>
|
>
|
||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<span :style="{'color': settings.color.label}" class="ml-2">
|
<span :style="{'color': settings.color.label}" class="ml-2">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
@@ -476,7 +476,7 @@
|
|||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<span :style="{'color': settings.color.label}" class="ml-2">
|
<span :style="{'color': settings.color.label}" class="ml-2">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
<i class="fas fa-info-circle cursor-pointer modal_trigger"
|
||||||
@@ -535,7 +535,7 @@
|
|||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<span :style="{'color': settings.color.label}" class="ml-2">
|
<span :style="{'color': settings.color.label}" class="ml-2">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -563,7 +563,7 @@
|
|||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<span :style="{'color': settings.color.label}" class="ml-2">
|
<span :style="{'color': settings.color.label}" class="ml-2">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -588,7 +588,7 @@
|
|||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<span :style="{'color': settings.color.label}" class="ml-2">
|
<span :style="{'color': settings.color.label}" class="ml-2">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -617,7 +617,7 @@
|
|||||||
:title="trans('messages.drag_element_using_icon')"></i>
|
:title="trans('messages.drag_element_using_icon')"></i>
|
||||||
<label :for="element.name">
|
<label :for="element.name">
|
||||||
<span :style="{'color': settings.color.label}" class="ml-2">
|
<span :style="{'color': settings.color.label}" class="ml-2">
|
||||||
{{ form_trans_label(element, 'label') }}
|
{{ element.label }}
|
||||||
</span>
|
</span>
|
||||||
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
<span :style="{'color': settings.color.required_asterisk_color}" v-if="element.required">*</span>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -508,21 +508,6 @@ export default {
|
|||||||
initialize_countdowntimer(element);
|
initialize_countdowntimer(element);
|
||||||
}, 2000); //initialize after 2 sec
|
}, 2000); //initialize after 2 sec
|
||||||
}
|
}
|
||||||
},
|
|
||||||
form_trans_label (data, key) {
|
|
||||||
const currLang = document.documentElement.lang
|
|
||||||
if (currLang === 'ru') {
|
|
||||||
if (data[`${key}_ru`] == '' || data[`${key}_ru`] === undefined || data[`${key}_ru`] === null) {
|
|
||||||
return data[key];
|
|
||||||
}
|
|
||||||
return data[`${key}_ru`]
|
|
||||||
} else if (currLang === 'est') {
|
|
||||||
if (data[`${key}_est`] == '' || data[`${key}_est`] === undefined || data[`${key}_est`] === null) {
|
|
||||||
return data[key];
|
|
||||||
}
|
|
||||||
return data[`${key}_est`]
|
|
||||||
}
|
|
||||||
return data[key];
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
$additional_js = $form->schema['additional_js_css']['js'];
|
$additional_js = $form->schema['additional_js_css']['js'];
|
||||||
$page_color = $form->schema['settings']['color']['page_color'] ?? '#f4f6f9';
|
$page_color = $form->schema['settings']['color']['page_color'] ?? '#f4f6f9';
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
<div class="@if(!empty($iframe_enabled) && $iframe_enabled) container-fluid @else container @endif">
|
<div class="@if(!empty($iframe_enabled) && $iframe_enabled) container-fluid @else container @endif">
|
||||||
<div class="row justify-content-center">
|
<div class="row justify-content-center">
|
||||||
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-12">
|
<div class="col-sm-12 col-md-12 col-lg-12 col-xl-12">
|
||||||
|
|||||||
@@ -82,9 +82,7 @@
|
|||||||
<table class="table" id="submitted_data_table" style="width: 100%;">
|
<table class="table" id="submitted_data_table" style="width: 100%;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
@if(auth()->user()->hasRole([\App\Enums\User\RoleEnum::SUPERVISOR->value, \App\Enums\User\RoleEnum::ADMIN->value], 'web'))
|
|
||||||
<th>@lang('messages.action')</th>
|
<th>@lang('messages.action')</th>
|
||||||
@endif
|
|
||||||
|
|
||||||
@if($is_enabled_sub_ref_no)
|
@if($is_enabled_sub_ref_no)
|
||||||
<th>@lang('messages.submission_numbering')</th>
|
<th>@lang('messages.submission_numbering')</th>
|
||||||
@@ -104,16 +102,19 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
@foreach($data as $k => $row)
|
@foreach($data as $k => $row)
|
||||||
<tr>
|
<tr>
|
||||||
@if(auth()->user()->hasRole([\App\Enums\User\RoleEnum::SUPERVISOR->value, \App\Enums\User\RoleEnum::ADMIN->value], 'web'))
|
<td>
|
||||||
<td>
|
{{-- Кнопка просмотра для всех, у кого есть права --}}
|
||||||
@if(in_array('view', $btn_enabled))
|
@if(in_array('view', $btn_enabled) && $has_permission)
|
||||||
<button type="button" class="btn btn-info btn-sm view_form_data m-1"
|
<button type="button" class="btn btn-info btn-sm view_form_data m-1"
|
||||||
data-href="{{action([\App\Http\Controllers\FormDataController::class, 'viewData'], [$row->id])}}"
|
data-href="{{action([\App\Http\Controllers\FormDataController::class, 'viewData'], [$row->id])}}"
|
||||||
data-toggle="modal">
|
data-toggle="modal">
|
||||||
<i class="fa fa-eye" aria-hidden="true"></i>
|
<i class="fa fa-eye" aria-hidden="true"></i>
|
||||||
@lang('messages.view')
|
@lang('messages.view')
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
|
{{-- Кнопки только для админов/супервайзеров --}}
|
||||||
|
@if(auth()->user()->hasRole([\App\Enums\User\RoleEnum::SUPERVISOR->value, \App\Enums\User\RoleEnum::ADMIN->value], 'web'))
|
||||||
@if(in_array('delete', $btn_enabled))
|
@if(in_array('delete', $btn_enabled))
|
||||||
<button type="button"
|
<button type="button"
|
||||||
class="btn btn-danger btn-sm delete_form_data m-1"
|
class="btn btn-danger btn-sm delete_form_data m-1"
|
||||||
@@ -122,16 +123,17 @@
|
|||||||
@lang('messages.delete')
|
@lang('messages.delete')
|
||||||
</button>
|
</button>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$form_id = !empty($form->slug) ? $form->slug : $form->id;
|
$form_id = !empty($form->slug) ? $form->slug : $form->id;
|
||||||
@endphp
|
@endphp
|
||||||
<a class="btn btn-dark btn-sm m-1"
|
<a class="btn btn-dark btn-sm m-1"
|
||||||
href="{{action([\App\Http\Controllers\FormDataController::class, 'getEditformData'], ['slug' => $form_id,'id' => $row->id])}}">
|
href="{{action([\App\Http\Controllers\FormDataController::class, 'getEditformData'], ['slug' => $form_id,'id' => $row->id])}}">
|
||||||
<i class="far fa-edit" aria-hidden="true"></i>
|
<i class="far fa-edit" aria-hidden="true"></i>
|
||||||
@lang('messages.edit')
|
@lang('messages.edit')
|
||||||
</a>
|
</a>
|
||||||
</td>
|
@endif
|
||||||
@endif
|
</td>
|
||||||
|
|
||||||
@if($is_enabled_sub_ref_no)
|
@if($is_enabled_sub_ref_no)
|
||||||
<td>
|
<td>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
@include('layouts/partials/status')
|
@include('layouts/partials/status')
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@if (auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) || auth()->user()->can_create_form)
|
@if(auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) || auth()->user()->can_create_form)
|
||||||
<div class="row mb-5">
|
<div class="row mb-5">
|
||||||
<div class="col-12 col-sm-6 col-md-3">
|
<div class="col-12 col-sm-6 col-md-3">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
@@ -16,19 +16,20 @@
|
|||||||
|
|
||||||
<div class="info-box-content">
|
<div class="info-box-content">
|
||||||
<span class="info-box-text">@lang('messages.forms')</span>
|
<span class="info-box-text">@lang('messages.forms')</span>
|
||||||
<span class="info-box-number">{{ $form_count }}</span>
|
<span class="info-box-number">{{$form_count}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@if (!auth()->user()->hasRole(\App\Enums\User\RoleEnum::ADMIN->value))
|
@if(!auth()->user()->hasRole(\App\Enums\User\RoleEnum::ADMIN->value))
|
||||||
<div class="col-12 col-sm-6 col-md-3">
|
<div class="col-12 col-sm-6 col-md-3">
|
||||||
<div class="info-box">
|
<div class="info-box">
|
||||||
<span class="info-box-icon bg-danger elevation-1"><i class="fas fa-align-justify"></i></span>
|
<span class="info-box-icon bg-danger elevation-1"><i
|
||||||
|
class="fas fa-align-justify"></i></span>
|
||||||
|
|
||||||
<div class="info-box-content">
|
<div class="info-box-content">
|
||||||
<span class="info-box-text">@lang('messages.templates')</span>
|
<span class="info-box-text">@lang('messages.templates')</span>
|
||||||
<span class="info-box-number">{{ $template_count }}</span>
|
<span class="info-box-number">{{$template_count}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -40,14 +41,15 @@
|
|||||||
|
|
||||||
<div class="info-box-content">
|
<div class="info-box-content">
|
||||||
<span class="info-box-text">@lang('messages.submissions')</span>
|
<span class="info-box-text">@lang('messages.submissions')</span>
|
||||||
<span class="info-box-number">{{ $submission_count }}</span>
|
<span class="info-box-number">{{$submission_count}}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-12 col-sm-6 col-md-3">
|
<div class="col-12 col-sm-6 col-md-3">
|
||||||
<button type="button" data-href="{{ action([\App\Http\Controllers\FormController::class, 'create']) }}"
|
<button type="button"
|
||||||
class="btn btn-primary float-right col-md-9 createForm mt-3">
|
data-href="{{action([\App\Http\Controllers\FormController::class, 'create'])}}"
|
||||||
|
class="btn btn-primary float-right col-md-9 createForm mt-3">
|
||||||
<i class="fas fa-plus" aria-hidden="true"></i> @lang('messages.new_form')</button>
|
<i class="fas fa-plus" aria-hidden="true"></i> @lang('messages.new_form')</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,76 +59,84 @@
|
|||||||
<div class="card card-primary card-outline card-outline-tabs">
|
<div class="card card-primary card-outline card-outline-tabs">
|
||||||
<div class="card-header p-0 border-bottom-0">
|
<div class="card-header p-0 border-bottom-0">
|
||||||
<ul class="nav nav-tabs
|
<ul class="nav nav-tabs
|
||||||
@if (auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) || auth()->user()->can_create_form) nav-justified @endif"
|
@if(auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) || auth()->user()->can_create_form)
|
||||||
|
nav-justified
|
||||||
|
@endif"
|
||||||
id="custom-tabs-four-tab" role="tablist">
|
id="custom-tabs-four-tab" role="tablist">
|
||||||
@if (auth()->user()->hasRole([\App\Enums\User\RoleEnum::SUPERVISOR->value]) || auth()->user()->can_create_form)
|
@if(auth()->user()->hasRole([\App\Enums\User\RoleEnum::SUPERVISOR->value]) || auth()->user()->can_create_form)
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link active" id="custome-tabs-all-forms" data-toggle="pill"
|
<a class="nav-link active" id="custome-tabs-all-forms" data-toggle="pill"
|
||||||
href="#custome-tabs-forms" role="tab" aria-controls="custome-tabs-forms"
|
href="#custome-tabs-forms" role="tab" aria-controls="custome-tabs-forms"
|
||||||
aria-selected="true">
|
aria-selected="true">
|
||||||
<i class="fas fa-file-alt" aria-hidden="true"></i> @lang('messages.all_forms')
|
<i class="fas fa-file-alt" aria-hidden="true"></i> @lang('messages.all_forms')
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@if (!auth()->user()->hasRole(\App\Enums\User\RoleEnum::ADMIN->value))
|
@if(!auth()->user()->hasRole(\App\Enums\User\RoleEnum::ADMIN->value))
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link" id="custome-tabs-all-templates" data-toggle="pill"
|
<a class="nav-link" id="custome-tabs-all-templates" data-toggle="pill"
|
||||||
href="#custome-tabs-templates" role="tab"
|
href="#custome-tabs-templates" role="tab"
|
||||||
aria-controls="custome-tabs-templates">
|
aria-controls="custome-tabs-templates">
|
||||||
<i class="fas fa-align-justify" aria-hidden="true"></i> @lang('messages.all_templates')
|
<i class="fas fa-align-justify"
|
||||||
|
aria-hidden="true"></i> @lang('messages.all_templates')
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@endif
|
@endif
|
||||||
@endif
|
@endif
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link
|
<a class="nav-link
|
||||||
@if (!auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) && !auth()->user()->can_create_form) active @endif
|
@if(!auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) && !auth()->user()->can_create_form)
|
||||||
"
|
active
|
||||||
id="custome-tabs-shared-forms" data-toggle="pill"
|
@endif
|
||||||
href="#custome-tabs-shared-forms-assigned" role="tab"
|
" id="custome-tabs-shared-forms" data-toggle="pill"
|
||||||
aria-controls="custome-tabs-shared-forms-assigned"
|
href="#custome-tabs-shared-forms-assigned" role="tab"
|
||||||
@if (!auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) && !auth()->user()->can_create_form) aria-selected="true" @endif>
|
aria-controls="custome-tabs-shared-forms-assigned"
|
||||||
<i class="fas fa-file-alt" aria-hidden="true"></i> @lang('messages.assigned_forms')
|
@if(!auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) && !auth()->user()->can_create_form)
|
||||||
|
aria-selected="true"
|
||||||
|
@endif>
|
||||||
|
<i class="fas fa-file-alt"
|
||||||
|
aria-hidden="true"></i> @lang('messages.assigned_forms')
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="tab-content" id="custom-tabs-four-tabContent">
|
<div class="tab-content" id="custom-tabs-four-tabContent">
|
||||||
@if (auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) || auth()->user()->can_create_form)
|
@if(auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) || auth()->user()->can_create_form)
|
||||||
<div class="tab-pane fade active show" id="custome-tabs-forms" role="tabpanel"
|
<div class="tab-pane fade active show" id="custome-tabs-forms" role="tabpanel"
|
||||||
aria-labelledby="custome-tabs-all-forms">
|
aria-labelledby="custome-tabs-all-forms">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table" id="form_table" style="width: 100%;">
|
<table class="table" id="form_table" style="width: 100%;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>@lang('messages.description')</th>
|
<th>@lang('messages.description')</th>
|
||||||
<th>@lang('messages.name')</th>
|
<th>@lang('messages.name')</th>
|
||||||
<th>@lang('messages.created_at')</th>
|
<th>@lang('messages.created_at')</th>
|
||||||
<th>@lang('messages.submissions')</th>
|
<th>@lang('messages.submissions')</th>
|
||||||
<th>@lang('messages.action')</th>
|
<th>@lang('messages.action')</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="tab-pane fade" id="custome-tabs-templates" role="tabpanel"
|
<div class="tab-pane fade" id="custome-tabs-templates" role="tabpanel"
|
||||||
aria-labelledby="custome-tabs-all-templates">
|
aria-labelledby="custome-tabs-all-templates">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table" id="template_table" style="width: 100%;">
|
<table class="table" id="template_table" style="width: 100%;">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>@lang('messages.description')</th>
|
<th>@lang('messages.description')</th>
|
||||||
<th>@lang('messages.name')</th>
|
<th>@lang('messages.name')</th>
|
||||||
@if (auth()->user()->can('superadmin'))
|
@if(auth()->user()->can('superadmin'))
|
||||||
<th>
|
<th>
|
||||||
@lang('messages.is_global_template')
|
@lang('messages.is_global_template')
|
||||||
<i class="fas fa-info-circle" data-toggle="tooltip"
|
<i class="fas fa-info-circle"
|
||||||
title="@lang('messages.is_global_template_tooltip')"></i>
|
data-toggle="tooltip"
|
||||||
</th>
|
title="@lang('messages.is_global_template_tooltip')"></i>
|
||||||
@endif
|
</th>
|
||||||
<th>@lang('messages.action')</th>
|
@endif
|
||||||
</tr>
|
<th>@lang('messages.action')</th>
|
||||||
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -134,22 +144,22 @@
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<div class="tab-pane fade
|
<div class="tab-pane fade
|
||||||
@if (!auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) || !auth()->user()->can_create_form) active show @endif
|
@if(!auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) || !auth()->user()->can_create_form)
|
||||||
"
|
active show
|
||||||
id="custome-tabs-shared-forms-assigned" role="tabpanel"
|
@endif
|
||||||
aria-labelledby="custome-tabs-shared-forms">
|
" id="custome-tabs-shared-forms-assigned" role="tabpanel"
|
||||||
|
aria-labelledby="custome-tabs-shared-forms">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table" id="assigned_form_table" style="width: 100%;">
|
<table class="table" id="assigned_form_table" style="width: 100%;">
|
||||||
<thead>
|
<thead>
|
||||||
|
<tr>
|
||||||
<tr>
|
<th>@lang('messages.description')</th>
|
||||||
<th>@lang('messages.description')</th>
|
<th>@lang('messages.name')</th>
|
||||||
<th>@lang('messages.name')</th>
|
@if (auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value))
|
||||||
@if (auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value))
|
<th>@lang('messages.created_by')</th>
|
||||||
<th>@lang('messages.created_by')</th>
|
@endif
|
||||||
@endif
|
<th>@lang('messages.action')</th>
|
||||||
<th>@lang('messages.action')</th>
|
</tr>
|
||||||
</tr>
|
|
||||||
</thead>
|
</thead>
|
||||||
<tbody></tbody>
|
<tbody></tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -167,21 +177,7 @@
|
|||||||
|
|
||||||
@section('footer')
|
@section('footer')
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
$(document).ready(function() {
|
$(document).ready(function () {
|
||||||
var lang = `{{ session()->get('locale') }}`
|
|
||||||
var titleColName = 'name';
|
|
||||||
if (lang === 'est') {
|
|
||||||
titleColName = 'name_est'
|
|
||||||
} else if (lang === 'ru') {
|
|
||||||
titleColName = 'name_ru'
|
|
||||||
}
|
|
||||||
|
|
||||||
var descColName = 'description';
|
|
||||||
if (lang === 'est') {
|
|
||||||
descColName = 'description_est'
|
|
||||||
} else if (lang === 'ru') {
|
|
||||||
descColName = 'description_ru'
|
|
||||||
}
|
|
||||||
|
|
||||||
// form dataTable
|
// form dataTable
|
||||||
var form_table = $('#form_table').DataTable({
|
var form_table = $('#form_table').DataTable({
|
||||||
@@ -191,62 +187,20 @@
|
|||||||
buttons: [],
|
buttons: [],
|
||||||
dom: 'lfrtip',
|
dom: 'lfrtip',
|
||||||
fixedHeader: false,
|
fixedHeader: false,
|
||||||
aaSorting: [
|
aaSorting: [[2, 'desc']],
|
||||||
[1, 'desc']
|
"columnDefs": [
|
||||||
|
{"width": "22%", "targets": 0},
|
||||||
|
{"width": "40%", "targets": 1},
|
||||||
|
{"width": "15%", "targets": 2},
|
||||||
|
{"width": "3%", "targets": 3},
|
||||||
|
{"width": "20%", "targets": 4}
|
||||||
],
|
],
|
||||||
"columnDefs": [{
|
columns: [
|
||||||
"width": "22%",
|
{data: 'name', name: 'name'},
|
||||||
"targets": 0
|
{data: 'description', name: 'description'},
|
||||||
},
|
{data: 'created_at', name: 'created_at'},
|
||||||
{
|
{data: 'data_count', name: 'data_count', searchable: false},
|
||||||
"width": "40%",
|
{data: 'action', name: 'action', sortable: false}
|
||||||
"targets": 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"width": "15%",
|
|
||||||
"targets": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"width": "3%",
|
|
||||||
"targets": 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"width": "20%",
|
|
||||||
"targets": 4
|
|
||||||
}
|
|
||||||
],
|
|
||||||
columns: [{
|
|
||||||
data: descColName,
|
|
||||||
name: descColName,
|
|
||||||
createdCell: function(td, cellData, rowData, row, col) {
|
|
||||||
if (td.innerHTML.length === 0) {
|
|
||||||
td.innerHTML = rowData.name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: titleColName,
|
|
||||||
name: titleColName,
|
|
||||||
createdCell: function(td, cellData, rowData, row, col) {
|
|
||||||
if (td.innerHTML.length === 0) {
|
|
||||||
td.innerHTML = rowData.name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: 'created_at',
|
|
||||||
name: 'created_at'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: 'data_count',
|
|
||||||
name: 'data_count',
|
|
||||||
searchable: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
data: 'action',
|
|
||||||
name: 'action',
|
|
||||||
sortable: false
|
|
||||||
}
|
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,41 +212,22 @@
|
|||||||
buttons: [],
|
buttons: [],
|
||||||
dom: 'lfrtip',
|
dom: 'lfrtip',
|
||||||
fixedHeader: false,
|
fixedHeader: false,
|
||||||
columns: [{
|
columns: [
|
||||||
data: descColName,
|
{data: 'name', name: 'name'},
|
||||||
name: descColName,
|
{data: 'description', name: 'description'},
|
||||||
createdCell: function(td, cellData, rowData, row, col) {
|
@if(auth()->user()->can('superadmin'))
|
||||||
if (td.innerHTML.length === 0) {
|
|
||||||
td.innerHTML = rowData.name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
data: titleColName,
|
data: 'is_global_template', name: 'is_global_template', sortable: false, searchable: false
|
||||||
name: titleColName,
|
|
||||||
createdCell: function(td, cellData, rowData, row, col) {
|
|
||||||
if (td.innerHTML.length === 0) {
|
|
||||||
td.innerHTML = rowData.name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
@if (auth()->user()->can('superadmin'))
|
@endif
|
||||||
{
|
{
|
||||||
data: 'is_global_template',
|
data: 'action', name: 'action', sortable: false
|
||||||
name: 'is_global_template',
|
|
||||||
sortable: false,
|
|
||||||
searchable: false
|
|
||||||
},
|
|
||||||
@endif {
|
|
||||||
data: 'action',
|
|
||||||
name: 'action',
|
|
||||||
sortable: false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
//delete form
|
//delete form
|
||||||
$(document).on('click', '.delete_form', function() {
|
$(document).on('click', '.delete_form', function () {
|
||||||
var url = $(this).data("href");
|
var url = $(this).data("href");
|
||||||
var result = confirm('Are You Sure?');
|
var result = confirm('Are You Sure?');
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
@@ -300,7 +235,7 @@
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: url,
|
url: url,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(result) {
|
success: function (result) {
|
||||||
if (result.success == true) {
|
if (result.success == true) {
|
||||||
toastr.success(result.msg);
|
toastr.success(result.msg);
|
||||||
form_table.ajax.reload();
|
form_table.ajax.reload();
|
||||||
@@ -313,7 +248,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
//delete template
|
//delete template
|
||||||
$(document).on('click', '.delete_template', function() {
|
$(document).on('click', '.delete_template', function () {
|
||||||
var url = $(this).data("href");
|
var url = $(this).data("href");
|
||||||
var result = confirm('Are You Sure?');
|
var result = confirm('Are You Sure?');
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
@@ -321,7 +256,7 @@
|
|||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
url: url,
|
url: url,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(result) {
|
success: function (result) {
|
||||||
if (result.success == true) {
|
if (result.success == true) {
|
||||||
toastr.success(result.msg);
|
toastr.success(result.msg);
|
||||||
template_table.ajax.reload();
|
template_table.ajax.reload();
|
||||||
@@ -334,39 +269,39 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
// create form
|
// create form
|
||||||
$(document).on('click', '.createForm', function() {
|
$(document).on('click', '.createForm', function () {
|
||||||
var url = $(this).data('href');
|
var url = $(this).data('href');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: url,
|
url: url,
|
||||||
dataType: "html",
|
dataType: "html",
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
$("#modal_div").html(response).modal("show");
|
$("#modal_div").html(response).modal("show");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// create widget
|
// create widget
|
||||||
$(document).on('click', '.generate_widget', function() {
|
$(document).on('click', '.generate_widget', function () {
|
||||||
var url = $(this).data('href');
|
var url = $(this).data('href');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: url,
|
url: url,
|
||||||
dataType: "html",
|
dataType: "html",
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
$("#modal_div").html(response).modal("show");
|
$("#modal_div").html(response).modal("show");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
//copy form
|
//copy form
|
||||||
$(document).on('click', '.copy_form', function() {
|
$(document).on('click', '.copy_form', function () {
|
||||||
var url = $(this).data('href');
|
var url = $(this).data('href');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: url,
|
url: url,
|
||||||
dataType: "html",
|
dataType: "html",
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
$("#modal_div").html(response).modal("show");
|
$("#modal_div").html(response).modal("show");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -380,81 +315,49 @@
|
|||||||
buttons: [],
|
buttons: [],
|
||||||
dom: 'lfrtip',
|
dom: 'lfrtip',
|
||||||
fixedHeader: false,
|
fixedHeader: false,
|
||||||
aaSorting: [
|
aaSorting: [[0, 'desc']],
|
||||||
[0, 'desc']
|
"columnDefs": [
|
||||||
],
|
{"width": "25%", "targets": 0},
|
||||||
"columnDefs": [{
|
{"width": "40%", "targets": 1},
|
||||||
"width": "25%",
|
@if(auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value))
|
||||||
"targets": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"width": "40%",
|
"width": "15%", "targets": 2
|
||||||
"targets": 1
|
|
||||||
},
|
},
|
||||||
@if (auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value))
|
@endif
|
||||||
{
|
{
|
||||||
"width": "15%",
|
|
||||||
"targets": 2
|
|
||||||
},
|
|
||||||
@endif {
|
|
||||||
"width": "20%",
|
"width": "20%",
|
||||||
"targets": @php
|
"targets": @php echo auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value) ? 3 : 2 @endphp }
|
||||||
echo auth()
|
|
||||||
->user()
|
|
||||||
->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value)
|
|
||||||
? 3
|
|
||||||
: 2;
|
|
||||||
@endphp
|
|
||||||
}
|
|
||||||
],
|
],
|
||||||
columns: [{
|
columns: [
|
||||||
data: descColName,
|
{data: 'name', name: 'forms.name'},
|
||||||
name: 'forms.' + descColName,
|
{data: 'description', name: 'forms.description'},
|
||||||
createdCell: function(td, cellData, rowData, row, col) {
|
@if(auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value))
|
||||||
if (td.innerHTML.length === 0) {
|
|
||||||
td.innerHTML = rowData.description
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
data: titleColName,
|
data: 'created_by', name: 'forms.created_by'
|
||||||
name: 'forms.' + titleColName,
|
|
||||||
createdCell: function(td, cellData, rowData, row, col) {
|
|
||||||
if (td.innerHTML.length === 0) {
|
|
||||||
td.innerHTML = rowData.name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
@if (auth()->user()->hasRole(\App\Enums\User\RoleEnum::SUPERVISOR->value))
|
@endif
|
||||||
{
|
{
|
||||||
data: 'created_by',
|
data: 'action', name: 'action', sortable: false
|
||||||
name: 'forms.created_by'
|
|
||||||
},
|
|
||||||
@endif {
|
|
||||||
data: 'action',
|
|
||||||
name: 'action',
|
|
||||||
sortable: false
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
//form collaborate
|
//form collaborate
|
||||||
$(document).on('click', '.collab_btn', function() {
|
$(document).on('click', '.collab_btn', function () {
|
||||||
var url = $(this).data('href');
|
var url = $(this).data('href');
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: url,
|
url: url,
|
||||||
dataType: "html",
|
dataType: "html",
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
$("#collab_modal").html(response).modal("show");
|
$("#collab_modal").html(response).modal("show");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$("#collab_modal").on('shown.bs.modal', function() {
|
$("#collab_modal").on('shown.bs.modal', function () {
|
||||||
if ($("#form_design").length) {
|
if ($("#form_design").length) {
|
||||||
$(document).on('change', '#form_design', function() {
|
$(document).on('change', '#form_design', function () {
|
||||||
if ($("#form_design").is(":checked")) {
|
if ($("#form_design").is(":checked")) {
|
||||||
$("#form_view").attr('checked', true);
|
$("#form_view").attr('checked', true);
|
||||||
} else {
|
} else {
|
||||||
@@ -464,7 +367,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on('submit', 'form#collaborate_form', function(e) {
|
$(document).on('submit', 'form#collaborate_form', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
var data = $("form#collaborate_form").serialize();
|
var data = $("form#collaborate_form").serialize();
|
||||||
var url = $("form#collaborate_form").attr('action');
|
var url = $("form#collaborate_form").attr('action');
|
||||||
@@ -475,7 +378,7 @@
|
|||||||
url: url,
|
url: url,
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
data: data,
|
data: data,
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
ladda.stop();
|
ladda.stop();
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
$("#collab_modal").modal('hide');
|
$("#collab_modal").modal('hide');
|
||||||
@@ -487,7 +390,7 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
$('a[data-toggle="pill"]').on('shown.bs.tab', function(e) {
|
$('a[data-toggle="pill"]').on('shown.bs.tab', function (e) {
|
||||||
var target = $(e.target).attr('href');
|
var target = $(e.target).attr('href');
|
||||||
if (target == '#custome-tabs-forms') {
|
if (target == '#custome-tabs-forms') {
|
||||||
if (typeof form_table != 'undefined') {
|
if (typeof form_table != 'undefined') {
|
||||||
@@ -504,25 +407,25 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@if (auth()->user()->can('superadmin'))
|
@if(auth()->user()->can('superadmin'))
|
||||||
$(document).on('click', '.toggle_global_template', function() {
|
$(document).on('click', '.toggle_global_template', function () {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "{{ route('toggle.global.template') }}",
|
url: "{{route('toggle.global.template')}}",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
data: {
|
data: {
|
||||||
is_checked: $(this).is(":checked") ? 1 : 0,
|
is_checked: $(this).is(":checked") ? 1 : 0,
|
||||||
form_id: $(this).data("form_id"),
|
form_id: $(this).data("form_id"),
|
||||||
},
|
},
|
||||||
success: function(response) {
|
success: function (response) {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
template_table.ajax.reload();
|
template_table.ajax.reload();
|
||||||
} else {
|
} else {
|
||||||
toastr.error(response.msg);
|
toastr.error(response.msg);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
|
});
|
||||||
@endif
|
@endif
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="{{ str_replace('_', '-', session()->get('locale', 'en')) }}">
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
|||||||
@@ -19,33 +19,6 @@
|
|||||||
<!-- Right Side Of Navbar -->
|
<!-- Right Side Of Navbar -->
|
||||||
<ul class="navbar-nav ml-auto">
|
<ul class="navbar-nav ml-auto">
|
||||||
<!-- Authentication Links -->
|
<!-- Authentication Links -->
|
||||||
<li class="nav-item dropdown">
|
|
||||||
<a id="superadminDropdown" href="#" data-toggle="dropdown" aria-haspopup="true"
|
|
||||||
aria-expanded="false" class="nav-link dropdown-toggle">
|
|
||||||
{{str(session()->get('locale'))->upper()}}
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<ul aria-labelledby="superadminDropdown" class="dropdown-menu border-0 shadow">
|
|
||||||
<li>
|
|
||||||
<a href="{{route('locale', 'en')}}"
|
|
||||||
class="dropdown-item @if (session()->get('locale') == 'en') active @endif">
|
|
||||||
English
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{route('locale', 'ru')}}"
|
|
||||||
class="dropdown-item @if (session()->get('locale') == 'ru') active @endif">
|
|
||||||
Русский
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a href="{{route('locale', 'est')}}"
|
|
||||||
class="dropdown-item @if (session()->get('locale') == 'est') active @endif">
|
|
||||||
Eesti keel
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</li>
|
|
||||||
@guest
|
@guest
|
||||||
<li class="nav-item d-none d-sm-inline-block">
|
<li class="nav-item d-none d-sm-inline-block">
|
||||||
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
|
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ use App\Http\Controllers\FormController;
|
|||||||
use App\Http\Controllers\FormDataCommentController;
|
use App\Http\Controllers\FormDataCommentController;
|
||||||
use App\Http\Controllers\FormDataController;
|
use App\Http\Controllers\FormDataController;
|
||||||
use App\Http\Controllers\HomeController;
|
use App\Http\Controllers\HomeController;
|
||||||
use App\Http\Controllers\LocaleController;
|
|
||||||
use App\Http\Controllers\ManageProfileController;
|
use App\Http\Controllers\ManageProfileController;
|
||||||
use App\Http\Controllers\ManageSettingsController;
|
use App\Http\Controllers\ManageSettingsController;
|
||||||
use App\Http\Controllers\RegistrationController;
|
use App\Http\Controllers\RegistrationController;
|
||||||
@@ -34,15 +33,13 @@ Route::get('/', function () {
|
|||||||
return view('welcome');
|
return view('welcome');
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::get('locale/{locale}', LocaleController::class)->name('locale');
|
|
||||||
|
|
||||||
Route::middleware(['IsInstalled'])->group(function () {
|
Route::middleware(['IsInstalled'])->group(function () {
|
||||||
Auth::routes(['register' => env('ENABLE_REGISTRATION', false)]);
|
Auth::routes(['register' => env('ENABLE_REGISTRATION', false)]);
|
||||||
|
|
||||||
Route::post('registration', [RegistrationController::class, 'store']);
|
Route::post('registration', [RegistrationController::class, 'store']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::middleware(['IsInstalled', 'auth', 'bootstrap', 'setDefaultConfig', 'locale',])->group(function () {
|
Route::middleware(['IsInstalled', 'auth', 'bootstrap', 'setDefaultConfig'])->group(function () {
|
||||||
Route::get('/home', [HomeController::class, 'index'])->name('home');
|
Route::get('/home', [HomeController::class, 'index'])->name('home');
|
||||||
Route::get('/home-template', [HomeController::class, 'getTemplate']);
|
Route::get('/home-template', [HomeController::class, 'getTemplate']);
|
||||||
Route::get('/home-assigned-forms', [HomeController::class, 'getAssignedForms']);
|
Route::get('/home-assigned-forms', [HomeController::class, 'getAssignedForms']);
|
||||||
|
|||||||
761
src/index.tsx
Normal file
761
src/index.tsx
Normal file
@@ -0,0 +1,761 @@
|
|||||||
|
import { Hono } from 'hono'
|
||||||
|
import { cors } from 'hono/cors'
|
||||||
|
import { serveStatic } from 'hono/cloudflare-workers'
|
||||||
|
import { authMiddleware, optionalAuthMiddleware } from './middleware/auth'
|
||||||
|
import { generateToken, verifyPassword, hashPassword } from './utils/auth'
|
||||||
|
import { ORIGINAL_HTML } from './original-html'
|
||||||
|
|
||||||
|
type Bindings = {
|
||||||
|
DB: D1Database;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Variables = {
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const app = new Hono<{ Bindings: Bindings; Variables: Variables }>()
|
||||||
|
|
||||||
|
// Enable CORS
|
||||||
|
app.use('/api/*', cors())
|
||||||
|
|
||||||
|
// Serve static files
|
||||||
|
app.use('/static/*', serveStatic({ root: './public' }))
|
||||||
|
// Serve favicon (empty response to avoid 404)
|
||||||
|
app.get('/favicon.ico', (c) => {
|
||||||
|
return new Response(null, { status: 204 })
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== AUTH ROUTES ====================
|
||||||
|
|
||||||
|
// Login endpoint
|
||||||
|
app.post('/api/auth/login', async (c) => {
|
||||||
|
try {
|
||||||
|
const { username, password } = await c.req.json()
|
||||||
|
|
||||||
|
const user = await c.env.DB.prepare(
|
||||||
|
'SELECT id, username, password_hash, full_name, role FROM users WHERE username = ? AND deleted_at IS NULL'
|
||||||
|
).bind(username).first()
|
||||||
|
|
||||||
|
if (!user || !await verifyPassword(password, user.password_hash as string)) {
|
||||||
|
return c.json({ error: 'Invalid credentials' }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = generateToken(user.id as number, user.username as string)
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
username: user.username,
|
||||||
|
fullName: user.full_name,
|
||||||
|
role: user.role
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login error:', error)
|
||||||
|
return c.json({ error: 'Login failed' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update user profile (password change)
|
||||||
|
app.patch('/api/users/profile', authMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const body = await c.req.json()
|
||||||
|
const fullName = body.full_name || body.fullName
|
||||||
|
const currentPassword = body.current_password || body.currentPassword
|
||||||
|
const newPassword = body.new_password || body.newPassword
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
console.log('[PROFILE UPDATE]', { userId, fullName, hasCurrentPwd: !!currentPassword, hasNewPwd: !!newPassword })
|
||||||
|
|
||||||
|
// Get user from database
|
||||||
|
const user = await c.env.DB.prepare(
|
||||||
|
'SELECT password_hash, full_name FROM users WHERE id = ?'
|
||||||
|
).bind(userId).first()
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return c.json({ error: 'Kasutajat ei leitud' }, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If changing password
|
||||||
|
if (newPassword) {
|
||||||
|
// Verify current password is provided
|
||||||
|
if (!currentPassword) {
|
||||||
|
return c.json({ error: 'Praegune parool on kohustuslik parooli muutmiseks' }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify current password
|
||||||
|
if (!await verifyPassword(currentPassword, user.password_hash as string)) {
|
||||||
|
return c.json({ error: 'Vale praegune parool' }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update password and full name
|
||||||
|
const newHash = await hashPassword(newPassword)
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
'UPDATE users SET password_hash = ?, full_name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
|
||||||
|
).bind(newHash, fullName, userId).run()
|
||||||
|
} else {
|
||||||
|
// Only update full name (no password change)
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
'UPDATE users SET full_name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
|
||||||
|
).bind(fullName, userId).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({
|
||||||
|
success: true,
|
||||||
|
message: 'Profiil uuendatud',
|
||||||
|
user: {
|
||||||
|
full_name: fullName
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Profile update error:', error)
|
||||||
|
return c.json({ error: 'Profiili uuendamine ebaõnnestus' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== DATA ROUTES ====================
|
||||||
|
|
||||||
|
// Get years for dropdown (with optional auth)
|
||||||
|
app.get('/api/years', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const result = await c.env.DB.prepare(
|
||||||
|
'SELECT MIN(year) as min_year FROM production_records WHERE deleted_at IS NULL'
|
||||||
|
).first()
|
||||||
|
|
||||||
|
const minYear = result?.min_year || new Date().getFullYear()
|
||||||
|
const maxYear = new Date().getFullYear() + 1
|
||||||
|
|
||||||
|
// Create array of years from minYear to maxYear
|
||||||
|
const years = []
|
||||||
|
for (let year = minYear; year <= maxYear; year++) {
|
||||||
|
years.push(year)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ years })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching years:', error)
|
||||||
|
return c.json({ error: 'Failed to fetch years' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get records (with optional auth for token refresh)
|
||||||
|
app.get('/api/records', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const month = c.req.query('month')
|
||||||
|
const year = c.req.query('year')
|
||||||
|
|
||||||
|
if (!month || !year) {
|
||||||
|
return c.json({ error: 'Month and year required' }, 400)
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = await c.env.DB.prepare(`
|
||||||
|
SELECT
|
||||||
|
pr.*,
|
||||||
|
sc.material_date,
|
||||||
|
sc.material2_date,
|
||||||
|
sc.package_date,
|
||||||
|
sc.worksheets_date,
|
||||||
|
sc.cutting_date,
|
||||||
|
sc.glazing_date,
|
||||||
|
sc.ready_date,
|
||||||
|
sc.issued_date,
|
||||||
|
sc.worksheets_error,
|
||||||
|
sc.cutting_error,
|
||||||
|
sc.glazing_error,
|
||||||
|
sc.ready_error,
|
||||||
|
sc.issued_error,
|
||||||
|
sc.material_confirmed,
|
||||||
|
sc.material2_confirmed,
|
||||||
|
sc.worksheets_confirmed
|
||||||
|
FROM production_records pr
|
||||||
|
LEFT JOIN status_checkboxes sc ON pr.id = sc.record_id
|
||||||
|
WHERE pr.month = ? AND pr.year = ? AND pr.deleted_at IS NULL
|
||||||
|
ORDER BY pr.created_at DESC
|
||||||
|
`).bind(month, year).all()
|
||||||
|
|
||||||
|
return c.json(records.results || [])
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching records:', error)
|
||||||
|
return c.json({ error: 'Failed to fetch records' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create new record
|
||||||
|
app.post('/api/records', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const data = await c.req.json()
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
// Validate and convert numeric fields
|
||||||
|
const quantity = data.quantity ? parseInt(data.quantity, 10) : 0
|
||||||
|
const price = data.price ? parseFloat(data.price) : 0
|
||||||
|
|
||||||
|
const arveChecked = data.arve_checked ? parseInt(data.arve_checked, 10) : 0
|
||||||
|
|
||||||
|
const result = await c.env.DB.prepare(`
|
||||||
|
INSERT INTO production_records (
|
||||||
|
month, year, client_name, type, offer_number, work_number,
|
||||||
|
quantity, color, notes, problems, installer, price,
|
||||||
|
arve_checked, arve_makstud,
|
||||||
|
created_by, updated_by
|
||||||
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||||
|
`).bind(
|
||||||
|
data.month, data.year, data.client_name, data.type || null,
|
||||||
|
data.offer_number, data.work_number, quantity, data.color || null,
|
||||||
|
data.notes || null, data.problems || null, data.installer || null,
|
||||||
|
price, arveChecked, data.arve_makstud || null,
|
||||||
|
userId, userId
|
||||||
|
).run()
|
||||||
|
|
||||||
|
// Create status checkboxes entry with dates if provided
|
||||||
|
const materialDate = (data.material_date && data.material_date !== 'null') ? data.material_date : null
|
||||||
|
const material2Date = (data.material2_date && data.material2_date !== 'null') ? data.material2_date : null
|
||||||
|
const packageDate = (data.package_date && data.package_date !== 'null') ? data.package_date : null
|
||||||
|
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
INSERT INTO status_checkboxes (
|
||||||
|
record_id, material_date, material2_date, package_date
|
||||||
|
) VALUES (?, ?, ?, ?)
|
||||||
|
`).bind(result.meta.last_row_id, materialDate, material2Date, packageDate).run()
|
||||||
|
|
||||||
|
return c.json({ success: true, id: result.meta.last_row_id })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error creating record:', error)
|
||||||
|
return c.json({ error: 'Failed to create record' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update record
|
||||||
|
app.put('/api/records/:id', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const id = c.req.param('id')
|
||||||
|
const data = await c.req.json()
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
// Validate and convert numeric fields
|
||||||
|
const quantity = data.quantity ? parseInt(data.quantity, 10) : 0
|
||||||
|
const price = data.price ? parseFloat(data.price) : 0
|
||||||
|
|
||||||
|
const arveChecked = data.arve_checked ? parseInt(data.arve_checked, 10) : 0
|
||||||
|
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
UPDATE production_records
|
||||||
|
SET client_name = ?, type = ?, offer_number = ?, work_number = ?,
|
||||||
|
quantity = ?, color = ?, notes = ?, problems = ?, installer = ?, price = ?,
|
||||||
|
arve_checked = ?, arve_makstud = ?,
|
||||||
|
updated_by = ?, updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE id = ? AND deleted_at IS NULL
|
||||||
|
`).bind(
|
||||||
|
data.client_name, data.type || null, data.offer_number, data.work_number,
|
||||||
|
quantity, data.color || null, data.notes || null, data.problems || null,
|
||||||
|
data.installer || null, price, arveChecked, data.arve_makstud || null,
|
||||||
|
userId, id
|
||||||
|
).run()
|
||||||
|
|
||||||
|
// Update status_checkboxes dates if provided
|
||||||
|
if (data.material_date !== undefined || data.material2_date !== undefined || data.package_date !== undefined) {
|
||||||
|
// Convert empty strings and "null" strings to actual NULL
|
||||||
|
const materialDate = (data.material_date && data.material_date !== 'null') ? data.material_date : null
|
||||||
|
const material2Date = (data.material2_date && data.material2_date !== 'null') ? data.material2_date : null
|
||||||
|
const packageDate = (data.package_date && data.package_date !== 'null') ? data.package_date : null
|
||||||
|
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
UPDATE status_checkboxes
|
||||||
|
SET material_date = ?,
|
||||||
|
material2_date = ?,
|
||||||
|
package_date = ?,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE record_id = ?
|
||||||
|
`).bind(materialDate, material2Date, packageDate, id).run()
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating record:', error)
|
||||||
|
return c.json({ error: 'Failed to update record' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Get single record
|
||||||
|
app.get('/api/records/:id', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const id = c.req.param('id')
|
||||||
|
|
||||||
|
const record = await c.env.DB.prepare(`
|
||||||
|
SELECT * FROM production_records WHERE id = ? AND deleted_at IS NULL
|
||||||
|
`).bind(id).first()
|
||||||
|
|
||||||
|
if (!record) {
|
||||||
|
return c.json({ error: 'Record not found' }, 404)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.json(record)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching record:', error)
|
||||||
|
return c.json({ error: 'Failed to fetch record' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Delete record (soft delete)
|
||||||
|
app.delete('/api/records/:id', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const id = c.req.param('id')
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
console.log('[DELETE] Deleting record:', id, 'by user:', userId)
|
||||||
|
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
UPDATE production_records
|
||||||
|
SET deleted_at = CURRENT_TIMESTAMP, deleted = 1
|
||||||
|
WHERE id = ?
|
||||||
|
`).bind(id).run()
|
||||||
|
|
||||||
|
console.log('[DELETE] Record deleted successfully:', id)
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error deleting record:', error)
|
||||||
|
return c.json({ error: 'Failed to delete record' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== STATUS CHECKBOX ROUTES ====================
|
||||||
|
|
||||||
|
// Toggle date - simplified endpoint for frontend compatibility
|
||||||
|
app.patch('/api/records/:id/status', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
const { field, date } = await c.req.json()
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
console.log(`[TOGGLE] recordId=${recordId}, field=${field}, date=${JSON.stringify(date)}`)
|
||||||
|
|
||||||
|
// Field name with _date suffix for database column
|
||||||
|
const dbField = `${field}_date`
|
||||||
|
|
||||||
|
// Get old date for audit
|
||||||
|
const oldRecord = await c.env.DB.prepare(
|
||||||
|
`SELECT ${dbField} FROM status_checkboxes WHERE record_id = ?`
|
||||||
|
).bind(recordId).first()
|
||||||
|
|
||||||
|
// Check if ready or issued fields are blocked by error flags
|
||||||
|
if (field === 'ready' || field === 'issued') {
|
||||||
|
const statusCheckbox = await c.env.DB.prepare(
|
||||||
|
'SELECT worksheets_error, cutting_error, glazing_error, ready_error, issued_error FROM status_checkboxes WHERE record_id = ?'
|
||||||
|
).bind(recordId).first()
|
||||||
|
|
||||||
|
const hasErrorFlags = statusCheckbox && (
|
||||||
|
statusCheckbox.worksheets_error ||
|
||||||
|
statusCheckbox.cutting_error ||
|
||||||
|
statusCheckbox.glazing_error ||
|
||||||
|
statusCheckbox.ready_error ||
|
||||||
|
statusCheckbox.issued_error
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasErrorFlags) {
|
||||||
|
return c.json({
|
||||||
|
error: 'blocked',
|
||||||
|
message: 'Vigade märked on seatud (punased kolmnurgad)'
|
||||||
|
}, 403)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle logic:
|
||||||
|
// 1. If date is null/empty → check if cell is empty → add today's date OR clear
|
||||||
|
// 2. If date matches current date → toggle off (clear)
|
||||||
|
// 3. Otherwise → use provided date
|
||||||
|
let newDate: string | null
|
||||||
|
if (!date || date === 'null') {
|
||||||
|
// null/empty clicked
|
||||||
|
if (oldRecord?.[dbField]) {
|
||||||
|
// Cell has date → clear it
|
||||||
|
newDate = null
|
||||||
|
} else {
|
||||||
|
// Cell is empty → add today's date
|
||||||
|
newDate = new Date().toISOString().split('T')[0]
|
||||||
|
}
|
||||||
|
} else if (date === oldRecord?.[dbField]) {
|
||||||
|
// Same date as current → toggle off (clear)
|
||||||
|
newDate = null
|
||||||
|
} else {
|
||||||
|
// Different date provided → use it
|
||||||
|
newDate = date
|
||||||
|
}
|
||||||
|
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE status_checkboxes SET ${dbField} = ? WHERE record_id = ?`
|
||||||
|
).bind(newDate, recordId).run()
|
||||||
|
|
||||||
|
// Log to audit
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'toggle_status')
|
||||||
|
`).bind(userId || null, recordId, field, oldRecord?.[dbField] || null, newDate).run()
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error toggling status:', error)
|
||||||
|
return c.json({ error: 'Failed to toggle status' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update status checkbox date
|
||||||
|
app.patch('/api/status/:recordId/:field', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
const field = c.req.param('field')
|
||||||
|
const { date } = await c.req.json()
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
// Get old date for audit
|
||||||
|
const oldRecord = await c.env.DB.prepare(
|
||||||
|
`SELECT ${field}_date FROM status_checkboxes WHERE record_id = ?`
|
||||||
|
).bind(recordId).first()
|
||||||
|
|
||||||
|
// Check if ready or issued fields are blocked by error flags
|
||||||
|
if (field === 'ready' || field === 'issued') {
|
||||||
|
const statusCheckbox = await c.env.DB.prepare(
|
||||||
|
'SELECT worksheets_error, cutting_error, glazing_error, ready_error, issued_error FROM status_checkboxes WHERE record_id = ?'
|
||||||
|
).bind(recordId).first()
|
||||||
|
|
||||||
|
const hasErrorFlags = statusCheckbox && (
|
||||||
|
statusCheckbox.worksheets_error ||
|
||||||
|
statusCheckbox.cutting_error ||
|
||||||
|
statusCheckbox.glazing_error ||
|
||||||
|
statusCheckbox.ready_error ||
|
||||||
|
statusCheckbox.issued_error
|
||||||
|
)
|
||||||
|
|
||||||
|
if (hasErrorFlags) {
|
||||||
|
return c.json({
|
||||||
|
error: 'Väli blokeeritud',
|
||||||
|
blocked: true,
|
||||||
|
reason: 'Vigade märked on seatud (punased kolmnurgad)'
|
||||||
|
}, 400)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the date
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE status_checkboxes SET ${field}_date = ? WHERE record_id = ?`
|
||||||
|
).bind(date, recordId).run()
|
||||||
|
|
||||||
|
// Log to audit
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'update_status')
|
||||||
|
`).bind(userId || null, recordId, field, oldRecord?.[`${field}_date`] || null, date).run()
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating status:', error)
|
||||||
|
return c.json({ error: 'Failed to update status' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update error flag
|
||||||
|
app.patch('/api/status/:recordId/:field/error', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
const field = c.req.param('field')
|
||||||
|
const { value } = await c.req.json()
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE status_checkboxes SET ${field}_error = ? WHERE record_id = ?`
|
||||||
|
).bind(value ? 1 : 0, recordId).run()
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating error flag:', error)
|
||||||
|
return c.json({ error: 'Failed to update error flag' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update confirmation flag
|
||||||
|
app.patch('/api/status/:recordId/:field/confirm', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
const field = c.req.param('field')
|
||||||
|
const { value } = await c.req.json()
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
`UPDATE status_checkboxes SET ${field}_confirmed = ? WHERE record_id = ?`
|
||||||
|
).bind(value ? 1 : 0, recordId).run()
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating confirmation flag:', error)
|
||||||
|
return c.json({ error: 'Failed to update confirmation flag' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== ADDITIONAL RECORD ROUTES ====================
|
||||||
|
|
||||||
|
// Worksheets cycle (3-step: empty -> confirmed -> with date -> empty)
|
||||||
|
app.patch('/api/records/:id/worksheets-cycle', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
const userId = c.get('userId')
|
||||||
|
|
||||||
|
// Get current worksheets state
|
||||||
|
const statusRecord = await c.env.DB.prepare(
|
||||||
|
'SELECT worksheets_date, worksheets_confirmed FROM status_checkboxes WHERE record_id = ?'
|
||||||
|
).bind(recordId).first()
|
||||||
|
|
||||||
|
let newDate = null
|
||||||
|
let newConfirmed = 0
|
||||||
|
|
||||||
|
// 3-step cycle logic:
|
||||||
|
// Step 1: empty (null date, confirmed=0) -> gray with date (date, confirmed=0)
|
||||||
|
// Step 2: gray with date (date, confirmed=0) -> green with date (date, confirmed=1)
|
||||||
|
// Step 3: green with date (date, confirmed=1) -> empty (null date, confirmed=0)
|
||||||
|
if (!statusRecord?.worksheets_date) {
|
||||||
|
// Step 1: empty -> gray with date
|
||||||
|
newConfirmed = 0
|
||||||
|
newDate = new Date().toISOString().split('T')[0]
|
||||||
|
} else if (statusRecord.worksheets_confirmed === 0) {
|
||||||
|
// Step 2: gray with date -> green with date
|
||||||
|
newConfirmed = 1
|
||||||
|
newDate = statusRecord.worksheets_date // Keep existing date
|
||||||
|
} else {
|
||||||
|
// Step 3: green with date -> empty
|
||||||
|
newConfirmed = 0
|
||||||
|
newDate = null
|
||||||
|
}
|
||||||
|
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
'UPDATE status_checkboxes SET worksheets_date = ?, worksheets_confirmed = ? WHERE record_id = ?'
|
||||||
|
).bind(newDate, newConfirmed, recordId).run()
|
||||||
|
|
||||||
|
// Log to audit
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'worksheets_cycle')
|
||||||
|
`).bind(userId || null, recordId, 'worksheets', statusRecord?.worksheets_date || '', newDate || '').run()
|
||||||
|
|
||||||
|
return c.json({ success: true, date: newDate, confirmed: newConfirmed })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error cycling worksheets:', error)
|
||||||
|
return c.json({ error: 'Failed to cycle worksheets' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update notes
|
||||||
|
app.patch('/api/records/:id/notes', authMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
const { notes } = await c.req.json()
|
||||||
|
const userId = c.get('userId')
|
||||||
|
const userRole = c.get('role')
|
||||||
|
|
||||||
|
// Only admin can edit notes
|
||||||
|
if (userRole !== 'admin') {
|
||||||
|
return c.json({ error: 'Permission denied. Only admin can edit notes.' }, 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
'UPDATE production_records SET notes = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
|
||||||
|
).bind(notes, recordId).run()
|
||||||
|
|
||||||
|
// Log to audit
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'update_notes')
|
||||||
|
`).bind(userId || null, recordId, 'notes', '', notes).run()
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating notes:', error)
|
||||||
|
return c.json({ error: 'Failed to update notes' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update problems and error flags
|
||||||
|
app.patch('/api/records/:id/problems', authMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
const { problems, errorFlags } = await c.req.json()
|
||||||
|
const userId = c.get('userId')
|
||||||
|
const userRole = c.get('role')
|
||||||
|
|
||||||
|
// User and admin can edit problems
|
||||||
|
if (userRole !== 'admin' && userRole !== 'user') {
|
||||||
|
return c.json({ error: 'Permission denied. Only admin and user can edit problems.' }, 403)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update problems text
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
'UPDATE production_records SET problems = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?'
|
||||||
|
).bind(problems, recordId).run()
|
||||||
|
|
||||||
|
// Update error flags
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
UPDATE status_checkboxes
|
||||||
|
SET worksheets_error = ?,
|
||||||
|
cutting_error = ?,
|
||||||
|
glazing_error = ?,
|
||||||
|
ready_error = ?,
|
||||||
|
issued_error = ?
|
||||||
|
WHERE record_id = ?
|
||||||
|
`).bind(
|
||||||
|
errorFlags.worksheets ? 1 : 0,
|
||||||
|
errorFlags.cutting ? 1 : 0,
|
||||||
|
errorFlags.glazing ? 1 : 0,
|
||||||
|
errorFlags.ready ? 1 : 0,
|
||||||
|
errorFlags.issued ? 1 : 0,
|
||||||
|
recordId
|
||||||
|
).run()
|
||||||
|
|
||||||
|
// Log to audit
|
||||||
|
await c.env.DB.prepare(`
|
||||||
|
INSERT INTO audit_log (user_id, record_id, field, old_value, new_value, action)
|
||||||
|
VALUES (?, ?, ?, ?, ?, 'update_problems')
|
||||||
|
`).bind(userId || null, recordId, 'problems', '', problems).run()
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating problems:', error)
|
||||||
|
return c.json({ error: 'Failed to update problems' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Toggle material confirmed
|
||||||
|
app.patch('/api/records/:id/material-confirmed', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
console.log('[MAT1] Toggle request for record:', recordId)
|
||||||
|
|
||||||
|
// Get current value
|
||||||
|
const current = await c.env.DB.prepare(
|
||||||
|
'SELECT material_confirmed FROM status_checkboxes WHERE record_id = ?'
|
||||||
|
).bind(recordId).first()
|
||||||
|
|
||||||
|
console.log('[MAT1] Current value:', current?.material_confirmed)
|
||||||
|
|
||||||
|
// Toggle value
|
||||||
|
const newValue = current?.material_confirmed === 1 ? 0 : 1
|
||||||
|
console.log('[MAT1] New value:', newValue)
|
||||||
|
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
'UPDATE status_checkboxes SET material_confirmed = ? WHERE record_id = ?'
|
||||||
|
).bind(newValue, recordId).run()
|
||||||
|
|
||||||
|
console.log('[MAT1] Update completed successfully')
|
||||||
|
return c.json({ success: true, newValue })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MAT1] Error updating material confirmed:', error)
|
||||||
|
return c.json({ error: 'Failed to update material confirmed' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Toggle material2 confirmed
|
||||||
|
app.patch('/api/records/:id/material2-confirmed', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
console.log('[MAT2] Toggle request for record:', recordId)
|
||||||
|
|
||||||
|
// Get current value
|
||||||
|
const current = await c.env.DB.prepare(
|
||||||
|
'SELECT material2_confirmed FROM status_checkboxes WHERE record_id = ?'
|
||||||
|
).bind(recordId).first()
|
||||||
|
|
||||||
|
console.log('[MAT2] Current value:', current?.material2_confirmed)
|
||||||
|
|
||||||
|
// Toggle value
|
||||||
|
const newValue = current?.material2_confirmed === 1 ? 0 : 1
|
||||||
|
console.log('[MAT2] New value:', newValue)
|
||||||
|
|
||||||
|
await c.env.DB.prepare(
|
||||||
|
'UPDATE status_checkboxes SET material2_confirmed = ? WHERE record_id = ?'
|
||||||
|
).bind(newValue, recordId).run()
|
||||||
|
|
||||||
|
console.log('[MAT2] Update completed successfully')
|
||||||
|
return c.json({ success: true, newValue })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[MAT2] Error updating material2 confirmed:', error)
|
||||||
|
return c.json({ error: 'Failed to update material2 confirmed' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Update price paid status (for invoice tracking)
|
||||||
|
app.patch('/api/records/:id/price-paid', optionalAuthMiddleware, async (c) => {
|
||||||
|
try {
|
||||||
|
const recordId = c.req.param('id')
|
||||||
|
const { paid } = await c.req.json()
|
||||||
|
|
||||||
|
// You might want to add a 'paid' field to production_records table
|
||||||
|
// For now, we'll just return success
|
||||||
|
// TODO: Add paid field to schema if needed
|
||||||
|
|
||||||
|
return c.json({ success: true })
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error updating price paid:', error)
|
||||||
|
return c.json({ error: 'Failed to update price paid' }, 500)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// ==================== DEFAULT ROUTE ====================
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== MAIN PAGE - ORIGINAL HTML FROM ARCHIVE ====================
|
||||||
|
app.get('/', (c) => {
|
||||||
|
return c.html(ORIGINAL_HTML)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test page for debugging clicks
|
||||||
|
app.get('/test-click', (c) => {
|
||||||
|
return c.html(`<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Click Test</title>
|
||||||
|
<style>
|
||||||
|
body { font-family: Arial; padding: 50px; }
|
||||||
|
.box {
|
||||||
|
width: 200px;
|
||||||
|
height: 100px;
|
||||||
|
background: #4F46E5;
|
||||||
|
color: white;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
#result {
|
||||||
|
margin-top: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
background: #f0f0f0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Click Test Page</h1>
|
||||||
|
<div class="box" onclick="handleClick(1)">Click Me (onclick)</div>
|
||||||
|
<div class="box" id="box2">Click Me (addEventListener)</div>
|
||||||
|
<div id="result">Waiting for click...</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Test 1: inline onclick
|
||||||
|
function handleClick(num) {
|
||||||
|
document.getElementById('result').innerHTML = '✅ Test ' + num + ': onclick works!';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test 2: addEventListener
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
document.getElementById('box2').addEventListener('click', () => {
|
||||||
|
document.getElementById('result').innerHTML = '✅ Test 2: addEventListener works!';
|
||||||
|
});
|
||||||
|
console.log('✅ DOMContentLoaded fired and event listener attached');
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>`)
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
export default app
|
||||||
1016
src/index.tsx.backup
Normal file
1016
src/index.tsx.backup
Normal file
File diff suppressed because it is too large
Load Diff
83
src/middleware/auth.ts
Normal file
83
src/middleware/auth.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import { Context, Next } from 'hono'
|
||||||
|
import { verifyToken, refreshToken } from '../utils/auth'
|
||||||
|
|
||||||
|
type Bindings = {
|
||||||
|
DB: D1Database;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Variables = {
|
||||||
|
userId?: number;
|
||||||
|
username?: string;
|
||||||
|
role?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware that requires authentication
|
||||||
|
export async function authMiddleware(c: Context<{ Bindings: Bindings; Variables: Variables }>, next: Next) {
|
||||||
|
const authHeader = c.req.header('Authorization')
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
return c.json({ error: 'Unauthorized' }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.substring(7)
|
||||||
|
const payload = verifyToken(token)
|
||||||
|
|
||||||
|
if (!payload) {
|
||||||
|
return c.json({ error: 'Invalid or expired token' }, 401)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set user context
|
||||||
|
c.set('userId', payload.userId)
|
||||||
|
c.set('username', payload.username)
|
||||||
|
|
||||||
|
// Get user role from database
|
||||||
|
const user = await c.env.DB.prepare(
|
||||||
|
'SELECT role FROM users WHERE id = ?'
|
||||||
|
).bind(payload.userId).first()
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
c.set('role', user.role as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh token and send in header
|
||||||
|
const newToken = refreshToken(token)
|
||||||
|
if (newToken) {
|
||||||
|
c.header('X-Refreshed-Token', newToken)
|
||||||
|
}
|
||||||
|
|
||||||
|
await next()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middleware that allows but doesn't require authentication
|
||||||
|
export async function optionalAuthMiddleware(c: Context<{ Bindings: Bindings; Variables: Variables }>, next: Next) {
|
||||||
|
const authHeader = c.req.header('Authorization')
|
||||||
|
|
||||||
|
if (authHeader && authHeader.startsWith('Bearer ')) {
|
||||||
|
const token = authHeader.substring(7)
|
||||||
|
const payload = verifyToken(token)
|
||||||
|
|
||||||
|
if (payload) {
|
||||||
|
c.set('userId', payload.userId)
|
||||||
|
c.set('username', payload.username)
|
||||||
|
|
||||||
|
const user = await c.env.DB.prepare(
|
||||||
|
'SELECT role FROM users WHERE id = ?'
|
||||||
|
).bind(payload.userId).first()
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
c.set('role', user.role as string)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh token
|
||||||
|
const newToken = refreshToken(token)
|
||||||
|
if (newToken) {
|
||||||
|
c.header('X-Refreshed-Token', newToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Public user - set default values
|
||||||
|
c.set('username', 'Public')
|
||||||
|
}
|
||||||
|
|
||||||
|
await next()
|
||||||
|
}
|
||||||
1232
src/original-html.ts
Normal file
1232
src/original-html.ts
Normal file
File diff suppressed because one or more lines are too long
12
src/renderer.tsx
Normal file
12
src/renderer.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { jsxRenderer } from 'hono/jsx-renderer'
|
||||||
|
|
||||||
|
export const renderer = jsxRenderer(({ children }) => {
|
||||||
|
return (
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<link href="/static/style.css" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
)
|
||||||
|
})
|
||||||
72
src/utils/auth.ts
Normal file
72
src/utils/auth.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
// Simple authentication utilities for demo purposes
|
||||||
|
// NOTE: In production, use bcrypt for passwords and proper JWT library for tokens
|
||||||
|
|
||||||
|
// Password hashing using SHA-256 (demo only - use bcrypt in production)
|
||||||
|
export async function hashPassword(password: string): Promise<string> {
|
||||||
|
// For demo, we'll use a simple approach with crypto API
|
||||||
|
// In production, use bcrypt or similar
|
||||||
|
const msgBuffer = new TextEncoder().encode(password)
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer)
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
|
||||||
|
return hashHex
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password (with demo123 fallback for easy testing)
|
||||||
|
export async function verifyPassword(password: string, storedHash: string): Promise<boolean> {
|
||||||
|
// First check against stored hash
|
||||||
|
if (storedHash) {
|
||||||
|
const passwordHash = await hashPassword(password)
|
||||||
|
return passwordHash === storedHash
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback for demo users without hash (legacy)
|
||||||
|
return password === 'demo123'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token generation (simple demo - use JWT library in production)
|
||||||
|
export function generateToken(userId: number, username: string): string {
|
||||||
|
const expiry = Date.now() + (240 * 60 * 1000) // 4 hours
|
||||||
|
const payload = { userId, username, exp: expiry }
|
||||||
|
return btoa(JSON.stringify(payload))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh token with new expiry
|
||||||
|
export function refreshToken(token: string): string | null {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token))
|
||||||
|
const newExpiry = Date.now() + (240 * 60 * 1000) // Reset to 4 hours
|
||||||
|
const newPayload = { ...payload, exp: newExpiry }
|
||||||
|
return btoa(JSON.stringify(newPayload))
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify token
|
||||||
|
export function verifyToken(token: string): { userId: number; username: string } | null {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token))
|
||||||
|
|
||||||
|
if (payload.exp < Date.now()) {
|
||||||
|
return null // Token expired
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
userId: payload.userId,
|
||||||
|
username: payload.username
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get token expiry time
|
||||||
|
export function getTokenExpiry(token: string): number | null {
|
||||||
|
try {
|
||||||
|
const payload = JSON.parse(atob(token))
|
||||||
|
return payload.exp
|
||||||
|
} catch {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
15
tsconfig.json
Normal file
15
tsconfig.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"lib": [
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx"
|
||||||
|
},
|
||||||
|
}
|
||||||
14
vite.config.ts
Normal file
14
vite.config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import build from '@hono/vite-build/cloudflare-pages'
|
||||||
|
import devServer from '@hono/vite-dev-server'
|
||||||
|
import adapter from '@hono/vite-dev-server/cloudflare'
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
build(),
|
||||||
|
devServer({
|
||||||
|
adapter,
|
||||||
|
entry: 'src/index.tsx'
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|
||||||
17
wrangler.jsonc
Normal file
17
wrangler.jsonc
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"$schema": "node_modules/wrangler/config-schema.json",
|
||||||
|
"name": "webapp",
|
||||||
|
"compatibility_date": "2025-11-28",
|
||||||
|
"compatibility_flags": ["nodejs_compat"],
|
||||||
|
"pages_build_output_dir": "./dist",
|
||||||
|
|
||||||
|
// D1 Database configuration
|
||||||
|
// ВАЖНО: Имя БД ВСЕГДА aknaproff-db (используется в docker-entrypoint.sh)
|
||||||
|
"d1_databases": [
|
||||||
|
{
|
||||||
|
"binding": "DB",
|
||||||
|
"database_name": "aknaproff-db",
|
||||||
|
"database_id": "local"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user