This commit is contained in:
lejay
2026-02-13 00:21:53 +01:00
parent 81d2412835
commit 6923c319a1
85 changed files with 17836 additions and 0 deletions
+62
View File
@@ -0,0 +1,62 @@
import initSqlJs from 'sql.js';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dataDir = path.join(__dirname, '..', 'data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
const dbPath = process.env.DATABASE_PATH || path.join(dataDir, 'configurateur.db');
let db = null;
function toSqlitePlaceholders(sql) {
return sql.replace(/\$(\d+)/g, '?');
}
function save() {
if (db && dbPath) {
const data = db.export();
fs.writeFileSync(dbPath, Buffer.from(data));
}
}
async function getDb() {
if (db) return db;
const SQL = await initSqlJs({
locateFile: (file) => path.join(__dirname, '..', 'node_modules', 'sql.js', 'dist', file)
});
let buffer;
if (fs.existsSync(dbPath)) {
buffer = fs.readFileSync(dbPath);
}
db = new SQL.Database(buffer || null);
return db;
}
export const dbReady = getDb();
export const query = async (sql, params = []) => {
const database = await getDb();
const sqliteSql = toSqlitePlaceholders(sql);
const stmt = database.prepare(sqliteSql);
stmt.bind(Array.isArray(params) ? params : []);
const rows = [];
while (stmt.step()) rows.push(stmt.getAsObject());
stmt.free();
return { rows };
};
export const run = async (sql, params = []) => {
const database = await getDb();
const sqliteSql = toSqlitePlaceholders(sql);
database.run(sqliteSql, Array.isArray(params) ? params : []);
save();
return {};
};
const pool = {
query: (sql, params) => query(sql, params)
};
export default pool;
+19
View File
@@ -0,0 +1,19 @@
import initSqlJs from 'sql.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dataDir = path.join(__dirname, '..', 'data');
if (!fs.existsSync(dataDir)) fs.mkdirSync(dataDir, { recursive: true });
const dbPath = process.env.DATABASE_PATH || path.join(dataDir, 'configurateur.db');
const SQL = await initSqlJs({
locateFile: (file) => path.join(__dirname, '..', 'node_modules', 'sql.js', 'dist', file)
});
const db = new SQL.Database();
const schema = fs.readFileSync(path.join(__dirname, 'schema.sql'), 'utf8');
db.exec(schema);
fs.writeFileSync(dbPath, Buffer.from(db.export()));
db.close();
console.log('Schema applied.');
+56
View File
@@ -0,0 +1,56 @@
/**
* Run pending migrations on an existing database
*/
import initSqlJs from 'sql.js';
import path from 'path';
import fs from 'fs';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dataDir = path.join(__dirname, '..', 'data');
const dbPath = process.env.DATABASE_PATH || path.join(dataDir, 'configurateur.db');
const migrationsDir = path.join(__dirname, 'migrations');
async function migrate() {
if (!fs.existsSync(dbPath)) {
console.error(`Database not found at ${dbPath}. Run db:init first.`);
process.exit(1);
}
const SQL = await initSqlJs({
locateFile: (file) => path.join(__dirname, '..', 'node_modules', 'sql.js', 'dist', file)
});
const buffer = fs.readFileSync(dbPath);
const db = new SQL.Database(buffer);
// Get migration files
const migrationFiles = fs.readdirSync(migrationsDir)
.filter(f => f.endsWith('.sql'))
.sort();
console.log(`Found ${migrationFiles.length} migration(s)`);
for (const file of migrationFiles) {
console.log(`Running migration: ${file}`);
const sql = fs.readFileSync(path.join(migrationsDir, file), 'utf8');
try {
db.exec(sql);
console.log(` ✓ ${file} applied`);
} catch (err) {
// IF NOT EXISTS should prevent errors on re-run
console.log(` ✓ ${file} (already applied or no changes)`);
}
}
// Save database
fs.writeFileSync(dbPath, Buffer.from(db.export()));
db.close();
console.log('Migrations complete.');
}
migrate().catch(err => {
console.error('Migration failed:', err);
process.exit(1);
});
@@ -0,0 +1,25 @@
-- Migration: Add Formex materials and sync logs tables
-- Run this on existing databases to add crawler support
-- Matériaux Formex (données scrapées)
CREATE TABLE IF NOT EXISTS formex_materials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
formex_code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
collection TEXT NOT NULL,
width_mm INTEGER,
height_mm INTEGER,
thickness_mm INTEGER,
formex_url TEXT,
last_synced_at TEXT DEFAULT (datetime('now'))
);
-- Journal de synchronisation
CREATE TABLE IF NOT EXISTS sync_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
status TEXT NOT NULL,
items_count INTEGER,
error TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
@@ -0,0 +1,34 @@
-- Migration: Enhanced Formex materials with dimensions, variants, images
-- Adds support for multiple dimension formats and color variants per product
-- Add new columns to formex_materials
ALTER TABLE formex_materials ADD COLUMN category TEXT;
ALTER TABLE formex_materials ADD COLUMN image_url TEXT;
ALTER TABLE formex_materials ADD COLUMN price_chf REAL;
ALTER TABLE formex_materials ADD COLUMN active INTEGER DEFAULT 1;
-- Dimension variants (multiple dimensions per material)
CREATE TABLE IF NOT EXISTS formex_dimensions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
material_id INTEGER NOT NULL REFERENCES formex_materials(id) ON DELETE CASCADE,
width_mm INTEGER NOT NULL,
height_mm INTEGER NOT NULL,
thickness_mm INTEGER NOT NULL,
is_default INTEGER DEFAULT 0
);
-- Color/decor variants (linked products)
CREATE TABLE IF NOT EXISTS formex_variants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
material_id INTEGER NOT NULL REFERENCES formex_materials(id) ON DELETE CASCADE,
variant_code TEXT NOT NULL,
variant_name TEXT NOT NULL,
variant_url TEXT,
image_url TEXT
);
-- Create indexes for better query performance
CREATE INDEX IF NOT EXISTS idx_formex_dimensions_material ON formex_dimensions(material_id);
CREATE INDEX IF NOT EXISTS idx_formex_variants_material ON formex_variants(material_id);
CREATE INDEX IF NOT EXISTS idx_formex_materials_active ON formex_materials(active);
CREATE INDEX IF NOT EXISTS idx_formex_materials_collection ON formex_materials(collection);
+12
View File
@@ -0,0 +1,12 @@
-- Migration: Room configurations table for AI-analyzed room layouts
-- Stores room analysis results linked to orders
CREATE TABLE IF NOT EXISTS room_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER REFERENCES orders(id),
image_path TEXT,
analysis_json TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_room_configs_order ON room_configs(order_id);
+72
View File
@@ -0,0 +1,72 @@
-- Migration: Modules, hardware, and variants for V2 configurator
-- Catalogue de modules standards
CREATE TABLE IF NOT EXISTS modules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
category TEXT NOT NULL, -- 'penderie', 'rayonnage', 'tiroirs', 'mixte'
width_mm INTEGER NOT NULL,
height_mm INTEGER,
depth_mm INTEGER,
shelves_count INTEGER DEFAULT 0,
drawers_count INTEGER DEFAULT 0,
has_rod INTEGER DEFAULT 0, -- penderie
customizable INTEGER DEFAULT 1,
base_price_chf REAL NOT NULL,
image_url TEXT,
active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
);
-- Quincaillerie Blum
CREATE TABLE IF NOT EXISTS blum_hardware (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'drawer_system', 'hinge', 'lift_system', 'runner'
variant TEXT, -- 'legrabox', 'tandembox', 'clip_top', etc.
price_chf REAL NOT NULL,
specs_json TEXT,
image_url TEXT,
active INTEGER DEFAULT 1
);
-- Variantes poignées
CREATE TABLE IF NOT EXISTS handle_variants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
style TEXT, -- 'moderne', 'classique', 'minimaliste', etc.
length_mm INTEGER,
price_chf REAL NOT NULL,
image_url TEXT,
active INTEGER DEFAULT 1
);
-- Variantes charnières
CREATE TABLE IF NOT EXISTS hinge_variants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'vissee', 'plantee'
brand TEXT DEFAULT 'Blum',
price_chf REAL NOT NULL,
active INTEGER DEFAULT 1
);
-- Configuration couleurs par élément (pour sauvegarder les configs)
CREATE TABLE IF NOT EXISTS element_colors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE,
element_type TEXT NOT NULL, -- 'plintheBas', 'portes', 'cotes', etc.
finish_id INTEGER NOT NULL,
finish_label TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_modules_category ON modules(category);
CREATE INDEX IF NOT EXISTS idx_modules_active ON modules(active);
CREATE INDEX IF NOT EXISTS idx_blum_hardware_type ON blum_hardware(type);
CREATE INDEX IF NOT EXISTS idx_handle_variants_style ON handle_variants(style);
CREATE INDEX IF NOT EXISTS idx_hinge_variants_type ON hinge_variants(type);
CREATE INDEX IF NOT EXISTS idx_element_colors_order ON element_colors(order_id);
@@ -0,0 +1,5 @@
-- Add dominant_color column to formex_materials for 3D preview color representation
ALTER TABLE formex_materials ADD COLUMN dominant_color TEXT;
-- Create index for quick lookups
CREATE INDEX IF NOT EXISTS idx_formex_materials_dominant_color ON formex_materials(dominant_color);
+202
View File
@@ -0,0 +1,202 @@
-- Configurateur armoires sur mesure — Schéma V1 (SQLite)
-- Paramètres globaux (forfait kit, plinthe, épaisseur panneaux, etc.)
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
-- Produits / gammes (ex. SIGNATURE)
CREATE TABLE IF NOT EXISTS products (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
label TEXT NOT NULL,
active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
);
-- Types de blocs (penderie, rayonnage, mixte vertical, mixte horizontal)
CREATE TABLE IF NOT EXISTS block_types (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
label TEXT NOT NULL,
base_price_chf REAL NOT NULL,
width_min_mm INTEGER NOT NULL,
width_max_mm INTEGER NOT NULL,
active INTEGER DEFAULT 1
);
-- Formats de panneaux (par finition : dimensions + prix panneau entier)
CREATE TABLE IF NOT EXISTS panel_formats (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
width_mm INTEGER NOT NULL,
height_mm INTEGER NOT NULL,
price_chf REAL NOT NULL
);
-- Finitions visibles (stratifié décor, couleur, effet bois, etc.)
CREATE TABLE IF NOT EXISTS finishes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
label TEXT NOT NULL,
panel_format_id INTEGER NOT NULL REFERENCES panel_formats(id),
active INTEGER DEFAULT 1
);
-- Options de service (prise de mesure, montage)
CREATE TABLE IF NOT EXISTS service_options (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
label TEXT NOT NULL,
price_chf REAL NOT NULL,
price_type TEXT NOT NULL CHECK (price_type IN ('fixed', 'per_hour', 'per_block')),
active INTEGER DEFAULT 1
);
-- Commandes / leads (config snapshot + coordonnées)
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
config TEXT NOT NULL,
total_chf REAL,
email TEXT,
name TEXT,
phone TEXT,
notes TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- Matériaux Formex (données scrapées)
CREATE TABLE IF NOT EXISTS formex_materials (
id INTEGER PRIMARY KEY AUTOINCREMENT,
formex_code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
collection TEXT NOT NULL,
category TEXT,
width_mm INTEGER,
height_mm INTEGER,
thickness_mm INTEGER,
image_url TEXT,
formex_url TEXT,
price_chf REAL,
dominant_color TEXT,
active INTEGER DEFAULT 1,
last_synced_at TEXT DEFAULT (datetime('now'))
);
-- Dimension variants (multiple dimensions per material)
CREATE TABLE IF NOT EXISTS formex_dimensions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
material_id INTEGER NOT NULL REFERENCES formex_materials(id) ON DELETE CASCADE,
width_mm INTEGER NOT NULL,
height_mm INTEGER NOT NULL,
thickness_mm INTEGER NOT NULL,
is_default INTEGER DEFAULT 0
);
-- Color/decor variants (linked products)
CREATE TABLE IF NOT EXISTS formex_variants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
material_id INTEGER NOT NULL REFERENCES formex_materials(id) ON DELETE CASCADE,
variant_code TEXT NOT NULL,
variant_name TEXT NOT NULL,
variant_url TEXT,
image_url TEXT
);
CREATE INDEX IF NOT EXISTS idx_formex_dimensions_material ON formex_dimensions(material_id);
CREATE INDEX IF NOT EXISTS idx_formex_variants_material ON formex_variants(material_id);
CREATE INDEX IF NOT EXISTS idx_formex_materials_active ON formex_materials(active);
CREATE INDEX IF NOT EXISTS idx_formex_materials_collection ON formex_materials(collection);
-- Journal de synchronisation
CREATE TABLE IF NOT EXISTS sync_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
source TEXT NOT NULL,
status TEXT NOT NULL,
items_count INTEGER,
error TEXT,
created_at TEXT DEFAULT (datetime('now'))
);
-- Room configurations (AI-analyzed room layouts)
CREATE TABLE IF NOT EXISTS room_configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER REFERENCES orders(id),
image_path TEXT,
analysis_json TEXT NOT NULL,
created_at TEXT DEFAULT (datetime('now'))
);
CREATE INDEX IF NOT EXISTS idx_room_configs_order ON room_configs(order_id);
-- Catalogue de modules standards
CREATE TABLE IF NOT EXISTS modules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
category TEXT NOT NULL, -- 'penderie', 'rayonnage', 'tiroirs', 'mixte'
width_mm INTEGER NOT NULL,
height_mm INTEGER,
depth_mm INTEGER,
shelves_count INTEGER DEFAULT 0,
drawers_count INTEGER DEFAULT 0,
has_rod INTEGER DEFAULT 0, -- penderie
customizable INTEGER DEFAULT 1,
base_price_chf REAL NOT NULL,
image_url TEXT,
active INTEGER DEFAULT 1,
created_at TEXT DEFAULT (datetime('now'))
);
-- Quincaillerie Blum
CREATE TABLE IF NOT EXISTS blum_hardware (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'drawer_system', 'hinge', 'lift_system', 'runner'
variant TEXT, -- 'legrabox', 'tandembox', 'clip_top', etc.
price_chf REAL NOT NULL,
specs_json TEXT,
image_url TEXT,
active INTEGER DEFAULT 1
);
-- Variantes poignées
CREATE TABLE IF NOT EXISTS handle_variants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
style TEXT, -- 'moderne', 'classique', 'minimaliste', etc.
length_mm INTEGER,
price_chf REAL NOT NULL,
image_url TEXT,
active INTEGER DEFAULT 1
);
-- Variantes charnières
CREATE TABLE IF NOT EXISTS hinge_variants (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL, -- 'vissee', 'plantee'
brand TEXT DEFAULT 'Blum',
price_chf REAL NOT NULL,
active INTEGER DEFAULT 1
);
-- Configuration couleurs par élément (pour sauvegarder les configs)
CREATE TABLE IF NOT EXISTS element_colors (
id INTEGER PRIMARY KEY AUTOINCREMENT,
order_id INTEGER REFERENCES orders(id) ON DELETE CASCADE,
element_type TEXT NOT NULL, -- 'plintheBas', 'portes', 'cotes', etc.
finish_id INTEGER NOT NULL,
finish_label TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_modules_category ON modules(category);
CREATE INDEX IF NOT EXISTS idx_modules_active ON modules(active);
CREATE INDEX IF NOT EXISTS idx_blum_hardware_type ON blum_hardware(type);
CREATE INDEX IF NOT EXISTS idx_handle_variants_style ON handle_variants(style);
CREATE INDEX IF NOT EXISTS idx_hinge_variants_type ON hinge_variants(type);
CREATE INDEX IF NOT EXISTS idx_element_colors_order ON element_colors(order_id);
+23
View File
@@ -0,0 +1,23 @@
import initSqlJs from 'sql.js';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '..', 'data', 'configurateur.db');
if (!fs.existsSync(dbPath)) {
console.error('Database file not found. Run npm run db:init first.');
process.exit(1);
}
const SQL = await initSqlJs({
locateFile: (file) => path.join(__dirname, '..', 'node_modules', 'sql.js', 'dist', file)
});
const buffer = fs.readFileSync(dbPath);
const db = new SQL.Database(buffer);
const seedSql = fs.readFileSync(path.join(__dirname, 'seeds.sql'), 'utf8');
db.exec(seedSql);
fs.writeFileSync(dbPath, Buffer.from(db.export()));
db.close();
console.log('Seeds applied.');
+75
View File
@@ -0,0 +1,75 @@
-- Données initiales V1 (SQLite)
INSERT OR REPLACE INTO settings (key, value) VALUES
('kit_preparation_fee_chf', '350'),
('panel_thickness_mm', '19'),
('plinth_height_min_mm', '60'),
('plinth_height_max_mm', '160'),
('niche_clearance_multiplier', '2'),
('min_clearance_mm', '20'),
('sliding_doors_min_width_mm', '1200');
INSERT OR IGNORE INTO products (code, label, active) VALUES
('SIGNATURE', 'Armoire SIGNATURE', 1);
INSERT OR IGNORE INTO block_types (code, label, base_price_chf, width_min_mm, width_max_mm) VALUES
('penderie', 'Penderie', 550, 400, 1200),
('rayonnage', 'Rayonnage', 550, 400, 1200),
('mixte_vertical', 'Mixte vertical', 550, 400, 1200),
('mixte_horizontal', 'Mixte horizontal', 550, 400, 1200);
INSERT INTO panel_formats (name, width_mm, height_mm, price_chf) VALUES
('Stratifié standard', 2800, 2070, 85),
('Stratifié couleur', 2800, 2070, 95),
('Stratifié effet bois', 2800, 2070, 110),
('Stratifié vieux bois', 2800, 2070, 120);
INSERT OR IGNORE INTO finishes (code, label, panel_format_id) VALUES
('stratifie_decor', 'Stratifié décor', (SELECT id FROM panel_formats WHERE name = 'Stratifié standard' LIMIT 1)),
('stratifie_couleur', 'Stratifié couleur', (SELECT id FROM panel_formats WHERE name = 'Stratifié couleur' LIMIT 1)),
('stratifie_effet_bois', 'Stratifié effet bois', (SELECT id FROM panel_formats WHERE name = 'Stratifié effet bois' LIMIT 1)),
('stratifie_vieux_bois', 'Stratifié vieux bois', (SELECT id FROM panel_formats WHERE name = 'Stratifié vieux bois' LIMIT 1));
INSERT OR IGNORE INTO service_options (code, label, price_chf, price_type) VALUES
('prise_mesure', 'Prise de mesure', 95, 'per_hour'),
('montage', 'Montage', 95, 'per_hour');
-- Modules standards V2
INSERT OR IGNORE INTO modules (code, name, category, width_mm, height_mm, depth_mm, shelves_count, drawers_count, has_rod, customizable, base_price_chf) VALUES
('penderie_simple', 'Penderie simple', 'penderie', 600, NULL, 580, 1, 0, 1, 1, 450),
('penderie_double', 'Penderie double', 'penderie', 900, NULL, 580, 2, 0, 1, 1, 550),
('penderie_large', 'Penderie large', 'penderie', 1200, NULL, 580, 2, 0, 1, 1, 650),
('rayonnage_4', 'Rayonnage 4 étagères', 'rayonnage', 600, NULL, 580, 4, 0, 0, 1, 380),
('rayonnage_5', 'Rayonnage 5 étagères', 'rayonnage', 600, NULL, 580, 5, 0, 0, 1, 420),
('rayonnage_6', 'Rayonnage 6 étagères', 'rayonnage', 600, NULL, 580, 6, 0, 0, 1, 460),
('tiroirs_3', 'Bloc 3 tiroirs', 'tiroirs', 600, 600, 580, 0, 3, 0, 0, 520),
('tiroirs_4', 'Bloc 4 tiroirs', 'tiroirs', 600, 800, 580, 0, 4, 0, 0, 620),
('tiroirs_5', 'Bloc 5 tiroirs', 'tiroirs', 600, 1000, 580, 0, 5, 0, 0, 720),
('mixte_penderie_tiroirs', 'Penderie + tiroirs', 'mixte', 600, NULL, 580, 0, 2, 1, 1, 580),
('mixte_rayonnage_tiroirs', 'Rayonnage + tiroirs', 'mixte', 600, NULL, 580, 3, 2, 0, 1, 550),
('mixte_complet', 'Module complet', 'mixte', 900, NULL, 580, 2, 2, 1, 1, 720);
-- Quincaillerie Blum
INSERT OR IGNORE INTO blum_hardware (code, name, type, variant, price_chf, specs_json) VALUES
('legrabox_pure', 'LEGRABOX pure', 'drawer_system', 'legrabox', 85, '{"height_mm": 177, "load_kg": 70, "soft_close": true}'),
('legrabox_free', 'LEGRABOX free', 'drawer_system', 'legrabox', 95, '{"height_mm": 177, "load_kg": 70, "soft_close": true, "tip_on": true}'),
('tandembox_antaro', 'TANDEMBOX antaro', 'drawer_system', 'tandembox', 65, '{"height_mm": 83, "load_kg": 65, "soft_close": true}'),
('tandembox_intivo', 'TANDEMBOX intivo', 'drawer_system', 'tandembox', 75, '{"height_mm": 83, "load_kg": 65, "soft_close": true}'),
('movento_runner', 'MOVENTO coulisse', 'runner', 'movento', 45, '{"load_kg": 60, "soft_close": true}'),
('clip_top_soft', 'CLIP top BLUMOTION', 'hinge', 'clip_top', 12, '{"opening_angle": 110, "soft_close": true}'),
('clip_top_standard', 'CLIP top Standard', 'hinge', 'clip_top', 8, '{"opening_angle": 110, "soft_close": false}'),
('aventos_hf', 'AVENTOS HF', 'lift_system', 'aventos', 180, '{"type": "bi-fold", "soft_close": true}'),
('aventos_hl', 'AVENTOS HL', 'lift_system', 'aventos', 150, '{"type": "lift_up", "soft_close": true}');
-- Variantes poignées
INSERT OR IGNORE INTO handle_variants (code, name, style, length_mm, price_chf) VALUES
('barre_moderne', 'Barre moderne', 'moderne', 160, 25),
('barre_longue', 'Barre longue', 'moderne', 320, 35),
('bouton_rond', 'Bouton rond', 'classique', 35, 15),
('coquille', 'Coquille encastrée', 'minimaliste', 100, 28),
('profil_alu', 'Profil aluminium', 'minimaliste', NULL, 45);
-- Variantes charnières
INSERT OR IGNORE INTO hinge_variants (code, name, type, brand, price_chf) VALUES
('blum_vissee', 'Blum vissée', 'vissee', 'Blum', 12),
('blum_plantee', 'Blum plantée (INSERTA)', 'plantee', 'Blum', 18);