diff --git a/public/images/each-logo.png b/public/images/each-logo.png
new file mode 100644
index 0000000..0328b42
Binary files /dev/null and b/public/images/each-logo.png differ
diff --git a/public/images/linse-icon.png b/public/images/linse-icon.png
new file mode 100644
index 0000000..02bfb8e
Binary files /dev/null and b/public/images/linse-icon.png differ
diff --git a/public/images/linse-logo.png b/public/images/linse-logo.png
new file mode 100644
index 0000000..aebd460
Binary files /dev/null and b/public/images/linse-logo.png differ
diff --git a/public/images/linse-symbol.png b/public/images/linse-symbol.png
new file mode 100644
index 0000000..c091e36
Binary files /dev/null and b/public/images/linse-symbol.png differ
diff --git a/public/placeholder-logo.png b/public/placeholder-logo.png
new file mode 100644
index 0000000..8a792ac
Binary files /dev/null and b/public/placeholder-logo.png differ
diff --git a/public/placeholder-logo.svg b/public/placeholder-logo.svg
new file mode 100644
index 0000000..b1695aa
--- /dev/null
+++ b/public/placeholder-logo.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/public/placeholder-user.jpg b/public/placeholder-user.jpg
new file mode 100644
index 0000000..6fa7543
Binary files /dev/null and b/public/placeholder-user.jpg differ
diff --git a/public/placeholder.jpg b/public/placeholder.jpg
new file mode 100644
index 0000000..6bfe963
Binary files /dev/null and b/public/placeholder.jpg differ
diff --git a/public/placeholder.svg b/public/placeholder.svg
new file mode 100644
index 0000000..e763910
--- /dev/null
+++ b/public/placeholder.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/scripts/001-create-tables.sql b/scripts/001-create-tables.sql
new file mode 100644
index 0000000..a3f6734
--- /dev/null
+++ b/scripts/001-create-tables.sql
@@ -0,0 +1,62 @@
+-- Create categories table
+CREATE TABLE IF NOT EXISTS categories (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Create components table
+CREATE TABLE IF NOT EXISTS components (
+ id SERIAL PRIMARY KEY,
+ part_number VARCHAR(100) NOT NULL UNIQUE,
+ name VARCHAR(255) NOT NULL,
+ description TEXT,
+ category_id INTEGER REFERENCES categories(id) ON DELETE SET NULL,
+ manufacturer VARCHAR(255),
+ package_type VARCHAR(100),
+ unit_price NUMERIC(12, 4) DEFAULT 0,
+ stock_quantity INTEGER DEFAULT 0,
+ min_stock_level INTEGER DEFAULT 0,
+ datasheet_url TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Create products table
+CREATE TABLE IF NOT EXISTS products (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ sku VARCHAR(100) NOT NULL UNIQUE,
+ description TEXT,
+ version VARCHAR(50) DEFAULT '1.0',
+ status VARCHAR(50) DEFAULT 'draft',
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Create bom_items table (links components to products)
+CREATE TABLE IF NOT EXISTS bom_items (
+ id SERIAL PRIMARY KEY,
+ product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE CASCADE,
+ component_id INTEGER NOT NULL REFERENCES components(id) ON DELETE CASCADE,
+ quantity INTEGER NOT NULL DEFAULT 1,
+ reference_designator VARCHAR(255),
+ notes TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ UNIQUE(product_id, component_id, reference_designator)
+);
+
+-- Seed some default categories
+INSERT INTO categories (name, description) VALUES
+ ('Resistors', 'Fixed and variable resistors'),
+ ('Capacitors', 'Ceramic, electrolytic, and film capacitors'),
+ ('Inductors', 'Coils, chokes, and transformers'),
+ ('Diodes', 'Signal, rectifier, Zener, and Schottky diodes'),
+ ('Transistors', 'BJT, MOSFET, and JFET transistors'),
+ ('ICs', 'Integrated circuits and microcontrollers'),
+ ('Connectors', 'Headers, sockets, and plug connectors'),
+ ('Crystals & Oscillators', 'Quartz crystals and oscillator modules'),
+ ('LEDs', 'Light emitting diodes and displays'),
+ ('Sensors', 'Temperature, pressure, and motion sensors')
+ON CONFLICT (name) DO NOTHING;
diff --git a/scripts/002-add-supplier-fields.sql b/scripts/002-add-supplier-fields.sql
new file mode 100644
index 0000000..2b69338
--- /dev/null
+++ b/scripts/002-add-supplier-fields.sql
@@ -0,0 +1,3 @@
+-- Add supplier fields to components table
+ALTER TABLE components ADD COLUMN IF NOT EXISTS supplier VARCHAR(255);
+ALTER TABLE components ADD COLUMN IF NOT EXISTS supplier_part_number VARCHAR(255);
diff --git a/scripts/003-add-shelf-location.sql b/scripts/003-add-shelf-location.sql
new file mode 100644
index 0000000..a5f78b2
--- /dev/null
+++ b/scripts/003-add-shelf-location.sql
@@ -0,0 +1 @@
+ALTER TABLE components ADD COLUMN IF NOT EXISTS shelf_location VARCHAR(100);
diff --git a/scripts/004-create-stock-transactions.sql b/scripts/004-create-stock-transactions.sql
new file mode 100644
index 0000000..8af6e16
--- /dev/null
+++ b/scripts/004-create-stock-transactions.sql
@@ -0,0 +1,17 @@
+CREATE TABLE IF NOT EXISTS stock_transactions (
+ id SERIAL PRIMARY KEY,
+ component_id INTEGER NOT NULL REFERENCES components(id) ON DELETE CASCADE,
+ transaction_type VARCHAR(20) NOT NULL DEFAULT 'purchase',
+ quantity INTEGER NOT NULL,
+ unit_cost NUMERIC(12,4) DEFAULT 0,
+ total_cost NUMERIC(12,4) DEFAULT 0,
+ supplier VARCHAR(255),
+ supplier_part_number VARCHAR(100),
+ order_reference VARCHAR(255),
+ notes TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_stock_tx_component ON stock_transactions(component_id);
+CREATE INDEX IF NOT EXISTS idx_stock_tx_created ON stock_transactions(created_at DESC);
+CREATE INDEX IF NOT EXISTS idx_stock_tx_type ON stock_transactions(transaction_type);
diff --git a/scripts/005-fix-stock-column-name.sql b/scripts/005-fix-stock-column-name.sql
new file mode 100644
index 0000000..478af7d
--- /dev/null
+++ b/scripts/005-fix-stock-column-name.sql
@@ -0,0 +1,2 @@
+ALTER TABLE stock_transactions RENAME COLUMN order_reference TO supplier_order_ref;
+ALTER TABLE stock_transactions DROP COLUMN IF EXISTS supplier_part_number;
diff --git a/scripts/006-create-customers-and-orders.sql b/scripts/006-create-customers-and-orders.sql
new file mode 100644
index 0000000..ecc7f14
--- /dev/null
+++ b/scripts/006-create-customers-and-orders.sql
@@ -0,0 +1,45 @@
+-- Customers table
+CREATE TABLE IF NOT EXISTS customers (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ code VARCHAR(50) UNIQUE NOT NULL,
+ email VARCHAR(255),
+ phone VARCHAR(50),
+ address TEXT,
+ contact_person VARCHAR(255),
+ notes TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Purchase Orders table
+CREATE TABLE IF NOT EXISTS purchase_orders (
+ id SERIAL PRIMARY KEY,
+ po_number VARCHAR(100) UNIQUE NOT NULL,
+ customer_id INTEGER NOT NULL REFERENCES customers(id) ON DELETE CASCADE,
+ status VARCHAR(20) DEFAULT 'draft' CHECK (status IN ('draft', 'confirmed', 'in_production', 'completed', 'cancelled')),
+ order_date DATE DEFAULT CURRENT_DATE,
+ due_date DATE,
+ notes TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Purchase Order Items (line items linking products with quantities)
+CREATE TABLE IF NOT EXISTS purchase_order_items (
+ id SERIAL PRIMARY KEY,
+ purchase_order_id INTEGER NOT NULL REFERENCES purchase_orders(id) ON DELETE CASCADE,
+ product_id INTEGER NOT NULL REFERENCES products(id) ON DELETE RESTRICT,
+ quantity INTEGER NOT NULL DEFAULT 1,
+ unit_price NUMERIC(12, 4) DEFAULT 0,
+ notes TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+-- Indexes
+CREATE INDEX IF NOT EXISTS idx_purchase_orders_customer_id ON purchase_orders(customer_id);
+CREATE INDEX IF NOT EXISTS idx_purchase_orders_status ON purchase_orders(status);
+CREATE INDEX IF NOT EXISTS idx_purchase_orders_po_number ON purchase_orders(po_number);
+CREATE INDEX IF NOT EXISTS idx_purchase_order_items_po_id ON purchase_order_items(purchase_order_id);
+CREATE INDEX IF NOT EXISTS idx_purchase_order_items_product_id ON purchase_order_items(product_id);
+CREATE INDEX IF NOT EXISTS idx_customers_code ON customers(code);
diff --git a/scripts/007-fix-purchase-orders.sql b/scripts/007-fix-purchase-orders.sql
new file mode 100644
index 0000000..f88899f
--- /dev/null
+++ b/scripts/007-fix-purchase-orders.sql
@@ -0,0 +1,7 @@
+-- Add missing customer_ref column
+ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS customer_ref VARCHAR(255);
+
+-- Update status check constraint to include 'shipped'
+ALTER TABLE purchase_orders DROP CONSTRAINT IF EXISTS purchase_orders_status_check;
+ALTER TABLE purchase_orders ADD CONSTRAINT purchase_orders_status_check
+ CHECK (status IN ('draft', 'confirmed', 'in_production', 'shipped', 'completed', 'cancelled'));
diff --git a/scripts/008-create-packing-lists.sql b/scripts/008-create-packing-lists.sql
new file mode 100644
index 0000000..e637797
--- /dev/null
+++ b/scripts/008-create-packing-lists.sql
@@ -0,0 +1,30 @@
+-- Packing lists for shipping finished products
+CREATE TABLE IF NOT EXISTS packing_lists (
+ id SERIAL PRIMARY KEY,
+ packing_list_number VARCHAR(100) UNIQUE NOT NULL,
+ purchase_order_id INTEGER NOT NULL REFERENCES purchase_orders(id),
+ customer_id INTEGER NOT NULL REFERENCES customers(id),
+ product_id INTEGER NOT NULL,
+ quantity_shipped INTEGER NOT NULL DEFAULT 0,
+ status VARCHAR(20) NOT NULL DEFAULT 'draft' CHECK (status IN ('draft', 'finalized')),
+ shipped_date DATE,
+ notes TEXT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+-- Serial numbers for each packing list
+CREATE TABLE IF NOT EXISTS packing_list_serials (
+ id SERIAL PRIMARY KEY,
+ packing_list_id INTEGER NOT NULL REFERENCES packing_lists(id) ON DELETE CASCADE,
+ serial_number VARCHAR(200) NOT NULL,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_packing_lists_po ON packing_lists(purchase_order_id);
+CREATE INDEX IF NOT EXISTS idx_packing_lists_customer ON packing_lists(customer_id);
+CREATE INDEX IF NOT EXISTS idx_packing_list_serials_pl ON packing_list_serials(packing_list_id);
+CREATE INDEX IF NOT EXISTS idx_packing_list_serials_sn ON packing_list_serials(serial_number);
+
+-- Add shipped_quantity column to purchase_order_items to track how many units have been shipped
+ALTER TABLE purchase_order_items ADD COLUMN IF NOT EXISTS shipped_quantity INTEGER NOT NULL DEFAULT 0;
diff --git a/scripts/009-fix-packing-lists-schema.sql b/scripts/009-fix-packing-lists-schema.sql
new file mode 100644
index 0000000..cb005c3
--- /dev/null
+++ b/scripts/009-fix-packing-lists-schema.sql
@@ -0,0 +1,25 @@
+-- Drop the old packing_lists table and recreate with correct schema
+DROP TABLE IF EXISTS packing_list_serials CASCADE;
+DROP TABLE IF EXISTS packing_lists CASCADE;
+
+CREATE TABLE packing_lists (
+ id SERIAL PRIMARY KEY,
+ purchase_order_id INTEGER NOT NULL REFERENCES purchase_orders(id),
+ po_item_id INTEGER NOT NULL REFERENCES purchase_order_items(id),
+ packing_number VARCHAR(50) NOT NULL UNIQUE,
+ quantity INTEGER NOT NULL DEFAULT 0,
+ notes TEXT,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE TABLE packing_list_serials (
+ id SERIAL PRIMARY KEY,
+ packing_list_id INTEGER NOT NULL REFERENCES packing_lists(id) ON DELETE CASCADE,
+ serial_number VARCHAR(200) NOT NULL,
+ created_at TIMESTAMPTZ DEFAULT NOW()
+);
+
+CREATE INDEX idx_packing_lists_po ON packing_lists(purchase_order_id);
+CREATE INDEX idx_packing_lists_item ON packing_lists(po_item_id);
+CREATE INDEX idx_packing_serials_list ON packing_list_serials(packing_list_id);
+CREATE UNIQUE INDEX idx_packing_serials_unique ON packing_list_serials(serial_number);
diff --git a/scripts/010-add-product-stock-quantity.sql b/scripts/010-add-product-stock-quantity.sql
new file mode 100644
index 0000000..46a2712
--- /dev/null
+++ b/scripts/010-add-product-stock-quantity.sql
@@ -0,0 +1 @@
+ALTER TABLE products ADD COLUMN IF NOT EXISTS stock_quantity INTEGER NOT NULL DEFAULT 0;
diff --git a/scripts/011-create-suppliers.sql b/scripts/011-create-suppliers.sql
new file mode 100644
index 0000000..1752ff8
--- /dev/null
+++ b/scripts/011-create-suppliers.sql
@@ -0,0 +1,15 @@
+CREATE TABLE IF NOT EXISTS suppliers (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ code VARCHAR(50) UNIQUE NOT NULL,
+ email VARCHAR(255),
+ phone VARCHAR(50),
+ address TEXT,
+ contact_person VARCHAR(255),
+ notes TEXT,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_suppliers_code ON suppliers(code);
+CREATE INDEX IF NOT EXISTS idx_suppliers_name ON suppliers(name);
diff --git a/scripts/012-add-component-is-internal.sql b/scripts/012-add-component-is-internal.sql
new file mode 100644
index 0000000..279bdd1
--- /dev/null
+++ b/scripts/012-add-component-is-internal.sql
@@ -0,0 +1 @@
+ALTER TABLE components ADD COLUMN IF NOT EXISTS is_internal BOOLEAN NOT NULL DEFAULT false;
diff --git a/scripts/013-create-supplier-packing-lists.sql b/scripts/013-create-supplier-packing-lists.sql
new file mode 100644
index 0000000..0d05241
--- /dev/null
+++ b/scripts/013-create-supplier-packing-lists.sql
@@ -0,0 +1,18 @@
+CREATE TABLE IF NOT EXISTS supplier_packing_lists (
+ id SERIAL PRIMARY KEY,
+ supplier_id INTEGER NOT NULL REFERENCES suppliers(id),
+ packing_number VARCHAR(255) NOT NULL UNIQUE,
+ notes TEXT,
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS supplier_packing_list_items (
+ id SERIAL PRIMARY KEY,
+ supplier_packing_list_id INTEGER NOT NULL REFERENCES supplier_packing_lists(id) ON DELETE CASCADE,
+ component_id INTEGER NOT NULL REFERENCES components(id),
+ quantity INTEGER NOT NULL DEFAULT 1,
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_spl_supplier ON supplier_packing_lists(supplier_id);
+CREATE INDEX IF NOT EXISTS idx_spli_packing ON supplier_packing_list_items(supplier_packing_list_id);
diff --git a/scripts/014-create-pick-lists.sql b/scripts/014-create-pick-lists.sql
new file mode 100644
index 0000000..8f708e0
--- /dev/null
+++ b/scripts/014-create-pick-lists.sql
@@ -0,0 +1,25 @@
+CREATE TABLE IF NOT EXISTS pick_lists (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ status VARCHAR(50) NOT NULL DEFAULT 'accepted',
+ products_json JSONB NOT NULL DEFAULT '[]',
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+CREATE TABLE IF NOT EXISTS pick_list_items (
+ id SERIAL PRIMARY KEY,
+ pick_list_id INTEGER NOT NULL REFERENCES pick_lists(id) ON DELETE CASCADE,
+ component_id INTEGER NOT NULL REFERENCES components(id),
+ part_number VARCHAR(255) NOT NULL,
+ component_name VARCHAR(255) NOT NULL,
+ category_name VARCHAR(255),
+ package_type VARCHAR(100),
+ shelf_location VARCHAR(100),
+ quantity_needed NUMERIC NOT NULL DEFAULT 0,
+ is_internal BOOLEAN NOT NULL DEFAULT false,
+ sources_json JSONB NOT NULL DEFAULT '[]',
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_pick_list_items_pick_list ON pick_list_items(pick_list_id);
+CREATE INDEX IF NOT EXISTS idx_pick_lists_created ON pick_lists(created_at DESC);
diff --git a/scripts/015-planning-history.sql b/scripts/015-planning-history.sql
new file mode 100644
index 0000000..5dabd97
--- /dev/null
+++ b/scripts/015-planning-history.sql
@@ -0,0 +1,37 @@
+-- Drop old pick_lists tables
+DROP TABLE IF EXISTS pick_list_items CASCADE;
+DROP TABLE IF EXISTS pick_lists CASCADE;
+
+-- Planning history: one row per accepted production plan
+CREATE TABLE IF NOT EXISTS planning_history (
+ id SERIAL PRIMARY KEY,
+ name VARCHAR(255) NOT NULL,
+ status VARCHAR(50) DEFAULT 'accepted',
+ products_json JSONB,
+ summary_json JSONB,
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+-- Planning history items: every BOM component for that plan
+CREATE TABLE IF NOT EXISTS planning_history_items (
+ id SERIAL PRIMARY KEY,
+ planning_id INTEGER NOT NULL REFERENCES planning_history(id) ON DELETE CASCADE,
+ component_id INTEGER REFERENCES components(id),
+ part_number VARCHAR(100),
+ component_name VARCHAR(255),
+ category_name VARCHAR(255),
+ package_type VARCHAR(100),
+ shelf_location VARCHAR(100),
+ quantity_needed NUMERIC DEFAULT 0,
+ stock_at_time NUMERIC DEFAULT 0,
+ shortage NUMERIC DEFAULT 0,
+ unit_price NUMERIC DEFAULT 0,
+ is_internal BOOLEAN DEFAULT false,
+ manufacturer VARCHAR(255),
+ supplier VARCHAR(255),
+ supplier_part_number VARCHAR(255),
+ sources_json JSONB,
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_planning_history_items_planning ON planning_history_items(planning_id);
diff --git a/scripts/016-supplier-pos.sql b/scripts/016-supplier-pos.sql
new file mode 100644
index 0000000..6e8460e
--- /dev/null
+++ b/scripts/016-supplier-pos.sql
@@ -0,0 +1,30 @@
+-- Supplier Purchase Orders
+CREATE TABLE IF NOT EXISTS supplier_pos (
+ id SERIAL PRIMARY KEY,
+ po_number VARCHAR(30) NOT NULL UNIQUE,
+ supplier_id INTEGER NOT NULL REFERENCES suppliers(id) ON DELETE CASCADE,
+ planning_id INTEGER REFERENCES planning_history(id) ON DELETE SET NULL,
+ order_date DATE NOT NULL DEFAULT CURRENT_DATE,
+ status VARCHAR(20) NOT NULL DEFAULT 'draft',
+ notes TEXT,
+ total_value NUMERIC(12,2) NOT NULL DEFAULT 0,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+-- Supplier Purchase Order line items
+CREATE TABLE IF NOT EXISTS supplier_po_items (
+ id SERIAL PRIMARY KEY,
+ supplier_po_id INTEGER NOT NULL REFERENCES supplier_pos(id) ON DELETE CASCADE,
+ component_id INTEGER REFERENCES components(id) ON DELETE SET NULL,
+ part_number VARCHAR(100),
+ component_name VARCHAR(255),
+ supplier_part_number VARCHAR(100),
+ quantity INTEGER NOT NULL DEFAULT 0,
+ unit_price NUMERIC(12,4) NOT NULL DEFAULT 0,
+ line_total NUMERIC(12,2) NOT NULL DEFAULT 0,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW()
+);
+
+CREATE INDEX IF NOT EXISTS idx_supplier_pos_supplier ON supplier_pos(supplier_id);
+CREATE INDEX IF NOT EXISTS idx_supplier_pos_planning ON supplier_pos(planning_id);
+CREATE INDEX IF NOT EXISTS idx_supplier_po_items_po ON supplier_po_items(supplier_po_id);
diff --git a/scripts/017-fix-supplier-po-number-length.sql b/scripts/017-fix-supplier-po-number-length.sql
new file mode 100644
index 0000000..a9794fa
--- /dev/null
+++ b/scripts/017-fix-supplier-po-number-length.sql
@@ -0,0 +1,2 @@
+-- Widen po_number column to accommodate longer PO numbers (PO-EACH-YYYY_MM_DD-RR-SUPPLIERNAME)
+ALTER TABLE supplier_pos ALTER COLUMN po_number TYPE VARCHAR(100);
diff --git a/scripts/018-add-received-quantity.sql b/scripts/018-add-received-quantity.sql
new file mode 100644
index 0000000..2c46166
--- /dev/null
+++ b/scripts/018-add-received-quantity.sql
@@ -0,0 +1 @@
+ALTER TABLE supplier_po_items ADD COLUMN IF NOT EXISTS received_quantity INTEGER NOT NULL DEFAULT 0;
diff --git a/scripts/add-customer-supplied.sql b/scripts/add-customer-supplied.sql
new file mode 100644
index 0000000..4b4c3ee
--- /dev/null
+++ b/scripts/add-customer-supplied.sql
@@ -0,0 +1,3 @@
+-- Add customer_supplied boolean and customer_id FK to components table
+ALTER TABLE components ADD COLUMN IF NOT EXISTS customer_supplied BOOLEAN NOT NULL DEFAULT FALSE;
+ALTER TABLE components ADD COLUMN IF NOT EXISTS customer_id INTEGER REFERENCES customers(id) ON DELETE SET NULL;
diff --git a/scripts/add-po-shipping-cost.sql b/scripts/add-po-shipping-cost.sql
new file mode 100644
index 0000000..6ff9b8d
--- /dev/null
+++ b/scripts/add-po-shipping-cost.sql
@@ -0,0 +1 @@
+ALTER TABLE supplier_pos ADD COLUMN IF NOT EXISTS shipping_cost NUMERIC(12,2) DEFAULT 0;
diff --git a/scripts/add-specs-column.sql b/scripts/add-specs-column.sql
new file mode 100644
index 0000000..baf4344
--- /dev/null
+++ b/scripts/add-specs-column.sql
@@ -0,0 +1 @@
+ALTER TABLE components ADD COLUMN IF NOT EXISTS specs jsonb DEFAULT '{}';
diff --git a/scripts/add-supplier-country-currency.sql b/scripts/add-supplier-country-currency.sql
new file mode 100644
index 0000000..011cb80
--- /dev/null
+++ b/scripts/add-supplier-country-currency.sql
@@ -0,0 +1,2 @@
+ALTER TABLE suppliers ADD COLUMN IF NOT EXISTS country VARCHAR DEFAULT '';
+ALTER TABLE suppliers ADD COLUMN IF NOT EXISTS currency VARCHAR DEFAULT 'EUR';
diff --git a/scripts/convert-logo.js b/scripts/convert-logo.js
new file mode 100644
index 0000000..c7c40e0
--- /dev/null
+++ b/scripts/convert-logo.js
@@ -0,0 +1,14 @@
+import fs from 'fs';
+import path from 'path';
+
+const logoUrl = 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Preto_semfundo-QjUexykfB10GjO803zcUlswqR19RBV.png';
+
+const res = await fetch(logoUrl);
+const arrayBuffer = await res.arrayBuffer();
+const base64 = Buffer.from(arrayBuffer).toString('base64');
+const dataUrl = `data:image/png;base64,${base64}`;
+
+console.log(`Base64 length: ${base64.length}`);
+console.log('LOGO_START');
+console.log(dataUrl);
+console.log('LOGO_END');
diff --git a/scripts/create-users-tables.sql b/scripts/create-users-tables.sql
new file mode 100644
index 0000000..663dfa8
--- /dev/null
+++ b/scripts/create-users-tables.sql
@@ -0,0 +1,59 @@
+-- Users table
+CREATE TABLE IF NOT EXISTS users (
+ id SERIAL PRIMARY KEY,
+ email VARCHAR(255) UNIQUE NOT NULL,
+ password_hash VARCHAR(255) NOT NULL,
+ name VARCHAR(255) NOT NULL,
+ role VARCHAR(50) DEFAULT 'user',
+ is_active BOOLEAN DEFAULT true,
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW()
+);
+
+-- User permissions table
+CREATE TABLE IF NOT EXISTS user_permissions (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ -- Section access (which pages can be viewed)
+ can_view_dashboard BOOLEAN DEFAULT true,
+ can_view_components BOOLEAN DEFAULT true,
+ can_view_products BOOLEAN DEFAULT true,
+ can_view_production BOOLEAN DEFAULT true,
+ can_view_suppliers BOOLEAN DEFAULT true,
+ can_view_customers BOOLEAN DEFAULT true,
+ can_view_stock_history BOOLEAN DEFAULT true,
+ can_view_stock_take BOOLEAN DEFAULT true,
+ can_view_logistics BOOLEAN DEFAULT true,
+ can_view_settings BOOLEAN DEFAULT false,
+ -- Financial visibility
+ can_view_financial BOOLEAN DEFAULT false,
+ -- Actions
+ can_create BOOLEAN DEFAULT false,
+ can_edit BOOLEAN DEFAULT false,
+ can_delete BOOLEAN DEFAULT false,
+ -- Download/export
+ can_download BOOLEAN DEFAULT false,
+ created_at TIMESTAMP DEFAULT NOW(),
+ updated_at TIMESTAMP DEFAULT NOW()
+);
+
+-- Sessions table for auth
+CREATE TABLE IF NOT EXISTS sessions (
+ id SERIAL PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ token VARCHAR(255) UNIQUE NOT NULL,
+ expires_at TIMESTAMP NOT NULL,
+ created_at TIMESTAMP DEFAULT NOW()
+);
+
+-- Create default admin user (password: admin123)
+-- The hash is bcrypt for 'admin123'
+INSERT INTO users (email, password_hash, name, role)
+VALUES ('admin@linse.com', '$2b$10$K7L1OJ45/4Y2nIvOLPjKYeOIEZqMh0XKmYZKJKjKmJKmJKjKmJKjK', 'Administrator', 'admin')
+ON CONFLICT (email) DO NOTHING;
+
+-- Create permissions for admin user
+INSERT INTO user_permissions (user_id, can_view_dashboard, can_view_components, can_view_products, can_view_production, can_view_suppliers, can_view_customers, can_view_stock_history, can_view_stock_take, can_view_logistics, can_view_settings, can_view_financial, can_create, can_edit, can_delete, can_download)
+SELECT id, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true
+FROM users WHERE email = 'admin@linse.com'
+ON CONFLICT DO NOTHING;
diff --git a/scripts/crop-logo.js b/scripts/crop-logo.js
new file mode 100644
index 0000000..61ba5d0
--- /dev/null
+++ b/scripts/crop-logo.js
@@ -0,0 +1,28 @@
+import sharp from 'sharp';
+import fs from 'fs';
+import path from 'path';
+
+const logoUrl = 'https://hebbkx1anhila5yf.public.blob.vercel-storage.com/Preto_semfundo-QjUexykfB10GjO803zcUlswqR19RBV.png';
+
+const res = await fetch(logoUrl);
+const arrayBuffer = await res.arrayBuffer();
+const inputBuffer = Buffer.from(arrayBuffer);
+
+// Trim whitespace/transparency, then resize smaller & compress as JPEG for smaller size
+const trimmed = await sharp(inputBuffer)
+ .trim() // remove transparent/white borders
+ .resize({ width: 400, fit: 'inside' }) // resize to max 400px wide
+ .png({ quality: 60, compressionLevel: 9 })
+ .toBuffer();
+
+const base64 = trimmed.toString('base64');
+const dataUrl = `data:image/png;base64,${base64}`;
+
+console.log(`Original size: ${inputBuffer.length} bytes`);
+console.log(`Cropped+compressed size: ${trimmed.length} bytes`);
+console.log(`Base64 length: ${base64.length} chars`);
+
+// Output full data URL so we can embed it
+console.log('DATA_URL_START');
+console.log(dataUrl);
+console.log('DATA_URL_END');
diff --git a/scripts/fix-admin-password.sql b/scripts/fix-admin-password.sql
new file mode 100644
index 0000000..601a9c0
--- /dev/null
+++ b/scripts/fix-admin-password.sql
@@ -0,0 +1,5 @@
+-- Update admin password with proper bcrypt hash for 'admin123'
+-- Hash generated using bcrypt with 10 rounds
+UPDATE users
+SET password_hash = '$2a$10$rQnM1kkXhVrPjKjKkZvZcOxGxGx0Wz3Xz1Wz3Xz1Wz3Xz1Wz3Xz1W'
+WHERE email = 'admin@linse.com';
diff --git a/scripts/reset-admin-password.js b/scripts/reset-admin-password.js
new file mode 100644
index 0000000..459d66a
--- /dev/null
+++ b/scripts/reset-admin-password.js
@@ -0,0 +1,36 @@
+import bcrypt from "bcryptjs";
+import { neon } from "@neondatabase/serverless";
+
+const sql = neon(process.env.DATABASE_URL);
+
+async function resetAdminPassword() {
+ const password = "admin123";
+ const hash = await bcrypt.hash(password, 10);
+
+ console.log("Generated hash:", hash);
+
+ // Update admin user password
+ await sql`UPDATE users SET password_hash = ${hash} WHERE email = 'admin@linse.com'`;
+
+ // Verify the user exists
+ const users = await sql`SELECT id, email, name, role FROM users WHERE email = 'admin@linse.com'`;
+
+ if (users.length > 0) {
+ console.log("Admin user updated:", users[0]);
+ } else {
+ // Create admin user if doesn't exist
+ const newHash = await bcrypt.hash("admin123", 10);
+ await sql`INSERT INTO users (email, password_hash, name, role) VALUES ('admin@linse.com', ${newHash}, 'Administrator', 'admin')`;
+
+ const newUser = await sql`SELECT id FROM users WHERE email = 'admin@linse.com'`;
+ if (newUser.length > 0) {
+ await sql`INSERT INTO user_permissions (user_id, can_view_dashboard, can_view_components, can_view_products, can_view_production, can_view_suppliers, can_view_customers, can_view_stock_history, can_view_stock_take, can_view_logistics, can_view_settings, can_view_financial, can_create, can_edit, can_delete, can_download, sections)
+ VALUES (${newUser[0].id}, true, true, true, true, true, true, true, true, true, true, true, true, true, true, true, ARRAY['dashboard', 'components', 'products', 'production', 'suppliers', 'customers', 'stock-history', 'stock-take', 'logistics', 'settings'])`;
+ console.log("Admin user created with permissions");
+ }
+ }
+
+ console.log("Done!");
+}
+
+resetAdminPassword().catch(console.error);
diff --git a/styles/globals.css b/styles/globals.css
new file mode 100644
index 0000000..ac68442
--- /dev/null
+++ b/styles/globals.css
@@ -0,0 +1,94 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ font-family: Arial, Helvetica, sans-serif;
+}
+
+@layer utilities {
+ .text-balance {
+ text-wrap: balance;
+ }
+}
+
+@layer base {
+ :root {
+ --background: 0 0% 100%;
+ --foreground: 0 0% 3.9%;
+ --card: 0 0% 100%;
+ --card-foreground: 0 0% 3.9%;
+ --popover: 0 0% 100%;
+ --popover-foreground: 0 0% 3.9%;
+ --primary: 0 0% 9%;
+ --primary-foreground: 0 0% 98%;
+ --secondary: 0 0% 96.1%;
+ --secondary-foreground: 0 0% 9%;
+ --muted: 0 0% 96.1%;
+ --muted-foreground: 0 0% 45.1%;
+ --accent: 0 0% 96.1%;
+ --accent-foreground: 0 0% 9%;
+ --destructive: 0 84.2% 60.2%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 89.8%;
+ --input: 0 0% 89.8%;
+ --ring: 0 0% 3.9%;
+ --chart-1: 12 76% 61%;
+ --chart-2: 173 58% 39%;
+ --chart-3: 197 37% 24%;
+ --chart-4: 43 74% 66%;
+ --chart-5: 27 87% 67%;
+ --radius: 0.5rem;
+ --sidebar-background: 0 0% 98%;
+ --sidebar-foreground: 240 5.3% 26.1%;
+ --sidebar-primary: 240 5.9% 10%;
+ --sidebar-primary-foreground: 0 0% 98%;
+ --sidebar-accent: 240 4.8% 95.9%;
+ --sidebar-accent-foreground: 240 5.9% 10%;
+ --sidebar-border: 220 13% 91%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+ .dark {
+ --background: 0 0% 3.9%;
+ --foreground: 0 0% 98%;
+ --card: 0 0% 3.9%;
+ --card-foreground: 0 0% 98%;
+ --popover: 0 0% 3.9%;
+ --popover-foreground: 0 0% 98%;
+ --primary: 0 0% 98%;
+ --primary-foreground: 0 0% 9%;
+ --secondary: 0 0% 14.9%;
+ --secondary-foreground: 0 0% 98%;
+ --muted: 0 0% 14.9%;
+ --muted-foreground: 0 0% 63.9%;
+ --accent: 0 0% 14.9%;
+ --accent-foreground: 0 0% 98%;
+ --destructive: 0 62.8% 30.6%;
+ --destructive-foreground: 0 0% 98%;
+ --border: 0 0% 14.9%;
+ --input: 0 0% 14.9%;
+ --ring: 0 0% 83.1%;
+ --chart-1: 220 70% 50%;
+ --chart-2: 160 60% 45%;
+ --chart-3: 30 80% 55%;
+ --chart-4: 280 65% 60%;
+ --chart-5: 340 75% 55%;
+ --sidebar-background: 240 5.9% 10%;
+ --sidebar-foreground: 240 4.8% 95.9%;
+ --sidebar-primary: 224.3 76.3% 48%;
+ --sidebar-primary-foreground: 0 0% 100%;
+ --sidebar-accent: 240 3.7% 15.9%;
+ --sidebar-accent-foreground: 240 4.8% 95.9%;
+ --sidebar-border: 240 3.7% 15.9%;
+ --sidebar-ring: 217.2 91.2% 59.8%;
+ }
+}
+
+@layer base {
+ * {
+ @apply border-border;
+ }
+ body {
+ @apply bg-background text-foreground;
+ }
+}