Add Phase 2: Local SQLite database with sync functionality
Implements complete offline-first architecture with SQLite caching and MariaDB synchronization. Key features: - Local SQLite database for offline operation (data/quoteforge.db) - Connection settings with encrypted credentials - Component and pricelist caching with auto-sync - Sync API endpoints (/api/sync/status, /components, /pricelists, /all) - Real-time sync status indicator in UI with auto-refresh - Offline mode detection middleware - Migration tool for database initialization - Setup wizard for initial configuration New components: - internal/localdb: SQLite repository layer (components, pricelists, sync) - internal/services/sync: Synchronization service - internal/handlers/sync: Sync API handlers - internal/handlers/setup: Setup wizard handlers - internal/middleware/offline: Offline detection - cmd/migrate: Database migration tool UI improvements: - Setup page for database configuration - Sync status indicator with online/offline detection - Warning icons for pending synchronization - Auto-refresh every 30 seconds Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
683
CLAUDE.md
683
CLAUDE.md
@@ -1,638 +1,107 @@
|
||||
# QuoteForge - Claude Code Instructions
|
||||
|
||||
## Project Overview
|
||||
|
||||
QuoteForge — корпоративный инструмент для конфигурирования серверов и формирования коммерческих предложений (КП). Приложение работает с серверной базой данных MariaDB (RFQ_LOG) и локальной SQLite для оффлайн-работы.
|
||||
## Overview
|
||||
Корпоративный конфигуратор серверов и формирование КП. MariaDB (RFQ_LOG) + SQLite для оффлайн.
|
||||
|
||||
## Development Phases
|
||||
|
||||
### Phase 1: Pricelists in MariaDB
|
||||
- Настройка подключения к БД при первом запуске
|
||||
- Таблицы qt_pricelists и qt_pricelist_items
|
||||
- CRUD операции для прайслистов (при наличии прав записи)
|
||||
### Phase 1: Pricelists in MariaDB ✅ DONE
|
||||
### Phase 2: Local SQLite Database ✅ DONE
|
||||
|
||||
### Phase 2: Projects and Specifications
|
||||
- Таблицы qt_projects и qt_specifications
|
||||
- Замена qt_configurations на новую структуру
|
||||
- Поля: opty, customer_requirement, variant, qty, rev
|
||||
### Phase 2.5: Full Offline Mode 🔶 IN PROGRESS
|
||||
Приложение должно полностью работать без MariaDB, синхронизация при восстановлении связи.
|
||||
|
||||
### Phase 3: Local SQLite Database
|
||||
- Локальное хранение настроек подключения
|
||||
- Кэширование прайслистов
|
||||
- Локальные проекты и спецификации
|
||||
- Синхронизация с сервером
|
||||
**Architecture:**
|
||||
- Dual-source pattern: все операции идут через unified service layer
|
||||
- Online: read/write MariaDB, async cache to SQLite
|
||||
- Offline: read/write SQLite, queue changes for sync
|
||||
|
||||
**TODO:**
|
||||
- ❌ Unified repository interface (online/offline transparent switching)
|
||||
- ❌ Sync queue table (pending_changes: entity_type, entity_uuid, operation, payload, created_at)
|
||||
- ❌ Background sync worker (push local changes when online)
|
||||
- ❌ Conflict resolution (last-write-wins by updated_at, or manual)
|
||||
- ❌ Initial data bootstrap (first sync downloads all needed data)
|
||||
- ❌ Handlers use context.IsOffline to choose data source
|
||||
- ❌ UI: pending changes counter, manual sync button, conflict alerts
|
||||
|
||||
**Sync flow:**
|
||||
1. Online → Offline: continue work, changes saved locally with sync_status='pending'
|
||||
2. Offline → Online: background worker pushes pending_changes, pulls updates
|
||||
3. Conflict: if server version newer, mark as 'conflict' for manual resolution
|
||||
|
||||
### Phase 3: Projects and Specifications
|
||||
- qt_projects, qt_specifications tables (MariaDB)
|
||||
- Replace qt_configurations → Project/Specification hierarchy
|
||||
- Fields: opty, customer_requirement, variant, qty, rev
|
||||
- Local projects/specs with server sync
|
||||
|
||||
### Phase 4: Price Versioning
|
||||
- Привязка спецификаций к версиям прайслистов
|
||||
- Актуализация прайслистов с показом разницы цен
|
||||
- Автоочистка старых прайслистов (>1 года, usage_count=0)
|
||||
- Bind specifications to pricelist versions
|
||||
- Price diff comparison
|
||||
- Auto-cleanup expired pricelists (>1 year, usage_count=0)
|
||||
|
||||
## Tech Stack
|
||||
Go 1.22+ | Gin | GORM | MariaDB 11 | SQLite (glebarez/sqlite) | htmx + Tailwind CDN | excelize
|
||||
|
||||
- **Language:** Go 1.22+
|
||||
- **Web Framework:** Gin (github.com/gin-gonic/gin)
|
||||
- **ORM:** GORM (gorm.io/gorm)
|
||||
- **Server Database:** MariaDB 11 (existing database RFQ_LOG)
|
||||
- **Local Database:** SQLite (github.com/glebarez/sqlite for pure Go)
|
||||
- **Frontend:** HTML templates + htmx + Tailwind CSS (CDN)
|
||||
- **Excel Export:** excelize (github.com/xuri/excelize/v2)
|
||||
## Key Tables
|
||||
|
||||
## Project Structure
|
||||
### READ-ONLY (external systems)
|
||||
- `lot` (lot_name PK, lot_description)
|
||||
- `lot_log` (lot, supplier, date, price, quality, comments)
|
||||
- `supplier` (supplier_name PK)
|
||||
|
||||
```
|
||||
quoteforge/
|
||||
├── cmd/
|
||||
│ ├── server/main.go # Main HTTP server
|
||||
│ ├── importer/main.go # Import metadata from lot table
|
||||
│ └── cron/main.go # Cron jobs
|
||||
├── internal/
|
||||
│ ├── config/
|
||||
│ │ └── config.go # Load settings from SQLite
|
||||
│ ├── db/
|
||||
│ │ ├── mariadb.go # Server DB connection
|
||||
│ │ └── sqlite.go # Local DB connection
|
||||
│ ├── models/
|
||||
│ │ ├── lot.go # Existing lot tables
|
||||
│ │ ├── pricelist.go # Pricelists
|
||||
│ │ ├── project.go # Projects
|
||||
│ │ ├── specification.go # Specifications
|
||||
│ │ └── local_models.go # SQLite models
|
||||
│ ├── handlers/
|
||||
│ │ ├── setup_handler.go # Initial DB setup
|
||||
│ │ ├── pricelist_handler.go # Pricelist CRUD
|
||||
│ │ ├── project_handler.go # Project CRUD
|
||||
│ │ ├── spec_handler.go # Specification CRUD
|
||||
│ │ └── sync_handler.go # Sync operations
|
||||
│ ├── services/
|
||||
│ │ ├── pricelist_service.go # Pricelist business logic
|
||||
│ │ ├── project_service.go # Project business logic
|
||||
│ │ ├── sync_service.go # Sync with server
|
||||
│ │ └── price_service.go # Price calculations
|
||||
│ ├── middleware/
|
||||
│ │ └── db_check.go # Check DB connection
|
||||
│ └── repository/
|
||||
│ ├── mariadb_repo.go # Server DB queries
|
||||
│ └── sqlite_repo.go # Local DB queries
|
||||
├── web/
|
||||
│ ├── templates/
|
||||
│ │ ├── setup.html # DB connection setup
|
||||
│ │ ├── projects.html # Project list
|
||||
│ │ ├── project_detail.html # Project with specs
|
||||
│ │ ├── spec_editor.html # Specification editor
|
||||
│ │ └── pricelists.html # Pricelist management
|
||||
│ └── static/
|
||||
├── data/ # SQLite database location
|
||||
│ └── quoteforge.db
|
||||
├── migrations/
|
||||
└── go.mod
|
||||
```
|
||||
### MariaDB (qt_* prefix)
|
||||
- `qt_lot_metadata` - component prices, methods, popularity
|
||||
- `qt_categories` - category codes and names
|
||||
- `qt_pricelists` - version snapshots (YYYY-MM-DD-NNN format)
|
||||
- `qt_pricelist_items` - prices per pricelist
|
||||
- `qt_projects` - uuid, opty, customer_requirement, name (Phase 3)
|
||||
- `qt_specifications` - project_id, pricelist_id, variant, rev, qty, items JSON (Phase 3)
|
||||
|
||||
## Existing Database Tables (READ-ONLY - DO NOT MODIFY)
|
||||
### SQLite (data/quoteforge.db)
|
||||
- `connection_settings` - encrypted DB credentials
|
||||
- `local_pricelists/items` - cached from server
|
||||
- `local_components` - lot cache for offline search
|
||||
- `local_configurations` - with sync_status (pending/synced/conflict)
|
||||
- `local_projects/specifications` - Phase 3
|
||||
- `pending_changes` - sync queue (entity_type, uuid, op, payload, created_at)
|
||||
|
||||
These tables are used by other systems. Our app only reads from them:
|
||||
## Business Logic
|
||||
|
||||
```sql
|
||||
-- Component catalog
|
||||
CREATE TABLE lot (
|
||||
lot_name CHAR(255) PRIMARY KEY,
|
||||
lot_description VARCHAR(10000)
|
||||
);
|
||||
**Part number parsing:** `CPU_AMD_9654` → category=`CPU`, model=`AMD_9654`
|
||||
|
||||
-- Price history from suppliers
|
||||
CREATE TABLE lot_log (
|
||||
lot_log_id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
lot CHAR(255) NOT NULL,
|
||||
supplier CHAR(255) NOT NULL,
|
||||
date DATE NOT NULL,
|
||||
price DOUBLE NOT NULL,
|
||||
quality CHAR(255),
|
||||
comments VARCHAR(15000),
|
||||
FOREIGN KEY (lot) REFERENCES lot(lot_name),
|
||||
FOREIGN KEY (supplier) REFERENCES supplier(supplier_name)
|
||||
);
|
||||
**Price methods:** manual | median | average | weighted_median
|
||||
|
||||
-- Supplier catalog
|
||||
CREATE TABLE supplier (
|
||||
supplier_name CHAR(255) PRIMARY KEY,
|
||||
supplier_comment VARCHAR(10000)
|
||||
);
|
||||
```
|
||||
**Price freshness:** fresh (<30d, ≥3 quotes) | normal (<60d) | stale (<90d) | critical
|
||||
|
||||
## New MariaDB Tables (prefix qt_)
|
||||
|
||||
### Core Tables
|
||||
|
||||
```sql
|
||||
-- Component metadata (extends lot table)
|
||||
CREATE TABLE qt_lot_metadata (
|
||||
lot_name CHAR(255) PRIMARY KEY,
|
||||
category_id INT,
|
||||
model VARCHAR(100),
|
||||
specs JSON,
|
||||
current_price DECIMAL(12,2),
|
||||
price_method ENUM('manual', 'median', 'average', 'weighted_median') DEFAULT 'median',
|
||||
price_period_days INT DEFAULT 90,
|
||||
price_updated_at TIMESTAMP,
|
||||
request_count INT DEFAULT 0,
|
||||
last_request_date DATE,
|
||||
popularity_score DECIMAL(10,4),
|
||||
FOREIGN KEY (lot_name) REFERENCES lot(lot_name)
|
||||
);
|
||||
|
||||
-- Categories
|
||||
CREATE TABLE qt_categories (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(20) UNIQUE NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
name_ru VARCHAR(100),
|
||||
display_order INT DEFAULT 0,
|
||||
is_required BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
-- Usage statistics
|
||||
CREATE TABLE qt_component_usage_stats (
|
||||
lot_name CHAR(255) PRIMARY KEY,
|
||||
quotes_total INT DEFAULT 0,
|
||||
quotes_last_30d INT DEFAULT 0,
|
||||
quotes_last_7d INT DEFAULT 0,
|
||||
total_quantity INT DEFAULT 0,
|
||||
total_revenue DECIMAL(14,2) DEFAULT 0,
|
||||
trend_direction ENUM('up', 'stable', 'down') DEFAULT 'stable',
|
||||
trend_percent DECIMAL(5,2) DEFAULT 0,
|
||||
last_used_at TIMESTAMP,
|
||||
config_count INT DEFAULT 0 -- Number of configurations using this component
|
||||
);
|
||||
```
|
||||
|
||||
### Pricelist Tables
|
||||
|
||||
```sql
|
||||
-- Pricelists (versioned price snapshots)
|
||||
CREATE TABLE qt_pricelists (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
version VARCHAR(20) NOT NULL, -- Format: "YYYY-MM-DD-NNN" (e.g., "2024-01-31-001")
|
||||
name VARCHAR(200), -- Optional display name
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
created_by VARCHAR(100), -- Username of creator
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
usage_count INT DEFAULT 0, -- How many specifications use this pricelist
|
||||
expires_at DATE, -- Auto-calculated: created_at + 1 year
|
||||
UNIQUE KEY (version)
|
||||
);
|
||||
|
||||
-- Pricelist items
|
||||
CREATE TABLE qt_pricelist_items (
|
||||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||||
pricelist_id INT NOT NULL,
|
||||
lot_name CHAR(255) NOT NULL,
|
||||
price DECIMAL(12,2) NOT NULL,
|
||||
price_method ENUM('manual', 'median', 'average', 'weighted_median'),
|
||||
FOREIGN KEY (pricelist_id) REFERENCES qt_pricelists(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (lot_name) REFERENCES lot(lot_name),
|
||||
INDEX idx_pricelist_lot (pricelist_id, lot_name)
|
||||
);
|
||||
```
|
||||
|
||||
### Project Tables
|
||||
|
||||
```sql
|
||||
-- Projects (group of specifications)
|
||||
CREATE TABLE qt_projects (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(36) UNIQUE NOT NULL,
|
||||
opty VARCHAR(50), -- Opportunity/project number
|
||||
customer_requirement TEXT, -- Link to customer requirements/TZ
|
||||
name VARCHAR(200) NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Specifications (replaces qt_configurations)
|
||||
CREATE TABLE qt_specifications (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
uuid VARCHAR(36) UNIQUE NOT NULL,
|
||||
project_id INT NOT NULL,
|
||||
pricelist_id INT NOT NULL, -- Bound to specific pricelist version
|
||||
variant VARCHAR(50) NOT NULL, -- Calculation variant (A, B, C, Base, Extended...)
|
||||
rev INT DEFAULT 1, -- Revision number
|
||||
qty INT DEFAULT 1, -- Number of servers
|
||||
items JSON NOT NULL, -- [{"lot_name": "CPU_AMD_9654", "quantity": 2, "unit_price": 11500}]
|
||||
total_price DECIMAL(12,2),
|
||||
custom_price DECIMAL(12,2), -- User-defined target price
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (project_id) REFERENCES qt_projects(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (pricelist_id) REFERENCES qt_pricelists(id),
|
||||
UNIQUE KEY (project_id, variant, rev)
|
||||
);
|
||||
```
|
||||
|
||||
### Legacy Tables (will be deprecated)
|
||||
|
||||
```sql
|
||||
-- Users (RBAC disabled in Phase 1-3)
|
||||
CREATE TABLE qt_users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(100) UNIQUE NOT NULL,
|
||||
email VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(255) NOT NULL,
|
||||
role ENUM('viewer', 'editor', 'pricing_admin', 'admin') DEFAULT 'viewer',
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Price overrides (for future use)
|
||||
CREATE TABLE qt_price_overrides (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
lot_name CHAR(255) NOT NULL,
|
||||
price DECIMAL(12,2) NOT NULL,
|
||||
valid_from DATE NOT NULL,
|
||||
valid_until DATE,
|
||||
reason TEXT,
|
||||
created_by INT NOT NULL,
|
||||
FOREIGN KEY (lot_name) REFERENCES lot(lot_name)
|
||||
);
|
||||
|
||||
-- Alerts (for future use)
|
||||
CREATE TABLE qt_pricing_alerts (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
lot_name CHAR(255) NOT NULL,
|
||||
alert_type ENUM('high_demand_stale_price', 'price_spike', 'price_drop', 'no_recent_quotes', 'trending_no_price') NOT NULL,
|
||||
severity ENUM('low', 'medium', 'high', 'critical') DEFAULT 'medium',
|
||||
message TEXT NOT NULL,
|
||||
details JSON,
|
||||
status ENUM('new', 'acknowledged', 'resolved', 'ignored') DEFAULT 'new',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## Local SQLite Database
|
||||
|
||||
Located at `data/quoteforge.db`:
|
||||
|
||||
```sql
|
||||
-- Application settings (connection credentials stored encrypted)
|
||||
CREATE TABLE app_settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
-- Keys: db_host, db_port, db_name, db_user, db_password (encrypted), last_sync
|
||||
|
||||
-- Cached pricelists from server
|
||||
CREATE TABLE local_pricelists (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
server_id INT NOT NULL, -- ID on MariaDB server
|
||||
version TEXT NOT NULL UNIQUE,
|
||||
name TEXT,
|
||||
created_at TEXT,
|
||||
synced_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
is_used INTEGER DEFAULT 0 -- 1 if used by any specification
|
||||
);
|
||||
|
||||
CREATE TABLE local_pricelist_items (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
pricelist_id INTEGER NOT NULL,
|
||||
lot_name TEXT NOT NULL,
|
||||
price REAL NOT NULL,
|
||||
FOREIGN KEY (pricelist_id) REFERENCES local_pricelists(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- Local projects (can be synced to server)
|
||||
CREATE TABLE local_projects (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT UNIQUE NOT NULL,
|
||||
server_id INTEGER, -- NULL if not synced yet
|
||||
opty TEXT,
|
||||
customer_requirement TEXT,
|
||||
name TEXT NOT NULL,
|
||||
notes TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
synced_at TEXT, -- NULL if has local changes
|
||||
sync_status TEXT DEFAULT 'local' -- 'local', 'synced', 'modified'
|
||||
);
|
||||
|
||||
-- Local specifications
|
||||
CREATE TABLE local_specifications (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
uuid TEXT UNIQUE NOT NULL,
|
||||
server_id INTEGER,
|
||||
project_id INTEGER NOT NULL,
|
||||
pricelist_id INTEGER NOT NULL,
|
||||
variant TEXT NOT NULL,
|
||||
rev INTEGER DEFAULT 1,
|
||||
qty INTEGER DEFAULT 1,
|
||||
items TEXT NOT NULL, -- JSON string
|
||||
total_price REAL,
|
||||
custom_price REAL,
|
||||
notes TEXT,
|
||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||
synced_at TEXT,
|
||||
sync_status TEXT DEFAULT 'local',
|
||||
FOREIGN KEY (project_id) REFERENCES local_projects(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (pricelist_id) REFERENCES local_pricelists(id)
|
||||
);
|
||||
|
||||
-- Component cache (for offline search)
|
||||
CREATE TABLE local_components (
|
||||
lot_name TEXT PRIMARY KEY,
|
||||
lot_description TEXT,
|
||||
category TEXT,
|
||||
model TEXT,
|
||||
synced_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
## Key Business Logic
|
||||
|
||||
### 1. Database Connection Setup
|
||||
|
||||
```go
|
||||
// First launch: show setup form
|
||||
// User provides: host, port, database, username, password
|
||||
// Credentials are encrypted and stored in SQLite
|
||||
// Connection is tested before saving
|
||||
|
||||
func SetupConnection(host, port, dbName, user, password string) error {
|
||||
// Test connection to MariaDB
|
||||
// If successful, encrypt and save to SQLite
|
||||
// Create/migrate qt_* tables if user has permissions
|
||||
}
|
||||
|
||||
func CheckWritePermission(db *gorm.DB, tableName string) bool {
|
||||
// Check if current user can INSERT into table
|
||||
// Used to enable/disable pricelist creation UI
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Pricelist Creation
|
||||
|
||||
```go
|
||||
// Create snapshot of current prices from qt_lot_metadata
|
||||
// Version format: YYYY-MM-DD-NNN (NNN = sequential number for the day)
|
||||
|
||||
func CreatePricelist(name string, createdBy string) (*Pricelist, error) {
|
||||
version := generateVersion() // e.g., "2024-01-31-001"
|
||||
expiresAt := time.Now().AddDate(1, 0, 0) // +1 year
|
||||
|
||||
// Copy all prices from qt_lot_metadata
|
||||
// Insert into qt_pricelists and qt_pricelist_items
|
||||
}
|
||||
|
||||
func generateVersion() string {
|
||||
today := time.Now().Format("2006-01-02")
|
||||
// Count existing pricelists for today
|
||||
// Return "YYYY-MM-DD-NNN"
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Pricelist Comparison
|
||||
|
||||
```go
|
||||
type PriceDiff struct {
|
||||
LotName string
|
||||
OldPrice float64
|
||||
NewPrice float64
|
||||
Difference float64
|
||||
PercentDiff float64
|
||||
}
|
||||
|
||||
// Compare two pricelists and return differences
|
||||
func ComparePricelists(oldID, newID int) ([]PriceDiff, error)
|
||||
|
||||
// Compare specification's pricelist with latest available
|
||||
func GetSpecificationPriceDiff(specUUID string) ([]PriceDiff, float64, error) {
|
||||
// Returns item diffs and total price difference
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Specification Upgrade
|
||||
|
||||
```go
|
||||
// Upgrade specification to use newer pricelist
|
||||
func UpgradeSpecificationPricelist(specUUID string, newPricelistID int) error {
|
||||
// Update pricelist_id
|
||||
// Recalculate prices from new pricelist
|
||||
// Increment revision number
|
||||
// Update old pricelist usage_count--
|
||||
// Update new pricelist usage_count++
|
||||
}
|
||||
```
|
||||
|
||||
### 5. Pricelist Cleanup
|
||||
|
||||
```go
|
||||
// Cron job: cleanup old unused pricelists
|
||||
// Run weekly: 0 4 * * 0
|
||||
func CleanupOldPricelists() error {
|
||||
// Delete pricelists where:
|
||||
// - expires_at < NOW()
|
||||
// - usage_count = 0
|
||||
// - is_active = false OR created_at < NOW() - 1 year
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Sync Service
|
||||
|
||||
```go
|
||||
// Sync pricelists from server to local SQLite
|
||||
func SyncPricelists() error {
|
||||
// Fetch all active pricelists from MariaDB
|
||||
// Update local_pricelists table
|
||||
// For pricelists used by local specs, also sync items
|
||||
}
|
||||
|
||||
// Check if sync is needed
|
||||
func NeedSync() bool {
|
||||
// Compare last_sync timestamp with server
|
||||
// Return true if new pricelists available
|
||||
}
|
||||
```
|
||||
|
||||
### 7. Part Number Parsing
|
||||
|
||||
```go
|
||||
// "CPU_AMD_9654" → category="CPU", model="AMD_9654"
|
||||
// "MB_INTEL_4.Sapphire_2S" → category="MB", model="INTEL_4.Sapphire_2S"
|
||||
|
||||
func ParsePartNumber(lotName string) (category, model string) {
|
||||
parts := strings.SplitN(lotName, "_", 2)
|
||||
if len(parts) >= 1 {
|
||||
category = parts[0]
|
||||
}
|
||||
if len(parts) >= 2 {
|
||||
model = parts[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
```
|
||||
|
||||
### 8. Price Calculation Methods
|
||||
|
||||
```go
|
||||
func CalculateMedian(prices []float64) float64
|
||||
func CalculateAverage(prices []float64) float64
|
||||
func CalculateWeightedMedian(prices []PricePoint, decayDays int) float64
|
||||
```
|
||||
|
||||
### 9. Price Freshness
|
||||
|
||||
```go
|
||||
func GetPriceFreshness(daysSinceUpdate int, quoteCount int) string {
|
||||
if daysSinceUpdate < 30 && quoteCount >= 3 {
|
||||
return "fresh" // green
|
||||
} else if daysSinceUpdate < 60 {
|
||||
return "normal" // yellow
|
||||
} else if daysSinceUpdate < 90 {
|
||||
return "stale" // orange
|
||||
}
|
||||
return "critical" // red
|
||||
}
|
||||
```
|
||||
**Pricelist version:** `YYYY-MM-DD-NNN` (e.g., `2024-01-31-001`)
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Setup (no auth required)
|
||||
```
|
||||
GET /setup → DB connection form (if not configured)
|
||||
POST /setup → Save connection settings
|
||||
POST /setup/test → Test connection without saving
|
||||
GET /setup/status → Check if configured and connected
|
||||
```
|
||||
|
||||
### Components
|
||||
```
|
||||
GET /api/components → List with pagination
|
||||
GET /api/components?category=CPU&search=AMD → Filtered list
|
||||
GET /api/components/:lot_name → Single component details
|
||||
GET /api/categories → Category list
|
||||
```
|
||||
|
||||
### Pricelists
|
||||
```
|
||||
GET /api/pricelists → List all pricelists
|
||||
POST /api/pricelists → Create new pricelist (requires write permission)
|
||||
GET /api/pricelists/:id → Pricelist details
|
||||
GET /api/pricelists/:id/items → Pricelist items with pagination
|
||||
DELETE /api/pricelists/:id → Delete pricelist (if usage_count=0)
|
||||
GET /api/pricelists/latest → Get latest active pricelist
|
||||
POST /api/pricelists/compare → Compare two pricelists
|
||||
```
|
||||
|
||||
### Projects
|
||||
```
|
||||
GET /api/projects → List all projects
|
||||
POST /api/projects → Create project
|
||||
GET /api/projects/:uuid → Project with specifications
|
||||
PUT /api/projects/:uuid → Update project
|
||||
DELETE /api/projects/:uuid → Delete project and specs
|
||||
```
|
||||
|
||||
### Specifications
|
||||
```
|
||||
GET /api/projects/:uuid/specs → List specifications
|
||||
POST /api/projects/:uuid/specs → Create specification
|
||||
GET /api/specs/:spec_uuid → Specification details
|
||||
PUT /api/specs/:spec_uuid → Update specification
|
||||
DELETE /api/specs/:spec_uuid → Delete specification
|
||||
POST /api/specs/:spec_uuid/upgrade → Upgrade to new pricelist
|
||||
GET /api/specs/:spec_uuid/diff → Show price diff with latest pricelist
|
||||
POST /api/specs/:spec_uuid/new-revision → Create new revision
|
||||
```
|
||||
|
||||
### Sync
|
||||
```
|
||||
GET /api/sync/status → Sync status (last sync, pending changes)
|
||||
POST /api/sync/pricelists → Sync pricelists from server
|
||||
POST /api/sync/push → Push local changes to server
|
||||
POST /api/sync/pull → Pull all data from server
|
||||
```
|
||||
|
||||
### Export
|
||||
```
|
||||
POST /api/export/xlsx → Export specification as XLSX
|
||||
POST /api/export/pdf → Export specification as PDF (future)
|
||||
GET /api/specs/:uuid/export → Export single spec
|
||||
GET /api/projects/:uuid/export → Export all project specs
|
||||
```
|
||||
|
||||
### htmx Partials
|
||||
```
|
||||
GET /partials/components?category=CPU → Component list HTML
|
||||
GET /partials/spec-items/:spec_uuid → Specification items HTML
|
||||
GET /partials/price-diff/:spec_uuid → Price diff table HTML
|
||||
GET /partials/project-specs/:project_uuid → Project specifications list
|
||||
```
|
||||
|
||||
## Frontend Guidelines
|
||||
|
||||
- **Mobile-first** design
|
||||
- Use **htmx** for interactivity (hx-get, hx-post, hx-target, hx-swap)
|
||||
- Use **Tailwind CSS** via CDN
|
||||
- Minimal custom JavaScript
|
||||
- Color scheme for price freshness:
|
||||
- `text-green-600 bg-green-50` - fresh
|
||||
- `text-yellow-600 bg-yellow-50` - normal
|
||||
- `text-orange-600 bg-orange-50` - stale
|
||||
- `text-red-600 bg-red-50` - critical
|
||||
- Sync status indicator in header
|
||||
- Offline mode indicator when server unavailable
|
||||
| Group | Endpoints |
|
||||
|-------|-----------|
|
||||
| Setup | GET/POST /setup, POST /setup/test |
|
||||
| Components | GET /api/components, /api/categories |
|
||||
| Pricelists | CRUD /api/pricelists, GET /latest, POST /compare |
|
||||
| Projects | CRUD /api/projects/:uuid (Phase 3) |
|
||||
| Specs | CRUD /api/specs/:uuid, POST /upgrade, GET /diff (Phase 3) |
|
||||
| Sync | GET /status, POST /components, /pricelists, /push, /pull, /resolve-conflict |
|
||||
| Export | GET /api/specs/:uuid/export, /api/projects/:uuid/export |
|
||||
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
# Run development server
|
||||
go run ./cmd/server
|
||||
|
||||
# Run importer (one-time setup)
|
||||
go run ./cmd/importer
|
||||
|
||||
# Run cron jobs manually
|
||||
go run ./cmd/cron -job=cleanup-pricelists # Remove old unused pricelists
|
||||
go run ./cmd/cron -job=update-prices # Recalculate all prices
|
||||
go run ./cmd/cron -job=update-popularity # Update popularity scores
|
||||
|
||||
# Build for production
|
||||
go run ./cmd/server # Dev server
|
||||
go run ./cmd/cron -job=X # cleanup-pricelists | update-prices | update-popularity
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge ./cmd/server
|
||||
CGO_ENABLED=0 go build -ldflags="-s -w" -o bin/quoteforge-cron ./cmd/cron
|
||||
|
||||
# Run tests
|
||||
go test ./...
|
||||
```
|
||||
|
||||
## Cron Jobs
|
||||
|
||||
- **Pricelist cleanup**: Weekly on Sunday at 4 AM (0 4 * * 0)
|
||||
- **Price updates**: Daily at 2 AM (0 2 * * *)
|
||||
- **Popularity score updates**: Daily at 3 AM (0 3 * * *)
|
||||
|
||||
## Code Style
|
||||
- gofmt, structured logging (slog), wrap errors with context
|
||||
- snake_case files, PascalCase types
|
||||
- RBAC disabled: DB username = user_id via `models.EnsureDBUser()`
|
||||
|
||||
- Use standard Go formatting (gofmt)
|
||||
- Error handling: always check errors, wrap with context
|
||||
- Logging: use structured logging (slog)
|
||||
- Comments: in Russian or English, be consistent
|
||||
- File naming: snake_case for files, PascalCase for types
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### From qt_configurations to qt_specifications
|
||||
|
||||
When migrating existing data:
|
||||
1. Create a default project for orphan configurations
|
||||
2. Create initial pricelist from current qt_lot_metadata prices
|
||||
3. Convert qt_configurations to qt_specifications with default variant="Base", rev=1
|
||||
4. Link all specs to initial pricelist
|
||||
|
||||
### RBAC Disabled
|
||||
|
||||
During Phase 1-3, RBAC is disabled:
|
||||
- No login required
|
||||
- All users have full access
|
||||
- Write permissions determined by MariaDB user privileges
|
||||
- qt_users table exists but not used
|
||||
## UI Guidelines
|
||||
- htmx (hx-get/post/target/swap), Tailwind CDN
|
||||
- Freshness colors: green (fresh) → yellow → orange → red (critical)
|
||||
- Sync status + offline indicator in header
|
||||
|
||||
Reference in New Issue
Block a user