# QuoteForge

**Server Configuration & Quotation Tool**

QuoteForge — корпоративный инструмент для конфигурирования серверов и формирования коммерческих предложений (КП).  
Приложение работает в strict local-first режиме: пользовательские операции выполняются через локальную SQLite, MariaDB используется для синхронизации и серверного администрирования прайслистов.

![Go Version](https://img.shields.io/badge/Go-1.22+-00ADD8?style=flat&logo=go)
![License](https://img.shields.io/badge/License-Proprietary-red)
![Status](https://img.shields.io/badge/Status-In%20Development-yellow)

## Возможности

### Для пользователей
- 📱 **Mobile-first интерфейс** — удобная работа с телефона и планшета
- 🖥️ **Конфигуратор серверов** — пошаговый выбор компонентов с проверкой совместимости
- 💰 **Автоматический расчёт цен** — актуальные цены на основе истории закупок
- 📊 **Экспорт в CSV/XLSX** — готовые спецификации для клиентов
- 💾 **Сохранение конфигураций** — история и шаблоны для повторного использования
- 🔌 **Полная офлайн-работа** — можно продолжать работу без сети и синхронизировать позже
- 🛡️ **Защищенная синхронизация** — sync блокируется preflight-проверкой, если локальная схема не готова

### Для ценовых администраторов
- 📈 **Умный расчёт цен** — медиана, взвешенная медиана, среднее
- 🎯 **Система алертов** — уведомления о популярных компонентах с устаревшими ценами
- 📉 **Аналитика использования** — какие компоненты востребованы в КП
- ⚙️ **Гибкие настройки** — периоды расчёта, методы, ручные переопределения

### Индикация актуальности цен
| Цвет | Статус | Условие |
|------|--------|---------|
| 🟢 Зелёный | Свежая | < 30 дней, ≥ 3 источника |
| 🟡 Жёлтый | Нормальная | 30-60 дней |
| 🟠 Оранжевый | Устаревающая | 60-90 дней |
| 🔴 Красный | Устаревшая | > 90 дней или нет данных |

## Технологии

- **Backend:** Go 1.22+, Gin, GORM
- **Frontend:** HTML, Tailwind CSS, htmx
- **Database:** SQLite (runtime/local-first), MariaDB 11+ (sync + server admin)
- **Export:** excelize (XLSX), encoding/csv

## Требования

- Go 1.22 или выше
- MariaDB 11.x (или MySQL 8.x)
- ~50 MB дискового пространства

## Установка

### 1. Клонирование репозитория

```bash
git clone https://github.com/your-company/quoteforge.git
cd quoteforge
```

### 2. Настройка runtime-конфига (опционально)

`config.yaml` создаётся автоматически при первом старте в той же user-state папке, где находится `qfs.db`.
Если найден старый формат, приложение автоматически мигрирует файл в актуальный runtime-формат
(оставляя только используемые секции `server` и `logging`).

При необходимости можно создать/отредактировать файл вручную:

```yaml
server:
  host: "0.0.0.0"
  port: 8080
  mode: "release"

logging:
  level: "info"
  format: "json"
  output: "stdout"
```

### 3. Миграции базы данных

```bash
go run ./cmd/qfs -migrate
```

### Мигратор OPS -> проекты (preview/apply)

Переносит квоты, чьи названия начинаются с `OPS-xxxx` (где `x` — цифра), в проект `OPS-xxxx`.
Если проекта нет, он будет создан; если архивный — реактивирован.

Сначала всегда смотрите preview:

```bash
go run ./cmd/migrate_ops_projects
```

Применение изменений:

```bash
go run ./cmd/migrate_ops_projects -apply
```

Без интерактивного подтверждения:

```bash
go run ./cmd/migrate_ops_projects -apply -yes
```

### Минимальные права БД для пользователя квотаций

Если нужен пользователь, который может работать с конфигурациями, но не может создавать/удалять прайслисты:

```sql
-- 1) Создать пользователя (если его ещё нет)
CREATE USER IF NOT EXISTS 'quote_user'@'%' IDENTIFIED BY '<DB_PASSWORD>';

-- 2) Если пользователь уже существовал, принудительно обновить пароль
ALTER USER 'quote_user'@'%' IDENTIFIED BY '<DB_PASSWORD>';

-- 3) (Опционально, но рекомендуется) удалить дубли пользователя с другими host,
-- чтобы не возникало конфликтов вида user@localhost vs user@'%'
DROP USER IF EXISTS 'quote_user'@'localhost';
DROP USER IF EXISTS 'quote_user'@'127.0.0.1';
DROP USER IF EXISTS 'quote_user'@'::1';

-- 4) Сбросить лишние права
REVOKE ALL PRIVILEGES, GRANT OPTION FROM 'quote_user'@'%';

-- 5) Чтение данных для конфигуратора и синка
GRANT SELECT ON RFQ_LOG.lot TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_categories TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelists TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO 'quote_user'@'%';

-- 6) Работа с конфигурациями
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO 'quote_user'@'%';

FLUSH PRIVILEGES;

SHOW GRANTS FOR 'quote_user'@'%';
SHOW CREATE USER 'quote_user'@'%';
```

Полный набор прав для пользователя квотаций:

```sql
GRANT USAGE ON *.* TO 'quote_user'@'%' IDENTIFIED BY '<DB_PASSWORD>';
GRANT SELECT ON RFQ_LOG.lot TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_lot_metadata TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_categories TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelists TO 'quote_user'@'%';
GRANT SELECT ON RFQ_LOG.qt_pricelist_items TO 'quote_user'@'%';
GRANT SELECT, INSERT, UPDATE ON RFQ_LOG.qt_configurations TO 'quote_user'@'%';
```

Важно:
- не выдавайте `INSERT/UPDATE/DELETE` на `qt_pricelists` и `qt_pricelist_items`, если пользователь не должен управлять прайслистами;
- если видите ошибку `Access denied for user ...@'<ip>'`, проверьте, что не осталось других записей `quote_user@host` кроме `quote_user@'%'`;
- после смены DB-настроек через `/setup` приложение перезапускается автоматически и подхватывает нового пользователя.

### 4. Импорт метаданных компонентов

```bash
go run ./cmd/importer
```

### 5. Запуск

```bash
# Development
go run ./cmd/qfs

# Production (with Makefile - recommended)
make build-release    # Builds with version info
./bin/qfs -version    # Check version

# Production (manual)
VERSION=$(git describe --tags --always --dirty)
CGO_ENABLED=0 go build -ldflags="-s -w -X main.Version=$VERSION" -o bin/qfs ./cmd/qfs
./bin/qfs -version
```

**Makefile команды:**
```bash
make build-release   # Оптимизированная сборка с версией
make build-all       # Сборка для всех платформ (Linux, macOS, Windows)
make build-windows   # Только для Windows
make run             # Запуск dev сервера
make test            # Запуск тестов
make install-hooks   # Установить git hooks (блокировка коммита с секретами)
make clean           # Очистка bin/
make help            # Показать все команды
```

Приложение будет доступно по адресу: http://localhost:8080

### Локальная SQLite база (state)

Локальная база приложения хранится в профиле пользователя и не зависит от расположения бинарника.
Имя файла: `qfs.db`.

- macOS: `~/Library/Application Support/QuoteForge/qfs.db`
- Linux: `$XDG_STATE_HOME/quoteforge/qfs.db` (или `~/.local/state/quoteforge/qfs.db`)
- Windows: `%LOCALAPPDATA%\\QuoteForge\\qfs.db`

Можно переопределить путь через `-localdb` или переменную окружения `QFS_DB_PATH`.

#### Sync readiness guard

Перед `push/pull` выполняется preflight-проверка:
- доступен ли сервер (MariaDB);
- можно ли проверить и применить централизованные миграции локальной БД;
- подходит ли версия приложения под `min_app_version` миграций.

Если проверка не пройдена:
- локальная работа (CRUD) продолжается;
- sync API возвращает `423 Locked` с `reason_code` и `reason_text`;
- в UI показывается красный индикатор и причина блокировки в модалке синхронизации.

#### Схема потоков данных синхронизации

```text
                          [ SERVER / MariaDB ]
                     ┌───────────────────────────┐
                     │ qt_projects               │
                     │ qt_configurations         │
                     │ qt_pricelists             │
                     │ qt_pricelist_items        │
                     │ qt_pricelist_sync_status  │
                     └─────────────┬─────────────┘
                                   │
                    pull (projects/configs/pricelists)
                                   │
                ┌──────────────────┴──────────────────┐
                │                                     │
      [ CLIENT A / local SQLite ]           [ CLIENT B / local SQLite ]
   ┌───────────────────────────────┐       ┌───────────────────────────────┐
   │ local_projects                │       │ local_projects                │
   │ local_configurations          │       │ local_configurations          │
   │ local_pricelists              │       │ local_pricelists              │
   │ local_pricelist_items         │       │ local_pricelist_items         │
   │ pending_changes (proj/config) │       │ pending_changes (proj/config) │
   └───────────────┬───────────────┘       └───────────────┬───────────────┘
                   │                                       │
      push (projects/configurations only)     push (projects/configurations only)
                   │                                       │
                   └──────────────────┬────────────────────┘
                                      │
                              [ SERVER / MariaDB ]
```

По сущностям:
- Конфигурации: `Client <-> Server <-> Other Clients`
- Проекты: `Client <-> Server <-> Other Clients`
- Прайслисты: `Server -> Clients only` (локальный push отсутствует)
- Локальная очистка прайслистов на клиенте: удаляются записи, которых нет на сервере и которые не используются активными локальными конфигурациями

### Версионность конфигураций (local-first)

Для `local_configurations` используется append-only versioning через полные snapshot-версии:

- таблица: `local_configuration_versions`
- для каждого изменения создаётся новая версия (`version_no = max + 1`)
- `local_configurations.current_version_id` указывает на активную версию
- старые версии не изменяются и не удаляются в обычном потоке
- rollback не "перематывает" историю, а создаёт новую версию из выбранного snapshot

При backfill (миграция `006_add_local_configuration_versions.sql`) для существующих конфигураций создаётся `v1` и проставляется `current_version_id`.

#### Rollback

Rollback выполняется API-методом:

```bash
POST /api/configs/:uuid/rollback
{
  "target_version": 3,
  "note": "optional"
}
```

Результат:
- создаётся новая версия `vN` с `data` из целевой версии
- `change_note = "rollback to v{target_version}"` (+ note, если передан)
- `current_version_id` переключается на новую версию
- конфигурация уходит в `sync_status = pending`

### Локальный config.yaml

По умолчанию `qfs` ищет `config.yaml` в той же user-state папке, где лежит `qfs.db` (а не рядом с бинарником).
Если файла нет, он создаётся автоматически. Если формат устарел, он автоматически мигрируется в runtime-формат (`server` + `logging`).
Можно переопределить путь через `-config` или `QFS_CONFIG_PATH`.

## Docker

```bash
# Сборка образа
docker build -t quoteforge .

# Запуск с docker-compose
docker-compose up -d
```

## Структура проекта

```
quoteforge/
├── cmd/
│   ├── server/main.go           # Main HTTP server
│   └── importer/main.go         # Import metadata from lot table
├── internal/
│   ├── config/          # Конфигурация
│   ├── models/          # GORM модели
│   ├── handlers/        # HTTP handlers
│   ├── services/        # Бизнес-логика
│   ├── middleware/      # Auth, CORS, etc.
│   └── repository/      # Работа с БД
├── web/
│   ├── templates/       # HTML шаблоны
│   └── static/          # CSS, JS, изображения
├── migrations/          # SQL миграции
├── config.example.yaml  # Пример конфигурации
└── go.mod
```

## Роли пользователей

| Роль | Описание |
|------|----------|
| `viewer` | Просмотр, создание квот, экспорт |
| `editor` | + сохранение конфигураций |
| `pricing_admin` | + управление ценами и алертами |
| `admin` | Полный доступ, управление пользователями |

## API

Документация API доступна по адресу `/api/docs` (в разработке).

Основные endpoints:

```
POST /api/auth/login          # Авторизация
GET  /api/components          # Список компонентов
POST /api/quote/calculate     # Расчёт цены
POST /api/export/xlsx         # Экспорт в Excel
GET  /api/configs             # Сохранённые конфигурации
GET  /api/configs/:uuid/versions            # Список версий конфигурации
GET  /api/configs/:uuid/versions/:version   # Получить конкретную версию
POST /api/configs/:uuid/rollback            # Rollback на указанную версию
POST /api/configs/:uuid/reactivate          # Вернуть архивную конфигурацию в активные
GET  /api/sync/readiness                   # Статус readiness guard (ready|blocked|unknown)
GET  /api/sync/status                      # Сводный статус синхронизации
GET  /api/sync/info                        # Данные для модалки синхронизации
POST /api/sync/push                        # Push pending changes (423, если blocked)
POST /api/sync/all                         # Full sync push+pull (423, если blocked)
POST /api/sync/components                  # Pull components (423, если blocked)
POST /api/sync/pricelists                  # Pull pricelists (423, если blocked)
```

### Краткая карта sync API

| Endpoint | Назначение | Поток |
|----------|------------|-------|
| `POST /api/sync/push` | Отправить локальные pending-изменения | `SQLite -> MariaDB` |
| `POST /api/sync/components` | Подтянуть справочник компонентов | `MariaDB -> SQLite` |
| `POST /api/sync/pricelists` | Подтянуть прайслисты и позиции | `MariaDB -> SQLite` |
| `POST /api/sync/all` | Полный цикл: push + pull + импорт проектов/конфигураций | `двунаправленно` |
| `GET /api/sync/readiness` | Статус preflight/readiness | `read-only` |
| `GET /api/sync/status` / `GET /api/sync/info` | Сводка статуса и данных синхронизации | `read-only` |

#### Sync payload для versioning

События в `pending_changes` для конфигураций содержат:
- `configuration_uuid`
- `operation` (`create` / `update` / `rollback`)
- `current_version_id` и `current_version_no`
- `snapshot` (текущее состояние конфигурации)
- `idempotency_key` и `conflict_policy` (`last_write_wins`)

Это позволяет push-слою отправлять на сервер актуальное состояние и готовит основу для будущего conflict resolution.

## Разработка

```bash
# Запуск в режиме разработки (hot reload)
go run ./cmd/qfs

# Запуск тестов
go test ./...

# Сборка для Linux
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/qfs ./cmd/qfs
```

## Переменные окружения

| Переменная | Описание | По умолчанию |
|------------|----------|--------------|
| `QF_DB_HOST` | Хост базы данных | localhost |
| `QF_DB_PORT` | Порт базы данных | 3306 |
| `QF_DB_NAME` | Имя базы данных | RFQ_LOG |
| `QF_DB_USER` | Пользователь БД | — |
| `QF_DB_PASSWORD` | Пароль БД | — |
| `QF_JWT_SECRET` | Секрет для JWT | — |
| `QF_SERVER_PORT` | Порт сервера | 8080 |
| `QFS_DB_PATH` | Полный путь к локальной SQLite БД | OS-specific user state dir |
| `QFS_STATE_DIR` | Каталог state (если `QFS_DB_PATH` не задан) | OS-specific user state dir |
| `QFS_CONFIG_PATH` | Полный путь к `config.yaml` | OS-specific user state dir |

## Интеграция с существующей БД

QuoteForge интегрируется с существующей базой RFQ_LOG:

- `lot` — справочник компонентов (только чтение)
- `lot_log` — история цен от поставщиков (только чтение)
- `supplier` — справочник поставщиков (только чтение)

Новые таблицы QuoteForge имеют префикс `qt_`:

- `qt_users` — пользователи приложения
- `qt_lot_metadata` — расширенные данные компонентов
- `qt_configurations` — сохранённые конфигурации
- `qt_pricing_alerts` — алерты для администраторов

## Поддержка

По вопросам работы приложения обращайтесь:
- Email: mike@mchus.pro
- Internal: @mchus

## Лицензия

Данное программное обеспечение является собственностью компании и предназначено исключительно для внутреннего использования. Распространение, копирование или модификация без письменного разрешения запрещены.

См. файл [LICENSE](LICENSE) для подробностей.
