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; + } +}