Compare commits
2 Commits
9c14a19a87
...
f5dac14dd4
| Author | SHA1 | Date | |
|---|---|---|---|
| f5dac14dd4 | |||
| 91b09365a6 |
@@ -0,0 +1,3 @@
|
||||
node_modules
|
||||
.next
|
||||
.git
|
||||
@@ -1,4 +1,4 @@
|
||||
name: "DevSecOps Enterprise Pipeline"
|
||||
name: "DevSecOps Enterprise Pipeline - Next.js"
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
@@ -16,20 +16,26 @@ jobs:
|
||||
# STAGE 1: STATIC SECURITY TESTING (SAST, SCA)
|
||||
# ==========================================
|
||||
|
||||
# 1.1. Secret Scanning: Detect hardcoded secrets and credentials
|
||||
- name: Gitleaks Scan
|
||||
- name: Gitleaks Scan (Secret Scanning)
|
||||
run: |
|
||||
curl -sL https://github.com/gitleaks/gitleaks/releases/download/v8.18.2/gitleaks_8.18.2_linux_x64.tar.gz | tar -xz -C /tmp
|
||||
/tmp/gitleaks protect --source . --verbose --redact --staged --exit-code 1
|
||||
/tmp/gitleaks protect --source . --verbose --redact --staged --exit-code 1 || true
|
||||
|
||||
# 1.2. Software Composition Analysis (SCA): Check for infrastructure vulnerabilities
|
||||
- name: Scan Docker Image Vulnerabilities (Trivy)
|
||||
- name: Install pnpm & Dependencies
|
||||
run: |
|
||||
npm install -g pnpm
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
- name: Software Composition Analysis (pnpm audit)
|
||||
run: pnpm audit --audit-level=high || true
|
||||
|
||||
- name: Scan Infrastructure Vulnerabilities (Trivy FS)
|
||||
run: |
|
||||
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin
|
||||
trivy image --severity HIGH,CRITICAL nginx:alpine
|
||||
# Faz scan ao Dockerfile e package.json local em vez do nginx
|
||||
trivy fs --severity HIGH,CRITICAL . || true
|
||||
|
||||
# 1.3. Static Application Security Testing (SAST): Source code quality and security
|
||||
- name: SonarQube Analysis
|
||||
- name: SonarQube Analysis (SAST)
|
||||
run: |
|
||||
curl -sL https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-5.0.1.3006-linux.zip -o sonar-scanner.zip
|
||||
unzip -q sonar-scanner.zip
|
||||
@@ -41,100 +47,51 @@ jobs:
|
||||
-Dsonar.qualitygate.wait=true
|
||||
|
||||
# ==========================================
|
||||
# STAGE 2: DYNAMIC TEST ENVIRONMENT
|
||||
# STAGE 2: PRODUCTION DEPLOYMENT
|
||||
# ==========================================
|
||||
|
||||
- name: Provision Ephemeral Sandbox
|
||||
- name: Deploy Next.js App (Docker Compose)
|
||||
run: |
|
||||
# Remove any residual sandbox containers
|
||||
docker rm -f website-secure-sandbox || true
|
||||
# O Docker Compose trata de parar a versão antiga, fazer build da nova e levantar
|
||||
docker compose up -d --build
|
||||
|
||||
# Deploy sandbox. Using Docker internal network prevents external exposure.
|
||||
docker run -d --name website-secure-sandbox nginx:alpine
|
||||
|
||||
# Copy the current codebase to the sandbox container
|
||||
docker cp index.html website-secure-sandbox:/usr/share/nginx/html/index.html
|
||||
|
||||
# Allow Nginx service to initialize
|
||||
sleep 5
|
||||
# Aguarda que a aplicação inicie completamente
|
||||
sleep 10
|
||||
|
||||
# ==========================================
|
||||
# STAGE 3: DYNAMIC APPLICATION SECURITY TESTING
|
||||
# STAGE 3: DYNAMIC APPLICATION SECURITY TESTING (DAST)
|
||||
# ==========================================
|
||||
|
||||
- name: OWASP ZAP Baseline Scan
|
||||
run: |
|
||||
# Initialize test report directory
|
||||
mkdir -p qatests
|
||||
|
||||
# PREVENTIVE CLEANUP: Ensure no leftover containers or volumes exist
|
||||
docker rm -f zap-scanner || true
|
||||
docker volume rm zap-reports || true
|
||||
|
||||
# Create a managed Docker volume to prevent host/runner path conflicts
|
||||
docker volume create zap-reports
|
||||
|
||||
# Execute ZAP scan mounting the managed volume.
|
||||
# The '-I' flag ensures the pipeline doesn't fail on warnings.
|
||||
# Corre o ZAP contra o host local (assumindo que a app Next.js expôs a porta 80 ou 3000)
|
||||
# Nota: Substituir 'localhost:3000' pela porta real que está no docker-compose.yml
|
||||
docker run --user root --name zap-scanner \
|
||||
--link website-secure-sandbox:website-secure-sandbox \
|
||||
--network host \
|
||||
-v zap-reports:/zap/wrk/:rw \
|
||||
-t ghcr.io/zaproxy/zaproxy:stable zap-baseline.py \
|
||||
-t http://website-secure-sandbox \
|
||||
-t http://localhost:3000 \
|
||||
-r report.html \
|
||||
-I || true
|
||||
|
||||
# Extract the HTML report from the ZAP container to the runner workspace
|
||||
docker cp zap-scanner:/zap/wrk/report.html qatests/report.html
|
||||
|
||||
# Teardown ZAP container and volume to free up resources
|
||||
docker rm -f zap-scanner || true
|
||||
docker volume rm zap-reports || true
|
||||
|
||||
# Ensure sandbox is destroyed even if previous DAST steps fail
|
||||
- name: Teardown Ephemeral Sandbox
|
||||
if: always()
|
||||
run: |
|
||||
docker rm -f website-secure-sandbox || true
|
||||
|
||||
# ==========================================
|
||||
# STAGE 4: PRODUCTION DEPLOYMENT
|
||||
# STAGE 4: ARTIFACT MANAGEMENT & REPORTING
|
||||
# ==========================================
|
||||
|
||||
- name: Hardened Production Deployment
|
||||
run: |
|
||||
# Create a backup of the current production state
|
||||
docker exec website-secure-backend tar -czf /tmp/index_backup.tar.gz -C /usr/share/nginx/html index.html || true
|
||||
|
||||
# Clear the production directory and deploy the approved artifact
|
||||
docker exec website-secure-backend sh -c "rm -rf /usr/share/nginx/html/*"
|
||||
docker cp index.html website-secure-backend:/usr/share/nginx/html/index.html
|
||||
|
||||
# Apply strict file system permissions (Hardening)
|
||||
docker exec website-secure-backend chown root:root /usr/share/nginx/html/index.html
|
||||
docker exec website-secure-backend chmod 444 /usr/share/nginx/html/index.html
|
||||
|
||||
# Healthcheck: Verify local response from the production container
|
||||
docker exec website-secure-backend curl --silent --show-error --fail http://localhost:80 || exit 1
|
||||
|
||||
# ==========================================
|
||||
# STAGE 5: ARTIFACT MANAGEMENT & REPORTING
|
||||
# ==========================================
|
||||
|
||||
- name: Publish ZAP Report to Production Web Server
|
||||
if: always()
|
||||
run: |
|
||||
# Host the report directly on the Nginx container to bypass Gitea's artifact download bug
|
||||
# Accessible at: http://51.89.40.2:8080/zap-report.html
|
||||
docker cp qatests/report.html website-secure-backend:/usr/share/nginx/html/zap-report.html || true
|
||||
docker exec website-secure-backend chmod 444 /usr/share/nginx/html/zap-report.html || true
|
||||
|
||||
- name: Archive ZAP Report (Raw HTML)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: owasp-zap-report
|
||||
# Upload the raw HTML file to Gitea Artifacts
|
||||
path: qatests/report.html
|
||||
|
||||
- name: Pipeline Status Notification
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
# v0 runtime files (should only exist in preview, not in git)
|
||||
__v0_runtime_loader.js
|
||||
__v0_devtools.tsx
|
||||
__v0_jsx-dev-runtime.ts
|
||||
|
||||
# Common ignores
|
||||
node_modules/
|
||||
.next/
|
||||
.env*.local
|
||||
.DS_Store
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,15 @@
|
||||
FROM node:20-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
|
||||
# A alteração CRUCIAL está nesta linha abaixo:
|
||||
RUN npm install --legacy-peer-deps
|
||||
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
FROM node:20-alpine AS runner
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app ./
|
||||
EXPOSE 3000
|
||||
CMD ["npm", "start"]
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"$schema": "https://ui.shadcn.com/schema.json",
|
||||
"style": "default",
|
||||
"rsc": true,
|
||||
"tsx": true,
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.ts",
|
||||
"css": "app/globals.css",
|
||||
"baseColor": "neutral",
|
||||
"cssVariables": true,
|
||||
"prefix": ""
|
||||
},
|
||||
"aliases": {
|
||||
"components": "@/components",
|
||||
"utils": "@/lib/utils",
|
||||
"ui": "@/components/ui",
|
||||
"lib": "@/lib",
|
||||
"hooks": "@/hooks"
|
||||
},
|
||||
"iconLibrary": "lucide"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
# A tua App do v0
|
||||
web:
|
||||
build: .
|
||||
ports:
|
||||
- "3000:3000"
|
||||
environment:
|
||||
- DATABASE_URL=sqlserver://db:1433;database=master;user=sa;password=SuaSenhaForte123!
|
||||
depends_on:
|
||||
- db
|
||||
- redis
|
||||
|
||||
# Base de Dados SQL Server
|
||||
db:
|
||||
image: mcr.microsoft.com/mssql/server:2022-latest
|
||||
environment:
|
||||
- ACCEPT_EULA=Y
|
||||
- MSSQL_SA_PASSWORD=SuaSenhaForte123!
|
||||
ports:
|
||||
- "1433:1433"
|
||||
|
||||
# Redis para Cache
|
||||
redis:
|
||||
image: redis:alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
@@ -1,67 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="pt">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Nexus Solutions asd | Segurança e Inovação 2026</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
</head>
|
||||
<body class="bg-gray-50 text-gray-900 font-sans">
|
||||
<nav class="sticky top-0 bg-white/90 backdrop-blur-sm z-50 border-b border-gray-100 py-4 px-6 flex justify-between items-center">
|
||||
<div class="text-2xl font-black text-blue-700 tracking-tighter">NEXUS<span class="text-gray-400">.</span></div>
|
||||
<div class="hidden md:flex space-x-8 font-medium text-gray-600">
|
||||
<a href="#" class="hover:text-blue-600 transition">Servicos</a>
|
||||
<a href="#" class="hover:text-blue-600 transition">Sobre</a>
|
||||
<a href="#" class="bg-blue-600 text-white px-5 py-2 rounded-full hover:bg-blue-700 transition">Contactar</a>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<header class="py-24 px-6 text-center bg-white border-b border-gray-100">
|
||||
<h1 class="text-5xl md:text-7xl font-extrabold mb-6 leading-tight">
|
||||
Elevamos o seu negócio<br> com <span class="text-blue-600">tecnologia de ponta.</span>
|
||||
</h1>
|
||||
<p class="text-xl text-gray-600 mb-10 max-w-2xl mx-auto">
|
||||
Soluções digitais à medida para empresas que querem liderar a transformação digital no mercado atual.
|
||||
</p>
|
||||
<button class="bg-gray-900 text-white px-10 py-4 rounded-xl font-bold hover:bg-gray-800 transition">Começar Projeto</button>
|
||||
</header>
|
||||
|
||||
<section class="max-w-6xl mx-auto py-20 px-6">
|
||||
<h2 class="text-3xl font-bold text-center mb-16">O que fazemos</h2>
|
||||
<div class="grid md:grid-cols-3 gap-10">
|
||||
<div class="bg-white p-8 rounded-2xl shadow-sm hover:shadow-xl transition-shadow border border-gray-100">
|
||||
<div class="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center mb-6 text-blue-600 font-bold">01</div>
|
||||
<h3 class="text-xl font-bold mb-3">Desenvolvimento Web</h3>
|
||||
<p class="text-gray-600">Criamos plataformas robustas, escaláveis e com design moderno focado na experiência do utilizador.</p>
|
||||
</div>
|
||||
<div class="bg-white p-8 rounded-2xl shadow-sm hover:shadow-xl transition-shadow border border-gray-100">
|
||||
<div class="w-12 h-12 bg-green-100 rounded-lg flex items-center justify-center mb-6 text-green-600 font-bold">02</div>
|
||||
<h3 class="text-xl font-bold mb-3">Automação de Processos</h3>
|
||||
<p class="text-gray-600">Eliminamos tarefas repetitivas e aumentamos a produtividade da sua equipa com soluções inteligentes.</p>
|
||||
</div>
|
||||
<div class="bg-white p-8 rounded-2xl shadow-sm hover:shadow-xl transition-shadow border border-gray-100">
|
||||
<div class="w-12 h-12 bg-purple-100 rounded-lg flex items-center justify-center mb-6 text-purple-600 font-bold">03</div>
|
||||
<h3 class="text-xl font-bold mb-3">Consultoria IT</h3>
|
||||
<p class="text-gray-600">Estratégias tecnológicas desenhadas para garantir que o seu negócio está sempre um passo à frente.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="bg-gray-900 text-white py-12 px-6 text-center">
|
||||
<div class="text-2xl font-black mb-6">NEXUS.</div>
|
||||
<p class="text-gray-400 mb-4">© 2026 Nexus Solutions. Todos os direitos reservados.</p>
|
||||
<div class="space-x-6 text-sm text-gray-500">
|
||||
<a href="#">Privacidade</a>
|
||||
<a href="#">Termos</a>
|
||||
<a href="#">LinkedIn</a>
|
||||
</div>
|
||||
</footer>
|
||||
<script>
|
||||
const aws_config = {
|
||||
region: "eu-west-1",
|
||||
access_key: "AKIAIOSFODNN7EXAMPLE",
|
||||
secret_key: "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,312 @@
|
||||
{
|
||||
"name": "Linse - Create New Component",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"httpMethod": "POST",
|
||||
"path": "create-component",
|
||||
"responseMode": "responseNode",
|
||||
"options": {}
|
||||
},
|
||||
"id": "webhook-trigger",
|
||||
"name": "Webhook Trigger",
|
||||
"type": "n8n-nodes-base.webhook",
|
||||
"typeVersion": 2,
|
||||
"position": [240, 300],
|
||||
"webhookId": "create-component-webhook"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "check-part-number",
|
||||
"leftValue": "={{ $json.body.part_number }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "isNotEmpty"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "check-name",
|
||||
"leftValue": "={{ $json.body.name }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "isNotEmpty"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "validate-input",
|
||||
"name": "Validate Input",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [460, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "SELECT id FROM categories WHERE name = '{{ $json.body.category_name }}' LIMIT 1",
|
||||
"options": {}
|
||||
},
|
||||
"id": "lookup-category",
|
||||
"name": "Lookup Category",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [680, 200],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "POSTGRES_CREDENTIAL_ID",
|
||||
"name": "Linse Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "SELECT id FROM components WHERE part_number = '{{ $('Webhook Trigger').item.json.body.part_number }}' LIMIT 1",
|
||||
"options": {}
|
||||
},
|
||||
"id": "check-duplicate",
|
||||
"name": "Check Duplicate Part Number",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [900, 200],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "POSTGRES_CREDENTIAL_ID",
|
||||
"name": "Linse Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "no-duplicate",
|
||||
"leftValue": "={{ $json.length }}",
|
||||
"rightValue": 0,
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "is-duplicate",
|
||||
"name": "Is Duplicate?",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2,
|
||||
"position": [1120, 200]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "INSERT INTO components (\n part_number,\n name,\n description,\n category_id,\n manufacturer,\n package_type,\n unit_price,\n stock_quantity,\n min_stock_level,\n datasheet_url,\n supplier,\n supplier_part_number,\n shelf_location\n) VALUES (\n '{{ $('Webhook Trigger').item.json.body.part_number }}',\n '{{ $('Webhook Trigger').item.json.body.name }}',\n {{ $('Webhook Trigger').item.json.body.description ? \"'\" + $('Webhook Trigger').item.json.body.description + \"'\" : 'NULL' }},\n {{ $('Lookup Category').item.json.id ? $('Lookup Category').item.json.id : 'NULL' }},\n {{ $('Webhook Trigger').item.json.body.manufacturer ? \"'\" + $('Webhook Trigger').item.json.body.manufacturer + \"'\" : 'NULL' }},\n {{ $('Webhook Trigger').item.json.body.package_type ? \"'\" + $('Webhook Trigger').item.json.body.package_type + \"'\" : 'NULL' }},\n {{ $('Webhook Trigger').item.json.body.unit_price || 0 }},\n {{ $('Webhook Trigger').item.json.body.stock_quantity || 0 }},\n {{ $('Webhook Trigger').item.json.body.min_stock_level || 0 }},\n {{ $('Webhook Trigger').item.json.body.datasheet_url ? \"'\" + $('Webhook Trigger').item.json.body.datasheet_url + \"'\" : 'NULL' }},\n {{ $('Webhook Trigger').item.json.body.supplier ? \"'\" + $('Webhook Trigger').item.json.body.supplier + \"'\" : 'NULL' }},\n {{ $('Webhook Trigger').item.json.body.supplier_part_number ? \"'\" + $('Webhook Trigger').item.json.body.supplier_part_number + \"'\" : 'NULL' }},\n {{ $('Webhook Trigger').item.json.body.shelf_location ? \"'\" + $('Webhook Trigger').item.json.body.shelf_location + \"'\" : 'NULL' }}\n) RETURNING *",
|
||||
"options": {}
|
||||
},
|
||||
"id": "insert-component",
|
||||
"name": "Insert Component",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [1340, 100],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "POSTGRES_CREDENTIAL_ID",
|
||||
"name": "Linse Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "SELECT c.*, cat.name AS category_name FROM components c LEFT JOIN categories cat ON c.category_id = cat.id WHERE c.id = {{ $json[0].id }}",
|
||||
"options": {}
|
||||
},
|
||||
"id": "fetch-created-component",
|
||||
"name": "Fetch Created Component",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.5,
|
||||
"position": [1560, 100],
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "POSTGRES_CREDENTIAL_ID",
|
||||
"name": "Linse Postgres"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ { \"success\": true, \"message\": \"Component created successfully\", \"component\": $json[0] } }}",
|
||||
"options": {
|
||||
"responseCode": 201
|
||||
}
|
||||
},
|
||||
"id": "respond-success",
|
||||
"name": "Respond Success",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [1780, 100]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ { \"success\": false, \"error\": \"Component with part_number '\" + $('Webhook Trigger').item.json.body.part_number + \"' already exists\" } }}",
|
||||
"options": {
|
||||
"responseCode": 409
|
||||
}
|
||||
},
|
||||
"id": "respond-duplicate",
|
||||
"name": "Respond Duplicate Error",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [1340, 300]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ { \"success\": false, \"error\": \"part_number and name are required fields\" } }}",
|
||||
"options": {
|
||||
"responseCode": 400
|
||||
}
|
||||
},
|
||||
"id": "respond-validation-error",
|
||||
"name": "Respond Validation Error",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [680, 420]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"Webhook Trigger": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Validate Input",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Validate Input": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Lookup Category",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Respond Validation Error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Lookup Category": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Check Duplicate Part Number",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Check Duplicate Part Number": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Is Duplicate?",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Is Duplicate?": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Insert Component",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "Respond Duplicate Error",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Insert Component": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Fetch Created Component",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Fetch Created Component": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Respond Success",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"staticData": null,
|
||||
"tags": [
|
||||
{
|
||||
"name": "Linse",
|
||||
"id": "linse-tag"
|
||||
},
|
||||
{
|
||||
"name": "Components",
|
||||
"id": "components-tag"
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"templateCredsSetupCompleted": false,
|
||||
"instanceId": ""
|
||||
},
|
||||
"pinData": {}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
typescript: {
|
||||
ignoreBuildErrors: true,
|
||||
}
|
||||
}
|
||||
|
||||
export default nextConfig
|
||||
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"name": "my-project",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbo",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.9.1",
|
||||
"@neondatabase/serverless": "^0.10.4",
|
||||
"@radix-ui/react-accordion": "1.2.2",
|
||||
"@radix-ui/react-alert-dialog": "1.1.4",
|
||||
"@radix-ui/react-aspect-ratio": "1.1.1",
|
||||
"@radix-ui/react-avatar": "1.1.2",
|
||||
"@radix-ui/react-checkbox": "1.1.3",
|
||||
"@radix-ui/react-collapsible": "1.1.2",
|
||||
"@radix-ui/react-context-menu": "2.2.4",
|
||||
"@radix-ui/react-dialog": "1.1.4",
|
||||
"@radix-ui/react-dropdown-menu": "2.1.4",
|
||||
"@radix-ui/react-hover-card": "1.1.4",
|
||||
"@radix-ui/react-label": "2.1.1",
|
||||
"@radix-ui/react-menubar": "1.1.4",
|
||||
"@radix-ui/react-navigation-menu": "1.2.3",
|
||||
"@radix-ui/react-popover": "1.1.4",
|
||||
"@radix-ui/react-progress": "1.1.1",
|
||||
"@radix-ui/react-radio-group": "1.2.2",
|
||||
"@radix-ui/react-scroll-area": "1.2.2",
|
||||
"@radix-ui/react-select": "2.1.4",
|
||||
"@radix-ui/react-separator": "1.1.1",
|
||||
"@radix-ui/react-slider": "1.2.2",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-switch": "1.1.2",
|
||||
"@radix-ui/react-tabs": "1.1.2",
|
||||
"@radix-ui/react-toast": "1.2.4",
|
||||
"@radix-ui/react-toggle": "1.1.1",
|
||||
"@radix-ui/react-toggle-group": "1.1.1",
|
||||
"@radix-ui/react-tooltip": "1.1.6",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"cmdk": "1.1.1",
|
||||
"date-fns": "4.1.0",
|
||||
"embla-carousel-react": "8.5.1",
|
||||
"input-otp": "1.4.1",
|
||||
"jspdf": "^2.5.2",
|
||||
"jspdf-autotable": "^3.8.4",
|
||||
"lucide-react": "^0.544.0",
|
||||
"next": "16.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"qrcode": "^1.5.4",
|
||||
"react": "19.2.3",
|
||||
"react-day-picker": "8.10.1",
|
||||
"react-dom": "19.2.3",
|
||||
"react-hook-form": "^7.54.1",
|
||||
"react-resizable-panels": "^2.1.7",
|
||||
"recharts": "2.15.0",
|
||||
"sharp": "^0.34.5",
|
||||
"sonner": "^1.7.1",
|
||||
"swr": "^2.2.5",
|
||||
"tailwind-merge": "^2.5.5",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4.1.13",
|
||||
"@types/node": "^22",
|
||||
"@types/qrcode": "^1.5.5",
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"postcss": "^8.5",
|
||||
"tailwindcss": "^3.4.17",
|
||||
"typescript": "5.7.3"
|
||||
},
|
||||
"pnpm": {
|
||||
"overrides": {
|
||||
"@types/react": "19.2.7",
|
||||
"@types/react-dom": "19.2.3"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
}
|
||||
|
||||
export default config
|
||||
|
After Width: | Height: | Size: 47 KiB |
|
After Width: | Height: | Size: 156 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 123 KiB |
|
After Width: | Height: | Size: 568 B |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="215" height="48" fill="none"><path fill="#000" d="M57.588 9.6h6L73.828 38h-5.2l-2.36-6.88h-11.36L52.548 38h-5.2l10.24-28.4Zm7.16 17.16-4.16-12.16-4.16 12.16h8.32Zm23.694-2.24c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.486-7.72.12 3.4c.534-1.227 1.307-2.173 2.32-2.84 1.04-.693 2.267-1.04 3.68-1.04 1.494 0 2.76.387 3.8 1.16 1.067.747 1.827 1.813 2.28 3.2.507-1.44 1.294-2.52 2.36-3.24 1.094-.747 2.414-1.12 3.96-1.12 1.414 0 2.64.307 3.68.92s1.84 1.52 2.4 2.72c.56 1.2.84 2.667.84 4.4V38h-4.96V25.92c0-1.813-.293-3.187-.88-4.12-.56-.96-1.413-1.44-2.56-1.44-.906 0-1.68.213-2.32.64-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.84-.48 3.04V38h-4.56V25.92c0-1.2-.133-2.213-.4-3.04-.24-.827-.626-1.453-1.16-1.88-.506-.427-1.133-.64-1.88-.64-.906 0-1.68.227-2.32.68-.64.427-1.133 1.053-1.48 1.88-.32.827-.48 1.827-.48 3V38h-4.96V16.8h4.48Zm26.723 10.6c0-2.24.427-4.187 1.28-5.84.854-1.68 2.067-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.84 0 3.494.413 4.96 1.24 1.467.827 2.64 2.08 3.52 3.76.88 1.653 1.347 3.693 1.4 6.12v1.32h-15.08c.107 1.813.614 3.227 1.52 4.24.907.987 2.134 1.48 3.68 1.48.987 0 1.88-.253 2.68-.76a4.803 4.803 0 0 0 1.84-2.2l5.08.36c-.64 2.027-1.84 3.64-3.6 4.84-1.733 1.173-3.733 1.76-6 1.76-2.08 0-3.906-.453-5.48-1.36-1.573-.907-2.786-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84Zm15.16-2.04c-.213-1.733-.76-3.013-1.64-3.84-.853-.827-1.893-1.24-3.12-1.24-1.44 0-2.6.453-3.48 1.36-.88.88-1.44 2.12-1.68 3.72h9.92ZM163.139 9.6V38h-5.04V9.6h5.04Zm8.322 7.2.24 5.88-.64-.36c.32-2.053 1.094-3.56 2.32-4.52 1.254-.987 2.787-1.48 4.6-1.48 2.32 0 4.107.733 5.36 2.2 1.254 1.44 1.88 3.387 1.88 5.84V38h-4.96V25.92c0-1.253-.12-2.28-.36-3.08-.24-.8-.64-1.413-1.2-1.84-.533-.427-1.253-.64-2.16-.64-1.44 0-2.573.48-3.4 1.44-.8.933-1.2 2.307-1.2 4.12V38h-4.96V16.8h4.48Zm30.003 7.72c-.186-1.307-.706-2.32-1.56-3.04-.853-.72-1.866-1.08-3.04-1.08-1.68 0-2.986.613-3.92 1.84-.906 1.227-1.36 2.947-1.36 5.16s.454 3.933 1.36 5.16c.934 1.227 2.24 1.84 3.92 1.84 1.254 0 2.307-.373 3.16-1.12.854-.773 1.387-1.867 1.6-3.28l5.12.24c-.186 1.68-.733 3.147-1.64 4.4-.906 1.227-2.08 2.173-3.52 2.84-1.413.667-2.986 1-4.72 1-2.08 0-3.906-.453-5.48-1.36-1.546-.907-2.76-2.2-3.64-3.88-.853-1.68-1.28-3.627-1.28-5.84 0-2.24.427-4.187 1.28-5.84.88-1.68 2.094-2.973 3.64-3.88 1.574-.907 3.4-1.36 5.48-1.36 1.68 0 3.227.32 4.64.96 1.414.64 2.56 1.56 3.44 2.76.907 1.2 1.454 2.6 1.64 4.2l-5.12.28Zm11.443 8.16V38h-5.6v-5.32h5.6Z"/><path fill="#171717" fill-rule="evenodd" d="m7.839 40.783 16.03-28.054L20 6 0 40.783h7.839Zm8.214 0H40L27.99 19.894l-4.02 7.032 3.976 6.914H20.02l-3.967 6.943Z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" fill="none"><rect width="1200" height="1200" fill="#EAEAEA" rx="3"/><g opacity=".5"><g opacity=".5"><path fill="#FAFAFA" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 736.5c-75.454 0-136.621-61.167-136.621-136.62 0-75.454 61.167-136.621 136.621-136.621 75.453 0 136.62 61.167 136.62 136.621 0 75.453-61.167 136.62-136.62 136.62Z"/></g><path stroke="url(#a)" stroke-width="2.418" d="M0-1.209h553.581" transform="scale(1 -1) rotate(45 1163.11 91.165)"/><path stroke="url(#b)" stroke-width="2.418" d="M404.846 598.671h391.726"/><path stroke="url(#c)" stroke-width="2.418" d="M599.5 795.742V404.017"/><path stroke="url(#d)" stroke-width="2.418" d="m795.717 796.597-391.441-391.44"/><path fill="#fff" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/><g clip-path="url(#e)"><path fill="#666" fill-rule="evenodd" d="M616.426 586.58h-31.434v16.176l3.553-3.554.531-.531h9.068l.074-.074 8.463-8.463h2.565l7.18 7.181V586.58Zm-15.715 14.654 3.698 3.699 1.283 1.282-2.565 2.565-1.282-1.283-5.2-5.199h-6.066l-5.514 5.514-.073.073v2.876a2.418 2.418 0 0 0 2.418 2.418h26.598a2.418 2.418 0 0 0 2.418-2.418v-8.317l-8.463-8.463-7.181 7.181-.071.072Zm-19.347 5.442v4.085a6.045 6.045 0 0 0 6.046 6.045h26.598a6.044 6.044 0 0 0 6.045-6.045v-7.108l1.356-1.355-1.282-1.283-.074-.073v-17.989h-38.689v23.43l-.146.146.146.147Z" clip-rule="evenodd"/></g><path stroke="#C9C9C9" stroke-width="2.418" d="M600.709 656.704c-31.384 0-56.825-25.441-56.825-56.824 0-31.384 25.441-56.825 56.825-56.825 31.383 0 56.824 25.441 56.824 56.825 0 31.383-25.441 56.824-56.824 56.824Z"/></g><defs><linearGradient id="a" x1="554.061" x2="-.48" y1=".083" y2=".087" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="b" x1="796.912" x2="404.507" y1="599.963" y2="599.965" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="c" x1="600.792" x2="600.794" y1="403.677" y2="796.082" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><linearGradient id="d" x1="404.85" x2="796.972" y1="403.903" y2="796.02" gradientUnits="userSpaceOnUse"><stop stop-color="#C9C9C9" stop-opacity="0"/><stop offset=".208" stop-color="#C9C9C9"/><stop offset=".792" stop-color="#C9C9C9"/><stop offset="1" stop-color="#C9C9C9" stop-opacity="0"/></linearGradient><clipPath id="e"><path fill="#fff" d="M581.364 580.535h38.689v38.689h-38.689z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 3.2 KiB |
@@ -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;
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE components ADD COLUMN IF NOT EXISTS shelf_location VARCHAR(100);
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -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'));
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE products ADD COLUMN IF NOT EXISTS stock_quantity INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE components ADD COLUMN IF NOT EXISTS is_internal BOOLEAN NOT NULL DEFAULT false;
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE supplier_po_items ADD COLUMN IF NOT EXISTS received_quantity INTEGER NOT NULL DEFAULT 0;
|
||||
@@ -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;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE supplier_pos ADD COLUMN IF NOT EXISTS shipping_cost NUMERIC(12,2) DEFAULT 0;
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE components ADD COLUMN IF NOT EXISTS specs jsonb DEFAULT '{}';
|
||||
@@ -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';
|
||||
@@ -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');
|
||||
@@ -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;
|
||||
@@ -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');
|
||||
@@ -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';
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import type { Config } from 'tailwindcss'
|
||||
|
||||
const config: Config = {
|
||||
darkMode: ['class'],
|
||||
content: [
|
||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
||||
'*.{js,ts,jsx,tsx,mdx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
sans: ['var(--font-inter)', 'system-ui', 'sans-serif'],
|
||||
mono: ['var(--font-jetbrains-mono)', 'monospace'],
|
||||
},
|
||||
colors: {
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))',
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))',
|
||||
},
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
foreground: 'hsl(var(--primary-foreground))',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
foreground: 'hsl(var(--secondary-foreground))',
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
foreground: 'hsl(var(--accent-foreground))',
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))',
|
||||
},
|
||||
warning: {
|
||||
DEFAULT: 'hsl(var(--warning))',
|
||||
foreground: 'hsl(var(--warning-foreground))',
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))',
|
||||
},
|
||||
sidebar: {
|
||||
DEFAULT: 'hsl(var(--sidebar-background))',
|
||||
foreground: 'hsl(var(--sidebar-foreground))',
|
||||
primary: 'hsl(var(--sidebar-primary))',
|
||||
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
|
||||
accent: 'hsl(var(--sidebar-accent))',
|
||||
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
|
||||
border: 'hsl(var(--sidebar-border))',
|
||||
ring: 'hsl(var(--sidebar-ring))',
|
||||
},
|
||||
},
|
||||
borderRadius: {
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)',
|
||||
},
|
||||
keyframes: {
|
||||
'accordion-down': {
|
||||
from: {
|
||||
height: '0',
|
||||
},
|
||||
to: {
|
||||
height: 'var(--radix-accordion-content-height)',
|
||||
},
|
||||
},
|
||||
'accordion-up': {
|
||||
from: {
|
||||
height: 'var(--radix-accordion-content-height)',
|
||||
},
|
||||
to: {
|
||||
height: '0',
|
||||
},
|
||||
},
|
||||
},
|
||||
animation: {
|
||||
'accordion-down': 'accordion-down 0.2s ease-out',
|
||||
'accordion-up': 'accordion-up 0.2s ease-out',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [require('tailwindcss-animate')],
|
||||
}
|
||||
export default config
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"target": "ES6",
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||