Compare commits
11 Commits
main
..
c3563b9513
| Author | SHA1 | Date | |
|---|---|---|---|
| c3563b9513 | |||
| e88d145df7 | |||
| 8c247d8afd | |||
| ede8c21c8d | |||
| cf90973671 | |||
| 2271946f1e | |||
| c92d7d7f88 | |||
| 9d34821c83 | |||
| e4f465f7c9 | |||
| 47f2f4c13f | |||
| 6898297935 |
@@ -75,15 +75,6 @@ When auto-applying an agent, inform the user:
|
|||||||
|
|
||||||
## TIER 0: UNIVERSAL RULES (Always Active)
|
## TIER 0: UNIVERSAL RULES (Always Active)
|
||||||
|
|
||||||
### 🌍 Global User Rules (MANDATORY)
|
|
||||||
|
|
||||||
1. **Idioma**: Conversar e desenvolver sempre em **Português do Brasil**.
|
|
||||||
2. **Automação Terminais**: Executar comandos de terminal, leitura de arquivos e edições de forma automática. Comandos de desenvolvimento (build, install, lint, git, etc.) são sempre `SafeToAutoRun`.
|
|
||||||
3. **Memória & Monitoramento**: Após cada evolução ou atualização no app, é OBRIGATÓRIO:
|
|
||||||
- Atualizar a memória do app em `/root/Antigravity_Memory/knowledge/{AppName}.md`.
|
|
||||||
- Atualizar o status e credenciais no `app.py` do Cronos Dashboard.
|
|
||||||
- Realizar `git commit`, `git push` e executar o `update.sh` do Cronos para deploy automático.
|
|
||||||
|
|
||||||
### 🌐 Language Handling
|
### 🌐 Language Handling
|
||||||
|
|
||||||
When user's prompt is NOT in English:
|
When user's prompt is NOT in English:
|
||||||
|
|||||||
+4
-3
@@ -1,6 +1,7 @@
|
|||||||
node_modules
|
node_modules
|
||||||
dist
|
dist
|
||||||
.git
|
.git
|
||||||
.gitignore
|
.agent
|
||||||
*.md
|
.antigravity
|
||||||
.env*
|
uploads
|
||||||
|
*.pdf
|
||||||
|
|||||||
+4
-10
@@ -1,15 +1,9 @@
|
|||||||
FROM node:22-alpine
|
FROM node:22-slim
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
COPY package*.json ./
|
COPY package*.json ./
|
||||||
RUN npm install
|
RUN npm ci
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# Build the frontend
|
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
ENV PORT=3000
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/server/src/server/index.js"]
|
||||||
CMD ["npm", "run", "start"]
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
Tue Mar 31 10:45:40 UTC 2026
|
Force Refresh Vercel - Timestamp: 2026-01-25 13:55
|
||||||
|
Commit Hash Target: 30f8b5c
|
||||||
|
|||||||
-55
@@ -1,55 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import cors from 'cors';
|
|
||||||
import projectRoutes from '../src/server/routes/projectRoutes.js';
|
|
||||||
import partRoutes from '../src/server/routes/partRoutes.js';
|
|
||||||
import paintingSchemeRoutes from '../src/server/routes/paintingSchemeRoutes.js';
|
|
||||||
import applicationRecordRoutes from '../src/server/routes/applicationRecordRoutes.js';
|
|
||||||
import inspectionRoutes from '../src/server/routes/inspectionRoutes.js';
|
|
||||||
import analysisRoutes from '../src/server/routes/analysisRoutes.js';
|
|
||||||
import dataSheetRoutes from '../src/server/routes/dataSheetRoutes.js';
|
|
||||||
import yieldStudyRoutes from '../src/server/routes/yieldStudyRoutes.js';
|
|
||||||
import userRoutes from '../src/server/routes/userRoutes.js';
|
|
||||||
import systemSettingsRoutes from '../src/server/routes/systemSettingsRoutes.js';
|
|
||||||
import geometryTypeRoutes from '../src/server/routes/geometryTypeRoutes.js';
|
|
||||||
import stockRoutes from '../src/server/routes/stockRoutes.js';
|
|
||||||
import notificationRoutes from '../src/server/routes/notificationRoutes.js';
|
|
||||||
import instrumentRoutes from '../src/server/routes/instrumentRoutes.js';
|
|
||||||
import { extractUser } from '../src/server/middleware/authMiddleware.js';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
|
|
||||||
app.use(cors({
|
|
||||||
origin: '*', // Be more specific in production
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id', 'x-organization-name']
|
|
||||||
}));
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Global Middleware
|
|
||||||
app.use(extractUser);
|
|
||||||
|
|
||||||
// Static Uploads
|
|
||||||
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')));
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
app.use('/api/users', userRoutes);
|
|
||||||
app.use('/api/projects', projectRoutes);
|
|
||||||
app.use('/api/parts', partRoutes);
|
|
||||||
app.use('/api/painting-schemes', paintingSchemeRoutes);
|
|
||||||
app.use('/api/application-records', applicationRecordRoutes);
|
|
||||||
app.use('/api/inspections', inspectionRoutes);
|
|
||||||
app.use('/api', analysisRoutes);
|
|
||||||
app.use('/api/datasheets', dataSheetRoutes);
|
|
||||||
app.use('/api/yield-studies', yieldStudyRoutes);
|
|
||||||
app.use('/api/system-settings', systemSettingsRoutes);
|
|
||||||
app.use('/api/geometry-types', geometryTypeRoutes);
|
|
||||||
app.use('/api/stock', stockRoutes);
|
|
||||||
app.use('/api/notifications', notificationRoutes);
|
|
||||||
app.use('/api/instruments', instrumentRoutes);
|
|
||||||
|
|
||||||
app.get('/health', (req, res) => {
|
|
||||||
res.json({ status: 'ok', timestamp: new Date() });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app;
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
|
|
||||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
||||||
try {
|
|
||||||
const uri = process.env.MONGODB_URI;
|
|
||||||
if (!uri) throw new Error('MONGODB_URI is missing from Vercel settings');
|
|
||||||
|
|
||||||
await mongoose.connect(uri);
|
|
||||||
const state = mongoose.connection.readyState;
|
|
||||||
await mongoose.disconnect();
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
success: true,
|
|
||||||
message: 'MongoDB Connection verified!',
|
|
||||||
state: state === 1 ? 'Connected' : state
|
|
||||||
});
|
|
||||||
} catch (error: unknown) {
|
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
const stack = error instanceof Error ? error.stack : undefined;
|
|
||||||
res.status(500).json({
|
|
||||||
success: false,
|
|
||||||
error: message,
|
|
||||||
stack: stack
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
||||||
import app from './app.js';
|
|
||||||
import { connectDB } from '../src/server/config/database.js';
|
|
||||||
|
|
||||||
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
||||||
try {
|
|
||||||
console.log('--- API CALL:', req.url);
|
|
||||||
|
|
||||||
// Conecta ao Banco de Dados (Supabase/Postgres)
|
|
||||||
await connectDB();
|
|
||||||
|
|
||||||
// Passa o controle para o Express
|
|
||||||
return app(req, res);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('SERVERLESS BOOT ERROR:', error);
|
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
return res.status(500).json({
|
|
||||||
error: 'Serverless Boot Error',
|
|
||||||
message: message,
|
|
||||||
path: req.url
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-10
@@ -1,10 +0,0 @@
|
|||||||
import type { VercelRequest, VercelResponse } from '@vercel/node';
|
|
||||||
|
|
||||||
export default function handler(req: VercelRequest, res: VercelResponse) {
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
message: 'Vercel API is alive',
|
|
||||||
time: new Date().toISOString(),
|
|
||||||
env_check: process.env.MONGODB_URI ? 'URI present' : 'URI MISSING'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
TRUNCATE gpi.projects, gpi.technical_data_sheets, gpi.painting_schemes, gpi.parts, gpi.stock_items, gpi.notifications, gpi.yield_studies, gpi.geometry_types, gpi.messages, gpi.stock_audit_logs, gpi.system_settings, gpi.stored_files CASCADE;
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('8d3d82a6-584b-4a3f-9202-72b4d6cac8f1', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Chaparia comum', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('f994e015-dece-4d57-a112-b4813ba0f7b1', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Chapas de pisos (>0,5m²)', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('e19abd8b-f1b5-4eb2-a232-db38a5c2a306', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Peças diversas (outras)', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('cc789669-5801-4885-be69-e981adf73682', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Guarda-corpo/escada', 50, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('9c1b96c3-f41d-4062-afd9-ba600ccbd441', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Vigas leves', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('37fc80e8-7c67-44cf-9dcf-9571bbc936c5', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Calhas', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('d464cab2-2053-4b22-a770-6db47c40e508', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Telhas', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('654f690b-dd45-4d89-8d05-81b0a3963989', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Vigas médias', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('8e4a569d-2a03-4740-bd2a-b0077a03ef4f', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Vigas pesadas', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('41aaea81-3880-43fe-8455-3cf5e8ecee74', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Cantoneiras', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('97f82995-bfa9-4af2-8b16-45664d180249', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Tubulações (ret/red) <100mm', 20, NOW());
|
|
||||||
INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES ('ff89d5bd-5b16-4462-8d5e-7ff79062b510', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Tubulações (ret/red) >100mm', 20, NOW());
|
|
||||||
INSERT INTO gpi.technical_data_sheets (id, organization_id, name, manufacturer, manufacturer_code, type, min_stock, typical_application, solids_volume, density, mixing_ratio, yield_theoretical, wft_min, wft_max, dft_min, dft_max, reducer, mixing_ratio_weight, mixing_ratio_volume, dft_reference, yield_factor, dilution, notes, created_at, updated_at) VALUES ('900da2be-c901-453d-9dff-7aff16dd9a35', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Revran PHZ 528', 'RENNER', NULL, 'Epóxi (N-2630)', NULL, NULL, 82, NULL, NULL, 8.2, 122, 183, 100, 150, '420.0000', '100,0 : 101,0', '1,0 : 1,0', 100, 820, 10, '', '2026-01-24T11:26:09.181Z', '2026-02-06T18:55:55.697Z');
|
|
||||||
INSERT INTO gpi.technical_data_sheets (id, organization_id, name, manufacturer, manufacturer_code, type, min_stock, typical_application, solids_volume, density, mixing_ratio, yield_theoretical, wft_min, wft_max, dft_min, dft_max, reducer, mixing_ratio_weight, mixing_ratio_volume, dft_reference, yield_factor, dilution, notes, created_at, updated_at) VALUES ('d5392776-e6a4-41f9-8c75-65660563c978', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Revran DST QD 721', 'RENNER', NULL, 'Epóxi dupla função', NULL, NULL, 80, NULL, NULL, 6.67, 150, 312, 120, 250, '420.0000', '100,0 : 88.0000', '1,0 : 1,0', 120, 800, 10, '', '2026-01-24T11:26:09.340Z', '2026-02-06T18:54:37.426Z');
|
|
||||||
INSERT INTO gpi.technical_data_sheets (id, organization_id, name, manufacturer, manufacturer_code, type, min_stock, typical_application, solids_volume, density, mixing_ratio, yield_theoretical, wft_min, wft_max, dft_min, dft_max, reducer, mixing_ratio_weight, mixing_ratio_volume, dft_reference, yield_factor, dilution, notes, created_at, updated_at) VALUES ('8e099c07-7ad4-4d94-8846-cd0c6dd08e06', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Oxibar DFC 707', 'RENNER', NULL, 'Epóxi dupla função', NULL, NULL, 82, NULL, NULL, 8.2, 121, 244, 100, 200, '420.0000', '100,0 : 15,0', '4,0 : 1,0', 100, 820, 10, '', '2026-01-30T10:53:06.720Z', '2026-02-06T18:54:01.241Z');
|
|
||||||
INSERT INTO gpi.technical_data_sheets (id, organization_id, name, manufacturer, manufacturer_code, type, min_stock, typical_application, solids_volume, density, mixing_ratio, yield_theoretical, wft_min, wft_max, dft_min, dft_max, reducer, mixing_ratio_weight, mixing_ratio_volume, dft_reference, yield_factor, dilution, notes, created_at, updated_at) VALUES ('3c0651fc-93e9-436c-a6b8-72c1d4c848a9', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Rethane FHB 658', 'RENNER', NULL, 'Poliuretano (PU)', NULL, NULL, 70, NULL, NULL, 11.7, 60, 125, 86, 178, '440.0000', '100,0 : 18,0', '4,0 : 1,0', 60, 700, 10, '', '2026-01-30T14:36:46.654Z', '2026-02-06T18:47:20.843Z');
|
|
||||||
INSERT INTO gpi.technical_data_sheets (id, organization_id, name, manufacturer, manufacturer_code, type, min_stock, typical_application, solids_volume, density, mixing_ratio, yield_theoretical, wft_min, wft_max, dft_min, dft_max, reducer, mixing_ratio_weight, mixing_ratio_volume, dft_reference, yield_factor, dilution, notes, created_at, updated_at) VALUES ('be6eaa90-03ff-4028-bae8-2928fbdfb254', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Revran DST PLUS 727', 'RENNER', NULL, 'Epóxi dupla função', NULL, NULL, 80, NULL, NULL, 6.7, 150, 188, 120, 150, '420.0000', '100,0 : 95,0', '1,0 : 1,0', 120, 800, 10, '', '2026-02-06T18:50:27.668Z', '2026-02-06T18:52:53.633Z');
|
|
||||||
INSERT INTO gpi.technical_data_sheets (id, organization_id, name, manufacturer, manufacturer_code, type, min_stock, typical_application, solids_volume, density, mixing_ratio, yield_theoretical, wft_min, wft_max, dft_min, dft_max, reducer, mixing_ratio_weight, mixing_ratio_volume, dft_reference, yield_factor, dilution, notes, created_at, updated_at) VALUES ('732f2322-6fab-4e1e-b10b-c5928edc2351', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Diluente para Epoxi (Revran)', 'RENNER', '420.0000', 'THINNER', 30, 'diluente para epoxi da linha Revran e demais que utilizam o mesmo codigo', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-02-12T14:33:21.603Z', '2026-02-12T14:33:21.603Z');
|
|
||||||
INSERT INTO gpi.technical_data_sheets (id, organization_id, name, manufacturer, manufacturer_code, type, min_stock, typical_application, solids_volume, density, mixing_ratio, yield_theoretical, wft_min, wft_max, dft_min, dft_max, reducer, mixing_ratio_weight, mixing_ratio_volume, dft_reference, yield_factor, dilution, notes, created_at, updated_at) VALUES ('c65de128-fa1e-4a93-bd3c-780d6de39199', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Diluente PU e Esmalte Sintético (Rethanne)', 'RENNER', '440.0000', 'THINNER', 10, 'diluente para tintas da linha Rethanne e demais que utilizam o mesmo codigo', NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, '2026-02-12T14:35:04.836Z', '2026-02-12T14:54:04.925Z');
|
|
||||||
INSERT INTO gpi.projects (id, organization_id, name, client, start_date, end_date, environment, technician, weight_kg, status, created_at, updated_at) VALUES ('1c7ed38a-a7b8-4ab4-965c-b74cbf9a6291', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'B121 - Residência Bia', 'FairBanks', '2025-12-01T00:00:00.000Z', '2026-03-31T00:00:00.000Z', 'C3', 'Eng. Baldon', 32165, 'active', '2026-02-09T10:21:58.928Z', '2026-02-09T10:24:35.727Z');
|
|
||||||
INSERT INTO gpi.projects (id, organization_id, name, client, start_date, end_date, environment, technician, weight_kg, status, created_at, updated_at) VALUES ('28ac813e-0d9d-4ef3-8b4e-ba34224f2e55', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'B129 - Rampa Toyota', 'Loja Toyota', '2026-02-11T00:00:00.000Z', '2026-04-15T00:00:00.000Z', 'C3', 'Eng. Baldon', 12430, 'active', '2026-02-09T10:36:24.278Z', '2026-02-25T17:54:11.346Z');
|
|
||||||
INSERT INTO gpi.projects (id, organization_id, name, client, start_date, end_date, environment, technician, weight_kg, status, created_at, updated_at) VALUES ('30db3f02-8631-4c11-bba9-74b40c6ebb1c', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'B128 - Cobertura Bridgestone', 'BRIDGESTONE', '2026-01-15T00:00:00.000Z', '2026-03-31T00:00:00.000Z', 'C4', 'Eng. Baldon', 6880, 'active', '2026-02-09T10:40:53.554Z', '2026-02-09T10:40:53.554Z');
|
|
||||||
INSERT INTO gpi.painting_schemes (id, organization_id, project_id, name, type, coat, solids_volume, yield_theoretical, eps_min, eps_max, dilution, manufacturer, color, paint_consumption, thinner_consumption, paint_id, thinner_id, color_hex, thinner_symbol, notes, created_at, updated_at) VALUES ('5ef2647b-4edd-48cf-9ae3-4c7e89d2ed4d', 'e47e6210-4879-4e5b-bf21-9285d2713123', '1c7ed38a-a7b8-4ab4-965c-b74cbf9a6291', 'Revran DST QD 721', 'epoxy', 'Primer', 80, 6.67, 100, 140, 10, 'RENNER', 'Cinza N6.5', 12, 15, '6974ac5112e9ddef6c122b81', NULL, '#dedede', '420.0000', '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.painting_schemes (id, organization_id, project_id, name, type, coat, solids_volume, yield_theoretical, eps_min, eps_max, dilution, manufacturer, color, paint_consumption, thinner_consumption, paint_id, thinner_id, color_hex, thinner_symbol, notes, created_at, updated_at) VALUES ('98efc99e-300c-4e3d-bb93-bcd7e27f25f0', 'e47e6210-4879-4e5b-bf21-9285d2713123', '28ac813e-0d9d-4ef3-8b4e-ba34224f2e55', 'Revran PHZ 528', 'epoxy', 'Primer', 82, 8.2, 100, 140, 15, 'RENNER', 'Cinza Grafite', 12, 20, '6974ac5112e9ddef6c122b7e', NULL, '#6b6b6b', '420.0000', '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.painting_schemes (id, organization_id, project_id, name, type, coat, solids_volume, yield_theoretical, eps_min, eps_max, dilution, manufacturer, color, paint_consumption, thinner_consumption, paint_id, thinner_id, color_hex, thinner_symbol, notes, created_at, updated_at) VALUES ('d49a9fae-1dc2-42ef-9d12-200d97207e59', 'e47e6210-4879-4e5b-bf21-9285d2713123', '30db3f02-8631-4c11-bba9-74b40c6ebb1c', 'Revran PHZ 528', 'epoxy', 'Primer', 82, 8.2, 100, 120, 10, 'RENNER', 'Cinza N6.5', 12, 15, '6974ac5112e9ddef6c122b7e', NULL, '#dedede', '420.0000', '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.painting_schemes (id, organization_id, project_id, name, type, coat, solids_volume, yield_theoretical, eps_min, eps_max, dilution, manufacturer, color, paint_consumption, thinner_consumption, paint_id, thinner_id, color_hex, thinner_symbol, notes, created_at, updated_at) VALUES ('6233be81-1de2-4a6f-9589-60db5b5dfc0f', 'e47e6210-4879-4e5b-bf21-9285d2713123', '30db3f02-8631-4c11-bba9-74b40c6ebb1c', 'Oxibar DFC 707', 'epoxy', 'Acabamento', 82, 8.2, 100, 120, 10, 'RENNER', 'Branco', 10, 15, '697c8d9210fff6d9214c398f', NULL, '#ffffff', '420.0000', '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.parts (id, organization_id, project_id, description, dimensions, weight, type, area, quantity, notes, created_at, updated_at) VALUES ('d9a46d4e-65bd-4fab-a45a-9c35f0373f6a', 'e47e6210-4879-4e5b-bf21-9285d2713123', '30db3f02-8631-4c11-bba9-74b40c6ebb1c', 'Peças diversas (outras)', NULL, 6880, 'Peças diversas (outras)', 450, 1, 'Colunas , vigas e terças (perfi UE) . Todos similares em area de pintura por tipo de peça', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.stock_items (id, organization_id, name, type, batch_number, quantity, unit, data_sheet_id, location, expiration_date, status, notes, created_at, updated_at) VALUES ('c71251e8-d5ad-4bda-9af5-4ee58a0d25a6', 'e47e6210-4879-4e5b-bf21-9285d2713123', NULL, NULL, '2256132', 160, 'L', '697cc1fe5e1c0a9d4ebb92e4', NULL, '2026-07-12T00:00:00.000Z', NULL, '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.stock_items (id, organization_id, name, type, batch_number, quantity, unit, data_sheet_id, location, expiration_date, status, notes, created_at, updated_at) VALUES ('35a3ee6a-6842-41c5-9348-569ffd18630b', 'e47e6210-4879-4e5b-bf21-9285d2713123', NULL, NULL, '2258096', 14.4, 'L', '6974ac5112e9ddef6c122b81', NULL, '2027-01-11T00:00:00.000Z', NULL, '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.stock_items (id, organization_id, name, type, batch_number, quantity, unit, data_sheet_id, location, expiration_date, status, notes, created_at, updated_at) VALUES ('1283b55c-2ceb-4285-b0b6-b10fcd9aa469', 'e47e6210-4879-4e5b-bf21-9285d2713123', NULL, NULL, '2280862', 80, 'L', '6974ac5112e9ddef6c122b81', NULL, '2026-12-08T00:00:00.000Z', NULL, '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.stock_items (id, organization_id, name, type, batch_number, quantity, unit, data_sheet_id, location, expiration_date, status, notes, created_at, updated_at) VALUES ('1b7e7116-8c3f-4b26-b789-bf1ced9563fa', 'e47e6210-4879-4e5b-bf21-9285d2713123', NULL, NULL, '2256094', 18, 'L', '697cc1fe5e1c0a9d4ebb92e4', NULL, '2027-03-04T00:00:00.000Z', NULL, '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.stock_items (id, organization_id, name, type, batch_number, quantity, unit, data_sheet_id, location, expiration_date, status, notes, created_at, updated_at) VALUES ('68297f1e-4248-45de-9447-d42aafd22665', 'e47e6210-4879-4e5b-bf21-9285d2713123', NULL, NULL, '2217893', 60, 'L', '6974ac5112e9ddef6c122b81', NULL, '2026-01-30T00:00:00.000Z', NULL, '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.stock_items (id, organization_id, name, type, batch_number, quantity, unit, data_sheet_id, location, expiration_date, status, notes, created_at, updated_at) VALUES ('d7c9eb46-300b-41b8-8055-5ef59e2fb3e8', 'e47e6210-4879-4e5b-bf21-9285d2713123', NULL, NULL, '2259482', 20, 'L', '697cc1fe5e1c0a9d4ebb92e4', NULL, '2026-12-01T00:00:00.000Z', NULL, '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.stock_items (id, organization_id, name, type, batch_number, quantity, unit, data_sheet_id, location, expiration_date, status, notes, created_at, updated_at) VALUES ('79a45e7b-b2e6-4c3f-852f-739028321142', 'e47e6210-4879-4e5b-bf21-9285d2713123', NULL, NULL, '2293040', 80, 'L', '697c8d9210fff6d9214c398f', NULL, '2027-08-30T00:00:00.000Z', NULL, '', NOW(), NOW());
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('0316dc9b-5fe7-4404-aff2-ff4abd9aeae3', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Item Vencido', 'O item 0107/24 - Lote 2217893 venceu em 29/01/2026.', 'error', false, false, '2026-02-12T12:37:19.420Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('39a10f4f-6c34-4bba-b863-9adb347bcb34', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Estoque Baixo!', 'O item 0305/25 (Rethane FHB 658) está com apenas 18L. (Mínimo: 20L)', 'error', false, false, '2026-02-12T12:40:46.684Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('eda70cc2-968b-4cce-a29e-7cd147b3a8c3', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Estoque Baixo (Total)', 'O produto Revran DST QD 721 (Cor: Cinza N6.5) atingiu o nível crítico. Total: 94.4L. (Mínimo: 100L)', 'error', false, false, '2026-02-12T13:26:38.421Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('d6d4cf44-bf16-4e24-a09f-3d33c0d2ec24', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Nova Ficha Técnica', 'A ficha técnica "Diluente para Epoxi (Revran)" (RENNER) foi adicionada à biblioteca.', 'info', false, false, '2026-02-12T14:33:21.615Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('6d470211-1f46-4f44-929e-f4048e71da30', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Nova Ficha Técnica', 'A ficha técnica "Diluente para tintas PU e Esmalte Sintético (Rethanne)" (RENNER) foi adicionada à biblioteca.', 'info', false, false, '2026-02-12T14:35:04.846Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('ea8be6bd-bd64-4719-8154-87df66ea07a4', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Recebimento de Material', 'Recebido: 12L de 4444 (Lote: N/A).', 'info', false, false, '2026-02-12T19:01:56.081Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('bf315f52-984f-436e-8323-2f17841807b1', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Estoque Baixo (Total)', 'O produto Diluente para Epoxi (Revran) (Cor: N/A) atingiu o nível crítico. Total: 7.0L. (Mínimo: 10L)', 'error', false, false, '2026-02-12T19:02:57.679Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('e2514cbc-0dd4-48cb-a4dd-336574dcc510', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Atualização de Obra', 'O peso da obra "B129 - Rampa Toyota" foi atualizado para 11925kg.', 'info', true, false, '2026-02-25T13:08:33.461Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('d7ef7575-c21b-499d-b9dc-bfb42d7302ac', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Atualização de Obra', 'O peso da obra "B129 - Rampa Toyota" foi atualizado para 12430kg.', 'info', false, false, '2026-02-25T17:54:11.359Z');
|
|
||||||
INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES ('ca87d8aa-e4f6-43bc-9316-6da0e2e185b4', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Item Vencido', 'O item 0107/24 - Lote 2217893 venceu em 1/30/2026.', 'error', false, false, '2026-03-14T15:52:41.972Z');
|
|
||||||
INSERT INTO gpi.yield_studies (id, organization_id, name, data_sheet_id, target_dft, dilution_percent, categories, total_weight, estimated_paint_volume, estimated_reducer_volume, estimated_paint_volume_by_area, estimated_reducer_volume_by_area, average_complexity, created_at, updated_at) VALUES ('6f9f5a49-90cf-47da-815d-3e726631e9fe', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Novo Estudo - 04/02/26 18:41', '697cc1fe5e1c0a9d4ebb92e4', 120, 10, '[{"name":"Calhas","weight":1.3,"area":160,"historicalYield":12,"historicalDft":120,"efficiency":10,"_id":"6983bd1c94117d5e52856de1"},{"name":"Vigas leves","weight":2,"area":200,"historicalYield":12,"historicalDft":100,"efficiency":24,"_id":"6983bf5294117d5e52856dfc"}]', 3.3, 48.91, 4.89, 75.59, 7.56, 1, NOW(), NOW());
|
|
||||||
INSERT INTO gpi.yield_studies (id, organization_id, name, data_sheet_id, target_dft, dilution_percent, categories, total_weight, estimated_paint_volume, estimated_reducer_volume, estimated_paint_volume_by_area, estimated_reducer_volume_by_area, average_complexity, created_at, updated_at) VALUES ('b9663cc7-0abe-4955-b2fc-05258bae6e88', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Novo Estudo - 05/02/26 16:07', '697cc1fe5e1c0a9d4ebb92e4', 120, 10, '[{"name":"Estrutura Primária","weight":1000,"area":0,"historicalYield":150,"historicalDft":120,"efficiency":85,"_id":"6984ea6a71299e618edb7c7c"}]', 1000, 0, 0, NULL, NULL, 1, NOW(), NOW());
|
|
||||||
INSERT INTO gpi.yield_studies (id, organization_id, name, data_sheet_id, target_dft, dilution_percent, categories, total_weight, estimated_paint_volume, estimated_reducer_volume, estimated_paint_volume_by_area, estimated_reducer_volume_by_area, average_complexity, created_at, updated_at) VALUES ('4d731b8f-968f-4039-8d3f-98d9d2327fd1', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'Novo Estudo - 09/02/26 10:25', '698637f3059732a5be0b3535', 120, 10, '[{"name":"Estrutura Primária","weight":1000,"area":0,"historicalYield":150,"historicalDft":120,"efficiency":85,"_id":"6989e053e35a6917a7779bc6"}]', 1000, 0, 0, NULL, NULL, 1, NOW(), NOW());
|
|
||||||
INSERT INTO gpi.yield_studies (id, organization_id, name, data_sheet_id, target_dft, dilution_percent, categories, total_weight, estimated_paint_volume, estimated_reducer_volume, estimated_paint_volume_by_area, estimated_reducer_volume_by_area, average_complexity, created_at, updated_at) VALUES ('bd7d8c62-4a14-4937-bdb9-bfa363e93151', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'ESTUDO TOYOTA OFICIAL - 25/02/26', '6974ac5112e9ddef6c122b7e', 120, 20, '[{"name":"Vigas leves","weight":0.62,"area":18,"historicalYield":11,"historicalDft":120,"efficiency":70,"_id":"699ef49982121ff9fb05b214"},{"name":"Vigas leves","weight":3.86,"area":152,"historicalYield":12,"historicalDft":100,"efficiency":70,"_id":"699ef8c425baf4a3df44f593"},{"name":"Chapas de pisos (>0,5m²)","weight":6.2,"area":161,"historicalYield":7,"historicalDft":100,"efficiency":90,"_id":"699ef8c425baf4a3df44f594"},{"name":"Vigas leves","weight":1.7,"area":62,"historicalYield":13,"historicalDft":100,"efficiency":65,"_id":"699ef8c425baf4a3df44f595"}]', 12.379999999999999, 118.64, 23.73, 75.68, 15.14, 1, NOW(), NOW());
|
|
||||||
INSERT INTO gpi.messages (id, organization_id, message, from_user_id, to_user_id, is_read, created_at) VALUES ('3f4a7b79-1fd8-4f5a-bf8d-ebbb66e443c9', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'teste de mensagem 1', NULL, NULL, false, '2026-02-11T10:50:53.420Z');
|
|
||||||
INSERT INTO gpi.stock_audit_logs (id, organization_id, stock_item_id, action, quantity_before, quantity_after, performed_by, details, created_at) VALUES ('2d7ea21b-60bc-4883-b3b6-702491cb72dd', 'e47e6210-4879-4e5b-bf21-9285d2713123', '6984ba67b558533c8f30f419', 'UPDATE', NULL, NULL, NULL, 'Edição de Movimentação (ADJUSTMENT): Qtd -15 -> -14', '2026-02-06T20:42:23.410Z');
|
|
||||||
INSERT INTO gpi.stock_audit_logs (id, organization_id, stock_item_id, action, quantity_before, quantity_after, performed_by, details, created_at) VALUES ('bf23c460-67c3-465c-9f47-ce6a9e7a55d8', 'e47e6210-4879-4e5b-bf21-9285d2713123', '6984ba67b558533c8f30f419', 'UPDATE', NULL, NULL, NULL, 'Edição de Movimentação (ADJUSTMENT): Qtd -14 -> -14', '2026-02-06T20:42:46.091Z');
|
|
||||||
INSERT INTO gpi.stock_audit_logs (id, organization_id, stock_item_id, action, quantity_before, quantity_after, performed_by, details, created_at) VALUES ('9288f2ea-11af-4567-bc3c-26a760216969', 'e47e6210-4879-4e5b-bf21-9285d2713123', '6984ba67b558533c8f30f419', 'UPDATE', NULL, NULL, NULL, 'Edição de Movimentação (ADJUSTMENT): Qtd -14 -> -14', '2026-02-06T20:47:21.549Z');
|
|
||||||
INSERT INTO gpi.stock_audit_logs (id, organization_id, stock_item_id, action, quantity_before, quantity_after, performed_by, details, created_at) VALUES ('6dd63755-920d-4845-8740-6a371b65b9c5', 'e47e6210-4879-4e5b-bf21-9285d2713123', '6984ba67b558533c8f30f419', 'UPDATE', NULL, NULL, NULL, 'Edição de Movimentação (#4 ADJUSTMENT): Qtd -3 -> -2', '2026-02-06T21:11:30.917Z');
|
|
||||||
INSERT INTO gpi.system_settings (id, organization_id, key, value, updated_at) VALUES ('01ce7ceb-5fdd-4c4e-9e05-3807eba280e0', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'global', '{"appName":"SteelPaint","appSubtitle":"Gestão de Pintura Industrial","createdAt":"2026-02-04T10:42:20.738Z","updatedAt":"2026-02-12T20:20:05.747Z","__v":0,"appLogoUrl":"/api/system-settings/logo-image/logo-1770927600175-e0488184-c2a1-4ba1-a8f4-eac55e875c94.png","updatedBy":"admtracksteel@gmail.com"}', '2026-02-12T20:20:05.747Z');
|
|
||||||
INSERT INTO gpi.stored_files (id, organization_id, filename, mime_type, size_bytes, storage_path, metadata, created_at) VALUES ('52cdfbc6-e9c9-4759-b59c-d1b8a76ea617', 'e47e6210-4879-4e5b-bf21-9285d2713123', 'E-412_Revran-DST-PLUS-727-BV_V01.pdf', NULL, 249614, NULL, NULL, NULL);
|
|
||||||
INSERT INTO gpi.stored_files (id, organization_id, filename, mime_type, size_bytes, storage_path, metadata, created_at) VALUES ('52885971-7e82-4ed7-afaf-b908c74c67eb', 'e47e6210-4879-4e5b-bf21-9285d2713123', '0590_Revran-DST-PLUS-727_V06.pdf', NULL, 182807, NULL, NULL, NULL);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
-- Criar tabela de mensagens no schema gpi
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.messages (
|
|
||||||
id uuid DEFAULT uuid_generate_v4() PRIMARY KEY,
|
|
||||||
organization_id uuid REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
|
||||||
from_user_id uuid REFERENCES gpi.users(id) ON DELETE SET NULL,
|
|
||||||
to_user_id uuid REFERENCES gpi.users(id) ON DELETE SET NULL,
|
|
||||||
message text NOT NULL,
|
|
||||||
is_read boolean DEFAULT false,
|
|
||||||
read_at timestamp with time zone,
|
|
||||||
is_archived boolean DEFAULT false,
|
|
||||||
is_deleted_by_recipient boolean DEFAULT false,
|
|
||||||
created_at timestamp with time zone DEFAULT now(),
|
|
||||||
updated_at timestamp with time zone DEFAULT now()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Permissões para as roles do Supabase
|
|
||||||
GRANT ALL ON gpi.messages TO postgres, anon, authenticated, service_role;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
-- Script para expor as tabelas do schema 'gpi' no schema 'public' via views
|
|
||||||
-- Isso facilita o acesso pela API padrão do Supabase/PostgREST
|
|
||||||
|
|
||||||
-- 1. Garantir que o schema public existe
|
|
||||||
CREATE SCHEMA IF NOT EXISTS public;
|
|
||||||
|
|
||||||
-- 2. Criar views no schema public para cada tabela do gpi
|
|
||||||
CREATE OR REPLACE VIEW public.organizations AS SELECT * FROM gpi.organizations;
|
|
||||||
CREATE OR REPLACE VIEW public.users AS SELECT * FROM gpi.users;
|
|
||||||
CREATE OR REPLACE VIEW public.projects AS SELECT * FROM gpi.projects;
|
|
||||||
CREATE OR REPLACE VIEW public.parts AS SELECT * FROM gpi.parts;
|
|
||||||
CREATE OR REPLACE VIEW public.painting_schemes AS SELECT * FROM gpi.painting_schemes;
|
|
||||||
CREATE OR REPLACE VIEW public.application_records AS SELECT * FROM gpi.application_records;
|
|
||||||
CREATE OR REPLACE VIEW public.inspections AS SELECT * FROM gpi.inspections;
|
|
||||||
CREATE OR REPLACE VIEW public.technical_data_sheets AS SELECT * FROM gpi.technical_data_sheets;
|
|
||||||
CREATE OR REPLACE VIEW public.yield_studies AS SELECT * FROM gpi.yield_studies;
|
|
||||||
CREATE OR REPLACE VIEW public.instruments AS SELECT * FROM gpi.instruments;
|
|
||||||
CREATE OR REPLACE VIEW public.stock_items AS SELECT * FROM gpi.stock_items;
|
|
||||||
CREATE OR REPLACE VIEW public.stock_movements AS SELECT * FROM gpi.stock_movements;
|
|
||||||
CREATE OR REPLACE VIEW public.notifications AS SELECT * FROM gpi.notifications;
|
|
||||||
CREATE OR REPLACE VIEW public.geometry_types AS SELECT * FROM gpi.geometry_types;
|
|
||||||
CREATE OR REPLACE VIEW public.messages AS SELECT * FROM gpi.messages;
|
|
||||||
CREATE OR REPLACE VIEW public.stock_audit_logs AS SELECT * FROM gpi.stock_audit_logs;
|
|
||||||
CREATE OR REPLACE VIEW public.system_settings AS SELECT * FROM gpi.system_settings;
|
|
||||||
CREATE OR REPLACE VIEW public.stored_files AS SELECT * FROM gpi.stored_files;
|
|
||||||
|
|
||||||
-- 3. Dar permissões de acesso às views
|
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO postgres, anon, authenticated, service_role;
|
|
||||||
|
|
||||||
SELECT '18 views criadas no schema public com sucesso!' AS resultado;
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { query } from './src/server/config/database.js';
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
try {
|
||||||
|
console.log("Creating gpi.files...");
|
||||||
|
await query("CREATE TABLE IF NOT EXISTS gpi.files (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), filename TEXT NOT NULL, content_type TEXT NOT NULL, data BYTEA NOT NULL, size INTEGER NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW());");
|
||||||
|
|
||||||
|
console.log("Creating gpi.stock_audit_logs...");
|
||||||
|
await query("CREATE TABLE IF NOT EXISTS gpi.stock_audit_logs (id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID, stock_item_id UUID, movement_id UUID, movement_number INTEGER, user_id TEXT, user_name TEXT, action TEXT, details TEXT, old_values JSONB, new_values JSONB, timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW());");
|
||||||
|
|
||||||
|
console.log("Done.");
|
||||||
|
process.exit(0);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error:", error);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
-- Habilitar schema gpi na PostgREST API
|
|
||||||
-- As permissões abaixo são suficientes para a PostgREST expor o schema
|
|
||||||
|
|
||||||
-- Permissões
|
|
||||||
GRANT USAGE ON SCHEMA gpi TO postgres, anon, authenticated, service_role;
|
|
||||||
|
|
||||||
-- Grant em todas as tabelas existentes do schema gpi
|
|
||||||
GRANT ALL ON ALL TABLES IN SCHEMA gpi TO postgres, anon, authenticated, service_role;
|
|
||||||
|
|
||||||
-- Grant em sequências
|
|
||||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA gpi TO postgres, anon, authenticated, service_role;
|
|
||||||
|
|
||||||
SELECT 'Schema gpi habilitado na PostgREST API!' AS result;
|
|
||||||
-337
@@ -1,337 +0,0 @@
|
|||||||
DROP SCHEMA IF EXISTS gpi CASCADE;
|
|
||||||
-- Criar schema gpi
|
|
||||||
CREATE SCHEMA IF NOT EXISTS gpi;
|
|
||||||
|
|
||||||
-- Tabela organizations
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.organizations (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Inserir organização padrão
|
|
||||||
INSERT INTO gpi.organizations (id, name)
|
|
||||||
VALUES ('e47e6210-4879-4e5b-bf21-9285d2713123', 'Organização Migrada')
|
|
||||||
ON CONFLICT (id) DO NOTHING;
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.users (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
logto_id TEXT UNIQUE,
|
|
||||||
email TEXT NOT NULL,
|
|
||||||
name TEXT,
|
|
||||||
role TEXT DEFAULT 'user',
|
|
||||||
is_banned BOOLEAN DEFAULT false,
|
|
||||||
last_seen_at TIMESTAMPTZ,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.projects (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
client TEXT,
|
|
||||||
start_date DATE,
|
|
||||||
end_date DATE,
|
|
||||||
environment TEXT,
|
|
||||||
technician TEXT,
|
|
||||||
weight_kg DECIMAL(10,2),
|
|
||||||
status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived')),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela parts
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.parts (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
|
|
||||||
description TEXT,
|
|
||||||
dimensions TEXT,
|
|
||||||
weight DECIMAL(10,3),
|
|
||||||
type TEXT,
|
|
||||||
area DECIMAL(10,3),
|
|
||||||
complexity INTEGER DEFAULT 1,
|
|
||||||
quantity INTEGER NOT NULL DEFAULT 1,
|
|
||||||
notes TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela painting_schemes
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.painting_schemes (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
type TEXT,
|
|
||||||
coat TEXT,
|
|
||||||
solids_volume DECIMAL(12,3),
|
|
||||||
yield_theoretical DECIMAL(12,3),
|
|
||||||
eps_min DECIMAL(12,3),
|
|
||||||
eps_max DECIMAL(12,3),
|
|
||||||
dilution DECIMAL(12,3),
|
|
||||||
manufacturer TEXT,
|
|
||||||
color TEXT,
|
|
||||||
paint_consumption DECIMAL(12,3),
|
|
||||||
thinner_consumption DECIMAL(12,3),
|
|
||||||
paint_id TEXT,
|
|
||||||
thinner_id TEXT,
|
|
||||||
color_hex TEXT,
|
|
||||||
thinner_symbol TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela application_records
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.application_records (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
|
|
||||||
coat_stage TEXT NOT NULL,
|
|
||||||
piece_description TEXT,
|
|
||||||
date DATE,
|
|
||||||
operator TEXT,
|
|
||||||
real_weight DECIMAL(10,3),
|
|
||||||
volume_used DECIMAL(10,3),
|
|
||||||
area_painted DECIMAL(10,3),
|
|
||||||
wet_thickness_avg DECIMAL(6,2),
|
|
||||||
dry_thickness_calc DECIMAL(6,2),
|
|
||||||
real_yield DECIMAL(10,3),
|
|
||||||
method TEXT,
|
|
||||||
diluent_used DECIMAL(10,3),
|
|
||||||
items JSONB,
|
|
||||||
notes TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela inspections
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.inspections (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
|
|
||||||
application_record_id UUID REFERENCES gpi.application_records(id),
|
|
||||||
stock_item_id TEXT,
|
|
||||||
instrument_id TEXT,
|
|
||||||
type TEXT CHECK (type IN ('painting', 'surface_treatment')),
|
|
||||||
date DATE,
|
|
||||||
inspector TEXT,
|
|
||||||
part_temperature DECIMAL(6,2),
|
|
||||||
weight_kg DECIMAL(10,3),
|
|
||||||
appearance TEXT,
|
|
||||||
defects TEXT,
|
|
||||||
photos TEXT[],
|
|
||||||
piece_description TEXT,
|
|
||||||
eps_points DECIMAL(6,2)[],
|
|
||||||
adhesion_test TEXT,
|
|
||||||
batch TEXT,
|
|
||||||
treatment_executor TEXT,
|
|
||||||
treatment_type TEXT,
|
|
||||||
cleaning_degree TEXT,
|
|
||||||
roughness_readings DECIMAL(6,2)[],
|
|
||||||
flash_rust TEXT,
|
|
||||||
temperature DECIMAL(6,2),
|
|
||||||
relative_humidity DECIMAL(5,2),
|
|
||||||
period TEXT CHECK (period IN ('morning', 'afternoon', 'night')),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela technical_data_sheets
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.technical_data_sheets (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
manufacturer TEXT,
|
|
||||||
type TEXT,
|
|
||||||
file_url TEXT,
|
|
||||||
upload_date DATE,
|
|
||||||
solids_volume DECIMAL(12,3),
|
|
||||||
density DECIMAL(12,3),
|
|
||||||
mixing_ratio TEXT,
|
|
||||||
yield_theoretical DECIMAL(12,3),
|
|
||||||
wft_min DECIMAL(12,3),
|
|
||||||
wft_max DECIMAL(12,3),
|
|
||||||
dft_min DECIMAL(12,3),
|
|
||||||
dft_max DECIMAL(12,3),
|
|
||||||
reducer TEXT,
|
|
||||||
mixing_ratio_weight TEXT,
|
|
||||||
mixing_ratio_volume TEXT,
|
|
||||||
dft_reference DECIMAL(12,3),
|
|
||||||
yield_factor DECIMAL(12,3),
|
|
||||||
dilution DECIMAL(12,3),
|
|
||||||
notes TEXT,
|
|
||||||
manufacturer_code TEXT,
|
|
||||||
min_stock DECIMAL(12,3),
|
|
||||||
typical_application TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela yield_studies
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.yield_studies (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
data_sheet_id TEXT NOT NULL,
|
|
||||||
target_dft DECIMAL(6,2) NOT NULL,
|
|
||||||
dilution_percent DECIMAL(5,2) NOT NULL,
|
|
||||||
categories JSONB NOT NULL,
|
|
||||||
total_weight DECIMAL(10,3),
|
|
||||||
estimated_paint_volume DECIMAL(10,3),
|
|
||||||
estimated_reducer_volume DECIMAL(10,3),
|
|
||||||
estimated_paint_volume_by_area DECIMAL(10,3),
|
|
||||||
estimated_reducer_volume_by_area DECIMAL(10,3),
|
|
||||||
average_complexity DECIMAL(3,1),
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela instruments
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.instruments (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
type TEXT NOT NULL,
|
|
||||||
serial_number TEXT,
|
|
||||||
manufacturer TEXT,
|
|
||||||
model TEXT,
|
|
||||||
last_calibration DATE,
|
|
||||||
next_calibration DATE,
|
|
||||||
status TEXT DEFAULT 'active',
|
|
||||||
notes TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela stock_items
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.stock_items (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
name TEXT,
|
|
||||||
type TEXT,
|
|
||||||
batch_number TEXT,
|
|
||||||
quantity DECIMAL(12,3) DEFAULT 0,
|
|
||||||
unit TEXT DEFAULT 'L',
|
|
||||||
data_sheet_id TEXT,
|
|
||||||
location TEXT,
|
|
||||||
expiration_date DATE,
|
|
||||||
status TEXT DEFAULT 'available',
|
|
||||||
notes TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela stock_movements
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.stock_movements (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
stock_item_id UUID REFERENCES gpi.stock_items(id) ON DELETE CASCADE,
|
|
||||||
type TEXT NOT NULL CHECK (type IN ('in', 'out', 'adjustment')),
|
|
||||||
quantity DECIMAL(10,3) NOT NULL,
|
|
||||||
reason TEXT,
|
|
||||||
performed_by TEXT,
|
|
||||||
notes TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela notifications
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.notifications (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
title TEXT NOT NULL,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
type TEXT DEFAULT 'info' CHECK (type IN ('info', 'warning', 'error', 'success')),
|
|
||||||
is_read BOOLEAN DEFAULT false,
|
|
||||||
is_archived BOOLEAN DEFAULT false,
|
|
||||||
archived_by TEXT[],
|
|
||||||
deleted_by TEXT[],
|
|
||||||
metadata JSONB,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela geometry_types
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.geometry_types (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
name TEXT NOT NULL,
|
|
||||||
efficiency_loss DECIMAL(5,2),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela messages
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.messages (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
from_user_id UUID,
|
|
||||||
to_user_id UUID,
|
|
||||||
message TEXT NOT NULL,
|
|
||||||
is_read BOOLEAN DEFAULT false,
|
|
||||||
read_at TIMESTAMPTZ,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela stock_audit_logs
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.stock_audit_logs (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
stock_item_id TEXT,
|
|
||||||
action TEXT NOT NULL,
|
|
||||||
quantity_before DECIMAL(12,3),
|
|
||||||
quantity_after DECIMAL(12,3),
|
|
||||||
performed_by TEXT,
|
|
||||||
details TEXT,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela system_settings
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.system_settings (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
key TEXT UNIQUE NOT NULL,
|
|
||||||
value JSONB,
|
|
||||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Tabela stored_files (Metadados dos PDFs)
|
|
||||||
CREATE TABLE IF NOT EXISTS gpi.stored_files (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
organization_id UUID NOT NULL,
|
|
||||||
filename TEXT NOT NULL,
|
|
||||||
mime_type TEXT,
|
|
||||||
size_bytes BIGINT,
|
|
||||||
storage_path TEXT, -- Caminho no Supabase Storage
|
|
||||||
metadata JSONB,
|
|
||||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
-- Habilitar PostgREST para o schema gpi
|
|
||||||
GRANT USAGE ON SCHEMA gpi TO postgres, anon, authenticated, service_role;
|
|
||||||
|
|
||||||
-- Grant permissions em todas as tabelas
|
|
||||||
GRANT ALL ON TABLE gpi.organizations TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.users TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.projects TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.parts TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.painting_schemes TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.application_records TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.inspections TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.technical_data_sheets TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.yield_studies TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.instruments TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.stock_items TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.stock_movements TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.notifications TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.geometry_types TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.messages TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.stock_audit_logs TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.system_settings TO postgres, anon, authenticated, service_role;
|
|
||||||
GRANT ALL ON TABLE gpi.stored_files TO postgres, anon, authenticated, service_role;
|
|
||||||
|
|
||||||
-- Grant sequences
|
|
||||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA gpi TO postgres, anon, authenticated, service_role;
|
|
||||||
|
|
||||||
SELECT 'Schema gpi criado com sucesso!' AS result;
|
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACAp2sxQE/eO30etME9tLKJjguVPUg3W8k3j2H7F/kBVogAAAJAeNjMSHjYz
|
||||||
|
EgAAAAtzc2gtZWQyNTUxOQAAACAp2sxQE/eO30etME9tLKJjguVPUg3W8k3j2H7F/kBVog
|
||||||
|
AAAEDLm78AwM6lbNoz7iVUh1xvlphZzNhitquW4jHyR7lIhCnazFAT947fR60wT20somOC
|
||||||
|
5U9SDdbyTePYfsX+QFWiAAAAC2FudGlncmF2aXR5AQI=
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAICnazFAT947fR60wT20somOC5U9SDdbyTePYfsX+QFWi antigravity
|
||||||
+1
-1
@@ -10,7 +10,7 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<meta name="theme-color" content="#0f172a" />
|
<meta name="theme-color" content="#0f172a" />
|
||||||
<link rel="apple-touch-icon" href="/pwa-192x192.png">
|
<link rel="apple-touch-icon" href="/pwa-192x192.png">
|
||||||
<title>SteelPaint</title>
|
<title>GPI - JWT VERSION 1.6</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
-147
@@ -1,147 +0,0 @@
|
|||||||
import { MongoClient } from 'mongodb';
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
import crypto from 'crypto';
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
const MONGO_URI = "mongodb+srv://admtracksteel:mongodb26@cluster0.a4xiilu.mongodb.net/ts_gpi";
|
|
||||||
const DB_NAME = "ts_gpi";
|
|
||||||
const DEFAULT_ORG_ID = "e47e6210-4879-4e5b-bf21-9285d2713123";
|
|
||||||
|
|
||||||
const client = new MongoClient(MONGO_URI);
|
|
||||||
|
|
||||||
const idMap = new Map();
|
|
||||||
|
|
||||||
function getUUID(mongoId) {
|
|
||||||
if (!mongoId) return null;
|
|
||||||
const mid = mongoId.toString();
|
|
||||||
if (idMap.has(mid)) return idMap.get(mid);
|
|
||||||
const newUuid = crypto.randomUUID();
|
|
||||||
idMap.set(mid, newUuid);
|
|
||||||
return newUuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
function sqlSafe(val) {
|
|
||||||
if (val === null || val === undefined) return 'NULL';
|
|
||||||
if (val instanceof Date) return `'${val.toISOString()}'`;
|
|
||||||
if (Array.isArray(val)) return `'{"${val.join('","')}"}'`;
|
|
||||||
if (typeof val === 'object' && val.toString && !val.getMonth) { // not a date
|
|
||||||
const s = val.toString();
|
|
||||||
return `'${s.replace(/'/g, "''")}'`;
|
|
||||||
}
|
|
||||||
if (typeof val === 'string') return `'${val.replace(/'/g, "''")}'`;
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function run() {
|
|
||||||
try {
|
|
||||||
await client.connect();
|
|
||||||
console.log("🚀 Conectado ao MongoDB Atlas...");
|
|
||||||
const db = client.db(DB_NAME);
|
|
||||||
|
|
||||||
let allSQL = [];
|
|
||||||
|
|
||||||
// 1. Geometrias (Geometry Types)
|
|
||||||
const geoms = await db.collection('geometrytypes').find().toArray();
|
|
||||||
console.log(`📐 Migrando ${geoms.length} tipos de geometria...`);
|
|
||||||
for (const g of geoms) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.geometry_types (id, organization_id, name, efficiency_loss, updated_at) VALUES (${sqlSafe(getUUID(g._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(g.name)}, ${sqlSafe(g.efficiencyLoss)}, NOW());`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Fichas Técnicas (Technical Data Sheets)
|
|
||||||
const tds = await db.collection('technicaldatasheets').find().toArray();
|
|
||||||
console.log(`📚 Migrando ${tds.length} fichas técnicas...`);
|
|
||||||
for (const t of tds) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.technical_data_sheets (id, organization_id, name, manufacturer, manufacturer_code, type, min_stock, typical_application, solids_volume, density, mixing_ratio, yield_theoretical, wft_min, wft_max, dft_min, dft_max, reducer, mixing_ratio_weight, mixing_ratio_volume, dft_reference, yield_factor, dilution, notes, created_at, updated_at) VALUES (${sqlSafe(getUUID(t._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(t.name)}, ${sqlSafe(t.manufacturer)}, ${sqlSafe(t.manufacturerCode)}, ${sqlSafe(t.type)}, ${sqlSafe(t.minStock)}, ${sqlSafe(t.typicalApplication)}, ${sqlSafe(t.solidsVolume)}, ${sqlSafe(t.density)}, ${sqlSafe(t.mixingRatio)}, ${sqlSafe(t.yieldTheoretical)}, ${sqlSafe(t.wftMin)}, ${sqlSafe(t.wftMax)}, ${sqlSafe(t.dftMin)}, ${sqlSafe(t.dftMax)}, ${sqlSafe(t.reducer)}, ${sqlSafe(t.mixingRatioWeight)}, ${sqlSafe(t.mixingRatioVolume)}, ${sqlSafe(t.dftReference)}, ${sqlSafe(t.yieldFactor)}, ${sqlSafe(t.dilution)}, ${sqlSafe(t.notes)}, ${sqlSafe(t.createdAt)}, ${sqlSafe(t.updatedAt)});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Projetos (Projects)
|
|
||||||
const projs = await db.collection('projects').find().toArray();
|
|
||||||
console.log(`📦 Migrando ${projs.length} projetos...`);
|
|
||||||
for (const p of projs) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.projects (id, organization_id, name, client, start_date, end_date, environment, technician, weight_kg, status, created_at, updated_at) VALUES (${sqlSafe(getUUID(p._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(p.name)}, ${sqlSafe(p.client)}, ${sqlSafe(p.startDate)}, ${sqlSafe(p.endDate)}, ${sqlSafe(p.environment)}, ${sqlSafe(p.technician)}, ${sqlSafe(p.weightKg)}, 'active', ${sqlSafe(p.createdAt)}, ${sqlSafe(p.updatedAt)});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. Esquemas de Pintura (Painting Schemes)
|
|
||||||
const schemes = await db.collection('paintingschemes').find().toArray();
|
|
||||||
console.log(`🎨 Migrando ${schemes.length} esquemas de pintura...`);
|
|
||||||
for (const s of schemes) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.painting_schemes (id, organization_id, project_id, name, type, coat, solids_volume, yield_theoretical, eps_min, eps_max, dilution, manufacturer, color, paint_consumption, thinner_consumption, paint_id, thinner_id, color_hex, thinner_symbol, notes, created_at, updated_at) VALUES (${sqlSafe(crypto.randomUUID())}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(getUUID(s.projectId))}, ${sqlSafe(s.name)}, ${sqlSafe(s.type)}, ${sqlSafe(s.coat)}, ${sqlSafe(s.solidsVolume)}, ${sqlSafe(s.yieldTheoretical)}, ${sqlSafe(s.epsMin)}, ${sqlSafe(s.epsMax)}, ${sqlSafe(s.dilution)}, ${sqlSafe(s.manufacturer)}, ${sqlSafe(s.color)}, ${sqlSafe(s.paintConsumption)}, ${sqlSafe(s.thinnerConsumption)}, ${sqlSafe(s.paintId)}, ${sqlSafe(s.thinnerId)}, ${sqlSafe(s.colorHex)}, ${sqlSafe(s.thinnerSymbol)}, ${sqlSafe(s.notes)}, NOW(), NOW());`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. Peças (Parts)
|
|
||||||
const parts = await db.collection('parts').find().toArray();
|
|
||||||
console.log(`🧩 Migrando ${parts.length} peças...`);
|
|
||||||
for (const pt of parts) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.parts (id, organization_id, project_id, description, dimensions, weight, type, area, quantity, notes, created_at, updated_at) VALUES (${sqlSafe(getUUID(pt._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(getUUID(pt.projectId))}, ${sqlSafe(pt.description)}, ${sqlSafe(pt.dimensions)}, ${sqlSafe(pt.weight)}, ${sqlSafe(pt.type)}, ${sqlSafe(pt.area)}, ${sqlSafe(pt.quantity)}, ${sqlSafe(pt.notes)}, NOW(), NOW());`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Itens de Estoque (Stock Items)
|
|
||||||
const stockItems = await db.collection('stockitems').find().toArray();
|
|
||||||
console.log(`🏭 Migrando ${stockItems.length} itens de estoque...`);
|
|
||||||
for (const item of stockItems) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.stock_items (id, organization_id, name, type, batch_number, quantity, unit, data_sheet_id, location, expiration_date, status, notes, created_at, updated_at) VALUES (${sqlSafe(getUUID(item._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(item.name)}, ${sqlSafe(item.type)}, ${sqlSafe(item.batchNumber)}, ${sqlSafe(item.quantity)}, ${sqlSafe(item.unit)}, ${sqlSafe(item.dataSheetId)}, ${sqlSafe(item.location)}, ${sqlSafe(item.expirationDate)}, ${sqlSafe(item.status)}, ${sqlSafe(item.notes)}, NOW(), NOW());`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. Notificações (Notifications)
|
|
||||||
const notifs = await db.collection('notifications').find().toArray();
|
|
||||||
console.log(`🔔 Migrando ${notifs.length} notificações...`);
|
|
||||||
for (const n of notifs) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.notifications (id, organization_id, title, message, type, is_read, is_archived, created_at) VALUES (${sqlSafe(getUUID(n._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(n.title)}, ${sqlSafe(n.message)}, ${sqlSafe(n.type)}, ${sqlSafe(n.isRead)}, ${sqlSafe(n.isArchived)}, ${sqlSafe(n.createdAt)});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 8. Estudos de Rendimento (Yield Studies)
|
|
||||||
const yields = await db.collection('yieldstudies').find().toArray();
|
|
||||||
console.log(`📈 Migrando ${yields.length} estudos de rendimento...`);
|
|
||||||
for (const y of yields) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.yield_studies (id, organization_id, name, data_sheet_id, target_dft, dilution_percent, categories, total_weight, estimated_paint_volume, estimated_reducer_volume, estimated_paint_volume_by_area, estimated_reducer_volume_by_area, average_complexity, created_at, updated_at) VALUES (${sqlSafe(getUUID(y._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(y.name)}, ${sqlSafe(y.dataSheetId)}, ${sqlSafe(y.targetDft)}, ${sqlSafe(y.dilutionPercent)}, ${sqlSafe(JSON.stringify(y.categories))}, ${sqlSafe(y.totalWeight)}, ${sqlSafe(y.estimatedPaintVolume)}, ${sqlSafe(y.estimatedReducerVolume)}, ${sqlSafe(y.estimatedPaintVolumeByArea)}, ${sqlSafe(y.estimatedReducerVolumeByArea)}, ${sqlSafe(y.averageComplexity)}, NOW(), NOW());`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 9. Mensagens (Messages)
|
|
||||||
const msgs = await db.collection('messages').find().toArray();
|
|
||||||
console.log(`💬 Migrando ${msgs.length} mensagens...`);
|
|
||||||
for (const m of msgs) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.messages (id, organization_id, message, from_user_id, to_user_id, is_read, created_at) VALUES (${sqlSafe(getUUID(m._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(m.content || m.message)}, NULL, NULL, ${sqlSafe(m.isRead)}, ${sqlSafe(m.createdAt)});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 10. Logs de Auditoria de Estoque
|
|
||||||
const auditLogs = await db.collection('stockauditlogs').find().toArray();
|
|
||||||
console.log(`📝 Migrando ${auditLogs.length} logs de auditoria...`);
|
|
||||||
for (const al of auditLogs) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.stock_audit_logs (id, organization_id, stock_item_id, action, quantity_before, quantity_after, performed_by, details, created_at) VALUES (${sqlSafe(getUUID(al._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(al.stockItemId)}, ${sqlSafe(al.action)}, ${sqlSafe(al.quantityBefore)}, ${sqlSafe(al.quantityAfter)}, ${sqlSafe(al.performedBy)}, ${sqlSafe(al.details)}, ${sqlSafe(al.createdAt)});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 11. Configurações do Sistema
|
|
||||||
const settings = await db.collection('systemsettings').find().toArray();
|
|
||||||
console.log(`⚙️ Migrando ${settings.length} configurações...`);
|
|
||||||
for (const st of settings) {
|
|
||||||
const { _id, settingsId, ...rest } = st;
|
|
||||||
allSQL.push(`INSERT INTO gpi.system_settings (id, organization_id, key, value, updated_at) VALUES (${sqlSafe(getUUID(_id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(settingsId || 'global')}, ${sqlSafe(JSON.stringify(rest))}, ${sqlSafe(st.updatedAt)});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 12. Metadados de Arquivos
|
|
||||||
const files = await db.collection('storedfiles').find().toArray();
|
|
||||||
console.log(`📁 Migrando ${files.length} metadados de arquivos...`);
|
|
||||||
for (const f of files) {
|
|
||||||
allSQL.push(`INSERT INTO gpi.stored_files (id, organization_id, filename, mime_type, size_bytes, storage_path, metadata, created_at) VALUES (${sqlSafe(getUUID(f._id))}, ${sqlSafe(DEFAULT_ORG_ID)}, ${sqlSafe(f.filename)}, ${sqlSafe(f.mimeType)}, ${sqlSafe(f.size)}, ${sqlSafe(f.path)}, ${sqlSafe(JSON.stringify(f.metadata))}, ${sqlSafe(f.createdAt)});`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allSQL.length === 0) {
|
|
||||||
console.log("⚠️ Nenhum dado novo encontrado.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("💾 Executando SQL em massa no Supabase...");
|
|
||||||
fs.writeFileSync('bulk_migration_final.sql', `TRUNCATE gpi.projects, gpi.technical_data_sheets, gpi.painting_schemes, gpi.parts, gpi.stock_items, gpi.notifications, gpi.yield_studies, gpi.geometry_types, gpi.messages, gpi.stock_audit_logs, gpi.system_settings, gpi.stored_files CASCADE;\n` + allSQL.join('\n'));
|
|
||||||
|
|
||||||
execSync(`export PGPASSWORD=Xz0oyb6ArGYG5uAVTVwcvJxRrMuT7EIJ && docker exec -i supabase-db-h0oggskgs0ws0sco8kc4s8ws psql -U supabase_admin -d postgres < bulk_migration_final.sql`);
|
|
||||||
|
|
||||||
console.log("✅ Migração final concluída com sucesso!");
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("❌ ERRO DURANTE MIGRAÇÃO:", e.message);
|
|
||||||
} finally {
|
|
||||||
await client.close();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
run();
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
console.log('Loading Netlify Function...');
|
|
||||||
import serverless from 'serverless-http';
|
|
||||||
import express from 'express';
|
|
||||||
import cors from 'cors';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
|
|
||||||
// Import local database connection that sets up GridFS/Bucket
|
|
||||||
import { connectDB } from '../../src/server/config/database.js';
|
|
||||||
|
|
||||||
// Static imports for routes to ensure esbuild bundles them correctly
|
|
||||||
import projectRoutes from '../../src/server/routes/projectRoutes.js';
|
|
||||||
import partRoutes from '../../src/server/routes/partRoutes.js';
|
|
||||||
import paintingSchemeRoutes from '../../src/server/routes/paintingSchemeRoutes.js';
|
|
||||||
import applicationRecordRoutes from '../../src/server/routes/applicationRecordRoutes.js';
|
|
||||||
import inspectionRoutes from '../../src/server/routes/inspectionRoutes.js';
|
|
||||||
import analysisRoutes from '../../src/server/routes/analysisRoutes.js';
|
|
||||||
import dataSheetRoutes from '../../src/server/routes/dataSheetRoutes.js';
|
|
||||||
import yieldStudyRoutes from '../../src/server/routes/yieldStudyRoutes.js';
|
|
||||||
import userRoutes from '../../src/server/routes/userRoutes.js';
|
|
||||||
import uploadsRoutes from '../../src/server/routes/uploadsRoutes.js';
|
|
||||||
import systemSettingsRoutes from '../../src/server/routes/systemSettingsRoutes.js';
|
|
||||||
import geometryTypeRoutes from '../../src/server/routes/geometryTypeRoutes.js';
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
app.use(cors({
|
|
||||||
origin: '*',
|
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-clerk-user-id', 'x-organization-id']
|
|
||||||
}));
|
|
||||||
app.use(express.json());
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
// We register them immediately since we use serverless-http's cold start handling
|
|
||||||
app.use('/api/users', userRoutes);
|
|
||||||
app.use('/api/projects', projectRoutes);
|
|
||||||
app.use('/api/parts', partRoutes);
|
|
||||||
app.use('/api/painting-schemes', paintingSchemeRoutes);
|
|
||||||
app.use('/api/application-records', applicationRecordRoutes);
|
|
||||||
app.use('/api/inspections', inspectionRoutes);
|
|
||||||
app.use('/api', analysisRoutes);
|
|
||||||
app.use('/api/datasheets', dataSheetRoutes);
|
|
||||||
app.use('/api/yield-studies', yieldStudyRoutes);
|
|
||||||
app.use('/api/system-settings', systemSettingsRoutes);
|
|
||||||
app.use('/api/geometry-types', geometryTypeRoutes);
|
|
||||||
|
|
||||||
// Serve uploads (from /tmp in serverless or local dir)
|
|
||||||
app.use('/uploads', uploadsRoutes);
|
|
||||||
|
|
||||||
// Simple test endpoint
|
|
||||||
app.get('/api/health', (req, res) => {
|
|
||||||
res.json({
|
|
||||||
status: 'ok',
|
|
||||||
timestamp: new Date(),
|
|
||||||
mongoState: mongoose.connection.readyState
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
export const handler = async (event: any, context: any) => {
|
|
||||||
context.callbackWaitsForEmptyEventLoop = false;
|
|
||||||
|
|
||||||
// Ensure DB is connected before processing request
|
|
||||||
try {
|
|
||||||
await connectDB();
|
|
||||||
} catch (e) {
|
|
||||||
console.error('DB Connection Failed:', e);
|
|
||||||
// We let it proceed, maybe it's a health check or will fail gracefully later
|
|
||||||
}
|
|
||||||
|
|
||||||
const httpHandler = serverless(app);
|
|
||||||
return await httpHandler(event, context);
|
|
||||||
};
|
|
||||||
Generated
+1146
-988
File diff suppressed because it is too large
Load Diff
+11
-18
@@ -3,27 +3,20 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"engines": {
|
|
||||||
"node": ">=20.12.0"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "concurrently \"vite\" \"tsx watch src/server/index.ts\"",
|
"dev": "concurrently \"vite\" \"tsx watch src/server/index.ts\"",
|
||||||
"build:client": "vite build",
|
"build:client": "vite build",
|
||||||
"build:server": "tsc -p tsconfig.server.json",
|
"build:server": "tsc -p tsconfig.server.json",
|
||||||
"prebuild": "npm install",
|
"build": "npm run build:client && npm run build:server",
|
||||||
"build": "npm run build:client",
|
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"start": "tsx src/server/index.ts"
|
"start": "node dist/server/index.js"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@logto/node": "^2.4.0",
|
|
||||||
"@logto/react": "^4.0.13",
|
|
||||||
"@supabase/supabase-js": "^2.47.0",
|
|
||||||
"@tailwindcss/postcss": "^4.1.18",
|
"@tailwindcss/postcss": "^4.1.18",
|
||||||
"@types/uuid": "^10.0.0",
|
"@types/uuid": "^10.0.0",
|
||||||
"@vercel/speed-insights": "^1.3.1",
|
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
|
"bcryptjs": "^3.0.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
@@ -31,11 +24,11 @@
|
|||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"enhanced-resolve": "^5.18.4",
|
"enhanced-resolve": "^5.18.4",
|
||||||
"express": "^5.2.1",
|
"express": "^5.2.1",
|
||||||
"jose": "^5.2.0",
|
"jsonwebtoken": "^9.0.3",
|
||||||
"lucide-react": "^0.562.0",
|
"lucide-react": "^0.562.0",
|
||||||
"mongodb": "^7.1.1",
|
|
||||||
"multer": "^2.0.2",
|
"multer": "^2.0.2",
|
||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
|
"pg": "^8.20.0",
|
||||||
"prop-types": "^15.8.1",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
@@ -43,22 +36,22 @@
|
|||||||
"react-router-dom": "^7.12.0",
|
"react-router-dom": "^7.12.0",
|
||||||
"recharts": "^3.7.0",
|
"recharts": "^3.7.0",
|
||||||
"search-web": "^1.0.3",
|
"search-web": "^1.0.3",
|
||||||
"serverless-http": "^4.0.0",
|
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
"tesseract.js": "^7.0.0",
|
"tesseract.js": "^7.0.0",
|
||||||
"tsx": "^4.21.0",
|
|
||||||
"uuid": "^13.0.0"
|
"uuid": "^13.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.39.1",
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/cors": "^2.8.19",
|
"@types/cors": "^2.8.19",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.6",
|
||||||
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/pg": "^8.18.0",
|
||||||
"@types/react": "^19.2.5",
|
"@types/react": "^19.2.5",
|
||||||
"@types/react-dom": "^19.2.3",
|
"@types/react-dom": "^19.2.3",
|
||||||
"@vercel/node": "^5.5.28",
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
|
||||||
"autoprefixer": "^10.4.23",
|
"autoprefixer": "^10.4.23",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
"eslint": "^9.39.1",
|
"eslint": "^9.39.1",
|
||||||
@@ -72,7 +65,7 @@
|
|||||||
"tsx": "^4.21.0",
|
"tsx": "^4.21.0",
|
||||||
"typescript": "~5.9.3",
|
"typescript": "~5.9.3",
|
||||||
"typescript-eslint": "^8.46.4",
|
"typescript-eslint": "^8.46.4",
|
||||||
"vite": "^6.0.0",
|
"vite": "^7.2.4",
|
||||||
"vite-plugin-pwa": "^1.2.0"
|
"vite-plugin-pwa": "^1.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
DROP SCHEMA IF EXISTS gpi CASCADE;
|
|
||||||
-20
@@ -1,20 +0,0 @@
|
|||||||
# Segredos e Credenciais
|
|
||||||
|
|
||||||
## Supabase
|
|
||||||
- URL: https://supabase.reifonas.cloud
|
|
||||||
- Service Role Key: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc3Mjk5NTUwMCwiZXhwIjo0OTI4NjY5MTAwLCJyb2xlIjoic2VydmljZV9yb2xlIn0._n2Kj2f29z1u0pOYUGqAr-1Xjt-xQpK9KDhhhGvOIro
|
|
||||||
- Anon Key: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTc3Mjk5NTUwMCwiZXhwIjo0OTI4NjY5MTAwLCJzdWIiOiJ0cnVlIn0.fake_anon_key_for_gpi
|
|
||||||
|
|
||||||
## Logto
|
|
||||||
- URL: https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io
|
|
||||||
- App ID: gpi-app-final
|
|
||||||
- App Secret: gpi-secret-2026
|
|
||||||
- Callback URL: https://gpi.reifonas.cloud/callback
|
|
||||||
|
|
||||||
## Google OAuth ( usuário )
|
|
||||||
- Client ID: 516979078541-7f0cm821nls01eb2prtffi5t58phmgiq.apps.googleusercontent.com
|
|
||||||
- Client Secret: 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
|
|
||||||
|
|
||||||
## URLs de Produção
|
|
||||||
- App: https://gpi.reifonas.cloud
|
|
||||||
- API: https://gpi.reifonas.cloud/api
|
|
||||||
+84
-76
@@ -1,6 +1,6 @@
|
|||||||
import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
|
// App v1.5 - JWT Migration Final Check
|
||||||
import { AuthProvider } from './context/AuthContext';
|
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
import { useAuth } from './context/useAuth';
|
import { AuthProvider, useAuth } from './context/AuthContext';
|
||||||
import { SystemSettingsProvider } from './context/SystemSettingsContext';
|
import { SystemSettingsProvider } from './context/SystemSettingsContext';
|
||||||
import { NotificationProvider } from './contexts/NotificationContext';
|
import { NotificationProvider } from './contexts/NotificationContext';
|
||||||
import { Layout } from './components/Layout';
|
import { Layout } from './components/Layout';
|
||||||
@@ -18,6 +18,7 @@ import { CalculatorDashboard } from './pages/CalculatorDashboard';
|
|||||||
import { StockDashboard } from './pages/StockDashboard';
|
import { StockDashboard } from './pages/StockDashboard';
|
||||||
import { GuestDashboard } from './pages/GuestDashboard';
|
import { GuestDashboard } from './pages/GuestDashboard';
|
||||||
import { Login } from './pages/Login';
|
import { Login } from './pages/Login';
|
||||||
|
import { OrganizationSelector } from './pages/OrganizationSelector';
|
||||||
import InstrumentList from './pages/InstrumentList';
|
import InstrumentList from './pages/InstrumentList';
|
||||||
|
|
||||||
const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
@@ -30,91 +31,98 @@ const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) =
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AppContent: React.FC = () => {
|
const AppContent: React.FC = () => {
|
||||||
const { isSignedIn, isLoading } = useAuth();
|
const { appUser, isLoading } = useAuth();
|
||||||
const location = useLocation();
|
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) return <div className="flex h-screen items-center justify-center">Carregando...</div>;
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-surface-soft flex items-center justify-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary"></div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not signed in and not on the callback page, show login
|
// AppUser exists but hasn't selected an org yet (if your business logic requires orgs)
|
||||||
if (!isSignedIn && location.pathname !== '/callback') {
|
if (appUser && !appUser.organizationId) {
|
||||||
return <Login />;
|
return <OrganizationSelector />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToastProvider>
|
<Layout>
|
||||||
<SystemSettingsProvider>
|
<Routes>
|
||||||
<NotificationProvider>
|
<Route path="/" element={<ProjectList />} />
|
||||||
<Layout>
|
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
||||||
<Routes>
|
<Route path="/projects" element={<ProjectList />} />
|
||||||
<Route path="/" element={<ProjectList />} />
|
<Route path="/project/:id" element={<ProjectDetails />} />
|
||||||
<Route path="/login" element={<Login />} />
|
<Route path="/schemes" element={<SchemesList />} />
|
||||||
<Route path="/guest-dashboard" element={<GuestDashboard />} />
|
<Route path="/inspections" element={<InspectionsList />} />
|
||||||
<Route path="/projects" element={<ProjectList />} />
|
<Route path="/library" element={
|
||||||
<Route path="/project/:id" element={<ProjectDetails />} />
|
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
||||||
<Route path="/schemes" element={<SchemesList />} />
|
<DataSheetLibrary />
|
||||||
<Route path="/inspections" element={<InspectionsList />} />
|
</ProtectedRoute>
|
||||||
<Route path="/library" element={
|
} />
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
<Route path="/instruments" element={
|
||||||
<DataSheetLibrary />
|
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
||||||
</ProtectedRoute>
|
<InstrumentList />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/instruments" element={
|
} />
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
<Route path="/yield-study" element={
|
||||||
<InstrumentList />
|
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
||||||
</ProtectedRoute>
|
<YieldStudyDashboard />
|
||||||
} />
|
</ProtectedRoute>
|
||||||
<Route path="/yield-study" element={
|
} />
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
<Route path="/calculators" element={<CalculatorDashboard />} />
|
||||||
<YieldStudyDashboard />
|
<Route
|
||||||
</ProtectedRoute>
|
path="/admin"
|
||||||
} />
|
element={
|
||||||
<Route path="/calculators" element={<CalculatorDashboard />} />
|
<ProtectedRoute allowedRoles={['admin']}>
|
||||||
<Route
|
<AdminDashboard />
|
||||||
path="/admin"
|
</ProtectedRoute>
|
||||||
element={
|
}
|
||||||
<ProtectedRoute allowedRoles={['admin']}>
|
/>
|
||||||
<AdminDashboard />
|
<Route
|
||||||
</ProtectedRoute>
|
path="/stock"
|
||||||
}
|
element={
|
||||||
/>
|
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
||||||
<Route
|
<StockDashboard />
|
||||||
path="/stock"
|
</ProtectedRoute>
|
||||||
element={
|
}
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
/>
|
||||||
<StockDashboard />
|
<Route
|
||||||
</ProtectedRoute>
|
path="/developer"
|
||||||
}
|
element={
|
||||||
/>
|
<DeveloperRoute>
|
||||||
<Route
|
<DeveloperDashboard />
|
||||||
path="/developer"
|
</DeveloperRoute>
|
||||||
element={
|
}
|
||||||
<DeveloperRoute>
|
/>
|
||||||
<DeveloperDashboard />
|
</Routes>
|
||||||
</DeveloperRoute>
|
</Layout>
|
||||||
}
|
);
|
||||||
/>
|
};
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
|
||||||
</Routes>
|
const MainRouter: React.FC = () => {
|
||||||
</Layout>
|
const { appUser, isLoading } = useAuth();
|
||||||
</NotificationProvider>
|
|
||||||
</SystemSettingsProvider>
|
if (isLoading) {
|
||||||
</ToastProvider>
|
return <div className="flex h-screen items-center justify-center">Verificando sessão...</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Router>
|
||||||
|
{!appUser ? (
|
||||||
|
<Login />
|
||||||
|
) : (
|
||||||
|
<AppContent />
|
||||||
|
)}
|
||||||
|
</Router>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<ToastProvider>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<AppContent />
|
<SystemSettingsProvider>
|
||||||
|
<NotificationProvider>
|
||||||
|
<MainRouter />
|
||||||
|
</NotificationProvider>
|
||||||
|
</SystemSettingsProvider>
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
</Router>
|
</ToastProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 6.3 MiB After Width: | Height: | Size: 6.3 MiB |
|
Before Width: | Height: | Size: 48 KiB After Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 373 KiB |
@@ -2,11 +2,11 @@ import React, { useState } from 'react';
|
|||||||
import NotificationBell from './NotificationBell';
|
import NotificationBell from './NotificationBell';
|
||||||
import { TeamPresence } from './TeamPresence';
|
import { TeamPresence } from './TeamPresence';
|
||||||
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
import { Link, useLocation, useNavigate } from 'react-router-dom';
|
||||||
import { Menu, X, FolderOpen, Layers, ClipboardCheck, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer, User } from 'lucide-react';
|
import { Menu, X, FolderOpen, Layers, ClipboardCheck, LogOut, TrendingUp, Sun, Moon, HelpCircle, Shield, Wrench, Terminal, LayoutDashboard, Package, Thermometer } from 'lucide-react';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import { TechnicalManual } from './TechnicalManual';
|
|
||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
|
import { TechnicalManual } from './TechnicalManual';
|
||||||
|
// import { useSystemSettings } from '../context/SystemSettingsContext';
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,8 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
return saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
||||||
});
|
});
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const { isAdmin, isUser, isDeveloper, appUser } = useAuth();
|
const { isAdmin, isUser, isDeveloper, appUser, logout } = useAuth();
|
||||||
|
// const { settings } = useSystemSettings();
|
||||||
|
|
||||||
// Helper to get role display name
|
// Helper to get role display name
|
||||||
const getRoleDisplay = () => {
|
const getRoleDisplay = () => {
|
||||||
@@ -77,10 +78,6 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (location.pathname === '/login' || location.pathname === '/callback') {
|
|
||||||
return <>{children}</>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-surface-soft flex font-sans selection:bg-primary/30">
|
<div className="min-h-screen bg-surface-soft flex font-sans selection:bg-primary/30">
|
||||||
{/* Sidebar Desktop - Fixed */}
|
{/* Sidebar Desktop - Fixed */}
|
||||||
@@ -104,6 +101,18 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="px-6 mb-2">
|
||||||
|
<div className="w-full flex items-center gap-3 p-2 rounded-xl border border-border/50 bg-surface-hover/50 text-text-main opacity-80 cursor-default" title="Organização">
|
||||||
|
<div className="w-8 h-8 rounded-lg bg-surface-soft flex items-center justify-center font-bold text-xs">
|
||||||
|
ORG
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<p className="text-sm font-semibold truncate">Organização Matriz</p>
|
||||||
|
<p className="text-[10px] text-text-muted uppercase tracking-wider">Conta</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Team Presence - Shows all members with online/offline status */}
|
{/* Team Presence - Shows all members with online/offline status */}
|
||||||
<TeamPresence />
|
<TeamPresence />
|
||||||
|
|
||||||
@@ -202,12 +211,20 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => logout()}
|
||||||
|
className="flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-semibold text-text-secondary hover:text-error hover:bg-error/5 transition-all w-full"
|
||||||
|
>
|
||||||
|
<LogOut size={18} />
|
||||||
|
Sair
|
||||||
|
</button>
|
||||||
|
|
||||||
<div className="pt-2 flex items-center justify-between px-2">
|
<div className="pt-2 flex items-center justify-between px-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<NotificationBell />
|
<NotificationBell />
|
||||||
<div className="w-px h-6 bg-border/50 mx-1"></div>
|
<div className="w-px h-6 bg-border/50 mx-1"></div>
|
||||||
<div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center text-primary">
|
<div className="w-8 h-8 bg-primary text-white rounded-full flex items-center justify-center font-bold text-xs">
|
||||||
<User size={16} />
|
{appUser?.name?.substring(0, 2).toUpperCase() || 'US'}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[10px] text-text-main font-bold truncate max-w-[100px]">{appUser?.name || 'Usuário'}</span>
|
<span className="text-[10px] text-text-main font-bold truncate max-w-[100px]">{appUser?.name || 'Usuário'}</span>
|
||||||
@@ -355,3 +372,4 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,16 @@ export const ProtectedRoute: React.FC<ProtectedRouteProps> = ({
|
|||||||
requireEdit = false,
|
requireEdit = false,
|
||||||
redirectTo = '/',
|
redirectTo = '/',
|
||||||
}) => {
|
}) => {
|
||||||
const { appUser, canEdit } = useAuth();
|
const { appUser, isLoading, canEdit } = useAuth();
|
||||||
|
|
||||||
|
// Show loading state
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[60vh]">
|
||||||
|
<RefreshCw size={32} className="animate-spin text-primary" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Check role-based access
|
// Check role-based access
|
||||||
if (allowedRoles && appUser && !allowedRoles.includes(appUser.role)) {
|
if (allowedRoles && appUser && !allowedRoles.includes(appUser.role)) {
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import api from '../services/api';
|
|||||||
|
|
||||||
interface OrganizationMember {
|
interface OrganizationMember {
|
||||||
_id: string;
|
_id: string;
|
||||||
id: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
logto_id: string;
|
userId: string;
|
||||||
role: string;
|
role: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +30,7 @@ export const TeamPresence: React.FC = () => {
|
|||||||
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
const [isModalOpen, setIsModalOpen] = React.useState(false);
|
||||||
|
|
||||||
// Fetch all members
|
// Fetch all members
|
||||||
const fetchMembers = useCallback(async () => {
|
const fetchMembers = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<OrganizationMember[]>('/users');
|
const response = await api.get<OrganizationMember[]>('/users');
|
||||||
setAllMembers(response.data);
|
setAllMembers(response.data);
|
||||||
@@ -41,7 +40,7 @@ export const TeamPresence: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Fetch pending messages
|
// Fetch pending messages
|
||||||
const fetchPendingMessages = useCallback(async () => {
|
const fetchPendingMessages = React.useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
const response = await api.get<PendingMessage[]>('/messages/pending');
|
const response = await api.get<PendingMessage[]>('/messages/pending');
|
||||||
setPendingMessages(response.data);
|
setPendingMessages(response.data);
|
||||||
@@ -51,35 +50,35 @@ export const TeamPresence: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
fetchMembers();
|
if (appUser) {
|
||||||
fetchPendingMessages();
|
fetchMembers();
|
||||||
|
fetchPendingMessages();
|
||||||
const memberInterval = setInterval(fetchMembers, 60000);
|
const memberInterval = setInterval(fetchMembers, 60000);
|
||||||
const messageInterval = setInterval(fetchPendingMessages, 30000);
|
const messageInterval = setInterval(fetchPendingMessages, 30000);
|
||||||
|
return () => {
|
||||||
return () => {
|
clearInterval(memberInterval);
|
||||||
clearInterval(memberInterval);
|
clearInterval(messageInterval);
|
||||||
clearInterval(messageInterval);
|
};
|
||||||
};
|
}
|
||||||
}, [fetchMembers, fetchPendingMessages]);
|
}, [appUser, fetchMembers, fetchPendingMessages]);
|
||||||
|
|
||||||
if (allMembers.length === 0) {
|
if (allMembers.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a Set of active user IDs for fast lookup
|
// Create a Set of active user IDs for fast lookup
|
||||||
const activeUserLogtoIds = new Set(activeUsers.map(u => u.logtoId));
|
const activeUserEmails = new Set(activeUsers.map(u => u.email));
|
||||||
|
|
||||||
// Create a map of pending messages by recipient ID
|
// Create a map of pending messages by recipient ID
|
||||||
const pendingMessagesByRecipient = new Map(
|
const pendingMessagesByRecipient = new Map(
|
||||||
(pendingMessages || []).map(msg => [msg.toUser?.email, msg])
|
pendingMessages.map(msg => [msg.toUser?.email, msg])
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleMemberClick = (member: OrganizationMember) => {
|
const handleMemberClick = (member: OrganizationMember) => {
|
||||||
if (member.logto_id === appUser?.logtoId) {
|
if (member.email === appUser?.email) {
|
||||||
return; // Don't allow messaging yourself
|
return; // Don't allow messaging yourself
|
||||||
}
|
}
|
||||||
setSelectedUser({ id: member.logto_id, name: member.name });
|
setSelectedUser({ id: member.email, name: member.name });
|
||||||
setIsModalOpen(true);
|
setIsModalOpen(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -89,7 +88,7 @@ export const TeamPresence: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMessageSent = async () => {
|
const handleMessageSent = async () => {
|
||||||
await fetchPendingMessages();
|
fetchPendingMessages();
|
||||||
};
|
};
|
||||||
|
|
||||||
const getExistingMessage = (member: OrganizationMember) => {
|
const getExistingMessage = (member: OrganizationMember) => {
|
||||||
@@ -102,13 +101,13 @@ export const TeamPresence: React.FC = () => {
|
|||||||
<div className="px-6 py-3">
|
<div className="px-6 py-3">
|
||||||
<div className="mb-2">
|
<div className="mb-2">
|
||||||
<span className="text-[10px] font-bold text-text-muted uppercase tracking-[0.2em]">
|
<span className="text-[10px] font-bold text-text-muted uppercase tracking-[0.2em]">
|
||||||
Equipe ({(activeUsers || []).length}/{(allMembers || []).length} online)
|
Equipe ({activeUsers.length}/{allMembers.length} online)
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
{(allMembers || []).map((member) => {
|
{allMembers.map((member) => {
|
||||||
const isOnline = activeUserLogtoIds.has(member.logto_id);
|
const isOnline = activeUserEmails.has(member.email);
|
||||||
const isCurrentUser = member.logto_id === appUser?.logtoId;
|
const isCurrentUser = member.email === appUser?.email;
|
||||||
const hasPendingMessage = pendingMessagesByRecipient.has(member.email);
|
const hasPendingMessage = pendingMessagesByRecipient.has(member.email);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -176,13 +175,10 @@ export const TeamPresence: React.FC = () => {
|
|||||||
onClose={handleModalClose}
|
onClose={handleModalClose}
|
||||||
recipientId={selectedUser.id}
|
recipientId={selectedUser.id}
|
||||||
recipientName={selectedUser.name}
|
recipientName={selectedUser.name}
|
||||||
existingMessage={getExistingMessage(allMembers.find(m => m.logto_id === selectedUser.id)!)}
|
existingMessage={getExistingMessage(allMembers.find(m => m.email === selectedUser.id)!)}
|
||||||
onMessageSent={handleMessageSent}
|
onMessageSent={handleMessageSent}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// No the component body I used useCallback so I need to import it
|
|
||||||
import { useCallback } from 'react';
|
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export const BackupRestore: React.FC = () => {
|
|||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleExport = async () => {
|
const handleExport = async () => {
|
||||||
|
if (!appUser) return;
|
||||||
|
|
||||||
setIsExporting(true);
|
setIsExporting(true);
|
||||||
try {
|
try {
|
||||||
const response = await api.get('/backup/export', {
|
const response = await api.get('/backup/export', {
|
||||||
@@ -50,8 +52,7 @@ export const BackupRestore: React.FC = () => {
|
|||||||
|
|
||||||
// Nome do arquivo com timestamp
|
// Nome do arquivo com timestamp
|
||||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5);
|
||||||
const orgName = appUser?.name || 'GPI';
|
link.download = `backup_gpi_${timestamp}.json`;
|
||||||
link.download = `backup_${orgName}_${timestamp}.json`;
|
|
||||||
|
|
||||||
document.body.appendChild(link);
|
document.body.appendChild(link);
|
||||||
link.click();
|
link.click();
|
||||||
@@ -147,8 +148,8 @@ export const BackupRestore: React.FC = () => {
|
|||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<h3 className="text-lg font-bold text-text-main mb-2">Backup e Restauração de Dados</h3>
|
<h3 className="text-lg font-bold text-text-main mb-2">Backup e Restauração de Dados</h3>
|
||||||
<p className="text-sm text-text-muted leading-relaxed">
|
<p className="text-sm text-text-muted leading-relaxed">
|
||||||
Use esta ferramenta para criar cópias de segurança de todos os dados da organização ou restaurar dados de um backup anterior.
|
Use esta ferramenta para criar cópias de segurança de todos os dados do sistema ou restaurar dados de um backup anterior.
|
||||||
<strong className="text-amber-500"> Os backups são específicos para cada organização e não podem ser restaurados em outras organizações.</strong>
|
<strong className="text-amber-500"> Os backups são específicos para cada instalação e podem não ser compatíveis entre versões diferentes se houver mudanças estruturais.</strong>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,7 +191,7 @@ export const BackupRestore: React.FC = () => {
|
|||||||
>
|
>
|
||||||
{isExporting ? (
|
{isExporting ? (
|
||||||
<>
|
<>
|
||||||
<RefreshCw size={20} className="animate-spin" />
|
<RefreshCw size={20} className="animate-spin" />
|
||||||
Gerando Backup...
|
Gerando Backup...
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, { useEffect, useState, useCallback } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Plus, Pencil, Trash2, Box, RefreshCw } from 'lucide-react';
|
import { Plus, Pencil, Trash2, Box, RefreshCw } from 'lucide-react';
|
||||||
import { Button } from '../Button';
|
import { Button } from '../Button';
|
||||||
import { Modal } from '../Modal';
|
import { Modal } from '../Modal';
|
||||||
@@ -8,7 +8,7 @@ import type { GeometryType } from '../../types';
|
|||||||
import { useAuth } from '../../context/useAuth';
|
import { useAuth } from '../../context/useAuth';
|
||||||
|
|
||||||
export const GeometrySettings: React.FC = () => {
|
export const GeometrySettings: React.FC = () => {
|
||||||
const { isSignedIn } = useAuth();
|
const { appUser } = useAuth();
|
||||||
const [types, setTypes] = useState<GeometryType[]>([]);
|
const [types, setTypes] = useState<GeometryType[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
@@ -20,7 +20,7 @@ export const GeometrySettings: React.FC = () => {
|
|||||||
efficiencyLoss: '20'
|
efficiencyLoss: '20'
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchTypes = useCallback(async () => {
|
const fetchTypes = React.useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const response = await geometryService.getAllTypes();
|
const response = await geometryService.getAllTypes();
|
||||||
@@ -33,15 +33,15 @@ export const GeometrySettings: React.FC = () => {
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isSignedIn) {
|
if (appUser) {
|
||||||
fetchTypes();
|
fetchTypes();
|
||||||
}
|
}
|
||||||
}, [isSignedIn, fetchTypes]);
|
}, [appUser, fetchTypes]);
|
||||||
|
|
||||||
const handleOpenModal = (item?: GeometryType) => {
|
const handleOpenModal = (item?: GeometryType) => {
|
||||||
if (item) {
|
if (item) {
|
||||||
setEditingItem(item);
|
setEditingItem(item);
|
||||||
setForm({ name: item.name, efficiencyLoss: item.efficiencyLoss.toString() });
|
setForm({ name: item.name, efficiencyLoss: (item.efficiencyLoss ?? 0).toString() });
|
||||||
} else {
|
} else {
|
||||||
setEditingItem(null);
|
setEditingItem(null);
|
||||||
setForm({ name: '', efficiencyLoss: '20' });
|
setForm({ name: '', efficiencyLoss: '20' });
|
||||||
|
|||||||
@@ -48,10 +48,7 @@ export const StockModal: React.FC<StockModalProps> = ({ isOpen, onClose, onSucce
|
|||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
fetchDataSheets();
|
fetchDataSheets();
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
const dsId = (typeof initialData.dataSheetId === 'object')
|
setDataSheetId(typeof initialData.dataSheetId === 'object' ? initialData.dataSheetId._id : initialData.dataSheetId);
|
||||||
? (initialData.dataSheetId.id || initialData.dataSheetId._id)
|
|
||||||
: initialData.dataSheetId;
|
|
||||||
setDataSheetId(dsId || '');
|
|
||||||
setRrNumber(initialData.rrNumber);
|
setRrNumber(initialData.rrNumber);
|
||||||
setBatchNumber(initialData.batchNumber);
|
setBatchNumber(initialData.batchNumber);
|
||||||
setColor(initialData.color || '');
|
setColor(initialData.color || '');
|
||||||
@@ -111,8 +108,7 @@ export const StockModal: React.FC<StockModalProps> = ({ isOpen, onClose, onSucce
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (initialData) {
|
if (initialData) {
|
||||||
const itemId = initialData.id || initialData._id;
|
await stockService.update(initialData._id!, payload);
|
||||||
await stockService.update(itemId!, payload);
|
|
||||||
} else {
|
} else {
|
||||||
await stockService.create(payload);
|
await stockService.create(payload);
|
||||||
}
|
}
|
||||||
@@ -151,12 +147,12 @@ export const StockModal: React.FC<StockModalProps> = ({ isOpen, onClose, onSucce
|
|||||||
const val = e.target.value;
|
const val = e.target.value;
|
||||||
setDataSheetId(val);
|
setDataSheetId(val);
|
||||||
// Auto-fill minStock from DataSheet if set and current is empty/0
|
// Auto-fill minStock from DataSheet if set and current is empty/0
|
||||||
const ds = dataSheets.find(d => (d.id || d._id) === val);
|
const ds = dataSheets.find(d => d._id === val);
|
||||||
if (ds && ds.minStock && (!minStock || minStock === '0')) {
|
if (ds && ds.minStock && (!minStock || minStock === '0')) {
|
||||||
setMinStock(String(ds.minStock));
|
setMinStock(String(ds.minStock));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={filteredDataSheets.map(ds => ({ label: `${ds.name} - ${ds.manufacturer}`, value: ds.id || ds._id }))}
|
options={filteredDataSheets.map(ds => ({ label: `${ds.name} - ${ds.manufacturer}`, value: ds._id }))}
|
||||||
disabled={!!initialData} // Lock product on edit
|
disabled={!!initialData} // Lock product on edit
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,81 +1,127 @@
|
|||||||
import React, { useState, useEffect, useCallback, useMemo } from 'react';
|
import React, { createContext, useContext, useState, useEffect, useCallback } from 'react';
|
||||||
import type { AppUser } from '../types';
|
import type { AppUser } from '../types';
|
||||||
import { AuthContext } from './AuthContextType';
|
import { setApiToken, setApiOrganizationId, getBaseUrl } from '../services/api';
|
||||||
import { setApiOrganizationId } from '../services/api';
|
|
||||||
|
|
||||||
interface AuthProviderProps {
|
const API_URL = getBaseUrl();
|
||||||
children: React.ReactNode;
|
|
||||||
|
export interface AuthContextType {
|
||||||
|
appUser: AppUser | null;
|
||||||
|
isLoading: boolean;
|
||||||
|
isSignedIn: boolean;
|
||||||
|
error: string | null;
|
||||||
|
token: string | null;
|
||||||
|
login: (token: string, user: AppUser) => void;
|
||||||
|
logout: () => void;
|
||||||
|
isAdmin: () => boolean;
|
||||||
|
isUser: () => boolean;
|
||||||
|
isGuest: () => boolean;
|
||||||
|
isDeveloper: () => boolean;
|
||||||
|
canEdit: () => boolean;
|
||||||
|
refetchUser: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultUser: AppUser = {
|
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
||||||
id: '00000000-0000-0000-0000-000000000000',
|
|
||||||
email: 'guest@gpi.app',
|
|
||||||
name: 'Guest User',
|
|
||||||
role: 'user',
|
|
||||||
isBanned: false,
|
|
||||||
createdAt: new Date().toISOString(),
|
|
||||||
updatedAt: new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
const DEFAULT_ORGANIZATION_ID = 'e47e6210-4879-4e5b-bf21-9285d2713123';
|
export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const DEFAULT_ORGANIZATION_NAME = 'Organização Padrão';
|
|
||||||
|
|
||||||
export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
|
|
||||||
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
const [appUser, setAppUser] = useState<AppUser | null>(null);
|
||||||
|
const [token, setToken] = useState<string | null>(localStorage.getItem('jwt_token'));
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
// Initial load: se tem token, setar no interceptor e buscar dados do usuário
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const storedUser = localStorage.getItem('gpi_user');
|
if (token) {
|
||||||
if (storedUser) {
|
setApiToken(token);
|
||||||
try {
|
refetchUser();
|
||||||
setAppUser(JSON.parse(storedUser));
|
} else {
|
||||||
} catch (e) {
|
setIsLoading(false);
|
||||||
console.error("Error parsing stored user", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}, [token]);
|
||||||
|
|
||||||
setApiOrganizationId(DEFAULT_ORGANIZATION_ID, DEFAULT_ORGANIZATION_NAME);
|
const login = useCallback((newToken: string, user: AppUser) => {
|
||||||
|
localStorage.setItem('jwt_token', newToken);
|
||||||
|
setToken(newToken);
|
||||||
|
setAppUser(user);
|
||||||
|
setApiToken(newToken);
|
||||||
|
|
||||||
|
// Se a organização existir, setar o header
|
||||||
|
if (user.organizationId) {
|
||||||
|
setApiOrganizationId(user.organizationId);
|
||||||
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const signInWithPassword = async (password: string): Promise<boolean> => {
|
const logout = useCallback(() => {
|
||||||
if (password === '@@Gi05Br;;') {
|
localStorage.removeItem('jwt_token');
|
||||||
const adminUser: AppUser = {
|
setToken(null);
|
||||||
...defaultUser,
|
setAppUser(null);
|
||||||
id: 'admin-001',
|
setApiToken(null);
|
||||||
email: 'admtracksteel@gmail.com',
|
setApiOrganizationId(null);
|
||||||
name: 'Administrator / DEV',
|
}, []);
|
||||||
role: 'admin'
|
|
||||||
};
|
const refetchUser = useCallback(async () => {
|
||||||
setAppUser(adminUser);
|
if (!token) return;
|
||||||
localStorage.setItem('gpi_user', JSON.stringify(adminUser));
|
setIsLoading(true);
|
||||||
return true;
|
try {
|
||||||
|
const response = await fetch(`${API_URL}/auth/me`, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${token}`
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (response.ok) {
|
||||||
|
const userData = await response.json();
|
||||||
|
setAppUser(userData);
|
||||||
|
if (userData.organizationId) {
|
||||||
|
setApiOrganizationId(userData.organizationId);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Token inválido ou expirado
|
||||||
|
logout();
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error refetching user:', err);
|
||||||
|
setError('Falha na comunicação de autenticação.');
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
return false;
|
}, [token, logout]);
|
||||||
};
|
|
||||||
|
|
||||||
const isDeveloper = useCallback(() => appUser?.email === 'admtracksteel@gmail.com', [appUser]);
|
const isDeveloper = useCallback(() => {
|
||||||
const isAdmin = useCallback(() => appUser?.role === 'admin' || appUser?.email === 'admtracksteel@gmail.com', [appUser]);
|
return appUser?.email === 'admtracksteel@gmail.com';
|
||||||
const isUser = useCallback(() => !!appUser, [appUser]);
|
}, [appUser]);
|
||||||
const isGuest = useCallback(() => !appUser, [appUser]);
|
|
||||||
const canEdit = useCallback(() => isAdmin(), [isAdmin]);
|
|
||||||
const refetchUser = useCallback(async () => {}, []);
|
|
||||||
|
|
||||||
const value = useMemo(() => ({
|
const isAdmin = useCallback(() => appUser?.role === 'admin' || isDeveloper(), [appUser, isDeveloper]);
|
||||||
appUser,
|
const isUser = useCallback(() => appUser?.role === 'user' || isAdmin(), [appUser, isAdmin]);
|
||||||
isLoading: false,
|
const isGuest = useCallback(() => appUser?.role === 'guest' && !isDeveloper(), [appUser, isDeveloper]);
|
||||||
isSignedIn: !!appUser,
|
const canEdit = useCallback(() => (appUser?.role !== 'guest' && appUser !== null) || isDeveloper(), [appUser, isDeveloper]);
|
||||||
error: null,
|
|
||||||
isAdmin,
|
|
||||||
isUser,
|
|
||||||
isGuest,
|
|
||||||
isDeveloper,
|
|
||||||
canEdit,
|
|
||||||
refetchUser,
|
|
||||||
signInWithPassword
|
|
||||||
}), [appUser, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser, signInWithPassword]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthContext.Provider value={value}>
|
<AuthContext.Provider
|
||||||
|
value={{
|
||||||
|
appUser,
|
||||||
|
isLoading,
|
||||||
|
isSignedIn: !!appUser,
|
||||||
|
error,
|
||||||
|
token,
|
||||||
|
login,
|
||||||
|
logout,
|
||||||
|
isAdmin,
|
||||||
|
isUser,
|
||||||
|
isGuest,
|
||||||
|
isDeveloper,
|
||||||
|
canEdit,
|
||||||
|
refetchUser,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</AuthContext.Provider>
|
</AuthContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useAuth = () => {
|
||||||
|
const context = useContext(AuthContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useAuth must be used within an AuthProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { createContext } from 'react';
|
|
||||||
import type { AppUser } from '../types';
|
import type { AppUser } from '../types';
|
||||||
|
|
||||||
export interface AuthContextType {
|
export interface AuthContextType {
|
||||||
@@ -12,7 +11,4 @@ export interface AuthContextType {
|
|||||||
isDeveloper: () => boolean;
|
isDeveloper: () => boolean;
|
||||||
canEdit: () => boolean;
|
canEdit: () => boolean;
|
||||||
refetchUser: () => Promise<void>;
|
refetchUser: () => Promise<void>;
|
||||||
signInWithPassword: (password: string) => Promise<boolean>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AuthContext = createContext<AuthContextType | undefined>(undefined);
|
|
||||||
|
|||||||
@@ -1,10 +1 @@
|
|||||||
import { useContext } from 'react';
|
export { useAuth } from './AuthContext';
|
||||||
import { AuthContext } from './AuthContextType';
|
|
||||||
|
|
||||||
export const useAuth = () => {
|
|
||||||
const context = useContext(AuthContext);
|
|
||||||
if (!context) {
|
|
||||||
throw new Error('useAuth must be used within an AuthProvider');
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -5,57 +5,48 @@ import type { INotification } from '../types';
|
|||||||
import { NotificationContext } from './NotificationContextState';
|
import { NotificationContext } from './NotificationContextState';
|
||||||
|
|
||||||
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const { isSignedIn } = useAuth();
|
const { appUser, isSignedIn } = useAuth();
|
||||||
|
const orgId = appUser?.organizationId;
|
||||||
const [notifications, setNotifications] = useState<INotification[]>([]);
|
const [notifications, setNotifications] = useState<INotification[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const fetchNotifications = useCallback(async () => {
|
const fetchNotifications = useCallback(async () => {
|
||||||
if (!isSignedIn) return;
|
if (!isSignedIn || !orgId) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (notifications.length === 0) setLoading(true);
|
setLoading(true);
|
||||||
const response = await api.get('/notifications');
|
const response = await api.get<INotification[]>('/notifications');
|
||||||
setNotifications(response.data);
|
setNotifications(response.data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to fetch notifications', error);
|
console.error('Error fetching notifications:', error);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [isSignedIn, notifications.length]);
|
}, [isSignedIn, orgId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSignedIn && orgId) {
|
||||||
|
fetchNotifications();
|
||||||
|
const interval = setInterval(fetchNotifications, 60000);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}
|
||||||
|
}, [isSignedIn, orgId, fetchNotifications]);
|
||||||
|
|
||||||
const markAsRead = async (id: string) => {
|
const markAsRead = async (id: string) => {
|
||||||
try {
|
try {
|
||||||
await api.put(`/notifications/${id}/read`);
|
await api.patch(`/notifications/${id}/read`);
|
||||||
setNotifications(prev => prev.map(n => n._id === id ? { ...n, isRead: true } : n));
|
setNotifications(prev => prev.map(n => n._id === id ? { ...n, isRead: true } : n));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to mark as read', error);
|
console.error('Error marking notification as read:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const markAllAsRead = async () => {
|
const markAllAsRead = async () => {
|
||||||
try {
|
try {
|
||||||
await api.put('/notifications/read-all');
|
await api.post('/notifications/read-all');
|
||||||
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
|
setNotifications(prev => prev.map(n => ({ ...n, isRead: true })));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to mark all as read', error);
|
console.error('Error marking all notifications as read:', error);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const clearAll = async () => {
|
|
||||||
try {
|
|
||||||
await api.delete('/notifications/clear-all');
|
|
||||||
setNotifications([]);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to clear all notifications', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const archiveNotification = async (id: string) => {
|
|
||||||
try {
|
|
||||||
await api.patch(`/notifications/${id}/archive`);
|
|
||||||
setNotifications(prev => prev.filter(n => n._id !== id));
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to archive notification', error);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,38 +55,21 @@ export const NotificationProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
await api.delete(`/notifications/${id}`);
|
await api.delete(`/notifications/${id}`);
|
||||||
setNotifications(prev => prev.filter(n => n._id !== id));
|
setNotifications(prev => prev.filter(n => n._id !== id));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete notification', error);
|
console.error('Error deleting notification:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Polling effect
|
const unreadCount = notifications.filter(n => !n.isRead).length;
|
||||||
useEffect(() => {
|
|
||||||
if (isSignedIn) {
|
|
||||||
fetchNotifications(); // Initial fetch
|
|
||||||
|
|
||||||
const interval = setInterval(() => {
|
|
||||||
fetchNotifications();
|
|
||||||
}, 30000); // Poll every 30 seconds
|
|
||||||
|
|
||||||
return () => clearInterval(interval);
|
|
||||||
} else {
|
|
||||||
setNotifications([]);
|
|
||||||
}
|
|
||||||
}, [isSignedIn, fetchNotifications]);
|
|
||||||
|
|
||||||
const unreadCount = (notifications || []).filter(n => !n.isRead).length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NotificationContext.Provider value={{
|
<NotificationContext.Provider value={{
|
||||||
notifications,
|
notifications,
|
||||||
unreadCount,
|
unreadCount,
|
||||||
loading,
|
loading,
|
||||||
|
fetchNotifications,
|
||||||
markAsRead,
|
markAsRead,
|
||||||
markAllAsRead,
|
markAllAsRead,
|
||||||
clearAll,
|
deleteNotification
|
||||||
archiveNotification,
|
|
||||||
deleteNotification,
|
|
||||||
fetchNotifications
|
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
</NotificationContext.Provider>
|
</NotificationContext.Provider>
|
||||||
|
|||||||
@@ -3,10 +3,10 @@ import api from '../services/api';
|
|||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
|
|
||||||
export interface ActiveUser {
|
export interface ActiveUser {
|
||||||
id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
logtoId: string;
|
externalId: string;
|
||||||
lastSeenAt: string;
|
lastSeenAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-21
@@ -1,22 +1,7 @@
|
|||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client';
|
||||||
import './index.css'
|
import './index.css';
|
||||||
import App from './App.tsx'
|
import App from './App.tsx';
|
||||||
|
|
||||||
export function getToken() {
|
createRoot(document.getElementById('root')!).render(
|
||||||
return 'guest-token';
|
<App />
|
||||||
}
|
);
|
||||||
|
|
||||||
export function getUser() {
|
|
||||||
return {
|
|
||||||
id: 'guest-user',
|
|
||||||
email: 'guest@gpi.app',
|
|
||||||
name: 'Guest User',
|
|
||||||
role: 'user'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setUser(token: string, user: any) {
|
|
||||||
console.log('User set (no auth):', user);
|
|
||||||
}
|
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(<App />)
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React, { useState, useEffect, useCallback } from 'react';
|
||||||
import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Upload, Info, Image as ImageIcon, Box, Database, Terminal } from 'lucide-react';
|
import { Shield, UserCheck, UserX, Users, Search, RefreshCw, Crown, Eye, User as UserIcon, Upload, Info, Image as ImageIcon, Box, Database } from 'lucide-react';
|
||||||
import { clsx } from 'clsx';
|
import { clsx } from 'clsx';
|
||||||
import type { AppUser, UserRole } from '../types';
|
import type { AppUser, UserRole } from '../types';
|
||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
@@ -14,7 +14,7 @@ const roleLabels: Record<UserRole, { label: string; color: string; icon: React.R
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const AdminDashboard: React.FC = () => {
|
export const AdminDashboard: React.FC = () => {
|
||||||
const { isAdmin, appUser: currentUser } = useAuth();
|
const { appUser, isAdmin } = useAuth();
|
||||||
const [users, setUsers] = useState<AppUser[]>([]);
|
const [users, setUsers] = useState<AppUser[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
@@ -24,6 +24,8 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
const [logoLoading, setLogoLoading] = useState(false);
|
const [logoLoading, setLogoLoading] = useState(false);
|
||||||
|
|
||||||
const fetchUsers = useCallback(async () => {
|
const fetchUsers = useCallback(async () => {
|
||||||
|
if (!appUser) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const response = await api.get('/users');
|
const response = await api.get('/users');
|
||||||
@@ -33,15 +35,15 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
} finally {
|
} finally {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [appUser]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isAdmin()) {
|
fetchUsers();
|
||||||
fetchUsers();
|
}, [fetchUsers]);
|
||||||
}
|
|
||||||
}, [isAdmin, fetchUsers]);
|
|
||||||
|
|
||||||
const handleRoleChange = async (userId: string, newRole: UserRole) => {
|
const handleRoleChange = async (userId: string, newRole: UserRole) => {
|
||||||
|
if (!appUser) return;
|
||||||
|
|
||||||
setActionLoading(userId);
|
setActionLoading(userId);
|
||||||
try {
|
try {
|
||||||
const response = await api.patch(`/users/${userId}/role`, { role: newRole });
|
const response = await api.patch(`/users/${userId}/role`, { role: newRole });
|
||||||
@@ -57,6 +59,8 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleToggleBan = async (userId: string, isBanned: boolean) => {
|
const handleToggleBan = async (userId: string, isBanned: boolean) => {
|
||||||
|
if (!appUser) return;
|
||||||
|
|
||||||
setActionLoading(userId);
|
setActionLoading(userId);
|
||||||
try {
|
try {
|
||||||
const response = await api.patch(`/users/${userId}/ban`, { isBanned });
|
const response = await api.patch(`/users/${userId}/ban`, { isBanned });
|
||||||
@@ -79,8 +83,32 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleLogoUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
// Implement Logo Upload via Backend API if needed
|
const file = e.target.files?.[0];
|
||||||
alert('Funcionalidade de upload de logo em migração para o novo sistema.');
|
if (!file) return;
|
||||||
|
|
||||||
|
// Validations
|
||||||
|
const validTypes = ['image/png', 'image/jpeg', 'image/jpg', 'image/svg+xml'];
|
||||||
|
if (!validTypes.includes(file.type)) {
|
||||||
|
alert('Por favor, selecione uma imagem PNG, JPG ou SVG.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 500 * 1024) {
|
||||||
|
alert('O arquivo deve ter no máximo 500KB.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLogoLoading(true);
|
||||||
|
try {
|
||||||
|
// Note: In the future, this should upload to our own backend
|
||||||
|
// For now, we'll keep the UI but mark it as pending backend integration
|
||||||
|
alert('Funcionalidade de upload de logo em migração para sistema nativo.');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error uploading logo:', error);
|
||||||
|
alert('Erro ao atualizar o logo.');
|
||||||
|
} finally {
|
||||||
|
setLogoLoading(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isAdmin()) {
|
if (!isAdmin()) {
|
||||||
@@ -273,7 +301,7 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
<tbody className="divide-y divide-border/40">
|
<tbody className="divide-y divide-border/40">
|
||||||
{filteredUsers.map((u) => {
|
{filteredUsers.map((u) => {
|
||||||
const roleInfo = roleLabels[u.role];
|
const roleInfo = roleLabels[u.role];
|
||||||
const isCurrentUser = u.email === currentUser?.email;
|
const isCurrentUser = u.email === appUser?.email;
|
||||||
const isActionDisabled = actionLoading === u.id;
|
const isActionDisabled = actionLoading === u.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -349,24 +377,32 @@ export const AdminDashboard: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
) : activeTab === 'organization' ? (
|
) : activeTab === 'organization' ? (
|
||||||
<div className="space-y-6 animate-in fade-in slide-in-from-left-4 duration-300">
|
<div className="space-y-6 animate-in fade-in slide-in-from-left-4 duration-300">
|
||||||
<div className="bg-surface rounded-2xl p-6 border border-border/40 space-y-6">
|
<div className="bg-surface rounded-2xl p-8 border border-border/40 text-center space-y-4">
|
||||||
<div className="flex items-center gap-3 pb-4 border-b border-border/20">
|
<ImageIcon size={48} className="mx-auto text-text-muted opacity-20" />
|
||||||
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
|
<h2 className="text-xl font-bold text-text-main">Gestão de Identidade Visual</h2>
|
||||||
<Info size={20} className="text-primary" />
|
<p className="text-text-muted max-w-md mx-auto">
|
||||||
</div>
|
O gerenciamento nativo de logos está sendo implementado. No momento, o logo atual é gerenciado via configurações do sistema.
|
||||||
<div>
|
</p>
|
||||||
<h2 className="text-lg font-bold text-text-main">Configurações da Organização</h2>
|
|
||||||
<p className="text-xs text-text-muted">Migrando para o novo sistema Logto</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<p className="text-text-muted">A gestão de identidade visual e dados da organização está sendo migrada para a API central.</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : activeTab === 'settings' ? (
|
) : activeTab === 'settings' ? (
|
||||||
<GeometrySettings />
|
<GeometrySettings />
|
||||||
) : activeTab === 'backup' ? (
|
) : activeTab === 'backup' ? (
|
||||||
<BackupRestore />
|
<BackupRestore />
|
||||||
) : null}
|
) : (
|
||||||
|
<div className="bg-surface rounded-2xl border border-border/40 p-6">
|
||||||
|
<div className="text-center py-10">
|
||||||
|
<h2 className="text-xl font-bold text-text-main">Gestão de Estoque</h2>
|
||||||
|
<p className="text-text-muted mt-2">Acesse a nova página dedicada ao controle de estoque.</p>
|
||||||
|
<a
|
||||||
|
href="/stock"
|
||||||
|
className="mt-6 inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-primary hover:bg-primary-dark focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary"
|
||||||
|
>
|
||||||
|
Ir para Estoque
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
import { useHandleSignInCallback } from '@logto/react';
|
|
||||||
import { useNavigate } from 'react-router-dom';
|
|
||||||
|
|
||||||
export const Callback = () => {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { isLoading } = useHandleSignInCallback(() => {
|
|
||||||
// Redireciona para a home após o login bem-sucedido
|
|
||||||
navigate('/');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen w-full flex items-center justify-center bg-surface-soft">
|
|
||||||
<div className="flex flex-col items-center gap-4">
|
|
||||||
<div className="w-12 h-12 border-4 border-primary border-t-transparent rounded-full animate-spin" />
|
|
||||||
<p className="text-text-muted font-medium">Finalizando autenticação...</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
+66
-44
@@ -1,30 +1,41 @@
|
|||||||
import React, { useState } from 'react';
|
import React, { useState } from "react";
|
||||||
import { Hammer, Lock, ShieldCheck } from "lucide-react";
|
import { Hammer } from "lucide-react";
|
||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from "../context/useAuth";
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { getBaseUrl } from "../services/api";
|
||||||
|
|
||||||
|
const API_URL = getBaseUrl();
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const [password, setPassword] = useState('');
|
const [email, setEmail] = useState("");
|
||||||
const [error, setError] = useState('');
|
const [password, setPassword] = useState("");
|
||||||
|
const [errorMsg, setErrorMsg] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { signInWithPassword } = useAuth();
|
|
||||||
const navigate = useNavigate();
|
const { login } = useAuth();
|
||||||
|
|
||||||
const handleSubmit = async (e: React.FormEvent) => {
|
const handleSubmit = async (e: React.FormEvent) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setError('');
|
setErrorMsg("");
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const success = await signInWithPassword(password);
|
const response = await fetch(`${API_URL}/auth/login`, {
|
||||||
if (success) {
|
method: "POST",
|
||||||
navigate('/');
|
headers: { "Content-Type": "application/json" },
|
||||||
} else {
|
body: JSON.stringify({ email, password })
|
||||||
setError('Senha incorreta. Acesso negado.');
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
setErrorMsg(data.error || "Erro ao efetuar login");
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login(data.token, data.user);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError('Erro ao processar login.');
|
setErrorMsg("Falha na conexão com o servidor.");
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -38,54 +49,65 @@ export const Login = () => {
|
|||||||
<div className="relative z-10 w-full max-w-md px-6 flex flex-col items-center">
|
<div className="relative z-10 w-full max-w-md px-6 flex flex-col items-center">
|
||||||
{/* Logo Area */}
|
{/* Logo Area */}
|
||||||
<div className="mb-8 flex flex-col items-center text-center">
|
<div className="mb-8 flex flex-col items-center text-center">
|
||||||
<div className="w-16 h-16 rounded-2xl bg-primary flex items-center justify-center text-white font-bold text-3xl shadow-2xl shadow-primary/40 mb-4">
|
<div className="w-16 h-16 rounded-2xl bg-primary flex items-center justify-center text-white font-bold text-3xl shadow-2xl shadow-primary/40 mb-4 animate-in zoom-in duration-700">
|
||||||
G
|
G
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold text-text-main tracking-tight mb-1">GPI RESTRICT</h1>
|
<h1 className="text-3xl font-bold text-text-main tracking-tight mb-1">GPI</h1>
|
||||||
<p className="text-text-muted text-[10px] font-black uppercase tracking-[0.3em]">Ambiente de Desenvolvimento</p>
|
<p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Login Form */}
|
{/* Custom Login Form */}
|
||||||
<div className="w-full bg-surface rounded-[2.5rem] border border-border/40 shadow-2xl shadow-primary/5 p-10 backdrop-blur-sm">
|
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-8 animate-in slide-in-from-bottom-8 duration-1000">
|
||||||
<div className="flex items-center gap-3 mb-8 text-primary">
|
<h2 className="text-xl font-bold text-text-main mb-6 text-center">Entrar na sua conta</h2>
|
||||||
<Lock size={20} className="opacity-70" />
|
|
||||||
<h2 className="text-lg font-bold uppercase tracking-tight">Chave de Acesso</h2>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-6">
|
{errorMsg && (
|
||||||
<div className="space-y-2">
|
<div className="mb-4 p-3 rounded-lg bg-error/10 border border-error/20 text-error text-sm text-center">
|
||||||
|
{errorMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<form onSubmit={handleSubmit} className="flex flex-col gap-4">
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<label className="text-sm font-semibold text-text-secondary" htmlFor="email">Email</label>
|
||||||
<input
|
<input
|
||||||
|
id="email"
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
value={email}
|
||||||
|
onChange={(e) => setEmail(e.target.value)}
|
||||||
|
className="bg-surface-soft border border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl px-4 py-3 text-text-main outline-none transition-all"
|
||||||
|
placeholder="seu@email.com"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<label className="text-sm font-semibold text-text-secondary" htmlFor="password">Senha</label>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
id="password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Digite a senha mestra..."
|
required
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
className="w-full h-14 bg-surface-soft border border-border/40 rounded-2xl px-6 text-sm focus:ring-4 focus:ring-primary/10 focus:border-primary transition-all font-bold placeholder:font-medium tracking-widest text-center"
|
className="bg-surface-soft border border-border/40 focus:ring-2 focus:ring-primary/20 focus:border-primary rounded-xl px-4 py-3 text-text-main outline-none transition-all"
|
||||||
required
|
placeholder="••••••••"
|
||||||
autoFocus
|
|
||||||
/>
|
/>
|
||||||
{error && <p className="text-error text-[10px] font-bold uppercase text-center mt-2 tracking-wider">{error}</p>}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="w-full h-14 bg-primary hover:bg-primary/90 text-white font-black uppercase tracking-widest rounded-2xl transition-all shadow-lg shadow-primary/20 flex items-center justify-center gap-3 active:scale-95 disabled:opacity-50"
|
className="mt-4 bg-primary hover:bg-primary/90 text-white font-bold py-3 rounded-xl transition-all shadow-lg shadow-primary/20 disabled:opacity-70 disabled:cursor-not-allowed"
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading ? "Entrando..." : "Entrar"}
|
||||||
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<ShieldCheck size={20} />
|
|
||||||
Entrar no Sistema
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-[10px] font-black uppercase tracking-widest">
|
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium">
|
||||||
<Hammer size={12} />
|
<Hammer size={14} />
|
||||||
<span>Desenvolvimento Ativo</span>
|
<span>© 2026 GPI - Eficiência Industrial</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import React, { useEffect } from 'react';
|
||||||
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
import { useAuth } from '../context/useAuth';
|
||||||
|
import { Building2, RefreshCw } from 'lucide-react';
|
||||||
|
|
||||||
|
export const OrganizationSelector: React.FC = () => {
|
||||||
|
const { appUser, isSignedIn } = useAuth();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSignedIn && appUser?.organizationId) {
|
||||||
|
navigate('/');
|
||||||
|
}
|
||||||
|
}, [isSignedIn, appUser, navigate]);
|
||||||
|
|
||||||
|
if (!isSignedIn) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center p-4">
|
||||||
|
<div className="max-w-md w-full bg-surface rounded-2xl border border-border/40 p-8 text-center">
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-amber-500/20 flex items-center justify-center mx-auto mb-4">
|
||||||
|
<Building2 className="w-8 h-8 text-amber-500" />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-2xl font-bold text-text-main mb-2">
|
||||||
|
Não Conectado
|
||||||
|
</h1>
|
||||||
|
<p className="text-text-muted mb-6">
|
||||||
|
Você precisa estar logado para acessar esta página.
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate('/login')}
|
||||||
|
className="w-full py-3 bg-primary text-white rounded-xl font-bold"
|
||||||
|
>
|
||||||
|
Ir para Login
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen bg-background flex items-center justify-center">
|
||||||
|
<div className="text-center">
|
||||||
|
<RefreshCw className="w-12 h-12 text-primary animate-spin mx-auto mb-4" />
|
||||||
|
<p className="text-text-main font-bold mb-2">Redirecionando...</p>
|
||||||
|
<p className="text-text-muted text-sm">Carregando sua organização</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -49,12 +49,13 @@ export const ProjectList: React.FC = () => {
|
|||||||
const [isPrintingGeneral, setIsPrintingGeneral] = useState(false);
|
const [isPrintingGeneral, setIsPrintingGeneral] = useState(false);
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { appUser, isAdmin: checkIsAdmin } = useAuth();
|
const { appUser } = useAuth();
|
||||||
const { showToast } = useToast();
|
const { showToast } = useToast();
|
||||||
const { settings } = useSystemSettings();
|
const { settings } = useSystemSettings();
|
||||||
|
|
||||||
const logoUrl = settings?.appLogoUrl;
|
const logoUrl = settings?.appLogoUrl;
|
||||||
const isAdmin = checkIsAdmin();
|
|
||||||
|
const isAdmin = appUser?.email === 'admtracksteel@gmail.com' || appUser?.role === 'admin';
|
||||||
|
|
||||||
const fetchProjects = useCallback(async () => {
|
const fetchProjects = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import type { PaintingScheme } from '../types';
|
|||||||
import { useAuth } from '../context/useAuth';
|
import { useAuth } from '../context/useAuth';
|
||||||
|
|
||||||
export const SchemesList: React.FC = () => {
|
export const SchemesList: React.FC = () => {
|
||||||
const { isAdmin } = useAuth();
|
|
||||||
const [schemes, setSchemes] = useState<PaintingScheme[]>([]);
|
const [schemes, setSchemes] = useState<PaintingScheme[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [editItem, setEditItem] = useState<PaintingScheme | undefined>(undefined);
|
const [editItem, setEditItem] = useState<PaintingScheme | undefined>(undefined);
|
||||||
@@ -18,6 +17,8 @@ export const SchemesList: React.FC = () => {
|
|||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isCloneModalOpen, setIsCloneModalOpen] = useState(false);
|
const [isCloneModalOpen, setIsCloneModalOpen] = useState(false);
|
||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
const { appUser } = useAuth();
|
||||||
|
const isAdmin = appUser?.email === 'admtracksteel@gmail.com' || appUser?.role === 'admin';
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSchemes();
|
fetchSchemes();
|
||||||
@@ -134,7 +135,7 @@ export const SchemesList: React.FC = () => {
|
|||||||
onChange={(e) => setSearchTerm(e.target.value)}
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isAdmin() && (
|
{isAdmin && (
|
||||||
<Button onClick={() => { setEditItem(undefined); setIsModalOpen(true); }} size="lg" className="shadow-primary/30 h-14">
|
<Button onClick={() => { setEditItem(undefined); setIsModalOpen(true); }} size="lg" className="shadow-primary/30 h-14">
|
||||||
<Plus className="w-5 h-5 mr-2" />
|
<Plus className="w-5 h-5 mr-2" />
|
||||||
Novo Esquema
|
Novo Esquema
|
||||||
@@ -150,7 +151,7 @@ export const SchemesList: React.FC = () => {
|
|||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
titleAccessor="name"
|
titleAccessor="name"
|
||||||
subtitleAccessor={(item) => `${item.manufacturer || ''} ${item.type || ''}`}
|
subtitleAccessor={(item) => `${item.manufacturer || ''} ${item.type || ''}`}
|
||||||
actionRender={(item) => isAdmin() ? (
|
actionRender={(item) => isAdmin ? (
|
||||||
<div className="flex gap-1 justify-end">
|
<div className="flex gap-1 justify-end">
|
||||||
<button
|
<button
|
||||||
onClick={() => { setCloneItem(item); setIsCloneModalOpen(true); }}
|
onClick={() => { setCloneItem(item); setIsCloneModalOpen(true); }}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useAuth } from '../context/useAuth';
|
|||||||
import { useSystemSettings } from '../context/SystemSettingsContext';
|
import { useSystemSettings } from '../context/SystemSettingsContext';
|
||||||
|
|
||||||
export const StockDashboard: React.FC = () => {
|
export const StockDashboard: React.FC = () => {
|
||||||
|
// ... rest of component
|
||||||
const { isAdmin } = useAuth();
|
const { isAdmin } = useAuth();
|
||||||
const { settings } = useSystemSettings();
|
const { settings } = useSystemSettings();
|
||||||
|
|
||||||
@@ -79,12 +80,11 @@ export const StockDashboard: React.FC = () => {
|
|||||||
await Promise.all(
|
await Promise.all(
|
||||||
items.map(async (item) => {
|
items.map(async (item) => {
|
||||||
try {
|
try {
|
||||||
const itemId = item.id || (item as any)._id;
|
const movements = await stockService.getMovements(item._id!);
|
||||||
if (!itemId) return;
|
movementsMap.set(item._id!, movements);
|
||||||
const movements = await stockService.getMovements(itemId);
|
|
||||||
movementsMap.set(itemId, movements);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching movements for ${item.id}:`, error);
|
console.error(`Error fetching movements for ${item._id}:`, error);
|
||||||
|
movementsMap.set(item._id!, []);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -107,19 +107,19 @@ export const StockDashboard: React.FC = () => {
|
|||||||
const filteredItems = items.filter(item => {
|
const filteredItems = items.filter(item => {
|
||||||
const searchLower = searchTerm.toLowerCase();
|
const searchLower = searchTerm.toLowerCase();
|
||||||
// Handle type checking carefully. If type is missing, assume PAINT.
|
// Handle type checking carefully. If type is missing, assume PAINT.
|
||||||
const type = (typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).type : '') || 'PAINT';
|
const type = (typeof item.dataSheetId === 'object' ? item.dataSheetId.type : '') || 'PAINT';
|
||||||
const isThinner = type === 'THINNER' || type === 'DILUENTE';
|
const isThinner = type === 'THINNER' || type === 'DILUENTE';
|
||||||
|
|
||||||
// Tab Filter
|
// Tab Filter
|
||||||
if (activeTab === 'THINNER' && !isThinner) return false;
|
if (activeTab === 'THINNER' && !isThinner) return false;
|
||||||
if (activeTab === 'PAINT' && isThinner) return false;
|
if (activeTab === 'PAINT' && isThinner) return false;
|
||||||
|
|
||||||
const productName = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).name : '';
|
const productName = typeof item.dataSheetId === 'object' ? item.dataSheetId.name : '';
|
||||||
const manufacturer = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).manufacturer : '';
|
const manufacturer = typeof item.dataSheetId === 'object' ? item.dataSheetId.manufacturer : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(item.rrNumber || '').toLowerCase().includes(searchLower) ||
|
item.rrNumber.toLowerCase().includes(searchLower) ||
|
||||||
(item.batchNumber || '').toLowerCase().includes(searchLower) ||
|
item.batchNumber.toLowerCase().includes(searchLower) ||
|
||||||
productName.toLowerCase().includes(searchLower) ||
|
productName.toLowerCase().includes(searchLower) ||
|
||||||
manufacturer.toLowerCase().includes(searchLower)
|
manufacturer.toLowerCase().includes(searchLower)
|
||||||
);
|
);
|
||||||
@@ -129,10 +129,9 @@ export const StockDashboard: React.FC = () => {
|
|||||||
const groups = new Map<string, { items: StockItem[], totalQty: number, minStock: number, unit: string, productName: string, color: string, manufacturer: string }>();
|
const groups = new Map<string, { items: StockItem[], totalQty: number, minStock: number, unit: string, productName: string, color: string, manufacturer: string }>();
|
||||||
|
|
||||||
filteredItems.forEach(item => {
|
filteredItems.forEach(item => {
|
||||||
const productName = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).name : 'Unknown';
|
const productName = typeof item.dataSheetId === 'object' ? item.dataSheetId.name : 'Unknown';
|
||||||
const manufacturer = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).manufacturer : '';
|
const manufacturer = typeof item.dataSheetId === 'object' ? item.dataSheetId.manufacturer : '';
|
||||||
const dsId = (item.dataSheetId as any).id || (item.dataSheetId as any)._id || item.dataSheetId;
|
const key = `${item.dataSheetId._id || item.dataSheetId}-${item.color}`;
|
||||||
const key = `${dsId}-${item.color}`;
|
|
||||||
|
|
||||||
if (!groups.has(key)) {
|
if (!groups.has(key)) {
|
||||||
groups.set(key, {
|
groups.set(key, {
|
||||||
@@ -297,7 +296,7 @@ export const StockDashboard: React.FC = () => {
|
|||||||
<td className="px-6 py-4 font-bold text-lg">
|
<td className="px-6 py-4 font-bold text-lg">
|
||||||
<span className={isLowStock ? 'text-red-500 animate-blink flex items-center gap-2' : 'text-green-500'}>
|
<span className={isLowStock ? 'text-red-500 animate-blink flex items-center gap-2' : 'text-green-500'}>
|
||||||
{isLowStock && <AlertCircle size={16} />}
|
{isLowStock && <AlertCircle size={16} />}
|
||||||
{(group.totalQty || 0).toFixed(1)} {group.unit}
|
{group.totalQty.toFixed(1)} {group.unit}
|
||||||
</span>
|
</span>
|
||||||
{group.minStock > 0 && (
|
{group.minStock > 0 && (
|
||||||
<span className="block text-[10px] text-text-muted font-normal">
|
<span className="block text-[10px] text-text-muted font-normal">
|
||||||
@@ -315,11 +314,11 @@ export const StockDashboard: React.FC = () => {
|
|||||||
|
|
||||||
{/* Expanded Item Rows */}
|
{/* Expanded Item Rows */}
|
||||||
{isExpanded && group.items.map(item => {
|
{isExpanded && group.items.map(item => {
|
||||||
const itemId = item.id || (item as any)._id;
|
|
||||||
const isExpired = item.expirationDate && new Date(item.expirationDate) < new Date();
|
const isExpired = item.expirationDate && new Date(item.expirationDate) < new Date();
|
||||||
|
// Check individual item min stock for legacy reasons? No, rely on group.
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr key={itemId} className="bg-surface-soft/50 hover:bg-surface-hover/80 transition-colors border-l-4 border-l-primary/20">
|
<tr key={item._id} className="bg-surface-soft/50 hover:bg-surface-hover/80 transition-colors border-l-4 border-l-primary/20">
|
||||||
<td className="px-6 py-3"></td> {/* Indentation */}
|
<td className="px-6 py-3"></td> {/* Indentation */}
|
||||||
<td className="px-6 py-3 font-mono text-xs text-text-muted">
|
<td className="px-6 py-3 font-mono text-xs text-text-muted">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
@@ -373,7 +372,7 @@ export const StockDashboard: React.FC = () => {
|
|||||||
<Edit size={16} />
|
<Edit size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={(e) => { e.stopPropagation(); handleDelete(itemId!); }}
|
onClick={(e) => { e.stopPropagation(); handleDelete(item._id!); }}
|
||||||
className="p-1.5 text-red-500 hover:bg-red-500/10 rounded-lg"
|
className="p-1.5 text-red-500 hover:bg-red-500/10 rounded-lg"
|
||||||
title="Excluir"
|
title="Excluir"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -287,7 +287,7 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
const dilutionFactor = 100 - study.dilutionPercent;
|
const dilutionFactor = 100 - study.dilutionPercent;
|
||||||
const svFactor = sv * dilutionFactor;
|
const svFactor = sv * dilutionFactor;
|
||||||
const calculatedEpu = svFactor > 0
|
const calculatedEpu = svFactor > 0
|
||||||
? Number((((study.targetDft || 0) * 10000) / svFactor).toFixed(1))
|
? Number((study.targetDft * 10000 / svFactor).toFixed(1))
|
||||||
: 0;
|
: 0;
|
||||||
|
|
||||||
let totalWeight = 0;
|
let totalWeight = 0;
|
||||||
@@ -320,8 +320,8 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...cat,
|
...cat,
|
||||||
litrosPeso: Number((litrosPeso || 0).toFixed(2)),
|
litrosPeso: Number(litrosPeso.toFixed(2)),
|
||||||
litrosArea: litrosArea > 0 ? Number((litrosArea || 0).toFixed(2)) : undefined
|
litrosArea: litrosArea > 0 ? Number(litrosArea.toFixed(2)) : undefined
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -332,10 +332,10 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
...study,
|
...study,
|
||||||
categories: updatedCategories,
|
categories: updatedCategories,
|
||||||
totalWeight,
|
totalWeight,
|
||||||
estimatedPaintVolume: Number((totalVolumeByWeight || 0).toFixed(2)),
|
estimatedPaintVolume: Number(totalVolumeByWeight.toFixed(2)),
|
||||||
estimatedReducerVolume: Number((reducerVolByWeight || 0).toFixed(2)),
|
estimatedReducerVolume: Number(reducerVolByWeight.toFixed(2)),
|
||||||
estimatedPaintVolumeByArea: Number((totalVolumeByArea || 0).toFixed(2)),
|
estimatedPaintVolumeByArea: Number(totalVolumeByArea.toFixed(2)),
|
||||||
estimatedReducerVolumeByArea: Number((reducerVolByArea || 0).toFixed(2)),
|
estimatedReducerVolumeByArea: Number(reducerVolByArea.toFixed(2)),
|
||||||
calculatedEpu: calculatedEpu
|
calculatedEpu: calculatedEpu
|
||||||
} as YieldStudy & { calculatedEpu: number });
|
} as YieldStudy & { calculatedEpu: number });
|
||||||
};
|
};
|
||||||
@@ -353,11 +353,11 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
// Data for deviation projection
|
// Data for deviation projection
|
||||||
// Data for deviation projection - Lógica Direta (Mais DFT = Mais Tinta)
|
// Data for deviation projection - Lógica Direta (Mais DFT = Mais Tinta)
|
||||||
const projectionData = selectedStudy ? [
|
const projectionData = selectedStudy ? [
|
||||||
{ dft: ((selectedStudy.targetDft || 0) * 0.8).toFixed(0), vol: Number(((selectedStudy.estimatedPaintVolume || 0) * 0.8).toFixed(1)), label: `-20%` },
|
{ dft: (selectedStudy.targetDft * 0.8).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 0.8).toFixed(1)), label: `-20%` },
|
||||||
{ dft: ((selectedStudy.targetDft || 0) * 0.9).toFixed(0), vol: Number(((selectedStudy.estimatedPaintVolume || 0) * 0.9).toFixed(1)), label: `-10%` },
|
{ dft: (selectedStudy.targetDft * 0.9).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 0.9).toFixed(1)), label: `-10%` },
|
||||||
{ dft: (selectedStudy.targetDft || 0).toFixed(0), vol: (selectedStudy.estimatedPaintVolume || 0), label: 'ALVO' },
|
{ dft: selectedStudy.targetDft.toFixed(0), vol: selectedStudy.estimatedPaintVolume, label: 'ALVO' },
|
||||||
{ dft: ((selectedStudy.targetDft || 0) * 1.1).toFixed(0), vol: Number(((selectedStudy.estimatedPaintVolume || 0) * 1.1).toFixed(1)), label: '+10%' },
|
{ dft: (selectedStudy.targetDft * 1.1).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 1.1).toFixed(1)), label: '+10%' },
|
||||||
{ dft: ((selectedStudy.targetDft || 0) * 1.3).toFixed(0), vol: Number(((selectedStudy.estimatedPaintVolume || 0) * 1.3).toFixed(1)), label: '+30%' },
|
{ dft: (selectedStudy.targetDft * 1.3).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 1.3).toFixed(1)), label: '+30%' },
|
||||||
] : [];
|
] : [];
|
||||||
|
|
||||||
if (loading) return <div className="p-8 text-center text-text-muted">Carregando estudos...</div>;
|
if (loading) return <div className="p-8 text-center text-text-muted">Carregando estudos...</div>;
|
||||||
@@ -466,11 +466,11 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[9px] font-black text-text-muted uppercase tracking-[0.2em] mb-1">Carga Total</span>
|
<span className="text-[9px] font-black text-text-muted uppercase tracking-[0.2em] mb-1">Carga Total</span>
|
||||||
<span className="text-sm font-black text-text-main">{(study.totalWeight || 0).toFixed(1)} t</span>
|
<span className="text-sm font-black text-text-main">{study.totalWeight.toFixed(1)} t</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="text-[9px] font-black text-text-muted uppercase tracking-[0.2em] mb-1">Target DFT</span>
|
<span className="text-[9px] font-black text-text-muted uppercase tracking-[0.2em] mb-1">Target DFT</span>
|
||||||
<span className="text-sm font-black text-text-main">{(study.targetDft || 0)} <span className="text-[10px] text-text-muted">μm</span></span>
|
<span className="text-sm font-black text-text-main">{study.targetDft} <span className="text-[10px] text-text-muted">μm</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -558,9 +558,9 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
const sheet = findSheet(selectedStudy.dataSheetId);
|
const sheet = findSheet(selectedStudy.dataSheetId);
|
||||||
let sv = sheet?.solidsVolume || 60;
|
let sv = sheet?.solidsVolume || 60;
|
||||||
if (sv <= 1) sv *= 100;
|
if (sv <= 1) sv *= 100;
|
||||||
const dilFactor = 100 - (selectedStudy.dilutionPercent || 0);
|
const dilFactor = 100 - selectedStudy.dilutionPercent;
|
||||||
const svFactor = sv * dilFactor;
|
const svFactor = sv * dilFactor;
|
||||||
return svFactor > 0 ? ((selectedStudy.targetDft || 0) * 10000 / svFactor).toFixed(1) : '0';
|
return svFactor > 0 ? (selectedStudy.targetDft * 10000 / svFactor).toFixed(1) : '0';
|
||||||
})()
|
})()
|
||||||
} <span className="text-xs">µm</span>
|
} <span className="text-xs">µm</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -572,19 +572,19 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
const hasRealSV = sheet?.solidsVolume && sheet.solidsVolume > 0;
|
const hasRealSV = sheet?.solidsVolume && sheet.solidsVolume > 0;
|
||||||
let sv = sheet?.solidsVolume || 60;
|
let sv = sheet?.solidsVolume || 60;
|
||||||
if (sv <= 1) sv *= 100;
|
if (sv <= 1) sv *= 100;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className={`text-[9px] font-black uppercase tracking-wider ${hasRealSV ? 'text-success' : 'text-amber-500'}`}>
|
<span className={`text-[9px] font-black uppercase tracking-wider ${hasRealSV ? 'text-success' : 'text-amber-500'}`}>
|
||||||
SV da Tinta {hasRealSV ? '✓' : '⚠️'}
|
SV da Tinta {hasRealSV ? '✓' : '⚠️'}
|
||||||
</span>
|
</span>
|
||||||
<div className={`text-2xl font-black leading-none ${hasRealSV ? 'text-text-main' : 'text-amber-500'}`}>
|
<div className={`text-2xl font-black leading-none ${hasRealSV ? 'text-text-main' : 'text-amber-500'}`}>
|
||||||
{(sv || 0).toFixed(0)} <span className="text-xs">%</span>
|
{sv.toFixed(0)} <span className="text-xs">%</span>
|
||||||
</div>
|
</div>
|
||||||
<span className="text-[8px] text-text-muted">
|
<span className="text-[8px] text-text-muted">
|
||||||
{hasRealSV ? 'Sólidos por Volume' : 'Valor padrão (edite a ficha)'}
|
{hasRealSV ? 'Sólidos por Volume' : 'Valor padrão (edite a ficha)'}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -617,13 +617,13 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-[10px] font-bold text-text-muted uppercase">Taxa Média</span>
|
<span className="text-[10px] font-bold text-text-muted uppercase">Taxa Média</span>
|
||||||
<span className="text-sm font-black text-text-main">
|
<span className="text-sm font-black text-text-main">
|
||||||
{selectedStudy.totalWeight > 0 ? (((selectedStudy.estimatedPaintVolume || 0) / selectedStudy.totalWeight).toFixed(2)) : '0.00'} <span className="text-[10px] text-text-muted">L/t</span>
|
{selectedStudy.totalWeight > 0 ? ((selectedStudy.estimatedPaintVolume / selectedStudy.totalWeight).toFixed(2)) : '0.00'} <span className="text-[10px] text-text-muted">L/t</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-[10px] font-bold text-text-muted uppercase">Peso Total</span>
|
<span className="text-[10px] font-bold text-text-muted uppercase">Peso Total</span>
|
||||||
<span className="text-sm font-black text-primary">
|
<span className="text-sm font-black text-primary">
|
||||||
{(selectedStudy.totalWeight || 0).toFixed(2)} TON
|
{selectedStudy.totalWeight.toFixed(2)} TON
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -926,7 +926,7 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
<div className="grid grid-cols-4 gap-4 mb-8">
|
<div className="grid grid-cols-4 gap-4 mb-8">
|
||||||
<div className="border border-gray-300 rounded-xl p-4 space-y-1">
|
<div className="border border-gray-300 rounded-xl p-4 space-y-1">
|
||||||
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Peso Total (Ton)</span>
|
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Peso Total (Ton)</span>
|
||||||
<div className="text-xl font-black">{(selectedStudy.totalWeight || 0).toFixed(2)}</div>
|
<div className="text-xl font-black">{selectedStudy.totalWeight.toFixed(2)}</div>
|
||||||
<p className="text-[7px] text-gray-400 font-bold uppercase">Soma das categorias</p>
|
<p className="text-[7px] text-gray-400 font-bold uppercase">Soma das categorias</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="border border-gray-300 rounded-xl p-4 space-y-1">
|
<div className="border border-gray-300 rounded-xl p-4 space-y-1">
|
||||||
@@ -941,7 +941,7 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="border border-gray-300 rounded-xl p-4 space-y-1 border-black bg-gray-50">
|
<div className="border border-gray-300 rounded-xl p-4 space-y-1 border-black bg-gray-50">
|
||||||
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Taxa Média</span>
|
<span className="text-[8px] font-black text-gray-500 uppercase tracking-widest">Taxa Média</span>
|
||||||
<div className="text-xl font-black">{selectedStudy.totalWeight > 0 ? ((selectedStudy.estimatedPaintVolume || 0) / selectedStudy.totalWeight).toFixed(2) : '0.00'} <span className="text-[10px]">L/t</span></div>
|
<div className="text-xl font-black">{selectedStudy.totalWeight > 0 ? (selectedStudy.estimatedPaintVolume / selectedStudy.totalWeight).toFixed(2) : '0.00'} <span className="text-[10px]">L/t</span></div>
|
||||||
<p className="text-[7px] text-gray-400 font-bold uppercase">Rendimento Global</p>
|
<p className="text-[7px] text-gray-400 font-bold uppercase">Rendimento Global</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -969,7 +969,7 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
<td className="py-3 pr-4">
|
<td className="py-3 pr-4">
|
||||||
<div className="text-[11px] font-black text-gray-800">{cat.name}</div>
|
<div className="text-[11px] font-black text-gray-800">{cat.name}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="py-3 text-center text-[10px] font-bold text-amber-700">{(cat.weight || 0).toFixed(2)}</td>
|
<td className="py-3 text-center text-[10px] font-bold text-amber-700">{cat.weight.toFixed(2)}</td>
|
||||||
<td className="py-3 text-center text-[10px] font-bold text-blue-700">{cat.area ? Math.round(cat.area) : '--'}</td>
|
<td className="py-3 text-center text-[10px] font-bold text-blue-700">{cat.area ? Math.round(cat.area) : '--'}</td>
|
||||||
<td className="py-3 text-center text-[10px] font-bold text-amber-700">{cat.historicalYield}</td>
|
<td className="py-3 text-center text-[10px] font-bold text-amber-700">{cat.historicalYield}</td>
|
||||||
<td className="py-3 text-center text-[10px] font-bold text-blue-700">{cat.efficiency}%</td>
|
<td className="py-3 text-center text-[10px] font-bold text-blue-700">{cat.efficiency}%</td>
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
// API service configuration v2.0 - Logto Auth
|
// API service configuration v1.4 - with auth and error interceptors
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { triggerGuestWarning } from '../utils/toastHandler';
|
import { triggerGuestWarning } from '../utils/toastHandler';
|
||||||
import { getToken } from '../main';
|
|
||||||
|
|
||||||
export const getBaseUrl = () => {
|
export const getBaseUrl = () => {
|
||||||
|
// Priority: Env var -> Relative path (handled by Vite proxy in dev, or Nginx/Vercel in prod)
|
||||||
if (import.meta.env.VITE_API_URL) {
|
if (import.meta.env.VITE_API_URL) {
|
||||||
return import.meta.env.VITE_API_URL;
|
return import.meta.env.VITE_API_URL;
|
||||||
}
|
}
|
||||||
@@ -17,26 +17,43 @@ const api = axios.create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let currentToken: string | null = null;
|
||||||
let currentOrgId: string | null = null;
|
let currentOrgId: string | null = null;
|
||||||
let currentOrgName: string | null = null;
|
let currentOrgName: string | null = null;
|
||||||
|
|
||||||
|
// Function to set the JWT token
|
||||||
|
export const setApiToken = (token: string | null) => {
|
||||||
|
currentToken = token;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Function to set the organization ID and Name (called from Layout/Context)
|
||||||
export const setApiOrgData = (orgId: string | null, orgName: string | null = null) => {
|
export const setApiOrgData = (orgId: string | null, orgName: string | null = null) => {
|
||||||
currentOrgId = orgId;
|
currentOrgId = orgId;
|
||||||
currentOrgName = orgName;
|
currentOrgName = orgName;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setApiOrganizationId = setApiOrgData;
|
// Legacy support
|
||||||
|
export const setApiOrgId = (orgId: string | null) => {
|
||||||
|
setApiOrgData(orgId, null);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Alias for consistency
|
||||||
|
export const setApiOrganizationId = setApiOrgId;
|
||||||
|
|
||||||
|
// Request interceptor to add clerk user ID and Org ID headers
|
||||||
api.interceptors.request.use(
|
api.interceptors.request.use(
|
||||||
(config) => {
|
(config) => {
|
||||||
const token = getToken();
|
console.log(`[API Request] ${config.method?.toUpperCase()} ${config.url}`, {
|
||||||
if (token) {
|
orgId: currentOrgId
|
||||||
config.headers['Authorization'] = `Bearer ${token}`;
|
});
|
||||||
|
if (currentToken) {
|
||||||
|
config.headers['Authorization'] = `Bearer ${currentToken}`;
|
||||||
}
|
}
|
||||||
if (currentOrgId) {
|
if (currentOrgId) {
|
||||||
config.headers['x-organization-id'] = currentOrgId;
|
config.headers['x-organization-id'] = currentOrgId;
|
||||||
}
|
}
|
||||||
if (currentOrgName) {
|
if (currentOrgName) {
|
||||||
|
// Encode to handle special characters
|
||||||
config.headers['x-organization-name'] = encodeURIComponent(currentOrgName);
|
config.headers['x-organization-name'] = encodeURIComponent(currentOrgName);
|
||||||
}
|
}
|
||||||
return config;
|
return config;
|
||||||
@@ -46,10 +63,12 @@ api.interceptors.request.use(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Response interceptor to handle 403 errors (guest access denied)
|
||||||
api.interceptors.response.use(
|
api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
(error) => {
|
(error) => {
|
||||||
if (error.response?.status === 403) {
|
if (error.response?.status === 403) {
|
||||||
|
// Check if it's a guest permission error
|
||||||
const errorMessage = error.response?.data?.error || '';
|
const errorMessage = error.response?.data?.error || '';
|
||||||
if (errorMessage.includes('Convidados') || errorMessage.includes('guest') || errorMessage.includes('permissão')) {
|
if (errorMessage.includes('Convidados') || errorMessage.includes('guest') || errorMessage.includes('permissão')) {
|
||||||
triggerGuestWarning();
|
triggerGuestWarning();
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const systemSettingsService = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
updateSettings: async (settings: Partial<SystemSettings>): Promise<SystemSettings> => {
|
updateSettings: async (settings: Partial<SystemSettings>): Promise<SystemSettings> => {
|
||||||
// Axios interceptors handle organization-id headers
|
// Axios interceptors in api.ts automatically handle x-auth-user-id and x-organization-id headers
|
||||||
const response = await api.put('/system-settings', settings);
|
const response = await api.put('/system-settings', settings);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
@@ -51,7 +51,7 @@ export const systemSettingsService = {
|
|||||||
|
|
||||||
export interface GlobalUser {
|
export interface GlobalUser {
|
||||||
_id: string;
|
_id: string;
|
||||||
id: string;
|
externalId: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
role: string;
|
role: string;
|
||||||
|
|||||||
+1
-1
@@ -190,7 +190,7 @@ export type UserRole = 'guest' | 'user' | 'admin';
|
|||||||
export interface AppUser {
|
export interface AppUser {
|
||||||
id: string;
|
id: string;
|
||||||
_id?: string;
|
_id?: string;
|
||||||
logtoId?: string;
|
externalId: string;
|
||||||
email: string;
|
email: string;
|
||||||
name: string;
|
name: string;
|
||||||
role: UserRole;
|
role: UserRole;
|
||||||
|
|||||||
+27
-23
@@ -11,6 +11,7 @@ import yieldStudyRoutes from './routes/yieldStudyRoutes.js';
|
|||||||
import userRoutes from './routes/userRoutes.js';
|
import userRoutes from './routes/userRoutes.js';
|
||||||
import systemSettingsRoutes from './routes/systemSettingsRoutes.js';
|
import systemSettingsRoutes from './routes/systemSettingsRoutes.js';
|
||||||
import geometryTypeRoutes from './routes/geometryTypeRoutes.js';
|
import geometryTypeRoutes from './routes/geometryTypeRoutes.js';
|
||||||
|
import authRoutes from './routes/authRoutes.js';
|
||||||
|
|
||||||
import stockRoutes from './routes/stockRoutes.js';
|
import stockRoutes from './routes/stockRoutes.js';
|
||||||
import notificationRoutes from './routes/notificationRoutes.js';
|
import notificationRoutes from './routes/notificationRoutes.js';
|
||||||
@@ -22,31 +23,36 @@ import path from 'path';
|
|||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors({
|
app.use(cors({
|
||||||
origin: '*',
|
origin: '*', // Be more specific in production
|
||||||
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'],
|
||||||
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id']
|
allowedHeaders: ['Content-Type', 'Authorization', 'x-organization-id']
|
||||||
}));
|
}));
|
||||||
|
app.use(express.json());
|
||||||
|
import { extractUser } from './middleware/roleMiddleware.js';
|
||||||
|
|
||||||
app.use(express.json({ limit: '50mb' }));
|
// LOG DE DEPURAÇÃO PARA CONEXÃO
|
||||||
app.use(express.urlencoded({ limit: '50mb', extended: true }));
|
app.use((req, res, next) => {
|
||||||
import { extractUser } from './middleware/authMiddleware.js';
|
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
import { authMiddleware } from './middleware/auth.js';
|
||||||
|
app.use(authMiddleware);
|
||||||
app.use(extractUser);
|
app.use(extractUser);
|
||||||
|
|
||||||
// Static Uploads
|
// Static Uploads
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
const uploadsPath = path.join(process.cwd(), 'uploads');
|
const uploadsPath = path.join(process.cwd(), 'uploads');
|
||||||
|
|
||||||
|
// Ensure uploads directory exists
|
||||||
if (!fs.existsSync(uploadsPath)) {
|
if (!fs.existsSync(uploadsPath)) {
|
||||||
fs.mkdirSync(uploadsPath, { recursive: true });
|
fs.mkdirSync(uploadsPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use('/uploads', express.static(uploadsPath));
|
app.use('/uploads', express.static(uploadsPath));
|
||||||
|
|
||||||
// Serve frontend static files
|
|
||||||
const distPath = path.join(process.cwd(), 'dist');
|
|
||||||
app.use(express.static(distPath));
|
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/users', userRoutes);
|
app.use('/api/users', userRoutes);
|
||||||
app.use('/api/projects', projectRoutes);
|
app.use('/api/projects', projectRoutes);
|
||||||
app.use('/api/parts', partRoutes);
|
app.use('/api/parts', partRoutes);
|
||||||
@@ -66,22 +72,20 @@ app.use('/api/messages', messageRoutes);
|
|||||||
app.use('/api/backup', backupRoutes);
|
app.use('/api/backup', backupRoutes);
|
||||||
|
|
||||||
app.get('/health', (req, res) => {
|
app.get('/health', (req, res) => {
|
||||||
res.json({ status: 'ok', timestamp: new Date(), auth: 'logto' });
|
res.json({ status: 'ok', timestamp: new Date() });
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get('/api/test', (req, res) => {
|
// Serve frontend static files
|
||||||
res.json({ status: 'ok', message: 'Test endpoint working' });
|
const clientPath = path.join(process.cwd(), 'dist', 'client');
|
||||||
});
|
if (fs.existsSync(clientPath)) {
|
||||||
|
app.use(express.static(clientPath));
|
||||||
// SPA fallback - must be last
|
app.use((req, res, next) => {
|
||||||
app.use((req, res, next) => {
|
if (!req.path.startsWith('/api') && !req.path.startsWith('/uploads')) {
|
||||||
res.sendFile(path.join(distPath, 'index.html'));
|
res.sendFile(path.join(clientPath, 'index.html'));
|
||||||
});
|
} else {
|
||||||
|
next();
|
||||||
// Error handler
|
}
|
||||||
app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
});
|
||||||
console.error('Express Error:', err);
|
}
|
||||||
res.status(500).json({ error: 'Internal server error' });
|
|
||||||
});
|
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,16 +1,26 @@
|
|||||||
import { supabase } from './supabase.js';
|
import pg from 'pg';
|
||||||
|
const { Pool } = pg;
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
export const pool = new Pool({
|
||||||
|
host: process.env.DB_HOST || 'supabase-db',
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
|
user: process.env.DB_USER || 'postgres',
|
||||||
|
password: process.env.DB_PASSWORD || 'Xz0oyb6ArGYG5uAVTVwcvJxRrMuT7EIJ',
|
||||||
|
database: process.env.DB_NAME || 'postgres',
|
||||||
|
ssl: false
|
||||||
|
});
|
||||||
|
|
||||||
export const connectDB = async () => {
|
export const connectDB = async () => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.from('users').select('count');
|
const client = await pool.connect();
|
||||||
|
console.log('✅ Postgres (Supabase) connected successfully');
|
||||||
if (error) {
|
client.release();
|
||||||
console.error('❌ Erro ao conectar no Supabase:', error);
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Conectado ao Supabase (schema: gpi)');
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ Erro de conexão:', error);
|
console.error('❌ Postgres connection error:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const query = (text: string, params?: any[]) => pool.query(text, params);
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import pg from 'pg';
|
||||||
|
const { Pool } = pg;
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
|
const pool = new Pool({
|
||||||
|
host: process.env.DB_HOST || 'supabase-db',
|
||||||
|
port: parseInt(process.env.DB_PORT || '5432'),
|
||||||
|
user: process.env.DB_USER || 'postgres',
|
||||||
|
password: process.env.DB_PASSWORD || 'Xz0oyb6ArGYG5uAVTVwcvJxRrMuT7EIJ',
|
||||||
|
database: process.env.DB_NAME || 'postgres',
|
||||||
|
ssl: false // Internal network usually doesn't need SSL
|
||||||
|
});
|
||||||
|
|
||||||
|
export const query = (text: string, params?: any[]) => pool.query(text, params);
|
||||||
|
|
||||||
|
export default pool;
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
import { createClient } from '@supabase/supabase-js';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const supabaseUrl = process.env.SUPABASE_URL || 'https://supabase.reifonas.cloud';
|
|
||||||
const supabaseServiceKey = process.env.SUPABASE_SERVICE_ROLE_KEY;
|
|
||||||
|
|
||||||
if (!supabaseServiceKey) {
|
|
||||||
throw new Error('❌ SUPABASE_SERVICE_ROLE_KEY is missing in environment variables');
|
|
||||||
}
|
|
||||||
|
|
||||||
export const GPI_SCHEMA = 'public';
|
|
||||||
|
|
||||||
export const supabase = createClient(supabaseUrl, supabaseServiceKey, {
|
|
||||||
auth: {
|
|
||||||
autoRefreshToken: false,
|
|
||||||
persistSession: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export async function queryGpi(table: string, query?: any) {
|
|
||||||
let dbQuery = supabase.from(table).select('*');
|
|
||||||
|
|
||||||
if (query) {
|
|
||||||
if (query.filter) {
|
|
||||||
Object.entries(query.filter).forEach(([key, value]) => {
|
|
||||||
dbQuery = dbQuery.eq(key, value);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (query.order) {
|
|
||||||
dbQuery = dbQuery.order(query.order.by || 'created_at', { ascending: query.order.asc ?? false });
|
|
||||||
}
|
|
||||||
if (query.limit) {
|
|
||||||
dbQuery = dbQuery.limit(query.limit);
|
|
||||||
}
|
|
||||||
if (query.offset) {
|
|
||||||
dbQuery = dbQuery.range(query.offset, query.offset + (query.limit || 10) - 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return await dbQuery;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function insertGpi(table: string, data: any) {
|
|
||||||
return await supabase.from(table).insert(data).select();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateGpi(table: string, id: string, data: any) {
|
|
||||||
return await supabase.from(table).update(data).eq('id', id).select();
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deleteGpi(table: string, id: string) {
|
|
||||||
return await supabase.from(table).delete().eq('id', id);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function findOneGpi(table: string, filters: Record<string, any>) {
|
|
||||||
let query = supabase.from(table).select('*');
|
|
||||||
|
|
||||||
Object.entries(filters).forEach(([key, value]) => {
|
|
||||||
query = query.eq(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data, error } = await query.single();
|
|
||||||
|
|
||||||
if (error && error.code !== 'PGRST116') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Supabase client initialized');
|
|
||||||
@@ -128,12 +128,8 @@ export const getProjectAnalysis = async (req: Request, res: Response) => {
|
|||||||
res.json(analysis);
|
res.json(analysis);
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('CRITICAL: Error in getProjectAnalysis controller:', error);
|
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({
|
res.status(500).json({ error: message });
|
||||||
error: message,
|
|
||||||
stack: error instanceof Error ? error.stack : undefined
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as appRecordService from '../services/applicationRecordService.js';
|
import * as appRecordService from '../services/applicationRecordService.js';
|
||||||
import '../middleware/authMiddleware.js'; // Ensure type augmentation
|
import '../middleware/roleMiddleware.js'; // Ensure type augmentation
|
||||||
|
|
||||||
export const createApplicationRecord = async (req: Request, res: Response) => {
|
export const createApplicationRecord = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const createdBy = req.appUser?.email || 'guest';
|
const createdBy = req.appUser?.externalId;
|
||||||
const record = await appRecordService.createApplicationRecord({ ...req.body, organizationId, createdBy });
|
const record = await appRecordService.createApplicationRecord({ ...req.body, organizationId, createdBy });
|
||||||
res.status(201).json(record);
|
res.status(201).json(record);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
@@ -29,10 +29,17 @@ export const getApplicationRecordsByProject = async (req: Request, res: Response
|
|||||||
export const updateApplicationRecord = async (req: Request, res: Response) => {
|
export const updateApplicationRecord = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const userId = req.appUser?.externalId;
|
||||||
|
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
||||||
|
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
const record = await appRecordService.updateApplicationRecord(
|
const record = await appRecordService.updateApplicationRecord(
|
||||||
req.params.id as string,
|
req.params.id as string,
|
||||||
req.body
|
req.body,
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
userRole as any,
|
||||||
|
isDeveloper
|
||||||
);
|
);
|
||||||
if (!record) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
if (!record) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||||
res.json(record);
|
res.json(record);
|
||||||
@@ -44,8 +51,17 @@ export const updateApplicationRecord = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const deleteApplicationRecord = async (req: Request, res: Response) => {
|
export const deleteApplicationRecord = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const userId = req.appUser?.externalId;
|
||||||
|
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
||||||
|
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
const success = await appRecordService.deleteApplicationRecord(
|
const success = await appRecordService.deleteApplicationRecord(
|
||||||
req.params.id as string
|
req.params.id as string,
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
userRole as any,
|
||||||
|
isDeveloper
|
||||||
);
|
);
|
||||||
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { query } from '../config/database.js';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_change_in_prod';
|
||||||
|
|
||||||
|
export const register = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { name, email, password } = req.body;
|
||||||
|
|
||||||
|
if (!name || !email || !password) {
|
||||||
|
res.status(400).json({ error: 'Todos os campos são obrigatórios' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingRes = await query('SELECT id FROM gpi.users WHERE email = $1', [email]);
|
||||||
|
if (existingRes.rows.length > 0) {
|
||||||
|
res.status(400).json({ error: 'Email já cadastrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const salt = await bcrypt.genSalt(10);
|
||||||
|
const passwordHash = await bcrypt.hash(password, salt);
|
||||||
|
|
||||||
|
// Gere um fakeAuthId para manter compatibilidade com sistemas que esperam um externalId
|
||||||
|
const fakeAuthId = `user_${uuidv4().replace(/-/g, '')}`;
|
||||||
|
|
||||||
|
const insertRes = await query(
|
||||||
|
`INSERT INTO gpi.users (name, email, clerk_id, role, is_banned, updated_at)
|
||||||
|
VALUES ($1, $2, $3, 'user', false, NOW()) RETURNING *`,
|
||||||
|
[name, email, fakeAuthId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const newUser = insertRes.rows[0];
|
||||||
|
|
||||||
|
// Se houver uma coluna de password_hash no banco (precisamos garantir que exista)
|
||||||
|
try {
|
||||||
|
await query('UPDATE gpi.users SET password_hash = $1 WHERE id = $2', [passwordHash, newUser.id]);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Could not update password_hash, check if column exists', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: newUser.id, externalId: newUser.clerk_id, role: newUser.role },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json({
|
||||||
|
message: 'Usuário criado com sucesso',
|
||||||
|
token,
|
||||||
|
user: { id: newUser.id, name: newUser.name, email: newUser.email, role: newUser.role, externalId: newUser.clerk_id }
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Register Error:', error);
|
||||||
|
res.status(500).json({ error: 'Erro no servidor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const login = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { email, password } = req.body;
|
||||||
|
|
||||||
|
if (!email || !password) {
|
||||||
|
res.status(400).json({ error: 'Email e senha são obrigatórios' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const userRes = await query('SELECT * FROM gpi.users WHERE email = $1', [email]);
|
||||||
|
const user = userRes.rows[0];
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
res.status(400).json({ error: 'Usuário não encontrado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recuperar password_hash de algum lugar se não estiver no select principal (alguns schemas escondem)
|
||||||
|
// No nosso caso a query SELECT * deve trazer se existir.
|
||||||
|
if (!user.password_hash) {
|
||||||
|
res.status(400).json({ error: 'Usuário sem senha definida ou método de login externo.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(password, user.password_hash);
|
||||||
|
if (!isMatch) {
|
||||||
|
res.status(400).json({ error: 'Credenciais inválidas' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = jwt.sign(
|
||||||
|
{ userId: user.id, externalId: user.clerk_id || user.logto_id, role: user.role },
|
||||||
|
JWT_SECRET,
|
||||||
|
{ expiresIn: '7d' }
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
message: 'Login realizado com sucesso',
|
||||||
|
token,
|
||||||
|
user: {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
role: user.role,
|
||||||
|
externalId: user.clerk_id || user.logto_id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Login Error:', error);
|
||||||
|
res.status(500).json({ error: 'Erro no servidor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMe = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
if (!req.appUser) {
|
||||||
|
res.status(401).json({ error: 'Não autorizado' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).json(req.appUser);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('GetMe Error:', error);
|
||||||
|
res.status(500).json({ error: 'Erro no servidor' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as dataSheetService from '../services/dataSheetService.js';
|
import * as dataSheetService from '../services/dataSheetService.js';
|
||||||
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
|
import fs from 'fs';
|
||||||
import { IAppUser } from '../middleware/authMiddleware.js';
|
import * as pdfExtractionService from '../services/pdfExtractionService.js';
|
||||||
|
import { IAppUser } from '../middleware/roleMiddleware.js';
|
||||||
|
import { notificationService } from '../services/notificationService.js';
|
||||||
|
import * as fileService from '../services/fileService.js';
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
interface AuthRequest extends Request {
|
||||||
appUser?: IAppUser;
|
appUser?: IAppUser;
|
||||||
@@ -11,9 +14,10 @@ export const getAllDataSheets = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const sheets = await dataSheetService.getAllDataSheets(organizationId);
|
const sheets = await dataSheetService.getAllDataSheets(organizationId);
|
||||||
res.json(toCamelCase(sheets));
|
res.json(sheets);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json([]);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -23,47 +27,195 @@ export const extractData = async (req: AuthRequest, res: Response) => {
|
|||||||
if (!file) {
|
if (!file) {
|
||||||
return res.status(400).json({ error: 'File is required' });
|
return res.status(400).json({ error: 'File is required' });
|
||||||
}
|
}
|
||||||
res.json({ extracted: true });
|
|
||||||
|
const fileBuffer = fs.readFileSync(file.path);
|
||||||
|
const data = await pdfExtractionService.extractDataFromPdf(fileBuffer);
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
...data,
|
||||||
|
tempFilePath: file.path
|
||||||
|
});
|
||||||
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json({ extracted: false });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createDataSheet = async (req: AuthRequest, res: Response) => {
|
export const createDataSheet = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
const file = req.file;
|
||||||
|
const {
|
||||||
|
name, manufacturer, type, solidsVolume, density,
|
||||||
|
mixingRatio, mixingRatioWeight, mixingRatioVolume,
|
||||||
|
yieldTheoretical, dftReference, yieldFactor,
|
||||||
|
wftMin, wftMax, dftMin, dftMax, reducer, dilution,
|
||||||
|
notes, fileUrl,
|
||||||
|
manufacturerCode, minStock, typicalApplication
|
||||||
|
} = req.body;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const payload = { ...req.body, organization_id: organizationId };
|
|
||||||
const newSheet = await dataSheetService.createDataSheet(toSnakeCase(payload));
|
let fileId: string | undefined = undefined;
|
||||||
res.status(201).json(toCamelCase(newSheet));
|
let finalFileUrl = fileUrl || '';
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const buffer = fs.readFileSync(file.path);
|
||||||
|
const savedFile = await fileService.saveFile(file.originalname, file.mimetype, buffer);
|
||||||
|
fileId = savedFile.id;
|
||||||
|
finalFileUrl = savedFile.id; // Using ID as reference
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(file.path);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to delete temp file:', file.path, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSheet = await dataSheetService.createDataSheet({
|
||||||
|
name,
|
||||||
|
manufacturer,
|
||||||
|
manufacturerCode,
|
||||||
|
type,
|
||||||
|
minStock: minStock ? Number(minStock) : undefined,
|
||||||
|
typicalApplication,
|
||||||
|
fileUrl: finalFileUrl,
|
||||||
|
fileId: fileId,
|
||||||
|
solidsVolume: solidsVolume ? Number(solidsVolume) : undefined,
|
||||||
|
density: density ? Number(density) : undefined,
|
||||||
|
mixingRatio,
|
||||||
|
mixingRatioWeight,
|
||||||
|
mixingRatioVolume,
|
||||||
|
yieldTheoretical: yieldTheoretical ? Number(yieldTheoretical) : undefined,
|
||||||
|
dftReference: dftReference ? Number(dftReference) : undefined,
|
||||||
|
yieldFactor: yieldFactor ? Number(yieldFactor) : undefined,
|
||||||
|
wftMin: wftMin ? Number(wftMin) : undefined,
|
||||||
|
wftMax: wftMax ? Number(wftMax) : undefined,
|
||||||
|
dftMin: dftMin ? Number(dftMin) : undefined,
|
||||||
|
dftMax: dftMax ? Number(dftMax) : undefined,
|
||||||
|
reducer,
|
||||||
|
dilution: dilution ? Number(dilution) : undefined,
|
||||||
|
notes,
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
await notificationService.create({
|
||||||
|
organizationId,
|
||||||
|
title: 'Nova Ficha Técnica',
|
||||||
|
message: `A ficha técnica "${name}" (${manufacturer}) foi adicionada à biblioteca.`,
|
||||||
|
type: 'info',
|
||||||
|
metadata: { dataSheetId: newSheet.id, triggerType: 'datasheet_created' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(newSheet);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(400).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
console.error('Error creating datasheet:', error);
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteDataSheet = async (req: AuthRequest, res: Response) => {
|
export const deleteDataSheet = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
await dataSheetService.deleteDataSheet(id as string);
|
const organizationId = req.appUser?.organizationId;
|
||||||
res.status(204).send();
|
const success = await dataSheetService.deleteDataSheet(id as string, organizationId);
|
||||||
|
if (success) {
|
||||||
|
res.status(204).send();
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'Data sheet not found' });
|
||||||
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateDataSheet = async (req: AuthRequest, res: Response) => {
|
export const updateDataSheet = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const id = req.params.id as string;
|
const id = req.params.id as string;
|
||||||
const updatedSheet = await dataSheetService.updateDataSheet(id, toSnakeCase(req.body));
|
const file = req.file;
|
||||||
res.json(toCamelCase(updatedSheet || req.body));
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const {
|
||||||
|
name, manufacturer, type, solidsVolume, density,
|
||||||
|
mixingRatio, mixingRatioWeight, mixingRatioVolume,
|
||||||
|
yieldTheoretical, dftReference, yieldFactor,
|
||||||
|
wftMin, wftMax, dftMin, dftMax, reducer, dilution,
|
||||||
|
notes, fileUrl
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const updates: any = {
|
||||||
|
name,
|
||||||
|
manufacturer,
|
||||||
|
type,
|
||||||
|
notes,
|
||||||
|
solidsVolume: solidsVolume ? Number(solidsVolume) : undefined,
|
||||||
|
density: density ? Number(density) : undefined,
|
||||||
|
yieldTheoretical: yieldTheoretical ? Number(yieldTheoretical) : undefined,
|
||||||
|
dftReference: dftReference ? Number(dftReference) : undefined,
|
||||||
|
yieldFactor: yieldFactor ? Number(yieldFactor) : undefined,
|
||||||
|
wftMin: wftMin ? Number(wftMin) : undefined,
|
||||||
|
wftMax: wftMax ? Number(wftMax) : undefined,
|
||||||
|
dftMin: dftMin ? Number(dftMin) : undefined,
|
||||||
|
dftMax: dftMax ? Number(dftMax) : undefined,
|
||||||
|
reducer,
|
||||||
|
dilution: dilution ? Number(dilution) : undefined,
|
||||||
|
mixingRatio,
|
||||||
|
mixingRatioWeight,
|
||||||
|
mixingRatioVolume
|
||||||
|
};
|
||||||
|
|
||||||
|
if (file) {
|
||||||
|
const buffer = fs.readFileSync(file.path);
|
||||||
|
const savedFile = await fileService.saveFile(file.originalname, file.mimetype, buffer);
|
||||||
|
updates.fileId = savedFile.id;
|
||||||
|
updates.fileUrl = savedFile.id;
|
||||||
|
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(file.path);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('Failed to delete temp file:', file.path, error);
|
||||||
|
}
|
||||||
|
} else if (fileUrl) {
|
||||||
|
updates.fileUrl = String(fileUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const updatedSheet = await dataSheetService.updateDataSheet(id, updates, organizationId);
|
||||||
|
|
||||||
|
if (updatedSheet) {
|
||||||
|
res.json(updatedSheet);
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'Data sheet not found' });
|
||||||
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(400).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
console.error('Error updating datasheet:', error);
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getFile = async (req: AuthRequest, res: Response) => {
|
export const getFile = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
const id = req.params.id as string;
|
||||||
|
|
||||||
|
// Check if it's a UUID
|
||||||
|
if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-5][0-9a-f]{3}-[089ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(id)) {
|
||||||
|
const fileDoc = await fileService.getFileById(id);
|
||||||
|
|
||||||
|
if (fileDoc) {
|
||||||
|
res.set('Content-Type', fileDoc.content_type || 'application/pdf');
|
||||||
|
res.set('Content-Disposition', `inline; filename="${fileDoc.filename}"`);
|
||||||
|
res.set('Access-Control-Allow-Origin', '*');
|
||||||
|
res.set('Cache-Control', 'public, max-age=3600');
|
||||||
|
return res.send(fileDoc.data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
res.status(404).json({ error: 'File not found' });
|
res.status(404).json({ error: 'File not found' });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: 'File not found' });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
console.error('Error getting file:', error);
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { supabase } from '../config/supabase.js';
|
import { query } from '../config/database.js';
|
||||||
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
|
|
||||||
|
|
||||||
|
// Default geometry types to seed if none exist
|
||||||
const DEFAULT_TYPES = [
|
const DEFAULT_TYPES = [
|
||||||
{ name: 'Guarda-corpo/escada', efficiencyLoss: 20 },
|
{ name: 'Guarda-corpo/escada', efficiencyLoss: 20 },
|
||||||
{ name: 'Vigas leves', efficiencyLoss: 20 },
|
{ name: 'Vigas leves', efficiencyLoss: 20 },
|
||||||
@@ -19,61 +19,108 @@ const DEFAULT_TYPES = [
|
|||||||
|
|
||||||
export const getAllnames = async (req: Request, res: Response) => {
|
export const getAllnames = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.from('geometry_types').select('*');
|
const organizationId = req.appUser?.organizationId;
|
||||||
if (error && error.code !== '42P01') throw error;
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
res.json(toCamelCase(data || []));
|
|
||||||
} catch (error: unknown) {
|
if (!organizationId && !isGlobalAdmin) {
|
||||||
res.json(DEFAULT_TYPES);
|
return res.status(400).json({ error: 'Organization ID missing' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let sql = 'SELECT * FROM gpi.geometry_types';
|
||||||
|
const params: any[] = [];
|
||||||
|
|
||||||
|
if (!isGlobalAdmin) {
|
||||||
|
sql += ' WHERE organization_id = $1';
|
||||||
|
params.push(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ' ORDER BY name ASC';
|
||||||
|
const result = await query(sql, params);
|
||||||
|
let types = result.rows;
|
||||||
|
|
||||||
|
// Auto-seed if empty for org
|
||||||
|
if (types.length === 0 && organizationId && !isGlobalAdmin) {
|
||||||
|
for (const t of DEFAULT_TYPES) {
|
||||||
|
await query(
|
||||||
|
'INSERT INTO gpi.geometry_types (organization_id, name, efficiency_loss) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING',
|
||||||
|
[organizationId, t.name, t.efficiencyLoss]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const reFetch = await query('SELECT * FROM gpi.geometry_types WHERE organization_id = $1 ORDER BY name ASC', [organizationId]);
|
||||||
|
types = reFetch.rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(types);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[GeometryType] Error:', error);
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const restoreDefaults = async (req: Request, res: Response) => {
|
export const restoreDefaults = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
res.json(DEFAULT_TYPES);
|
const organizationId = req.appUser?.organizationId;
|
||||||
} catch (error: unknown) {
|
if (!organizationId) return res.status(400).json({ error: 'Organization ID missing' });
|
||||||
res.json(DEFAULT_TYPES);
|
|
||||||
|
await query('DELETE FROM gpi.geometry_types WHERE organization_id = $1', [organizationId]);
|
||||||
|
|
||||||
|
for (const t of DEFAULT_TYPES) {
|
||||||
|
await query(
|
||||||
|
'INSERT INTO gpi.geometry_types (organization_id, name, efficiency_loss) VALUES ($1, $2, $3)',
|
||||||
|
[organizationId, t.name, t.efficiencyLoss]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query('SELECT * FROM gpi.geometry_types WHERE organization_id = $1 ORDER BY name ASC', [organizationId]);
|
||||||
|
res.json(result.rows);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createType = async (req: Request, res: Response) => {
|
export const createType = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const payload = toSnakeCase({
|
const organizationId = req.appUser?.organizationId;
|
||||||
...req.body,
|
const { name, efficiencyLoss } = req.body;
|
||||||
organizationId: (req as any).appUser?.organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const result = await query(
|
||||||
.from('geometry_types')
|
'INSERT INTO gpi.geometry_types (organization_id, name, efficiency_loss) VALUES ($1, $2, $3) RETURNING *',
|
||||||
.insert(payload)
|
[organizationId, name, Number(efficiencyLoss) || 0]
|
||||||
.select()
|
);
|
||||||
.single();
|
res.status(201).json(result.rows[0]);
|
||||||
if (error) throw error;
|
} catch (error: any) {
|
||||||
res.status(201).json(toCamelCase(data));
|
if (error.code === '23505') return res.status(409).json({ error: 'Already exists' });
|
||||||
} catch (error: unknown) {
|
res.status(500).json({ error: error.message });
|
||||||
res.json(req.body);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateType = async (req: Request, res: Response) => {
|
export const updateType = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const { id } = req.params;
|
||||||
.from('geometry_types')
|
const organizationId = req.appUser?.organizationId;
|
||||||
.update(toSnakeCase(req.body))
|
const { name, efficiencyLoss } = req.body;
|
||||||
.eq('id', req.params.id)
|
|
||||||
.select()
|
const result = await query(
|
||||||
.single();
|
'UPDATE gpi.geometry_types SET name = $1, efficiency_loss = $2, updated_at = NOW() WHERE id = $3 AND organization_id = $4 RETURNING *',
|
||||||
if (error) throw error;
|
[name, Number(efficiencyLoss), id, organizationId]
|
||||||
res.json(toCamelCase(data));
|
);
|
||||||
} catch (error: unknown) {
|
|
||||||
res.json(req.body);
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Not found' });
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteType = async (req: Request, res: Response) => {
|
export const deleteType = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await supabase.from('geometry_types').delete().eq('id', req.params.id);
|
const { id } = req.params;
|
||||||
res.status(204).send();
|
const organizationId = req.appUser?.organizationId;
|
||||||
} catch (error: unknown) {
|
|
||||||
|
const result = await query('DELETE FROM gpi.geometry_types WHERE id = $1 AND organization_id = $2', [id, organizationId]);
|
||||||
|
if (result.rowCount === 0) return res.status(404).json({ error: 'Not found' });
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
|
} catch (error: any) {
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,34 +1,30 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as inspectionService from '../services/inspectionService.js';
|
import * as inspectionService from '../services/inspectionService.js';
|
||||||
import { notificationService } from '../services/notificationService.js';
|
import { notificationService } from '../services/notificationService.js';
|
||||||
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
|
import * as fileService from '../services/fileService.js';
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
export const createInspection = async (req: Request, res: Response) => {
|
export const createInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const createdBy = req.appUser?.email || 'guest';
|
const createdBy = req.appUser?.externalId;
|
||||||
|
const inspection = await inspectionService.createInspection({
|
||||||
const payload = toSnakeCase({
|
|
||||||
...req.body,
|
...req.body,
|
||||||
organizationId,
|
organizationId,
|
||||||
createdBy
|
createdBy
|
||||||
});
|
});
|
||||||
|
|
||||||
const inspection = await inspectionService.createInspection(payload);
|
|
||||||
|
|
||||||
if (req.body.appearance === 'rejected' && organizationId) {
|
if (req.body.appearance === 'rejected' && organizationId) {
|
||||||
try {
|
await notificationService.create({
|
||||||
await notificationService.create({
|
organizationId,
|
||||||
organizationId,
|
title: 'Inspeção Reprovada',
|
||||||
title: 'Inspeção Reprovada',
|
message: `Uma inspeção foi reprovada na obra (ID: ${req.body.projectId}).`,
|
||||||
message: `Uma inspeção foi reprovada na obra (ID: ${req.body.projectId}).`,
|
type: 'error',
|
||||||
type: 'error',
|
metadata: { inspectionId: inspection.id, projectId: req.body.projectId, triggerType: 'inspection_rejected' }
|
||||||
metadata: { inspectionId: inspection?.id, projectId: req.body.projectId, triggerType: 'inspection_rejected' }
|
});
|
||||||
});
|
|
||||||
} catch (e) { /* ignore notification errors */ }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(201).json(toCamelCase(inspection));
|
res.status(201).json(inspection);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
@@ -40,20 +36,30 @@ export const getInspectionsByProject = async (req: Request, res: Response) => {
|
|||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const inspections = await inspectionService.getInspectionsByProject(projectId as string, organizationId);
|
const inspections = await inspectionService.getInspectionsByProject(projectId as string, organizationId);
|
||||||
res.json(toCamelCase(inspections || []));
|
res.json(inspections);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json([]);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateInspection = async (req: Request, res: Response) => {
|
export const updateInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const userId = req.appUser?.externalId;
|
||||||
|
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
||||||
|
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
const inspection = await inspectionService.updateInspection(
|
const inspection = await inspectionService.updateInspection(
|
||||||
req.params.id as string,
|
req.params.id as string,
|
||||||
toSnakeCase(req.body)
|
req.body,
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
userRole as any,
|
||||||
|
isDeveloper
|
||||||
);
|
);
|
||||||
if (!inspection) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
if (!inspection) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||||
res.json(toCamelCase(inspection));
|
res.json(inspection);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
@@ -62,22 +68,34 @@ export const updateInspection = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const deleteInspection = async (req: Request, res: Response) => {
|
export const deleteInspection = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await inspectionService.deleteInspection(req.params.id as string);
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const userId = req.appUser?.externalId;
|
||||||
|
const userRole = req.appUser?.organizationRole || req.appUser?.role;
|
||||||
|
const isDeveloper = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
|
||||||
|
const success = await inspectionService.deleteInspection(
|
||||||
|
req.params.id as string,
|
||||||
|
organizationId,
|
||||||
|
userId,
|
||||||
|
userRole as any,
|
||||||
|
isDeveloper
|
||||||
|
);
|
||||||
|
if (!success) return res.status(403).json({ error: 'Não autorizado ou não encontrado.' });
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(204).send();
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllInspections = async (req: Request, res: Response) => {
|
export const getAllInspections = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const inspections = organizationId
|
const inspections = await inspectionService.getAllInspections(organizationId);
|
||||||
? await inspectionService.getInspectionsByOrganization(organizationId)
|
res.json(inspections);
|
||||||
: await inspectionService.getInspectionStats();
|
|
||||||
res.json(toCamelCase(inspections || []));
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json({ total: 0, inspections: [] });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -86,7 +104,17 @@ export const uploadPhoto = async (req: Request, res: Response) => {
|
|||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).json({ error: 'No file uploaded' });
|
return res.status(400).json({ error: 'No file uploaded' });
|
||||||
}
|
}
|
||||||
const fileUrl = `/uploads/${req.file.filename}`;
|
|
||||||
|
const buffer = fs.readFileSync(req.file.path);
|
||||||
|
const savedFile = await fileService.saveFile(req.file.originalname, req.file.mimetype, buffer);
|
||||||
|
|
||||||
|
// Clean up temp file
|
||||||
|
fs.unlinkSync(req.file.path);
|
||||||
|
|
||||||
|
// We'll use /api/datasheets/file/:id to serve these if we use the same endpoint
|
||||||
|
// or create a new general /api/files/:id
|
||||||
|
const fileUrl = `/api/datasheets/file/${savedFile.id}`;
|
||||||
|
|
||||||
res.json({ url: fileUrl });
|
res.json({ url: fileUrl });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
|||||||
@@ -1,50 +1,59 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { supabase } from '../config/supabase.js';
|
import * as instrumentService from '../services/instrumentService.js';
|
||||||
|
import { IAppUser } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
export const createInstrument = async (req: Request, res: Response) => {
|
interface AuthRequest extends Request {
|
||||||
|
appUser?: IAppUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createInstrument = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const organizationId = req.appUser?.organizationId;
|
||||||
.from('instruments')
|
const instrument = await instrumentService.createInstrument({
|
||||||
.insert({ ...req.body, organization_id: req.appUser?.organizationId })
|
...req.body,
|
||||||
.select()
|
organizationId
|
||||||
.single();
|
});
|
||||||
if (error) throw error;
|
res.status(201).json(instrument);
|
||||||
res.status(201).json(data);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getInstruments = async (req: Request, res: Response) => {
|
export const getInstruments = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.from('instruments').select('*');
|
const organizationId = req.appUser?.organizationId;
|
||||||
if (error && error.code !== '42P01') throw error;
|
const { status } = req.query;
|
||||||
res.json(data || []);
|
const instruments = await instrumentService.getInstruments(organizationId, status as string);
|
||||||
|
res.json(instruments);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json([]);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateInstrument = async (req: Request, res: Response) => {
|
export const updateInstrument = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const { id } = req.params;
|
||||||
.from('instruments')
|
const organizationId = req.appUser?.organizationId;
|
||||||
.update(req.body)
|
const instrument = await instrumentService.updateInstrument(id, req.body, organizationId);
|
||||||
.eq('id', req.params.id)
|
if (!instrument) return res.status(404).json({ error: 'Instrumento não encontrado.' });
|
||||||
.select()
|
res.json(instrument);
|
||||||
.single();
|
|
||||||
if (error) throw error;
|
|
||||||
res.json(data);
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json(req.body);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteInstrument = async (req: Request, res: Response) => {
|
export const deleteInstrument = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await supabase.from('instruments').delete().eq('id', req.params.id);
|
const { id } = req.params;
|
||||||
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const success = await instrumentService.deleteInstrument(id, organizationId);
|
||||||
|
if (!success) return res.status(404).json({ error: 'Instrumento não encontrado.' });
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(204).send();
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,50 +1,47 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { supabase } from '../config/supabase.js';
|
import { query } from '../config/database.js';
|
||||||
|
|
||||||
const TABLE_NOT_FOUND_CODES = ['42P01', 'PGRST116'];
|
|
||||||
|
|
||||||
const safeSupabaseQuery = async (table: string, query: any) => {
|
|
||||||
try {
|
|
||||||
return await query;
|
|
||||||
} catch (error: any) {
|
|
||||||
if (error.code && TABLE_NOT_FOUND_CODES.includes(error.code)) {
|
|
||||||
console.log(`Table ${table} not found, returning empty result`);
|
|
||||||
return { data: [], error: null };
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send a message
|
// Send a message
|
||||||
export const sendMessage = async (req: Request, res: Response) => {
|
export const sendMessage = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { toUserId, message } = req.body;
|
const { toUserId, message } = req.body;
|
||||||
const fromUserId = req.appUser?.id;
|
const fromUserId = req.appUser?.externalId;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
return res.status(400).json({ error: 'Organização não selecionada.' });
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!fromUserId) {
|
||||||
|
return res.status(401).json({ error: 'Usuário não autenticado.' });
|
||||||
|
}
|
||||||
|
|
||||||
if (!toUserId || !message) {
|
if (!toUserId || !message) {
|
||||||
return res.status(400).json({ error: 'Destinatário e mensagem são obrigatórios.' });
|
return res.status(400).json({ error: 'Destinatário e mensagem são obrigatórios.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await supabase
|
// Check if there's already a pending (unread) message from this user to that user
|
||||||
.from('messages')
|
const existingRes = await query(
|
||||||
.insert({
|
'SELECT * FROM gpi.messages WHERE organization_id = $1 AND from_user_id = $2 AND to_user_id = $3 AND is_read = false',
|
||||||
organization_id: organizationId,
|
[organizationId, fromUserId, toUserId]
|
||||||
from_user_id: fromUserId,
|
);
|
||||||
to_user_id: toUserId,
|
|
||||||
message,
|
|
||||||
is_read: false
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (existingRes.rows.length > 0) {
|
||||||
res.status(201).json(data);
|
const updatedRes = await query(
|
||||||
} catch (error: any) {
|
'UPDATE gpi.messages SET message = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||||
|
[message, existingRes.rows[0].id]
|
||||||
|
);
|
||||||
|
return res.json(updatedRes.rows[0]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertRes = await query(
|
||||||
|
`INSERT INTO gpi.messages (organization_id, from_user_id, to_user_id, message)
|
||||||
|
VALUES ($1, $2, $3, $4) RETURNING *`,
|
||||||
|
[organizationId, fromUserId, toUserId, message]
|
||||||
|
);
|
||||||
|
|
||||||
|
res.status(201).json(insertRes.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
console.error('Error sending message:', error);
|
console.error('Error sending message:', error);
|
||||||
res.status(500).json({ error: 'Erro ao enviar mensagem.' });
|
res.status(500).json({ error: 'Erro ao enviar mensagem.' });
|
||||||
}
|
}
|
||||||
@@ -53,24 +50,29 @@ export const sendMessage = async (req: Request, res: Response) => {
|
|||||||
// Get unread messages for current user
|
// Get unread messages for current user
|
||||||
export const getUnreadMessages = async (req: Request, res: Response) => {
|
export const getUnreadMessages = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const toUserId = req.appUser?.id;
|
const toUserId = req.appUser?.externalId;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId || !toUserId) {
|
||||||
return res.status(400).json({ error: 'Organização não selecionada.' });
|
return res.status(400).json({ error: 'Contexto incompleto.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const sql = `
|
||||||
.from('messages')
|
SELECT m.*, u.name as "fromUserName", u.email as "fromUserEmail"
|
||||||
.select('*')
|
FROM gpi.messages m
|
||||||
.eq('organization_id', organizationId)
|
LEFT JOIN gpi.users u ON m.from_user_id = u.clerk_id OR m.from_user_id = u.logto_id
|
||||||
.eq('to_user_id', toUserId)
|
WHERE m.organization_id = $1 AND m.to_user_id = $2 AND m.is_read = false AND m.is_archived = false AND m.is_deleted_by_recipient = false
|
||||||
.eq('is_read', false)
|
ORDER BY m.created_at DESC
|
||||||
.eq('is_archived', false);
|
`;
|
||||||
|
const result = await query(sql, [organizationId, toUserId]);
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
const messages = result.rows.map(m => ({
|
||||||
res.json(data || []);
|
...m,
|
||||||
} catch (error: any) {
|
fromUser: { name: m.fromUserName, email: m.fromUserEmail }
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(messages);
|
||||||
|
} catch (error) {
|
||||||
console.error('Error getting unread messages:', error);
|
console.error('Error getting unread messages:', error);
|
||||||
res.status(500).json({ error: 'Erro ao buscar mensagens.' });
|
res.status(500).json({ error: 'Erro ao buscar mensagens.' });
|
||||||
}
|
}
|
||||||
@@ -80,21 +82,20 @@ export const getUnreadMessages = async (req: Request, res: Response) => {
|
|||||||
export const markMessageAsRead = async (req: Request, res: Response) => {
|
export const markMessageAsRead = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.id;
|
const userId = req.appUser?.externalId;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const result = await query(
|
||||||
.from('messages')
|
'UPDATE gpi.messages SET is_read = true, read_at = NOW() WHERE id = $1 AND organization_id = $2 AND to_user_id = $3 RETURNING *',
|
||||||
.update({ is_read: true, read_at: new Date().toISOString() })
|
[id, organizationId, userId]
|
||||||
.eq('id', id)
|
);
|
||||||
.eq('to_user_id', userId)
|
|
||||||
.eq('organization_id', organizationId)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (result.rows.length === 0) {
|
||||||
res.json(data);
|
return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
||||||
} catch (error: any) {
|
}
|
||||||
|
|
||||||
|
res.json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
console.error('Error marking message as read:', error);
|
console.error('Error marking message as read:', error);
|
||||||
res.status(500).json({ error: 'Erro ao marcar mensagem como lida.' });
|
res.status(500).json({ error: 'Erro ao marcar mensagem como lida.' });
|
||||||
}
|
}
|
||||||
@@ -103,42 +104,52 @@ export const markMessageAsRead = async (req: Request, res: Response) => {
|
|||||||
// Get my pending (unread) sent messages
|
// Get my pending (unread) sent messages
|
||||||
export const getMyPendingMessages = async (req: Request, res: Response) => {
|
export const getMyPendingMessages = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const fromUserId = req.appUser?.id;
|
const fromUserId = req.appUser?.externalId;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
if (!organizationId || !fromUserId) {
|
||||||
.from('messages')
|
return res.status(400).json({ error: 'Contexto incompleto.' });
|
||||||
.select('*')
|
}
|
||||||
.eq('organization_id', organizationId)
|
|
||||||
.eq('from_user_id', fromUserId)
|
|
||||||
.eq('is_read', false);
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
const sql = `
|
||||||
res.json(data || []);
|
SELECT m.*, u.name as "toUserName", u.email as "toUserEmail"
|
||||||
} catch (error: any) {
|
FROM gpi.messages m
|
||||||
|
LEFT JOIN gpi.users u ON m.to_user_id = u.clerk_id OR m.to_user_id = u.logto_id
|
||||||
|
WHERE m.organization_id = $1 AND m.from_user_id = $2 AND m.is_read = false
|
||||||
|
ORDER BY m.created_at DESC
|
||||||
|
`;
|
||||||
|
const result = await query(sql, [organizationId, fromUserId]);
|
||||||
|
|
||||||
|
const messages = result.rows.map(m => ({
|
||||||
|
...m,
|
||||||
|
toUser: { name: m.toUserName, email: m.toUserEmail }
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json(messages);
|
||||||
|
} catch (error) {
|
||||||
console.error('Error getting pending messages:', error);
|
console.error('Error getting pending messages:', error);
|
||||||
res.status(500).json({ error: 'Erro ao buscar mensagens pendentes.' });
|
res.status(500).json({ error: 'Erro ao buscar mensagens pendentes.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Delete a message
|
// Delete a message (only if unread and sender is the current user)
|
||||||
export const deleteMessage = async (req: Request, res: Response) => {
|
export const deleteMessage = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.id;
|
const userId = req.appUser?.externalId;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const { error } = await supabase
|
const result = await query(
|
||||||
.from('messages')
|
'DELETE FROM gpi.messages WHERE id = $1 AND from_user_id = $2 AND organization_id = $3 AND is_read = false',
|
||||||
.delete()
|
[id, userId, organizationId]
|
||||||
.eq('id', id)
|
);
|
||||||
.eq('from_user_id', userId)
|
|
||||||
.eq('organization_id', organizationId)
|
|
||||||
.eq('is_read', false);
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (result.rowCount === 0) {
|
||||||
res.status(204).send();
|
return res.status(404).json({ error: 'Mensagem não encontrada ou já lida.' });
|
||||||
} catch (error: any) {
|
}
|
||||||
|
|
||||||
|
res.json({ message: 'Mensagem deletada com sucesso.' });
|
||||||
|
} catch (error) {
|
||||||
console.error('Error deleting message:', error);
|
console.error('Error deleting message:', error);
|
||||||
res.status(500).json({ error: 'Erro ao deletar mensagem.' });
|
res.status(500).json({ error: 'Erro ao deletar mensagem.' });
|
||||||
}
|
}
|
||||||
@@ -148,21 +159,18 @@ export const deleteMessage = async (req: Request, res: Response) => {
|
|||||||
export const archiveMessage = async (req: Request, res: Response) => {
|
export const archiveMessage = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.id;
|
const userId = req.appUser?.externalId;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const result = await query(
|
||||||
.from('messages')
|
'UPDATE gpi.messages SET is_archived = true, is_read = true WHERE id = $1 AND to_user_id = $2 AND organization_id = $3 RETURNING *',
|
||||||
.update({ is_archived: true, is_read: true })
|
[id, userId, organizationId]
|
||||||
.eq('id', id)
|
);
|
||||||
.eq('to_user_id', userId)
|
|
||||||
.eq('organization_id', organizationId)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
||||||
res.json(data);
|
|
||||||
} catch (error: any) {
|
res.json(result.rows[0]);
|
||||||
|
} catch (error) {
|
||||||
console.error('Error archiving message:', error);
|
console.error('Error archiving message:', error);
|
||||||
res.status(500).json({ error: 'Erro ao arquivar mensagem.' });
|
res.status(500).json({ error: 'Erro ao arquivar mensagem.' });
|
||||||
}
|
}
|
||||||
@@ -171,21 +179,18 @@ export const archiveMessage = async (req: Request, res: Response) => {
|
|||||||
export const recipientDeleteMessage = async (req: Request, res: Response) => {
|
export const recipientDeleteMessage = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const userId = req.appUser?.id;
|
const userId = req.appUser?.externalId;
|
||||||
const organizationId = req.headers['x-organization-id'] as string;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const result = await query(
|
||||||
.from('messages')
|
'UPDATE gpi.messages SET is_deleted_by_recipient = true WHERE id = $1 AND to_user_id = $2 AND organization_id = $3 RETURNING *',
|
||||||
.update({ 'is_deleted_by_recipient': true })
|
[id, userId, organizationId]
|
||||||
.eq('id', id)
|
);
|
||||||
.eq('to_user_id', userId)
|
|
||||||
.eq('organization_id', organizationId)
|
if (result.rows.length === 0) return res.status(404).json({ error: 'Mensagem não encontrada.' });
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
res.json({ message: 'Mensagem excluída com sucesso.' });
|
res.json({ message: 'Mensagem excluída com sucesso.' });
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error('Error deleting message:', error);
|
console.error('Error deleting message:', error);
|
||||||
res.status(500).json({ error: 'Erro ao excluir mensagem.' });
|
res.status(500).json({ error: 'Erro ao excluir mensagem.' });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,116 +1,97 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { supabase } from '../config/supabase.js';
|
import { notificationService } from '../services/notificationService.js';
|
||||||
|
|
||||||
export const notificationController = {
|
export const notificationController = {
|
||||||
getUserNotifications: async (req: Request, res: Response) => {
|
getUserNotifications: async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.headers['x-organization-id'] as string || req.appUser?.organizationId;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
const userId = req.headers['x-user-id'] as string; // Assumindo que temos o ID do usuário (externalId ou email)
|
||||||
|
|
||||||
|
// Se não tiver userId no header (ainda não implementado auth full), tentar pegar do query ou usar um fallback
|
||||||
|
// Nota: Idealmente o middleware de auth popula req.user. Vamos assumir que passamos x-user-id no frontend por enquanto.
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
return res.json([]); // Return empty instead of error
|
return res.status(400).json({ error: 'Organization ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: notifications, error } = await supabase
|
const notifications = await notificationService.getUserNotifications(
|
||||||
.from('notifications')
|
userId,
|
||||||
.select('*')
|
organizationId,
|
||||||
.eq('organization_id', organizationId)
|
req.query.includeArchived === 'true'
|
||||||
.order('created_at', { ascending: false });
|
);
|
||||||
|
res.json(notifications);
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json(notifications || []);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.json([]);
|
res.status(500).json({ error: 'Error fetching notifications' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
markAsRead: async (req: Request, res: Response) => {
|
markAsRead: async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { error } = await supabase
|
const notification = await notificationService.markAsRead(id as string);
|
||||||
.from('notifications')
|
res.json(notification);
|
||||||
.update({ is_read: true })
|
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.json({ success: true });
|
res.status(500).json({ error: 'Error marking notification as read' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
markAllAsRead: async (req: Request, res: Response) => {
|
markAllAsRead: async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.headers['x-organization-id'] as string || req.appUser?.organizationId;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
const userId = req.headers['x-user-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
return res.json({ success: true });
|
return res.status(400).json({ error: 'Organization ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await supabase
|
await notificationService.markAllAsRead(userId, organizationId);
|
||||||
.from('notifications')
|
|
||||||
.update({ is_read: true })
|
|
||||||
.eq('organization_id', organizationId);
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.json({ success: true });
|
res.status(500).json({ error: 'Error marking all as read' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
clearAll: async (req: Request, res: Response) => {
|
clearAll: async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.headers['x-organization-id'] as string || req.appUser?.organizationId;
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
const userId = req.headers['x-user-id'] as string;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
return res.json({ success: true });
|
return res.status(400).json({ error: 'Organization ID is required' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await supabase
|
await notificationService.clearAll(userId, organizationId);
|
||||||
.from('notifications')
|
|
||||||
.delete()
|
|
||||||
.eq('organization_id', organizationId);
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.json({ success: true });
|
res.status(500).json({ error: 'Error clearing all notifications' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
archive: async (req: Request, res: Response) => {
|
archive: async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { error } = await supabase
|
const userId = req.headers['x-user-id'] as string;
|
||||||
.from('notifications')
|
const notification = await notificationService.archive(id as string, userId);
|
||||||
.update({ is_archived: true })
|
res.json(notification);
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json({ success: true });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.json({ success: true });
|
res.status(500).json({ error: 'Error archiving notification' });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
delete: async (req: Request, res: Response) => {
|
delete: async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { error } = await supabase
|
const userId = req.headers['x-user-id'] as string;
|
||||||
.from('notifications')
|
await notificationService.softDelete(id as string, userId);
|
||||||
.delete()
|
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.json({ success: true });
|
res.status(500).json({ error: 'Error deleting notification' });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,43 +1,63 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as paintingSchemeService from '../services/paintingSchemeService.js';
|
import * as paintingSchemeService from '../services/paintingSchemeService.js';
|
||||||
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
|
|
||||||
|
|
||||||
export const createPaintingScheme = async (req: Request, res: Response) => {
|
export const createPaintingScheme = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const schemeData = toSnakeCase({ ...req.body, organizationId });
|
console.log("Creating scheme with payload:", req.body);
|
||||||
const scheme = await paintingSchemeService.createPaintingScheme(schemeData);
|
const scheme = await paintingSchemeService.createPaintingScheme({ ...req.body, organizationId });
|
||||||
res.status(201).json(toCamelCase(scheme));
|
console.log("Created scheme result:", scheme);
|
||||||
|
res.status(201).json(scheme);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(400).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPaintingSchemesByProject = async (req: Request, res: Response) => {
|
export const getPaintingSchemesByProject = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { projectId } = req.params;
|
const { projectId } = req.params;
|
||||||
const schemes = await paintingSchemeService.getPaintingSchemesByProject(projectId as string);
|
const organizationId = req.appUser?.organizationId;
|
||||||
res.json(toCamelCase(schemes || []));
|
const schemes = await paintingSchemeService.getPaintingSchemesByProject(projectId as string, organizationId);
|
||||||
|
res.json(schemes);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json([]);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export const updatePaintingScheme = async (req: Request, res: Response) => {
|
export const updatePaintingScheme = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const scheme = await paintingSchemeService.updatePaintingScheme(req.params.id as string, toSnakeCase(req.body));
|
const organizationId = req.appUser?.organizationId;
|
||||||
res.json(toCamelCase(scheme || req.body));
|
console.log("---------------------------------------------------");
|
||||||
|
console.log(`UPDATE REQUEST: ID=${req.params.id}`);
|
||||||
|
console.log(`User Org ID: ${organizationId}`);
|
||||||
|
console.log(`Payload keys: ${Object.keys(req.body)}`);
|
||||||
|
|
||||||
|
const scheme = await paintingSchemeService.updatePaintingScheme(req.params.id as string, req.body, organizationId);
|
||||||
|
|
||||||
|
console.log(`UPDATE RESULT: ${scheme ? 'SUCCESS' : 'NULL (Doc not found or not matched)'}`);
|
||||||
|
if (scheme) {
|
||||||
|
console.log(`Updated Doc Coat: ${scheme.coat}`);
|
||||||
|
}
|
||||||
|
console.log("---------------------------------------------------");
|
||||||
|
|
||||||
|
res.json(scheme);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(400).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deletePaintingScheme = async (req: Request, res: Response) => {
|
export const deletePaintingScheme = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await paintingSchemeService.deletePaintingScheme(req.params.id as string);
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
await paintingSchemeService.deletePaintingScheme(req.params.id as string, organizationId);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,8 +65,9 @@ export const getAllPaintingSchemes = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const schemes = await paintingSchemeService.getAllSchemes(organizationId);
|
const schemes = await paintingSchemeService.getAllSchemes(organizationId);
|
||||||
res.json(toCamelCase(schemes || []));
|
res.json(schemes);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json([]);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as partService from '../services/partService.js';
|
import * as partService from '../services/partService.js';
|
||||||
import { IAppUser } from '../middleware/authMiddleware.js';
|
import { IAppUser } from '../middleware/roleMiddleware.js';
|
||||||
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
|
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
interface AuthRequest extends Request {
|
||||||
appUser?: IAppUser;
|
appUser?: IAppUser;
|
||||||
@@ -9,12 +8,14 @@ interface AuthRequest extends Request {
|
|||||||
|
|
||||||
export const createPart = async (req: AuthRequest, res: Response) => {
|
export const createPart = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
console.log('[CREATE PART] Received data:', JSON.stringify(req.body, null, 2));
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const payload = toSnakeCase({ ...req.body, organizationId });
|
const part = await partService.createPart({ ...req.body, organizationId });
|
||||||
const part = await partService.createPart(payload);
|
console.log('[CREATE PART] Success:', part);
|
||||||
res.status(201).json(toCamelCase(part));
|
res.status(201).json(part);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
console.error('[CREATE PART] Error:', message);
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -25,7 +26,7 @@ export const getPartsByProject = async (req: AuthRequest, res: Response) => {
|
|||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
const parts = await partService.getPartsByProject(projectId as string, organizationId, isGlobalAdmin);
|
const parts = await partService.getPartsByProject(projectId as string, organizationId, isGlobalAdmin);
|
||||||
res.json(toCamelCase(parts || []));
|
res.json(parts);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
@@ -36,8 +37,8 @@ export const updatePart = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
const part = await partService.updatePart(req.params.id as string, toSnakeCase(req.body), organizationId, isGlobalAdmin);
|
const part = await partService.updatePart(req.params.id as string, req.body, organizationId, isGlobalAdmin);
|
||||||
res.json(toCamelCase(part));
|
res.json(part);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
@@ -46,7 +47,9 @@ export const updatePart = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
export const deletePart = async (req: AuthRequest, res: Response) => {
|
export const deletePart = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await partService.deletePart(req.params.id as string);
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
await partService.deletePart(req.params.id as string, organizationId, isGlobalAdmin);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
@@ -57,9 +60,9 @@ export const deletePart = async (req: AuthRequest, res: Response) => {
|
|||||||
export const getAllParts = async (req: AuthRequest, res: Response) => {
|
export const getAllParts = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
if (!organizationId) return res.json([]);
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
const parts = await partService.getPartsByOrganization(organizationId);
|
const parts = await partService.getAllParts(organizationId, isGlobalAdmin);
|
||||||
res.json(toCamelCase(parts || []));
|
res.json(parts);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as projectService from '../services/projectService.js';
|
import * as projectService from '../services/projectService.js';
|
||||||
import { IAppUser } from '../middleware/authMiddleware.js';
|
import { IAppUser } from '../middleware/roleMiddleware.js';
|
||||||
import { notificationService } from '../services/notificationService.js';
|
import { notificationService } from '../services/notificationService.js';
|
||||||
import { toCamelCase } from '../utils/caseMapper.js';
|
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
interface AuthRequest extends Request {
|
||||||
appUser?: IAppUser;
|
appUser?: IAppUser;
|
||||||
@@ -12,7 +11,7 @@ export const createProject = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const project = await projectService.createProject({ ...req.body, organizationId });
|
const project = await projectService.createProject({ ...req.body, organizationId });
|
||||||
res.status(201).json(toCamelCase(project));
|
res.status(201).json(project);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
@@ -25,10 +24,9 @@ export const getAllProjects = async (req: AuthRequest, res: Response) => {
|
|||||||
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
const { status } = req.query;
|
const { status } = req.query;
|
||||||
const projects = await projectService.getAllProjects(organizationId, isGlobalAdmin, status as string);
|
const projects = await projectService.getAllProjects(organizationId, isGlobalAdmin, status as string);
|
||||||
res.json(toCamelCase(projects));
|
res.json(projects);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
console.error('Error in getAllProjects controller:', error);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
const message = error instanceof Error ? error.message : JSON.stringify(error);
|
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -38,7 +36,7 @@ export const archiveProject = async (req: AuthRequest, res: Response) => {
|
|||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
const project = await projectService.archiveProject(req.params.id as string, organizationId, isGlobalAdmin);
|
const project = await projectService.archiveProject(req.params.id as string, organizationId, isGlobalAdmin);
|
||||||
res.json(toCamelCase(project));
|
res.json(project);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
@@ -49,7 +47,7 @@ export const getDashboardProjects = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const projects = await projectService.getDashboardProjects(organizationId);
|
const projects = await projectService.getDashboardProjects(organizationId);
|
||||||
res.json(toCamelCase(projects));
|
res.json(projects);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
@@ -58,8 +56,10 @@ export const getDashboardProjects = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
export const getProjectById = async (req: AuthRequest, res: Response) => {
|
export const getProjectById = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const project = await projectService.getProjectById(req.params.id as string);
|
const organizationId = req.appUser?.organizationId;
|
||||||
res.json(toCamelCase(project));
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
const project = await projectService.getProjectById(req.params.id as string, organizationId, isGlobalAdmin);
|
||||||
|
res.json(project);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(404).json({ error: message });
|
res.status(404).json({ error: message });
|
||||||
@@ -69,19 +69,20 @@ export const getProjectById = async (req: AuthRequest, res: Response) => {
|
|||||||
export const updateProject = async (req: AuthRequest, res: Response) => {
|
export const updateProject = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const organizationId = req.appUser?.organizationId;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const project = await projectService.updateProject(req.params.id as string, req.body);
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
const project = await projectService.updateProject(req.params.id as string, req.body, organizationId, isGlobalAdmin);
|
||||||
|
|
||||||
if (req.body.weightKg !== undefined && organizationId && project) {
|
if (req.body.weightKg !== undefined && organizationId) {
|
||||||
await notificationService.create({
|
await notificationService.create({
|
||||||
organizationId,
|
organizationId,
|
||||||
title: 'Atualização de Obra',
|
title: 'Atualização de Obra',
|
||||||
message: `O peso da obra "${project.name}" foi atualizado para ${project.weight_kg}kg.`,
|
message: `O peso da obra "${project.name}" foi atualizado para ${project.weightKg}kg.`,
|
||||||
type: 'info',
|
type: 'info',
|
||||||
metadata: { projectId: project.id, triggerType: 'project_update' }
|
metadata: { projectId: project.id, triggerType: 'project_update' }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(toCamelCase(project));
|
res.json(project);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
res.status(500).json({ error: message });
|
res.status(500).json({ error: message });
|
||||||
@@ -90,7 +91,9 @@ export const updateProject = async (req: AuthRequest, res: Response) => {
|
|||||||
|
|
||||||
export const deleteProject = async (req: AuthRequest, res: Response) => {
|
export const deleteProject = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await projectService.deleteProject(req.params.id as string);
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com';
|
||||||
|
await projectService.deleteProject(req.params.id as string, organizationId, isGlobalAdmin);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const message = error instanceof Error ? error.message : 'Unknown error';
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
|||||||
@@ -1,152 +1,288 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { supabase } from '../config/supabase.js';
|
import * as stockItemService from '../services/stockItemService.js';
|
||||||
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
|
import { IAppUser } from '../middleware/roleMiddleware.js';
|
||||||
import { IAppUser } from '../middleware/authMiddleware.js';
|
import { notificationService } from '../services/notificationService.js';
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
interface AuthRequest extends Request {
|
||||||
appUser?: IAppUser;
|
appUser?: IAppUser;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStockItems = async (req: AuthRequest, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { data: items, error: itemsError } = await supabase.from('stock_items').select('*');
|
|
||||||
if (itemsError && itemsError.code !== '42P01') throw itemsError;
|
|
||||||
if (!items || items.length === 0) return res.json([]);
|
|
||||||
|
|
||||||
// Get unique data sheet IDs
|
|
||||||
const dsIds = [...new Set(items.map(i => i.data_sheet_id).filter(Boolean))];
|
|
||||||
|
|
||||||
let dataSheets: any[] = [];
|
|
||||||
if (dsIds.length > 0) {
|
|
||||||
const { data: sheets, error: dsError } = await supabase
|
|
||||||
.from('technical_data_sheets')
|
|
||||||
.select('*')
|
|
||||||
.in('id', dsIds);
|
|
||||||
if (!dsError) dataSheets = sheets || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map data sheets to a lookup object
|
|
||||||
const dsMap = Object.fromEntries(dataSheets.map(ds => [ds.id, ds]));
|
|
||||||
|
|
||||||
// Merge and convert to camelCase
|
|
||||||
const enrichedItems = items.map(item => ({
|
|
||||||
...item,
|
|
||||||
data_sheet_id: dsMap[item.data_sheet_id] || item.data_sheet_id
|
|
||||||
}));
|
|
||||||
|
|
||||||
res.json(toCamelCase(enrichedItems));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
console.error('Error fetching stock items:', error);
|
|
||||||
res.json([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStockItemById = async (req: AuthRequest, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabase.from('stock_items').select('*').eq('id', req.params.id).single();
|
|
||||||
if (error) throw error;
|
|
||||||
res.json(toCamelCase(data));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.json(null);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStockMovements = async (req: AuthRequest, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabase.from('stock_movements').select('*').eq('stock_item_id', req.params.id);
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json(toCamelCase(data || []));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.json([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getStockAuditLogs = async (req: AuthRequest, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { data, error } = await supabase.from('stock_audit_logs').select('*').eq('stock_item_id', req.params.id);
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json(toCamelCase(data || []));
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.json([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createStockItem = async (req: AuthRequest, res: Response) => {
|
export const createStockItem = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const itemData = toSnakeCase({ ...req.body, organizationId: req.appUser?.organizationId });
|
const organizationId = req.appUser?.organizationId;
|
||||||
const { data, error } = await supabase.from('stock_items').insert(itemData).select().single();
|
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
||||||
if (error) throw error;
|
const {
|
||||||
res.status(201).json(toCamelCase(data));
|
dataSheetId, rrNumber, batchNumber, quantity, unit,
|
||||||
|
expirationDate, notes, color, invoiceNumber, receivedBy, minStock
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
if (!dataSheetId || !rrNumber || !batchNumber || quantity === undefined || !unit) {
|
||||||
|
return res.status(400).json({ error: 'Campos obrigatórios: DataSheet, RR, Lote, Quantidade, Unidade.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const savedItem = await stockItemService.createStockItem({
|
||||||
|
...req.body,
|
||||||
|
organizationId,
|
||||||
|
createdBy: req.appUser?.externalId,
|
||||||
|
quantity: Number(quantity),
|
||||||
|
minStock: Number(minStock) || 0
|
||||||
|
});
|
||||||
|
|
||||||
|
await stockItemService.createStockMovement({
|
||||||
|
organizationId,
|
||||||
|
createdBy: req.appUser?.externalId,
|
||||||
|
stockItemId: savedItem.id,
|
||||||
|
movementNumber: 1,
|
||||||
|
type: 'ENTRY',
|
||||||
|
quantity: Number(quantity),
|
||||||
|
responsible: userName,
|
||||||
|
notes: 'Abertura de Lote / Entrada Inicial'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
await notificationService.create({
|
||||||
|
organizationId,
|
||||||
|
title: 'Recebimento de Material',
|
||||||
|
message: `Recebido: ${quantity}${unit} de ${savedItem.rr_number} (Lote: ${batchNumber}).`,
|
||||||
|
type: 'info',
|
||||||
|
metadata: { stockItemId: savedItem.id, triggerType: 'stock_received' }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(201).json(savedItem);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateStockItem = async (req: AuthRequest, res: Response) => {
|
export const updateStockItem = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const updateData = toSnakeCase(req.body);
|
const { id } = req.params;
|
||||||
const { data, error } = await supabase.from('stock_items').update(updateData).eq('id', req.params.id).select().single();
|
const organizationId = req.appUser?.organizationId;
|
||||||
if (error) throw error;
|
const { quantity, ...otherData } = req.body;
|
||||||
res.json(toCamelCase(data));
|
|
||||||
|
if (quantity !== undefined) {
|
||||||
|
return res.status(400).json({ error: 'Para alterar a quantidade, utilize as funções de Ajuste ou Consumo.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = await stockItemService.updateStockItem(id, organizationId!, otherData);
|
||||||
|
if (!updated) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
|
|
||||||
|
res.json(updated);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteStockItem = async (req: AuthRequest, res: Response) => {
|
|
||||||
try {
|
|
||||||
await supabase.from('stock_items').delete().eq('id', req.params.id);
|
|
||||||
res.status(204).send();
|
|
||||||
} catch (error: unknown) {
|
|
||||||
res.status(204).send();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export const adjustStock = async (req: AuthRequest, res: Response) => {
|
export const adjustStock = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const updateData = toSnakeCase(req.body);
|
const organizationId = req.appUser?.organizationId;
|
||||||
const { data, error } = await supabase.from('stock_items').update(updateData).eq('id', id).select().single();
|
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
||||||
if (error) throw error;
|
const { quantityDelta, reason } = req.body;
|
||||||
res.json(toCamelCase(data));
|
|
||||||
|
if (!reason) return res.status(400).json({ error: 'Motivo é obrigatório para ajustes técnicos.' });
|
||||||
|
|
||||||
|
const item = await stockItemService.getStockItemById(id, organizationId!);
|
||||||
|
if (!item) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
|
|
||||||
|
const newQuantity = Number(item.quantity) + Number(quantityDelta);
|
||||||
|
if (newQuantity < 0) return res.status(400).json({ error: 'Estoque insuficiente para este ajuste.' });
|
||||||
|
|
||||||
|
await stockItemService.updateStockItemQuantity(id, newQuantity);
|
||||||
|
|
||||||
|
const lastNum = await stockItemService.getLatestMovementNumber(id);
|
||||||
|
|
||||||
|
await stockItemService.createStockMovement({
|
||||||
|
organizationId,
|
||||||
|
createdBy: req.appUser?.externalId,
|
||||||
|
stockItemId: id,
|
||||||
|
movementNumber: lastNum + 1,
|
||||||
|
type: 'ADJUSTMENT',
|
||||||
|
quantity: Number(quantityDelta),
|
||||||
|
responsible: userName,
|
||||||
|
reason
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ ...item, quantity: newQuantity });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const consumeStock = async (req: AuthRequest, res: Response) => {
|
export const consumeStock = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { quantity } = req.body;
|
const organizationId = req.appUser?.organizationId;
|
||||||
const { data: item, error: fetchError } = await supabase.from('stock_items').select('quantity').eq('id', id).single();
|
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
||||||
if (fetchError) throw fetchError;
|
const { quantityConsumed, requester, date } = req.body;
|
||||||
|
|
||||||
const newQuantity = (item.quantity || 0) - (quantity || 0);
|
const item = await stockItemService.getStockItemById(id, organizationId!);
|
||||||
const { data, error } = await supabase.from('stock_items').update({ quantity: newQuantity }).eq('id', id).select().single();
|
if (!item) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
if (error) throw error;
|
|
||||||
|
|
||||||
res.json(toCamelCase(data));
|
if (item.quantity < Number(quantityConsumed)) return res.status(400).json({ error: 'Estoque insuficiente.' });
|
||||||
|
|
||||||
|
const newQuantity = item.quantity - Number(quantityConsumed);
|
||||||
|
await stockItemService.updateStockItemQuantity(id, newQuantity);
|
||||||
|
|
||||||
|
const lastNum = await stockItemService.getLatestMovementNumber(id);
|
||||||
|
|
||||||
|
await stockItemService.createStockMovement({
|
||||||
|
organizationId,
|
||||||
|
createdBy: req.appUser?.externalId,
|
||||||
|
stockItemId: id,
|
||||||
|
movementNumber: lastNum + 1,
|
||||||
|
type: 'CONSUMPTION',
|
||||||
|
quantity: -Number(quantityConsumed),
|
||||||
|
responsible: userName,
|
||||||
|
requester,
|
||||||
|
date: date || new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
res.json({ ...item, quantity: newQuantity });
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteStockItem = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const success = await stockItemService.deleteStockItem(id, organizationId!);
|
||||||
|
if (!success) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
|
res.status(204).send();
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockItems = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const { dataSheetId } = req.query;
|
||||||
|
const items = await stockItemService.getStockItems(organizationId!, dataSheetId as string);
|
||||||
|
res.json(items);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockItemById = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const item = await stockItemService.getStockItemById(id, organizationId!);
|
||||||
|
if (!item) return res.status(404).json({ error: 'Item não encontrado.' });
|
||||||
|
res.json(item);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockMovements = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const movements = await stockItemService.getStockMovements(id, organizationId!);
|
||||||
|
res.json(movements);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateStockMovement = async (req: AuthRequest, res: Response) => {
|
export const updateStockMovement = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { data, error } = await supabase.from('stock_movements').update(req.body).eq('id', id).select().single();
|
const organizationId = req.appUser?.organizationId;
|
||||||
if (error) throw error;
|
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
||||||
res.json(toCamelCase(data));
|
const userId = req.appUser?.externalId || 'system';
|
||||||
|
|
||||||
|
const oldMovement = await stockItemService.getStockMovementById(id, organizationId!);
|
||||||
|
if (!oldMovement) return res.status(404).json({ error: 'Movimentação não encontrada.' });
|
||||||
|
|
||||||
|
const item = await stockItemService.getStockItemById(oldMovement.stock_item_id, organizationId!);
|
||||||
|
const quantityDiff = Number(req.body.quantity) - Number(oldMovement.quantity);
|
||||||
|
|
||||||
|
const newStockLevel = Number(item.quantity) + quantityDiff;
|
||||||
|
if (newStockLevel < 0) return res.status(400).json({ error: 'A alteração resultaria em estoque negativo.' });
|
||||||
|
|
||||||
|
await stockItemService.updateStockItemQuantity(item.id, newStockLevel);
|
||||||
|
|
||||||
|
await stockItemService.createAuditLog({
|
||||||
|
organizationId,
|
||||||
|
stockItemId: item.id,
|
||||||
|
movementId: id,
|
||||||
|
movementNumber: oldMovement.movement_number,
|
||||||
|
userId,
|
||||||
|
userName,
|
||||||
|
action: 'UPDATE',
|
||||||
|
details: `Edição de Movimentação: Qtd ${oldMovement.quantity} -> ${req.body.quantity}`,
|
||||||
|
oldValues: oldMovement,
|
||||||
|
newValues: req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
const updated = await stockItemService.updateStockMovement(id, organizationId!, req.body);
|
||||||
|
res.json(updated);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteStockMovement = async (req: AuthRequest, res: Response) => {
|
export const deleteStockMovement = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
await supabase.from('stock_movements').delete().eq('id', id);
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const userName = req.appUser?.name || req.appUser?.email || 'Unknown User';
|
||||||
|
const userId = req.appUser?.externalId || 'system';
|
||||||
|
|
||||||
|
const movement = await stockItemService.getStockMovementById(id, organizationId!);
|
||||||
|
if (!movement) return res.status(404).json({ error: 'Movimentação não encontrada.' });
|
||||||
|
|
||||||
|
const item = await stockItemService.getStockItemById(movement.stock_item_id, organizationId!);
|
||||||
|
const newStockLevel = Number(item.quantity) - Number(movement.quantity);
|
||||||
|
|
||||||
|
if (newStockLevel < 0) return res.status(400).json({ error: 'A exclusão resultaria em estoque negativo.' });
|
||||||
|
|
||||||
|
await stockItemService.updateStockItemQuantity(item.id, newStockLevel);
|
||||||
|
|
||||||
|
await stockItemService.createAuditLog({
|
||||||
|
organizationId,
|
||||||
|
stockItemId: item.id,
|
||||||
|
movementId: id,
|
||||||
|
movementNumber: movement.movement_number,
|
||||||
|
userId,
|
||||||
|
userName,
|
||||||
|
action: 'DELETE',
|
||||||
|
details: `Exclusão de Movimentação: Qtd ${movement.quantity}`,
|
||||||
|
oldValues: movement
|
||||||
|
});
|
||||||
|
|
||||||
|
await stockItemService.deleteStockMovement(id, organizationId!);
|
||||||
res.status(204).send();
|
res.status(204).send();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStockAuditLogs = async (req: AuthRequest, res: Response) => {
|
||||||
|
try {
|
||||||
|
const { id } = req.params;
|
||||||
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const logs = await stockItemService.getStockAuditLogs(id, organizationId!);
|
||||||
|
res.json(logs);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,32 +1,26 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { supabase } from '../config/supabase.js';
|
import { query } from '../config/database.js';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
const DEFAULT_SETTINGS = {
|
|
||||||
settingsId: 'global',
|
|
||||||
appName: 'GPI',
|
|
||||||
appSubtitle: 'Gestão de Pintura Industrial'
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSettings = async (req: Request, res: Response) => {
|
export const getSettings = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data: settings, error } = await supabase
|
const resSettings = await query('SELECT * FROM gpi.system_settings WHERE settings_id = $1', ['global']);
|
||||||
.from('system_settings')
|
let settings = resSettings.rows[0];
|
||||||
.select('*')
|
|
||||||
.eq('settings_id', 'global')
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== 'PGRST116') {
|
if (!settings) {
|
||||||
console.log('System settings table not found, returning defaults');
|
const insertRes = await query(
|
||||||
return res.json(DEFAULT_SETTINGS);
|
'INSERT INTO gpi.system_settings (settings_id, app_name, app_subtitle) VALUES ($1, $2, $3) RETURNING *',
|
||||||
|
['global', 'GPI', 'Gestão de Pintura Industrial']
|
||||||
|
);
|
||||||
|
settings = insertRes.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(settings || DEFAULT_SETTINGS);
|
res.json(settings);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error fetching system settings:', error);
|
console.error('Error fetching system settings:', error);
|
||||||
res.json(DEFAULT_SETTINGS);
|
res.status(500).json({ error: 'Erro ao buscar configurações do sistema' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -34,47 +28,20 @@ export const updateSettings = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { appName, appSubtitle, appLogoUrl } = req.body;
|
const { appName, appSubtitle, appLogoUrl } = req.body;
|
||||||
|
|
||||||
const { data: existing, error: fetchError } = await supabase
|
const result = await query(
|
||||||
.from('system_settings')
|
`INSERT INTO gpi.system_settings (settings_id, app_name, app_subtitle, app_logo_url, updated_by, updated_at)
|
||||||
.select('*')
|
VALUES ('global', $1, $2, $3, $4, NOW())
|
||||||
.eq('settings_id', 'global')
|
ON CONFLICT (settings_id) DO UPDATE SET
|
||||||
.single();
|
app_name = EXCLUDED.app_name,
|
||||||
|
app_subtitle = EXCLUDED.app_subtitle,
|
||||||
|
app_logo_url = EXCLUDED.app_logo_url,
|
||||||
|
updated_by = EXCLUDED.updated_by,
|
||||||
|
updated_at = NOW()
|
||||||
|
RETURNING *`,
|
||||||
|
[appName, appSubtitle, appLogoUrl, req.appUser?.email]
|
||||||
|
);
|
||||||
|
|
||||||
let settings;
|
res.json(result.rows[0]);
|
||||||
if (fetchError || !existing) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('system_settings')
|
|
||||||
.insert({
|
|
||||||
settings_id: 'global',
|
|
||||||
app_name: appName,
|
|
||||||
app_subtitle: appSubtitle,
|
|
||||||
app_logo_url: appLogoUrl,
|
|
||||||
updated_by: req.appUser?.email
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
settings = data;
|
|
||||||
} else {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('system_settings')
|
|
||||||
.update({
|
|
||||||
app_name: appName,
|
|
||||||
app_subtitle: appSubtitle,
|
|
||||||
app_logo_url: appLogoUrl,
|
|
||||||
updated_by: req.appUser?.email
|
|
||||||
})
|
|
||||||
.eq('id', existing.id)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
settings = data;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`⚙️ System Settings updated by ${req.appUser?.email}`);
|
|
||||||
res.json(settings);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating system settings:', error);
|
console.error('Error updating system settings:', error);
|
||||||
res.status(500).json({ error: 'Erro ao atualizar configurações do sistema' });
|
res.status(500).json({ error: 'Erro ao atualizar configurações do sistema' });
|
||||||
@@ -84,15 +51,11 @@ export const updateSettings = async (req: Request, res: Response) => {
|
|||||||
export const serveLogo = async (req: Request, res: Response) => {
|
export const serveLogo = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { filename } = req.params as { filename: string };
|
const { filename } = req.params as { filename: string };
|
||||||
const tmpPath = path.join(os.tmpdir(), 'uploads', filename);
|
|
||||||
const localPath = path.join(process.cwd(), 'uploads', filename);
|
const localPath = path.join(process.cwd(), 'uploads', filename);
|
||||||
|
|
||||||
if (fs.existsSync(tmpPath)) {
|
if (fs.existsSync(localPath)) {
|
||||||
res.sendFile(tmpPath);
|
|
||||||
} else if (fs.existsSync(localPath)) {
|
|
||||||
res.sendFile(localPath);
|
res.sendFile(localPath);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Logo file not found in tmp or local: ${filename}`);
|
|
||||||
res.status(404).json({ error: 'Imagem não encontrada' });
|
res.status(404).json({ error: 'Imagem não encontrada' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -106,7 +69,6 @@ export const uploadLogo = async (req: Request, res: Response) => {
|
|||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
|
return res.status(400).json({ error: 'Nenhum arquivo enviado.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileUrl = `/api/system-settings/logo-image/${req.file.filename}`;
|
const fileUrl = `/api/system-settings/logo-image/${req.file.filename}`;
|
||||||
res.json({ url: fileUrl });
|
res.json({ url: fileUrl });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -117,13 +79,8 @@ export const uploadLogo = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const getGlobalUsers = async (req: Request, res: Response) => {
|
export const getGlobalUsers = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data: users, error } = await supabase
|
const resUsers = await query('SELECT * FROM gpi.users ORDER BY created_at DESC');
|
||||||
.from('users')
|
res.json(resUsers.rows);
|
||||||
.select('*')
|
|
||||||
.order('created_at', { ascending: false });
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json(users || []);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting global users:', error);
|
console.error('Error getting global users:', error);
|
||||||
res.status(500).json({ error: 'Erro ao buscar usuários globais.' });
|
res.status(500).json({ error: 'Erro ao buscar usuários globais.' });
|
||||||
@@ -132,32 +89,20 @@ export const getGlobalUsers = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
export const getGlobalOrganizations = async (req: Request, res: Response) => {
|
export const getGlobalOrganizations = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data: organizations, error } = await supabase
|
const sql = `
|
||||||
.from('organizations')
|
SELECT
|
||||||
.select('*');
|
o.id as _id,
|
||||||
|
o.name,
|
||||||
if (error && error.code !== '42P01') throw error;
|
o.is_banned as "isBanned",
|
||||||
|
COUNT(uo.user_id) as "memberCount",
|
||||||
if (!organizations || organizations.length === 0) {
|
MAX(uo.updated_at) as "lastActive"
|
||||||
return res.json([]);
|
FROM gpi.organizations o
|
||||||
}
|
LEFT JOIN gpi.user_organizations uo ON o.id = uo.organization_id
|
||||||
|
GROUP BY o.id, o.name, o.is_banned
|
||||||
const orgsWithMembers = await Promise.all(
|
ORDER BY "memberCount" DESC
|
||||||
organizations.map(async (org) => {
|
`;
|
||||||
const { data: members } = await supabase
|
const resOrgs = await query(sql);
|
||||||
.from('user_organizations')
|
res.json(resOrgs.rows);
|
||||||
.select('*')
|
|
||||||
.eq('organization_id', org.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...org,
|
|
||||||
members: members || [],
|
|
||||||
memberCount: members?.length || 0
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json(orgsWithMembers);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error getting global organizations:', error);
|
console.error('Error getting global organizations:', error);
|
||||||
res.status(500).json({ error: 'Erro ao buscar organizações globais.' });
|
res.status(500).json({ error: 'Erro ao buscar organizações globais.' });
|
||||||
@@ -172,16 +117,12 @@ export const toggleOrganizationBan = async (req: Request, res: Response) => {
|
|||||||
return res.status(400).json({ error: 'ID da organização é obrigatório.' });
|
return res.status(400).json({ error: 'ID da organização é obrigatório.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: org, error } = await supabase
|
const resOrg = await query(
|
||||||
.from('organizations')
|
'UPDATE gpi.organizations SET is_banned = $1, updated_at = NOW() WHERE id = $2 RETURNING *',
|
||||||
.update({ is_banned: isBanned })
|
[isBanned, organizationId]
|
||||||
.eq('id', organizationId)
|
);
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
res.json(resOrg.rows[0]);
|
||||||
console.log(`Organization ${organizationId} ban status set to ${isBanned} by ${req.appUser?.email}`);
|
|
||||||
res.json(org);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error toggling organization ban:', error);
|
console.error('Error toggling organization ban:', error);
|
||||||
res.status(500).json({ error: 'Erro ao atualizar status da organização.' });
|
res.status(500).json({ error: 'Erro ao atualizar status da organização.' });
|
||||||
|
|||||||
@@ -1,97 +1,66 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { supabase } from '../config/supabase.js';
|
import * as userService from '../services/userService.js';
|
||||||
|
|
||||||
interface AuthRequest extends Request {
|
interface AuthRequest extends Request {
|
||||||
appUser?: any;
|
appUser?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const syncUser = async (req: AuthRequest, res: Response) => {
|
export const syncUser = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
if (req.appUser) {
|
const { externalId, email, name, organizationId, clerkRole } = req.body;
|
||||||
return res.json(req.appUser);
|
|
||||||
|
if (!externalId || !email || !name) {
|
||||||
|
return res.status(400).json({ error: 'externalId, email e name são obrigatórios.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { email, name, logto_id } = req.body;
|
const user = await userService.syncUser({
|
||||||
|
externalId,
|
||||||
if (!email) {
|
email,
|
||||||
return res.status(400).json({ error: 'Email é obrigatório para sincronização.' });
|
name,
|
||||||
}
|
organizationId,
|
||||||
|
clerkRole
|
||||||
const { data: existingUser } = await supabase
|
});
|
||||||
.from('users')
|
|
||||||
.select('*')
|
|
||||||
.eq('email', email)
|
|
||||||
.single();
|
|
||||||
|
|
||||||
let user;
|
|
||||||
if (!existingUser) {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('users')
|
|
||||||
.insert({
|
|
||||||
email,
|
|
||||||
name: name || email.split('@')[0],
|
|
||||||
logto_id,
|
|
||||||
role: 'guest'
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
user = data;
|
|
||||||
} else {
|
|
||||||
if (logto_id && !existingUser.logto_id) {
|
|
||||||
const { data } = await supabase
|
|
||||||
.from('users')
|
|
||||||
.update({ logto_id })
|
|
||||||
.eq('id', existingUser.id)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
user = data;
|
|
||||||
} else {
|
|
||||||
user = existingUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json(user);
|
res.json(user);
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error('Error syncing user:', error);
|
console.error('Error syncing user:', error);
|
||||||
res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + error.message });
|
res.status(500).json({ error: 'Erro ao sincronizar usuário: ' + (error instanceof Error ? error.message : String(error)) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getCurrentUser = async (req: AuthRequest, res: Response) => {
|
export const getCurrentUser = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
if (!req.appUser) {
|
if (!req.appUser) {
|
||||||
return res.json({
|
return res.status(404).json({ error: 'Usuário não encontrado.' });
|
||||||
id: '00000000-0000-0000-0000-000000000000',
|
|
||||||
email: 'guest@gpi.app',
|
|
||||||
name: 'Guest User',
|
|
||||||
role: 'user'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
res.json(req.appUser);
|
|
||||||
} catch (error: any) {
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
const user = await userService.getCurrentUser(req.appUser.clerk_id || req.appUser.logto_id || req.appUser.externalId, organizationId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return res.status(404).json({ error: 'Usuário não encontrado.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(user);
|
||||||
|
} catch (error) {
|
||||||
console.error('Error getting current user:', error);
|
console.error('Error getting current user:', error);
|
||||||
res.json(req.appUser || { id: 'guest-user', email: 'guest@gpi.app', role: 'user' });
|
res.status(500).json({ error: 'Erro ao buscar usuário.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getAllUsers = async (req: Request, res: Response) => {
|
export const getAllUsers = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
// Always return all users from users table for now
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('users')
|
|
||||||
.select('*');
|
|
||||||
|
|
||||||
if (error) {
|
if (!organizationId) {
|
||||||
console.log('Error fetching users:', error.message);
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
||||||
return res.json([]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.json(data || []);
|
const members = await userService.getAllUsersInOrg(organizationId);
|
||||||
} catch (error: any) {
|
res.json(members);
|
||||||
|
} catch (error) {
|
||||||
console.error('Error getting users:', error);
|
console.error('Error getting users:', error);
|
||||||
res.json([]);
|
res.status(500).json({ error: 'Erro ao buscar usuários.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -99,23 +68,21 @@ export const updateUserRole = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { role } = req.body;
|
const { role } = req.body;
|
||||||
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
if (!['guest', 'user', 'admin'].includes(role)) {
|
if (!organizationId) {
|
||||||
return res.status(400).json({ error: 'Role inválido.' });
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await supabase
|
const member = await userService.updateUserRole(id, organizationId, role);
|
||||||
.from('user_organizations')
|
if (!member) {
|
||||||
.update({ role })
|
return res.status(404).json({ error: 'Usuário não encontrado nesta organização.' });
|
||||||
.eq('id', id)
|
}
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
res.json(member);
|
||||||
res.json(data || { message: 'Role atualizado' });
|
} catch (error) {
|
||||||
} catch (error: any) {
|
|
||||||
console.error('Error updating role:', error);
|
console.error('Error updating role:', error);
|
||||||
res.json({ message: 'Role atualizado' });
|
res.status(500).json({ error: 'Erro ao alterar role.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -123,72 +90,72 @@ export const toggleBanUser = async (req: AuthRequest, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
const { isBanned } = req.body;
|
const { isBanned } = req.body;
|
||||||
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
if (!organizationId) {
|
||||||
.from('user_organizations')
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
||||||
.update({ is_banned: isBanned })
|
}
|
||||||
.eq('id', id)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
const member = await userService.toggleBanUser(id, organizationId, isBanned);
|
||||||
res.json(data || { message: 'Ban atualizado' });
|
if (!member) {
|
||||||
} catch (error: any) {
|
return res.status(404).json({ error: 'Usuário não encontrado nesta organização.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(member);
|
||||||
|
} catch (error) {
|
||||||
console.error('Error toggling ban:', error);
|
console.error('Error toggling ban:', error);
|
||||||
res.json({ message: 'Ban atualizado' });
|
res.status(500).json({ error: 'Erro ao alterar status de banimento.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const heartbeat = async (req: AuthRequest, res: Response) => {
|
export const heartbeat = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
if (!req.appUser) {
|
if (!req.appUser) {
|
||||||
return res.status(200).send();
|
return res.status(401).json({ error: 'Não autenticado.' });
|
||||||
}
|
}
|
||||||
|
await userService.heartbeat(req.appUser.id);
|
||||||
try {
|
|
||||||
await supabase
|
|
||||||
.from('users')
|
|
||||||
.update({ last_seen_at: new Date().toISOString() })
|
|
||||||
.eq('id', req.appUser.id);
|
|
||||||
} catch (e) { /* ignore */ }
|
|
||||||
|
|
||||||
res.status(200).send();
|
res.status(200).send();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Heartbeat error:', error);
|
console.error('Heartbeat error:', error);
|
||||||
res.status(200).send();
|
res.status(500).send();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getActiveUsers = async (req: AuthRequest, res: Response) => {
|
export const getActiveUsers = async (req: AuthRequest, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const twoMinutesAgo = new Date(Date.now() - 2 * 60 * 1000).toISOString();
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
const currentUserId = req.appUser?.id;
|
||||||
|
|
||||||
const { data, error } = await supabase
|
if (!organizationId) {
|
||||||
.from('users')
|
return res.status(400).json([]);
|
||||||
.select('id, email, name, last_seen_at')
|
}
|
||||||
.gte('last_seen_at', twoMinutesAgo);
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
const activeUsers = await userService.getActiveUsers(organizationId, currentUserId);
|
||||||
res.json(data || []);
|
res.json(activeUsers);
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error('Error getting active users:', error);
|
console.error('Error getting active users:', error);
|
||||||
res.json([]);
|
res.status(500).json([]);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteUser = async (req: Request, res: Response) => {
|
export const deleteUser = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { id } = req.params;
|
const { id } = req.params;
|
||||||
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
const { error } = await supabase
|
if (!organizationId) {
|
||||||
.from('user_organizations')
|
return res.status(400).json({ error: 'Organização não selecionada.' });
|
||||||
.delete()
|
}
|
||||||
.eq('id', id);
|
|
||||||
|
const success = await userService.deleteUserFromOrg(id, organizationId);
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return res.status(404).json({ error: 'Membro não encontrado.' });
|
||||||
|
}
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
res.json({ message: 'Membro removido com sucesso.' });
|
res.json({ message: 'Membro removido com sucesso.' });
|
||||||
} catch (error: any) {
|
} catch (error) {
|
||||||
console.error('Error deleting user:', error);
|
console.error('Error deleting user:', error);
|
||||||
res.json({ message: 'Membro removido com sucesso.' });
|
res.status(500).json({ error: 'Erro ao remover membro.' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,52 +1,57 @@
|
|||||||
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { supabase } from '../config/supabase.js';
|
import * as yieldStudyService from '../services/yieldStudyService.js';
|
||||||
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
|
|
||||||
|
|
||||||
export const getAllStudies = async (req: Request, res: Response) => {
|
export const getAllStudies = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase.from('yield_studies').select('*');
|
const organizationId = req.appUser?.organizationId;
|
||||||
if (error && error.code !== '42P01') throw error;
|
const studies = await yieldStudyService.getAllStudies(organizationId);
|
||||||
res.json(toCamelCase(data || []));
|
res.json(studies);
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.json([]);
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const createStudy = async (req: Request, res: Response) => {
|
export const createStudy = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const payload = { ...req.body, organization_id: req.appUser?.organizationId };
|
const organizationId = req.appUser?.organizationId;
|
||||||
const { data, error } = await supabase
|
const study = await yieldStudyService.createStudy({ ...req.body, organizationId });
|
||||||
.from('yield_studies')
|
res.status(201).json(study);
|
||||||
.insert(toSnakeCase(payload))
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
if (error) throw error;
|
|
||||||
res.status(201).json(toCamelCase(data));
|
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(400).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const updateStudy = async (req: Request, res: Response) => {
|
export const updateStudy = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
const { data, error } = await supabase
|
const id = req.params.id as string;
|
||||||
.from('yield_studies')
|
const organizationId = req.appUser?.organizationId;
|
||||||
.update(toSnakeCase(req.body))
|
const study = await yieldStudyService.updateStudy(id, req.body, organizationId);
|
||||||
.eq('id', req.params.id)
|
if (study) {
|
||||||
.select()
|
res.json(study);
|
||||||
.single();
|
} else {
|
||||||
if (error) throw error;
|
res.status(404).json({ error: 'Study not found' });
|
||||||
res.json(toCamelCase(data));
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(400).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteStudy = async (req: Request, res: Response) => {
|
export const deleteStudy = async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
await supabase.from('yield_studies').delete().eq('id', req.params.id);
|
const id = req.params.id as string;
|
||||||
res.status(204).send();
|
const organizationId = req.appUser?.organizationId;
|
||||||
|
const success = await yieldStudyService.deleteStudy(id, organizationId);
|
||||||
|
if (success) {
|
||||||
|
res.status(204).send();
|
||||||
|
} else {
|
||||||
|
res.status(404).json({ error: 'Study not found' });
|
||||||
|
}
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(500).json({ error: (error as any).message });
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
||||||
|
res.status(500).json({ error: message });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
+16
-19
@@ -1,36 +1,33 @@
|
|||||||
import dotenv from 'dotenv';
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
import app from './app.js';
|
import app from './app.js';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
import { connectDB } from './config/database.js';
|
import { connectDB } from './config/database.js';
|
||||||
import { notificationService } from './services/notificationService.js';
|
import { notificationService } from './services/notificationService.js';
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
|
||||||
const startServer = async () => {
|
const startServer = async () => {
|
||||||
try {
|
try {
|
||||||
console.log('🔄 Connecting to database...');
|
|
||||||
await connectDB();
|
await connectDB();
|
||||||
|
|
||||||
const PORT = parseInt(process.env.PORT || '3000', 10);
|
const PORT = process.env.PORT || 3000;
|
||||||
|
app.listen(Number(PORT), '0.0.0.0', async () => {
|
||||||
|
console.log(`🚀 Server running on port ${PORT} (0.0.0.0)`);
|
||||||
|
|
||||||
const server = app.listen(PORT, '0.0.0.0', () => {
|
// Agendar verificação de vencimento de estoque (a cada 24 horas)
|
||||||
console.log(`🚀 Server running on port ${PORT}`);
|
console.log('📅 Scheduling stock expiration check...');
|
||||||
console.log('✅ Conectado ao Supabase (GPI schema)');
|
setInterval(() => {
|
||||||
|
notificationService.checkStockExpirations();
|
||||||
|
}, 24 * 60 * 60 * 1000);
|
||||||
|
|
||||||
|
// Executar uma vez no início para garantir (opcional, bom para dev)
|
||||||
|
// notificationService.checkStockExpirations();
|
||||||
});
|
});
|
||||||
|
|
||||||
server.timeout = 60000;
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to start server:', error);
|
console.error('Failed to start server:', error);
|
||||||
process.exit(1);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
startServer();
|
startServer();
|
||||||
|
|
||||||
process.on('uncaughtException', (err) => {
|
// Force keep-alive to debug why it exits
|
||||||
console.error('Uncaught Exception:', err);
|
setInterval(() => { }, 1000);
|
||||||
});
|
|
||||||
|
|
||||||
process.on('unhandledRejection', (reason) => {
|
|
||||||
console.error('Unhandled Rejection:', reason);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
import { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi } from '../config/supabase.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Mongoose Compatibility Layer for Supabase (v2025)
|
|
||||||
* Translates Mongoose-style calls to Supabase PostgREST queries.
|
|
||||||
* Automatically handles camelCase (JS) to snake_case (DB) mapping for core fields.
|
|
||||||
*/
|
|
||||||
function createModel(tableName: string) {
|
|
||||||
const mapToDb = (data: any) => {
|
|
||||||
const mapped: any = { ...data };
|
|
||||||
if (mapped.organizationId) { mapped.organization_id = mapped.organizationId; delete mapped.organizationId; }
|
|
||||||
if (mapped.projectId) { mapped.project_id = mapped.projectId; delete mapped.projectId; }
|
|
||||||
if (mapped.createdBy) { mapped.created_by = mapped.createdBy; delete mapped.createdBy; }
|
|
||||||
if (mapped.createdAt) { mapped.created_at = mapped.createdAt; delete mapped.createdAt; }
|
|
||||||
if (mapped.updatedAt) { mapped.updated_at = mapped.updatedAt; delete mapped.updatedAt; }
|
|
||||||
return mapped;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mapFromDb = (data: any) => {
|
|
||||||
if (!data) return data;
|
|
||||||
if (Array.isArray(data)) return data.map(mapFromDb);
|
|
||||||
const mapped: any = { ...data, id: data.id || data._id };
|
|
||||||
if (mapped.organization_id) mapped.organizationId = mapped.organization_id;
|
|
||||||
if (mapped.project_id) mapped.projectId = mapped.project_id;
|
|
||||||
if (mapped.created_by) mapped.createdBy = mapped.created_by;
|
|
||||||
return mapped;
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
find: function(query: any = {}) {
|
|
||||||
const dbQuery = mapToDb(query);
|
|
||||||
const promise = (async () => {
|
|
||||||
const { data, error } = await queryGpi(tableName, { filter: dbQuery });
|
|
||||||
if (error) throw error;
|
|
||||||
return mapFromDb(data || []);
|
|
||||||
})();
|
|
||||||
|
|
||||||
// Mock methods for chainability
|
|
||||||
(promise as any).sort = () => promise;
|
|
||||||
(promise as any).populate = () => promise;
|
|
||||||
(promise as any).lean = () => promise;
|
|
||||||
(promise as any).limit = () => promise;
|
|
||||||
(promise as any).select = () => promise;
|
|
||||||
|
|
||||||
return promise;
|
|
||||||
},
|
|
||||||
findOne: async (query: any) => {
|
|
||||||
const data = await findOneGpi(tableName, mapToDb(query));
|
|
||||||
return mapFromDb(data);
|
|
||||||
},
|
|
||||||
findById: async (id: string) => {
|
|
||||||
const data = await findOneGpi(tableName, { id });
|
|
||||||
return mapFromDb(data);
|
|
||||||
},
|
|
||||||
create: async (data: any) => {
|
|
||||||
const dbData = mapToDb(data);
|
|
||||||
const result = await insertGpi(tableName, dbData);
|
|
||||||
return mapFromDb(result.data?.[0] || result.data);
|
|
||||||
},
|
|
||||||
insertMany: async (dataArray: any[]) => {
|
|
||||||
const dbDataArray = dataArray.map(mapToDb);
|
|
||||||
const { data, error } = await supabase.from(tableName).insert(dbDataArray).select();
|
|
||||||
if (error) throw error;
|
|
||||||
return mapFromDb(data);
|
|
||||||
},
|
|
||||||
deleteMany: async (query: any) => {
|
|
||||||
const dbQuery = mapToDb(query);
|
|
||||||
let q = supabase.from(tableName).delete();
|
|
||||||
Object.keys(dbQuery).forEach(key => {
|
|
||||||
q = q.eq(key, dbQuery[key]);
|
|
||||||
});
|
|
||||||
const { error } = await q;
|
|
||||||
if (error) throw error;
|
|
||||||
return { deletedCount: 0 }; // Supabase doesn't return count easily here
|
|
||||||
},
|
|
||||||
countDocuments: async (query: any = {}) => {
|
|
||||||
const dbQuery = mapToDb(query);
|
|
||||||
let q = supabase.from(tableName).select('*', { count: 'exact', head: true });
|
|
||||||
Object.keys(dbQuery).forEach(key => {
|
|
||||||
q = q.eq(key, dbQuery[key]);
|
|
||||||
});
|
|
||||||
const { count, error } = await q;
|
|
||||||
if (error) throw error;
|
|
||||||
return count || 0;
|
|
||||||
},
|
|
||||||
findOneAndUpdate: async (query: any, update: any) => {
|
|
||||||
const existing = await findOneGpi(tableName, mapToDb(query));
|
|
||||||
if (!existing) return null;
|
|
||||||
const result = await updateGpi(tableName, existing.id, mapToDb(update));
|
|
||||||
return mapFromDb(result.data?.[0]);
|
|
||||||
},
|
|
||||||
findByIdAndUpdate: async (id: string, update: any) => {
|
|
||||||
const result = await updateGpi(tableName, id, mapToDb(update));
|
|
||||||
return mapFromDb(result.data?.[0]);
|
|
||||||
},
|
|
||||||
findByIdAndDelete: async (id: string) => {
|
|
||||||
await deleteGpi(tableName, id);
|
|
||||||
return { id };
|
|
||||||
},
|
|
||||||
findOneAndDelete: async (query: any) => {
|
|
||||||
const dbQuery = mapToDb(query);
|
|
||||||
const existing = await findOneGpi(tableName, dbQuery);
|
|
||||||
if (!existing) return null;
|
|
||||||
await deleteGpi(tableName, existing.id);
|
|
||||||
return mapFromDb(existing);
|
|
||||||
},
|
|
||||||
updateMany: async (query: any, update: any) => {
|
|
||||||
const dbQuery = mapToDb(query);
|
|
||||||
const dbUpdate = mapToDb(update.$set || update);
|
|
||||||
let q = supabase.from(tableName).update(dbUpdate);
|
|
||||||
Object.keys(dbQuery).forEach(key => {
|
|
||||||
q = q.eq(key, dbQuery[key]);
|
|
||||||
});
|
|
||||||
const { error } = await q;
|
|
||||||
if (error) throw error;
|
|
||||||
return { acknowledged: true, modifiedCount: 0 };
|
|
||||||
},
|
|
||||||
deleteOne: async (query: any) => {
|
|
||||||
const existing = await findOneGpi(tableName, mapToDb(query));
|
|
||||||
if (!existing) return null;
|
|
||||||
await deleteGpi(tableName, existing.id);
|
|
||||||
return existing;
|
|
||||||
},
|
|
||||||
aggregate: (pipeline: any[]) => ({ toArray: async () => [] }),
|
|
||||||
// For "new Model()" usage
|
|
||||||
new: function(data: any) {
|
|
||||||
const instance = { ...data };
|
|
||||||
(instance as any).save = async () => {
|
|
||||||
return await insertGpi(tableName, mapToDb(instance));
|
|
||||||
};
|
|
||||||
(instance as any).toObject = () => instance;
|
|
||||||
return instance;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Project = createModel('projects');
|
|
||||||
export const Part = createModel('parts');
|
|
||||||
export const PaintingScheme = createModel('painting_schemes');
|
|
||||||
export const ApplicationRecord = createModel('application_records');
|
|
||||||
export const Inspection = createModel('inspections');
|
|
||||||
export const User = createModel('users');
|
|
||||||
export const Organization = createModel('organizations');
|
|
||||||
export const OrganizationMember = createModel('user_organizations');
|
|
||||||
export const StockItem = createModel('stock_items');
|
|
||||||
export const StockMovement = createModel('stock_movements');
|
|
||||||
export const StockAuditLog = createModel('stock_audit_logs');
|
|
||||||
export const Instrument = createModel('instruments');
|
|
||||||
export const TechnicalDataSheet = createModel('technical_data_sheets');
|
|
||||||
export const SystemSettings = createModel('system_settings');
|
|
||||||
export const Notification = createModel('notifications');
|
|
||||||
export const Message = createModel('messages');
|
|
||||||
export const GeometryType = createModel('geometry_types');
|
|
||||||
export const YieldStudy = createModel('yield_studies');
|
|
||||||
export const StoredFile = createModel('stored_files');
|
|
||||||
|
|
||||||
export { queryGpi, findOneGpi, insertGpi, updateGpi, deleteGpi };
|
|
||||||
|
|
||||||
console.log('✅ Mongoose Compatibility Layer load complete (Extended Mode)');
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
import { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi } from '../config/supabase.js';
|
|
||||||
|
|
||||||
export { supabase, findOneGpi, queryGpi, insertGpi, updateGpi, deleteGpi };
|
|
||||||
|
|
||||||
export async function getModel(tableName: string) {
|
|
||||||
return {
|
|
||||||
find: async (query: any = {}) => {
|
|
||||||
const { data, error } = await queryGpi(tableName, query);
|
|
||||||
if (error) throw error;
|
|
||||||
return data || [];
|
|
||||||
},
|
|
||||||
findOne: async (query: any) => {
|
|
||||||
return await findOneGpi(tableName, query);
|
|
||||||
},
|
|
||||||
findById: async (id: string) => {
|
|
||||||
return await findOneGpi(tableName, { id });
|
|
||||||
},
|
|
||||||
create: async (data: any) => {
|
|
||||||
const result = await insertGpi(tableName, data);
|
|
||||||
return result.data?.[0];
|
|
||||||
},
|
|
||||||
findOneAndUpdate: async (query: any, data: any) => {
|
|
||||||
const existing = await findOneGpi(tableName, query);
|
|
||||||
if (!existing) return null;
|
|
||||||
const result = await updateGpi(tableName, existing.id, data);
|
|
||||||
return result.data?.[0];
|
|
||||||
},
|
|
||||||
findByIdAndUpdate: async (id: string, data: any) => {
|
|
||||||
const result = await updateGpi(tableName, id, data);
|
|
||||||
return result.data?.[0];
|
|
||||||
},
|
|
||||||
findOneAndDelete: async (query: any) => {
|
|
||||||
const existing = await findOneGpi(tableName, query);
|
|
||||||
if (!existing) return null;
|
|
||||||
await deleteGpi(tableName, existing.id);
|
|
||||||
return existing;
|
|
||||||
},
|
|
||||||
countDocuments: async (query: any = {}) => {
|
|
||||||
const { data, error } = await supabase.from(tableName).select('*', { count: 'exact', head: true });
|
|
||||||
if (error) throw error;
|
|
||||||
return data?.length || 0;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getModelById(tableName: string, idField: string = 'id') {
|
|
||||||
return {
|
|
||||||
find: async (query: any = {}) => {
|
|
||||||
const { data, error } = await queryGpi(tableName, { filter: query });
|
|
||||||
if (error) throw error;
|
|
||||||
return data || [];
|
|
||||||
},
|
|
||||||
findOne: async (query: any) => {
|
|
||||||
return await findOneGpi(tableName, query);
|
|
||||||
},
|
|
||||||
create: async (data: any) => {
|
|
||||||
const result = await insertGpi(tableName, data);
|
|
||||||
return result.data?.[0];
|
|
||||||
},
|
|
||||||
findByIdAndUpdate: async (id: string, data: any) => {
|
|
||||||
const result = await updateGpi(tableName, id, data);
|
|
||||||
return result.data?.[0];
|
|
||||||
},
|
|
||||||
findByIdAndDelete: async (id: string) => {
|
|
||||||
await deleteGpi(tableName, id);
|
|
||||||
return { id };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ DB Compatibility Layer initialized');
|
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'fallback_secret_key_change_in_prod';
|
||||||
|
|
||||||
|
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
||||||
|
// Se não houver token autêntico JWT, prossegue limpo
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(' ')[1];
|
||||||
|
const decoded = jwt.verify(token, JWT_SECRET) as any;
|
||||||
|
|
||||||
|
// Injeta o externalId no header para que o extractUser (roleMiddleware)
|
||||||
|
// continue seu trabalho de carregar o usuário do banco instanciado e popular req.appUser
|
||||||
|
req.headers['x-auth-user-id'] = decoded.externalId;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Auth Middleware Error:', error);
|
||||||
|
res.status(401).json({ error: 'Token inválido ou expirado' });
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
|
|
||||||
export interface IAppUser {
|
|
||||||
id: string;
|
|
||||||
logtoId: string;
|
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
role: string;
|
|
||||||
organizationId?: string;
|
|
||||||
organizationRole?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module 'express-serve-static-core' {
|
|
||||||
interface Request {
|
|
||||||
appUser?: IAppUser;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
req.appUser = {
|
|
||||||
id: '00000000-0000-0000-0000-000000000000',
|
|
||||||
logtoId: 'guest',
|
|
||||||
email: 'guest@gpi.app',
|
|
||||||
name: 'Guest User',
|
|
||||||
role: 'user',
|
|
||||||
organizationId: req.headers['x-organization-id'] as string || 'e47e6210-4879-4e5b-bf21-9285d2713123',
|
|
||||||
organizationRole: 'user'
|
|
||||||
};
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requireRole = (allowedRoles: string[]) => {
|
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
// No authentication required - allow all requests
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requireAdmin = requireRole(['admin']);
|
|
||||||
export const requireUser = requireRole(['user', 'admin']);
|
|
||||||
|
|
||||||
export const canEdit = (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
|
|
||||||
export const requireDeveloper = (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { createRemoteJWKSet, jwtVerify } from 'jose';
|
|
||||||
import { supabase, findOneGpi } from '../config/supabase.js';
|
|
||||||
import { IAppUser } from './authMiddleware.js';
|
|
||||||
|
|
||||||
const LOGTO_URL = process.env.LOGTO_URL || 'https://logto-admin-bzlued1boxl3t8ewsyn99an9.187.77.227.172.sslip.io';
|
|
||||||
const APP_ID = process.env.LOGTO_APP_ID || 'gpi-app-final';
|
|
||||||
const jwks = createRemoteJWKSet(new URL(`${LOGTO_URL}/oidc/jwks`));
|
|
||||||
|
|
||||||
export type AppUser = IAppUser;
|
|
||||||
|
|
||||||
export async function authenticateRequest(req: any): Promise<IAppUser | null> {
|
|
||||||
const authHeader = req.headers.authorization;
|
|
||||||
|
|
||||||
if (!authHeader?.startsWith('Bearer ')) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = authHeader.substring(7);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const { payload } = await jwtVerify(token, jwks, {
|
|
||||||
issuer: `${LOGTO_URL}/oidc`,
|
|
||||||
audience: APP_ID
|
|
||||||
});
|
|
||||||
|
|
||||||
const logtoId = payload.sub as string;
|
|
||||||
|
|
||||||
// Primeiro tenta pelo Logto ID
|
|
||||||
let user = await findOneGpi('users', { logto_id: logtoId });
|
|
||||||
|
|
||||||
// Se não encontrar, tenta pelo email (se houver no payload do token)
|
|
||||||
if (!user && payload.email) {
|
|
||||||
const email = payload.email as string;
|
|
||||||
user = await findOneGpi('users', { email });
|
|
||||||
|
|
||||||
if (user) {
|
|
||||||
// Vincula o Logto ID ao usuário existente
|
|
||||||
await supabase
|
|
||||||
.from('users')
|
|
||||||
.update({ logto_id: logtoId })
|
|
||||||
.eq('id', user.id);
|
|
||||||
|
|
||||||
user.logto_id = logtoId;
|
|
||||||
console.log(`[Auth] Usuário ${email} vinculado ao Logto ID ${logtoId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-registro se não encontrar
|
|
||||||
if (!user) {
|
|
||||||
console.log(`[Auth] Usuário Logto ${logtoId} sem registro no GPI. Criando...`);
|
|
||||||
|
|
||||||
const email = (payload.email as string) || '';
|
|
||||||
const name = (payload.name as string) || (payload.username as string) || email.split('@')[0];
|
|
||||||
|
|
||||||
const { data: newUser, error: createError } = await supabase
|
|
||||||
.from('users')
|
|
||||||
.insert({
|
|
||||||
email,
|
|
||||||
name,
|
|
||||||
logto_id: logtoId,
|
|
||||||
role: 'user'
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (createError) {
|
|
||||||
console.error('[Auth] Erro ao auto-registrar usuário:', createError);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
user = newUser;
|
|
||||||
console.log(`[Auth] Novo usuário auto-registrado: ${email}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: user.id || user._id,
|
|
||||||
logtoId: user.logto_id,
|
|
||||||
email: user.email,
|
|
||||||
name: user.name,
|
|
||||||
role: user.role
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
console.error('[Auth] Erro ao verificar token:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function requireAuth() {
|
|
||||||
return async (req: any, res: any, next: any) => {
|
|
||||||
const user = await authenticateRequest(req);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.appUser = user;
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export function requireRole(roles: string[]) {
|
|
||||||
return async (req: any, res: any, next: any) => {
|
|
||||||
const user = await authenticateRequest(req);
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
return res.status(401).json({ error: 'Unauthorized' });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!roles.includes(user.role)) {
|
|
||||||
return res.status(403).json({ error: 'Forbidden' });
|
|
||||||
}
|
|
||||||
|
|
||||||
req.appUser = user;
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { query } from '../config/database.js';
|
||||||
|
|
||||||
|
export type UserRole = 'guest' | 'user' | 'admin';
|
||||||
|
export type OrgRole = 'guest' | 'user' | 'admin';
|
||||||
|
|
||||||
|
export interface IAppUser {
|
||||||
|
id: string;
|
||||||
|
externalId: string;
|
||||||
|
email: string;
|
||||||
|
name: string;
|
||||||
|
role: UserRole;
|
||||||
|
isBanned: boolean;
|
||||||
|
organizationId?: string;
|
||||||
|
organizationRole?: OrgRole;
|
||||||
|
organizationBanned?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const extractUser = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const externalId = req.headers['x-auth-user-id'] as string;
|
||||||
|
const organizationId = req.headers['x-organization-id'] as string;
|
||||||
|
|
||||||
|
if (!externalId) {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar usuário (compatível com clerk_id ou logto_id salvos como external_id no futuro,
|
||||||
|
// ou usando as colunas atuais clerk_id/logto_id)
|
||||||
|
const userRes = await query(
|
||||||
|
'SELECT * FROM gpi.users WHERE clerk_id = $1 OR logto_id = $1',
|
||||||
|
[externalId]
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = userRes.rows[0];
|
||||||
|
|
||||||
|
if (user) {
|
||||||
|
if (user.is_banned) {
|
||||||
|
return res.status(403).json({ error: 'Conta bloqueada. Entre em contato com o administrador.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const appUser: IAppUser = {
|
||||||
|
id: user.id,
|
||||||
|
externalId: user.clerk_id || user.logto_id || externalId,
|
||||||
|
email: user.email,
|
||||||
|
name: user.name,
|
||||||
|
role: user.role,
|
||||||
|
isBanned: user.is_banned,
|
||||||
|
organizationId: organizationId
|
||||||
|
};
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
// Verificar organização
|
||||||
|
const orgRes = await query(
|
||||||
|
'SELECT * FROM gpi.organizations WHERE id = $1 OR clerk_id = $1 OR logto_id = $1',
|
||||||
|
[organizationId]
|
||||||
|
);
|
||||||
|
const org = orgRes.rows[0];
|
||||||
|
|
||||||
|
if (org) {
|
||||||
|
if (org.is_banned) {
|
||||||
|
return res.status(403).json({ error: 'Acesso bloqueado: Esta organização está suspensa.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Buscar papel na organização
|
||||||
|
const memberRes = await query(
|
||||||
|
'SELECT role, is_banned FROM gpi.user_organizations WHERE user_id = $1 AND organization_id = $2',
|
||||||
|
[user.id, org.id]
|
||||||
|
);
|
||||||
|
const member = memberRes.rows[0];
|
||||||
|
|
||||||
|
if (member) {
|
||||||
|
if (member.is_banned) {
|
||||||
|
return res.status(403).json({ error: 'Acesso bloqueado nesta organização.' });
|
||||||
|
}
|
||||||
|
appUser.organizationRole = member.role;
|
||||||
|
appUser.role = member.role; // Override global role
|
||||||
|
} else {
|
||||||
|
appUser.organizationRole = 'guest';
|
||||||
|
appUser.role = 'guest';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req.appUser = appUser;
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error extracting user:', error);
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireRole = (allowedRoles: OrgRole[]) => {
|
||||||
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
if (!req.appUser) {
|
||||||
|
return res.status(401).json({ error: 'Autenticação necessária.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.appUser.email === 'admtracksteel@gmail.com') {
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
const effectiveRole = req.appUser.organizationRole || req.appUser.role;
|
||||||
|
|
||||||
|
if (!allowedRoles.includes(effectiveRole as OrgRole)) {
|
||||||
|
return res.status(403).json({ error: 'Acesso negado. Permissões insuficientes.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireAdmin = requireRole(['admin']);
|
||||||
|
export const requireUser = requireRole(['user', 'admin']);
|
||||||
|
|
||||||
|
export const canEdit = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
if (!req.appUser) {
|
||||||
|
return res.status(401).json({ error: 'Autenticação necessária.' });
|
||||||
|
}
|
||||||
|
const effectiveRole = req.appUser.organizationRole || req.appUser.role;
|
||||||
|
if (effectiveRole === 'guest') {
|
||||||
|
return res.status(403).json({ error: 'Convidados não podem editar.' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const requireDeveloper = (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
if (!req.appUser) {
|
||||||
|
return res.status(401).json({ error: 'Autenticação necessária.' });
|
||||||
|
}
|
||||||
|
if (req.appUser.email !== 'admtracksteel@gmail.com') {
|
||||||
|
return res.status(403).json({ error: 'Acesso restrito ao desenvolvedor.' });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as appRecordController from '../controllers/applicationRecordController.js';
|
import * as appRecordController from '../controllers/applicationRecordController.js';
|
||||||
import { extractUser, requireUser } from '../middleware/authMiddleware.js';
|
import { extractUser, requireUser } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { login, register, getMe } from '../controllers/authController.js';
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
|
router.post('/login', login);
|
||||||
|
router.post('/register', register);
|
||||||
|
router.get('/me', getMe);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router, Request, Response } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
import { backupService } from '../services/backupService.js';
|
import { backupService } from '../services/backupService.js';
|
||||||
import { requireRole } from '../middleware/authMiddleware.js';
|
import { requireRole } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|||||||
@@ -3,22 +3,42 @@ import multer from 'multer';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import * as dataSheetController from '../controllers/dataSheetController.js';
|
import * as dataSheetController from '../controllers/dataSheetController.js';
|
||||||
|
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Configure Multer
|
||||||
const storage = multer.diskStorage({
|
const storage = multer.diskStorage({
|
||||||
destination: (req, file, cb) => cb(null, os.tmpdir()),
|
destination: (req, file, cb) => {
|
||||||
filename: (req, file, cb) => cb(null, `${Date.now()}-${uuidv4()}${path.extname(file.originalname)}`)
|
cb(null, os.tmpdir());
|
||||||
|
},
|
||||||
|
filename: (req, file, cb) => {
|
||||||
|
const uniqueSuffix = `${Date.now()}-${uuidv4()}`;
|
||||||
|
cb(null, `${uniqueSuffix}${path.extname(file.originalname)}`);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const upload = multer({ storage, limits: { fileSize: 10 * 1024 * 1024 } });
|
const upload = multer({
|
||||||
|
storage,
|
||||||
|
limits: { fileSize: 10 * 1024 * 1024 }, // 10MB limit
|
||||||
|
fileFilter: (req, file, cb) => {
|
||||||
|
if (file.mimetype === 'application/pdf' || file.mimetype.startsWith('image/')) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(new Error('Only PDF and image files are allowed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Public routes (read-only)
|
||||||
router.get('/', dataSheetController.getAllDataSheets);
|
router.get('/', dataSheetController.getAllDataSheets);
|
||||||
router.get('/file/:id', dataSheetController.getFile);
|
router.get('/file/:id', dataSheetController.getFile);
|
||||||
router.post('/', upload.single('file'), dataSheetController.createDataSheet);
|
|
||||||
router.post('/extract', upload.single('file'), dataSheetController.extractData);
|
// Protected routes (require edit permission)
|
||||||
router.put('/:id', upload.single('file'), dataSheetController.updateDataSheet);
|
router.post('/', extractUser, requireAdmin, upload.single('file'), dataSheetController.createDataSheet);
|
||||||
router.delete('/:id', dataSheetController.deleteDataSheet);
|
router.post('/extract', extractUser, requireAdmin, upload.single('file'), dataSheetController.extractData);
|
||||||
|
router.put('/:id', extractUser, requireAdmin, upload.single('file'), dataSheetController.updateDataSheet);
|
||||||
|
router.delete('/:id', extractUser, requireAdmin, dataSheetController.deleteDataSheet);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as geometryTypeController from '../controllers/geometryTypeController.js';
|
import * as geometryTypeController from '../controllers/geometryTypeController.js';
|
||||||
|
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/', geometryTypeController.getAllnames);
|
// Retrieve all types (public read for authenticated users, auto-seeds)
|
||||||
router.post('/restore', geometryTypeController.restoreDefaults);
|
router.get('/', extractUser, geometryTypeController.getAllnames);
|
||||||
router.post('/', geometryTypeController.createType);
|
|
||||||
router.put('/:id', geometryTypeController.updateType);
|
// Protected Routes (Edit Only)
|
||||||
router.delete('/:id', geometryTypeController.deleteType);
|
router.post('/restore', extractUser, requireAdmin, geometryTypeController.restoreDefaults);
|
||||||
|
router.post('/', extractUser, requireAdmin, geometryTypeController.createType);
|
||||||
|
router.put('/:id', extractUser, requireAdmin, geometryTypeController.updateType);
|
||||||
|
router.delete('/:id', extractUser, requireAdmin, geometryTypeController.deleteType);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as inspectionController from '../controllers/inspectionController.js';
|
import * as inspectionController from '../controllers/inspectionController.js';
|
||||||
import { extractUser, requireUser } from '../middleware/authMiddleware.js';
|
import { extractUser, requireUser } from '../middleware/roleMiddleware.js';
|
||||||
import multer from 'multer';
|
import multer from 'multer';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as instrumentController from '../controllers/instrumentController.js';
|
import * as instrumentController from '../controllers/instrumentController.js';
|
||||||
|
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/', instrumentController.getInstruments);
|
router.post('/', extractUser, requireAdmin, instrumentController.createInstrument);
|
||||||
router.post('/', instrumentController.createInstrument);
|
router.get('/', extractUser, instrumentController.getInstruments);
|
||||||
router.put('/:id', instrumentController.updateInstrument);
|
router.put('/:id', extractUser, requireAdmin, instrumentController.updateInstrument);
|
||||||
router.delete('/:id', instrumentController.deleteInstrument);
|
router.delete('/:id', extractUser, requireAdmin, instrumentController.deleteInstrument);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { sendMessage, getUnreadMessages, markMessageAsRead, getMyPendingMessages, deleteMessage, archiveMessage, recipientDeleteMessage } from '../controllers/messageController.js';
|
import { sendMessage, getUnreadMessages, markMessageAsRead, getMyPendingMessages, deleteMessage, archiveMessage, recipientDeleteMessage } from '../controllers/messageController.js';
|
||||||
import { extractUser } from '../middleware/authMiddleware.js';
|
import { extractUser } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as paintingSchemeController from '../controllers/paintingSchemeController.js';
|
import * as paintingSchemeController from '../controllers/paintingSchemeController.js';
|
||||||
|
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
// Public routes (read-only)
|
||||||
router.get('/', paintingSchemeController.getAllPaintingSchemes);
|
router.get('/', paintingSchemeController.getAllPaintingSchemes);
|
||||||
router.get('/project/:projectId', paintingSchemeController.getPaintingSchemesByProject);
|
router.get('/project/:projectId', paintingSchemeController.getPaintingSchemesByProject);
|
||||||
router.post('/', paintingSchemeController.createPaintingScheme);
|
|
||||||
router.put('/:id', paintingSchemeController.updatePaintingScheme);
|
// Protected routes (require admin permission)
|
||||||
router.delete('/:id', paintingSchemeController.deletePaintingScheme);
|
router.post('/', extractUser, requireAdmin, paintingSchemeController.createPaintingScheme);
|
||||||
|
router.put('/:id', extractUser, requireAdmin, paintingSchemeController.updatePaintingScheme);
|
||||||
|
router.delete('/:id', extractUser, requireAdmin, paintingSchemeController.deletePaintingScheme);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as partController from '../controllers/partController.js';
|
import * as partController from '../controllers/partController.js';
|
||||||
import { extractUser, requireAdmin } from '../middleware/authMiddleware.js';
|
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as projectController from '../controllers/projectController.js';
|
import * as projectController from '../controllers/projectController.js';
|
||||||
import { extractUser, requireAdmin } from '../middleware/authMiddleware.js';
|
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,40 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as stockController from '../controllers/stockController.js';
|
import * as stockController from '../controllers/stockController.js';
|
||||||
|
import { extractUser, requireAdmin, requireUser } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/', stockController.getStockItems);
|
// Retrieve all items
|
||||||
router.get('/:id', stockController.getStockItemById);
|
router.get('/', extractUser, stockController.getStockItems);
|
||||||
router.get('/:id/movements', stockController.getStockMovements);
|
|
||||||
router.get('/:id/logs', stockController.getStockAuditLogs);
|
// Retrieve Item Details
|
||||||
router.post('/', stockController.createStockItem);
|
router.get('/:id', extractUser, stockController.getStockItemById);
|
||||||
router.put('/:id', stockController.updateStockItem);
|
|
||||||
router.post('/:id/adjust', stockController.adjustStock);
|
// Retrieve movements for a specific item
|
||||||
router.post('/:id/consume', stockController.consumeStock);
|
router.get('/:id/movements', extractUser, stockController.getStockMovements);
|
||||||
router.delete('/:id', stockController.deleteStockItem);
|
|
||||||
router.put('/movements/:id', stockController.updateStockMovement);
|
// Retrieve logs for a specific item
|
||||||
router.delete('/movements/:id', stockController.deleteStockMovement);
|
router.get('/:id/logs', extractUser, stockController.getStockAuditLogs);
|
||||||
|
|
||||||
|
// Create (Entry)
|
||||||
|
router.post('/', extractUser, requireUser, stockController.createStockItem);
|
||||||
|
|
||||||
|
// Update Details (No quantity)
|
||||||
|
router.put('/:id', extractUser, requireAdmin, stockController.updateStockItem);
|
||||||
|
|
||||||
|
// Technical Adjustment (Baixa Técnica / Correção)
|
||||||
|
router.post('/:id/adjust', extractUser, requireAdmin, stockController.adjustStock);
|
||||||
|
|
||||||
|
// Consumption (Baixa por Obra)
|
||||||
|
router.post('/:id/consume', extractUser, requireUser, stockController.consumeStock);
|
||||||
|
|
||||||
|
// Delete Stock Item (and its movements)
|
||||||
|
router.delete('/:id', extractUser, requireAdmin, stockController.deleteStockItem);
|
||||||
|
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
// Movement CRUD
|
||||||
|
// -----------------------------------------------------------
|
||||||
|
router.put('/movements/:id', extractUser, requireAdmin, stockController.updateStockMovement);
|
||||||
|
router.delete('/movements/:id', extractUser, requireAdmin, stockController.deleteStockMovement);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getSettings, updateSettings, uploadLogo, serveLogo } from '../controllers/systemSettingsController.js';
|
import { getSettings, updateSettings, uploadLogo, serveLogo } from '../controllers/systemSettingsController.js';
|
||||||
import { extractUser, requireDeveloper } from '../middleware/authMiddleware.js';
|
import { extractUser, requireDeveloper } from '../middleware/roleMiddleware.js';
|
||||||
import { uploadLogoDetails } from '../middleware/uploadMiddleware.js';
|
import { uploadLogoDetails } from '../middleware/uploadMiddleware.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|||||||
@@ -1,20 +1,23 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { syncUser, getCurrentUser, getAllUsers, updateUserRole, toggleBanUser, heartbeat, getActiveUsers, deleteUser } from '../controllers/userController.js';
|
import { syncUser, getCurrentUser, getAllUsers, updateUserRole, toggleBanUser, heartbeat, getActiveUsers, deleteUser } from '../controllers/userController.js';
|
||||||
import { extractUser } from '../middleware/authMiddleware.js';
|
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Public routes (no auth required)
|
// Sync user from Auth (public - called on login)
|
||||||
router.get('/', getAllUsers);
|
router.post('/sync', syncUser);
|
||||||
|
|
||||||
|
// Get current user (requires extractUser middleware)
|
||||||
router.get('/me', extractUser, getCurrentUser);
|
router.get('/me', extractUser, getCurrentUser);
|
||||||
|
|
||||||
// Heartbeat & Presence
|
// Heartbeat & Presence
|
||||||
router.post('/heartbeat', extractUser, heartbeat);
|
router.post('/heartbeat', extractUser, heartbeat);
|
||||||
router.get('/active', extractUser, getActiveUsers);
|
router.get('/active', extractUser, getActiveUsers);
|
||||||
|
|
||||||
// Admin routes
|
// Admin-only routes
|
||||||
router.patch('/:id/role', extractUser, updateUserRole);
|
router.get('/', extractUser, requireAdmin, getAllUsers);
|
||||||
router.patch('/:id/ban', extractUser, toggleBanUser);
|
router.patch('/:id/role', extractUser, requireAdmin, updateUserRole);
|
||||||
router.delete('/:id', extractUser, deleteUser);
|
router.patch('/:id/ban', extractUser, requireAdmin, toggleBanUser);
|
||||||
|
router.delete('/:id', extractUser, requireAdmin, deleteUser);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
import { Router } from 'express';
|
import { Router } from 'express';
|
||||||
import * as yieldStudyController from '../controllers/yieldStudyController.js';
|
import * as yieldStudyController from '../controllers/yieldStudyController.js';
|
||||||
|
import { extractUser, requireAdmin } from '../middleware/roleMiddleware.js';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
|
||||||
router.get('/', yieldStudyController.getAllStudies);
|
// Public routes (read-only)
|
||||||
router.post('/', yieldStudyController.createStudy);
|
router.get('/', extractUser, yieldStudyController.getAllStudies);
|
||||||
router.put('/:id', yieldStudyController.updateStudy);
|
|
||||||
router.delete('/:id', yieldStudyController.deleteStudy);
|
// Protected routes (require admin permission)
|
||||||
|
router.post('/', extractUser, requireAdmin, yieldStudyController.createStudy);
|
||||||
|
router.put('/:id', extractUser, requireAdmin, yieldStudyController.updateStudy);
|
||||||
|
router.delete('/:id', extractUser, requireAdmin, yieldStudyController.deleteStudy);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -0,0 +1,326 @@
|
|||||||
|
-- Full Database Schema for GPI (PostgreSQL)
|
||||||
|
CREATE SCHEMA IF NOT EXISTS gpi;
|
||||||
|
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||||
|
|
||||||
|
-- 1. Organizations
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.organizations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
clerk_id TEXT UNIQUE, -- Legacy
|
||||||
|
logto_id TEXT UNIQUE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
is_banned BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 2. Users
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.users (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
clerk_id TEXT UNIQUE, -- Legacy
|
||||||
|
logto_id TEXT UNIQUE,
|
||||||
|
email TEXT UNIQUE NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
role TEXT CHECK (role IN ('guest', 'user', 'admin')) DEFAULT 'guest',
|
||||||
|
is_banned BOOLEAN DEFAULT FALSE,
|
||||||
|
last_seen_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 3. many-to-many user_organizations
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.user_organizations (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
user_id UUID REFERENCES gpi.users(id) ON DELETE CASCADE,
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
role TEXT CHECK (role IN ('guest', 'user', 'admin')) DEFAULT 'guest',
|
||||||
|
is_banned BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE (user_id, organization_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 4. Technical Data Sheets (Fichas Técnicas)
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.technical_data_sheets (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
manufacturer TEXT,
|
||||||
|
type TEXT,
|
||||||
|
file_url TEXT,
|
||||||
|
solids_volume DECIMAL,
|
||||||
|
density DECIMAL,
|
||||||
|
mixing_ratio TEXT,
|
||||||
|
yield_theoretical DECIMAL,
|
||||||
|
wft_min DECIMAL,
|
||||||
|
wft_max DECIMAL,
|
||||||
|
dft_min DECIMAL,
|
||||||
|
dft_max DECIMAL,
|
||||||
|
reducer TEXT,
|
||||||
|
yield_factor DECIMAL DEFAULT 1.0,
|
||||||
|
min_stock DECIMAL DEFAULT 0,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 5. Projects
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.projects (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
client TEXT NOT NULL,
|
||||||
|
start_date DATE,
|
||||||
|
end_date DATE,
|
||||||
|
technician TEXT,
|
||||||
|
environment TEXT,
|
||||||
|
weight_kg DECIMAL,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 6. Parts
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.parts (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
|
||||||
|
description TEXT NOT NULL,
|
||||||
|
dimensions TEXT,
|
||||||
|
weight DECIMAL,
|
||||||
|
type TEXT,
|
||||||
|
area DECIMAL,
|
||||||
|
complexity INTEGER,
|
||||||
|
quantity INTEGER DEFAULT 1,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 7. Painting Schemes
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.painting_schemes (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
type TEXT,
|
||||||
|
coat TEXT, -- Primer, Intermediate, Finish
|
||||||
|
solids_volume DECIMAL,
|
||||||
|
yield_theoretical DECIMAL,
|
||||||
|
eps_min DECIMAL,
|
||||||
|
eps_max DECIMAL,
|
||||||
|
dilution DECIMAL,
|
||||||
|
manufacturer TEXT,
|
||||||
|
color TEXT,
|
||||||
|
notes TEXT,
|
||||||
|
paint_id UUID REFERENCES gpi.technical_data_sheets(id),
|
||||||
|
thinner_id UUID REFERENCES gpi.technical_data_sheets(id),
|
||||||
|
color_hex TEXT,
|
||||||
|
thinner_symbol TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 8. Application Records (Lotes de Pintura)
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.application_records (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
|
||||||
|
coat_stage TEXT,
|
||||||
|
piece_description TEXT,
|
||||||
|
date DATE,
|
||||||
|
operator TEXT,
|
||||||
|
real_weight DECIMAL,
|
||||||
|
volume_used DECIMAL,
|
||||||
|
area_painted DECIMAL,
|
||||||
|
wet_thickness_avg DECIMAL,
|
||||||
|
dry_thickness_calc DECIMAL,
|
||||||
|
real_yield DECIMAL,
|
||||||
|
method TEXT,
|
||||||
|
diluent_used DECIMAL,
|
||||||
|
notes TEXT,
|
||||||
|
items JSONB DEFAULT '[]', -- List of {partId, quantity}
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 9. Instruments
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.instruments (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
serial_number TEXT,
|
||||||
|
type TEXT,
|
||||||
|
last_calibration DATE,
|
||||||
|
calibration_due DATE,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 10. Stock Items
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.stock_items (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
data_sheet_id UUID REFERENCES gpi.technical_data_sheets(id),
|
||||||
|
batch_number TEXT NOT NULL,
|
||||||
|
manufacturing_date DATE,
|
||||||
|
expiration_date DATE,
|
||||||
|
initial_quantity DECIMAL NOT NULL,
|
||||||
|
current_quantity DECIMAL NOT NULL,
|
||||||
|
unit TEXT DEFAULT 'L',
|
||||||
|
location TEXT,
|
||||||
|
status TEXT DEFAULT 'active',
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 11. Inspections
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.inspections (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
|
||||||
|
application_record_id UUID REFERENCES gpi.application_records(id),
|
||||||
|
stock_item_id UUID REFERENCES gpi.stock_items(id),
|
||||||
|
instrument_id UUID REFERENCES gpi.instruments(id),
|
||||||
|
type TEXT CHECK (type IN ('painting', 'surface_treatment')),
|
||||||
|
date DATE,
|
||||||
|
inspector TEXT,
|
||||||
|
part_temperature DECIMAL,
|
||||||
|
weight_kg DECIMAL,
|
||||||
|
appearance TEXT, -- approved, rejected, notes
|
||||||
|
defects TEXT,
|
||||||
|
photos TEXT[] DEFAULT '{}',
|
||||||
|
piece_description TEXT,
|
||||||
|
eps_points DECIMAL[] DEFAULT '{}',
|
||||||
|
adhesion_test TEXT,
|
||||||
|
batch TEXT,
|
||||||
|
treatment_executor TEXT,
|
||||||
|
treatment_type TEXT,
|
||||||
|
cleaning_degree TEXT,
|
||||||
|
roughness_readings DECIMAL[] DEFAULT '{}',
|
||||||
|
flash_rust TEXT,
|
||||||
|
temperature DECIMAL,
|
||||||
|
relative_humidity DECIMAL,
|
||||||
|
period TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 12. Stock Movements
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.stock_movements (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
stock_item_id UUID REFERENCES gpi.stock_items(id) ON DELETE CASCADE,
|
||||||
|
type TEXT CHECK (type IN ('in', 'out', 'adjust')),
|
||||||
|
quantity DECIMAL NOT NULL,
|
||||||
|
reason TEXT,
|
||||||
|
project_id UUID REFERENCES gpi.projects(id),
|
||||||
|
user_id TEXT, -- external_id
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 13. System Settings
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.system_settings (
|
||||||
|
settings_id TEXT PRIMARY KEY DEFAULT 'global',
|
||||||
|
app_name TEXT DEFAULT 'GPI',
|
||||||
|
app_subtitle TEXT DEFAULT 'Gestão de Pintura Industrial',
|
||||||
|
app_logo_url TEXT,
|
||||||
|
updated_by TEXT,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 14. Notifications
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.notifications (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
recipient_id UUID REFERENCES gpi.users(id) ON DELETE CASCADE,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
type TEXT CHECK (type IN ('info', 'warning', 'error', 'success')) DEFAULT 'info',
|
||||||
|
is_read BOOLEAN DEFAULT FALSE,
|
||||||
|
is_archived BOOLEAN DEFAULT FALSE,
|
||||||
|
archived_by UUID[] DEFAULT '{}',
|
||||||
|
deleted_by UUID[] DEFAULT '{}',
|
||||||
|
metadata JSONB DEFAULT '{}',
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 15. Yield Studies
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.yield_studies (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
data_sheet_id UUID REFERENCES gpi.technical_data_sheets(id),
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
target_dft DECIMAL NOT NULL,
|
||||||
|
dilution_percent DECIMAL DEFAULT 0,
|
||||||
|
categories JSONB DEFAULT '[]',
|
||||||
|
total_weight DECIMAL,
|
||||||
|
estimated_paint_volume DECIMAL,
|
||||||
|
estimated_reducer_volume DECIMAL,
|
||||||
|
estimated_paint_volume_by_area DECIMAL,
|
||||||
|
estimated_reducer_volume_by_area DECIMAL,
|
||||||
|
average_complexity DECIMAL,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 16. Stock Audit Logs
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.stock_audit_logs (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
stock_item_id UUID REFERENCES gpi.stock_items(id) ON DELETE CASCADE,
|
||||||
|
movement_id UUID,
|
||||||
|
movement_number INTEGER,
|
||||||
|
user_id TEXT, -- external_id
|
||||||
|
user_name TEXT,
|
||||||
|
action TEXT NOT NULL,
|
||||||
|
details TEXT,
|
||||||
|
old_values JSONB,
|
||||||
|
new_values JSONB,
|
||||||
|
timestamp TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 17. Messages
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.messages (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
from_user_id TEXT NOT NULL, -- external_id
|
||||||
|
to_user_id TEXT NOT NULL, -- external_id
|
||||||
|
message TEXT NOT NULL,
|
||||||
|
is_read BOOLEAN DEFAULT FALSE,
|
||||||
|
read_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
is_archived BOOLEAN DEFAULT FALSE,
|
||||||
|
is_deleted_by_recipient BOOLEAN DEFAULT FALSE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 18. Geometry Types
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.geometry_types (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
efficiency_loss DECIMAL DEFAULT 0,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
||||||
|
UNIQUE (organization_id, name)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 19. Stored Files
|
||||||
|
CREATE TABLE IF NOT EXISTS gpi.stored_files (
|
||||||
|
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
|
||||||
|
organization_id UUID REFERENCES gpi.organizations(id) ON DELETE CASCADE,
|
||||||
|
filename TEXT NOT NULL,
|
||||||
|
original_name TEXT,
|
||||||
|
mimetype TEXT,
|
||||||
|
size INTEGER,
|
||||||
|
path TEXT,
|
||||||
|
category TEXT,
|
||||||
|
reference_id TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Indexes
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_projects_org ON gpi.projects(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_parts_project ON gpi.parts(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_schemes_project ON gpi.painting_schemes(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_records_project ON gpi.application_records(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_inspections_project ON gpi.inspections(project_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_stock_org ON gpi.stock_items(organization_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_notifications_recipient ON gpi.notifications(recipient_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_messages_to ON gpi.messages(to_user_id, is_read);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_geometry_types_org ON gpi.geometry_types(organization_id);
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
import dotenv from 'dotenv';
|
|
||||||
|
|
||||||
// Import Models
|
|
||||||
import Project from '../models/Project.js';
|
|
||||||
import Part from '../models/Part.js';
|
|
||||||
import PaintingScheme from '../models/PaintingScheme.js';
|
|
||||||
import ApplicationRecord from '../models/ApplicationRecord.js';
|
|
||||||
import Inspection from '../models/Inspection.js';
|
|
||||||
import TechnicalDataSheet from '../models/TechnicalDataSheet.js';
|
|
||||||
import YieldStudy from '../models/YieldStudy.js';
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const DATA_DIR = path.join(process.cwd(), 'data');
|
|
||||||
|
|
||||||
const readJson = (filename: string) => {
|
|
||||||
const filePath = path.join(DATA_DIR, filename);
|
|
||||||
if (!fs.existsSync(filePath)) return [];
|
|
||||||
return JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
||||||
};
|
|
||||||
|
|
||||||
const migrate = async () => {
|
|
||||||
try {
|
|
||||||
const uri = process.env.MONGODB_URI;
|
|
||||||
if (!uri) throw new Error('MONGODB_URI not defined');
|
|
||||||
await mongoose.connect(uri);
|
|
||||||
console.log('Connected to MongoDB for migration...');
|
|
||||||
|
|
||||||
const idMap = new Map<string, mongoose.Types.ObjectId>();
|
|
||||||
|
|
||||||
// 1. TechnicalDataSheets
|
|
||||||
console.log('Migrating TechnicalDataSheets...');
|
|
||||||
const datasheets = readJson('datasheets.json');
|
|
||||||
for (const ds of datasheets) {
|
|
||||||
const newDs = new TechnicalDataSheet({
|
|
||||||
...ds,
|
|
||||||
_id: new mongoose.Types.ObjectId()
|
|
||||||
});
|
|
||||||
await newDs.save();
|
|
||||||
idMap.set(ds.id, newDs._id as mongoose.Types.ObjectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Projects
|
|
||||||
console.log('Migrating Projects...');
|
|
||||||
const projects = readJson('projects.json');
|
|
||||||
for (const p of projects) {
|
|
||||||
const newP = new Project({
|
|
||||||
...p,
|
|
||||||
_id: new mongoose.Types.ObjectId()
|
|
||||||
});
|
|
||||||
await newP.save();
|
|
||||||
idMap.set(p.id, newP._id as mongoose.Types.ObjectId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. Parts
|
|
||||||
console.log('Migrating Parts...');
|
|
||||||
const parts = readJson('parts.json');
|
|
||||||
for (const part of parts) {
|
|
||||||
const projectId = idMap.get(part.projectId);
|
|
||||||
if (projectId) {
|
|
||||||
const newPart = new Part({ ...part, projectId });
|
|
||||||
await newPart.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. PaintingSchemes
|
|
||||||
console.log('Migrating PaintingSchemes...');
|
|
||||||
const schemes = readJson('paintingSchemes.json');
|
|
||||||
for (const s of schemes) {
|
|
||||||
const projectId = idMap.get(s.projectId);
|
|
||||||
if (projectId) {
|
|
||||||
const newS = new PaintingScheme({ ...s, projectId });
|
|
||||||
await newS.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. ApplicationRecords
|
|
||||||
console.log('Migrating ApplicationRecords...');
|
|
||||||
const records = readJson('applicationRecords.json');
|
|
||||||
for (const r of records) {
|
|
||||||
const projectId = idMap.get(r.projectId);
|
|
||||||
if (projectId) {
|
|
||||||
const newR = new ApplicationRecord({ ...r, projectId });
|
|
||||||
await newR.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Inspections
|
|
||||||
console.log('Migrating Inspections...');
|
|
||||||
const inspections = readJson('inspections.json');
|
|
||||||
for (const i of inspections) {
|
|
||||||
const projectId = idMap.get(i.projectId);
|
|
||||||
if (projectId) {
|
|
||||||
const newI = new Inspection({ ...i, projectId });
|
|
||||||
await newI.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 7. YieldStudies
|
|
||||||
console.log('Migrating YieldStudies...');
|
|
||||||
const studies = readJson('yield_studies.json');
|
|
||||||
for (const s of studies) {
|
|
||||||
const dataSheetId = idMap.get(s.dataSheetId);
|
|
||||||
if (dataSheetId) {
|
|
||||||
const newS = new YieldStudy({ ...s, dataSheetId });
|
|
||||||
await newS.save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Migration completed successfully!');
|
|
||||||
process.exit(0);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('❌ Migration failed:', error);
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
migrate();
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { supabase } from '../config/supabase.js';
|
|
||||||
|
|
||||||
const LOGTO_USER_ID = 'i4czsf1m1ns7';
|
|
||||||
|
|
||||||
async function migrateUsersToLogto() {
|
|
||||||
console.log('🔄 Iniciando migração de usuários para Logto...');
|
|
||||||
|
|
||||||
const { data: users, error: fetchError } = await supabase
|
|
||||||
.from('users')
|
|
||||||
.select('*');
|
|
||||||
|
|
||||||
if (fetchError) {
|
|
||||||
console.error('❌ Erro ao buscar usuários:', fetchError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`📋 Encontrados ${users.length} usuários`);
|
|
||||||
|
|
||||||
for (const user of users) {
|
|
||||||
if (user.clerk_id && !user.logto_id) {
|
|
||||||
console.log(`⚠️ Usuário ${user.email} tem clerk_id mas não tem logto_id`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.logto_id && user.email === 'admtracksteel@gmail.com') {
|
|
||||||
const { error: updateError } = await supabase
|
|
||||||
.from('users')
|
|
||||||
.update({ logto_id: LOGTO_USER_ID })
|
|
||||||
.eq('id', user.id);
|
|
||||||
|
|
||||||
if (updateError) {
|
|
||||||
console.error(`❌ Erro ao atualizar ${user.email}:`, updateError);
|
|
||||||
} else {
|
|
||||||
console.log(`✅ Atualizado ${user.email} com logto_id: ${LOGTO_USER_ID}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('✅ Migração concluída!');
|
|
||||||
}
|
|
||||||
|
|
||||||
migrateUsersToLogto().catch(console.error);
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
import mongoose from 'mongoose';
|
|
||||||
|
|
||||||
const MONGODB_URI = 'mongodb+srv://admtracksteel:29OHAHpKTI8XcCNt@cluster0.a4xiilu.mongodb.net/ts_gpi?retryWrites=true&w=majority&appName=Cluster0';
|
|
||||||
|
|
||||||
const UserSchema = new mongoose.Schema({
|
|
||||||
clerkId: String,
|
|
||||||
email: String,
|
|
||||||
name: String,
|
|
||||||
role: { type: String, enum: ['guest', 'user', 'admin'], default: 'guest' },
|
|
||||||
isBanned: { type: Boolean, default: false }
|
|
||||||
}, { timestamps: true });
|
|
||||||
|
|
||||||
async function fixAdmin() {
|
|
||||||
await mongoose.connect(MONGODB_URI);
|
|
||||||
console.log('✅ Conectado ao MongoDB');
|
|
||||||
|
|
||||||
const User = mongoose.model('User', UserSchema);
|
|
||||||
|
|
||||||
// Resetar m.reifonas para guest
|
|
||||||
await User.updateOne(
|
|
||||||
{ email: 'm.reifonas@gmail.com' },
|
|
||||||
{ $set: { role: 'guest' } }
|
|
||||||
);
|
|
||||||
console.log('✅ m.reifonas@gmail.com resetado para guest');
|
|
||||||
|
|
||||||
// Atualizar admtracksteel para admin (o com clerkId real)
|
|
||||||
const result = await User.updateOne(
|
|
||||||
{ email: 'admtracksteel@gmail.com', clerkId: { $ne: 'pending' } },
|
|
||||||
{ $set: { role: 'admin' } }
|
|
||||||
);
|
|
||||||
console.log('✅ admtracksteel@gmail.com atualizado para admin', result.modifiedCount > 0 ? '(sucesso)' : '(não encontrado)');
|
|
||||||
|
|
||||||
// Listar todos os usuários
|
|
||||||
const users = await User.find({});
|
|
||||||
console.log('\n📋 Usuários atualizados:');
|
|
||||||
users.forEach((u, i) => {
|
|
||||||
console.log(` ${i + 1}. ${u.email} | role: ${u.role}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
await mongoose.disconnect();
|
|
||||||
console.log('\n✅ Desconectado do MongoDB');
|
|
||||||
process.exit(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
fixAdmin().catch(err => {
|
|
||||||
console.error('❌ Erro:', err);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
@@ -1,70 +1,162 @@
|
|||||||
import { ApplicationRecord } from '../lib/compat.js';
|
import { query } from '../config/database.js';
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
interface ApplicationRecordItem {
|
||||||
export const createApplicationRecord = async (data: any & { organizationId?: string, createdBy?: string }) => {
|
partId: string;
|
||||||
const record = await ApplicationRecord.create({
|
quantity: number;
|
||||||
...data,
|
}
|
||||||
date: data.date ? new Date(data.date).toISOString() : null,
|
|
||||||
organization_id: data.organizationId,
|
interface ApplicationRecordData {
|
||||||
created_by: data.createdBy,
|
organizationId?: string;
|
||||||
project_id: data.projectId
|
createdBy?: string;
|
||||||
});
|
projectId: string;
|
||||||
return record;
|
coatStage: string;
|
||||||
|
pieceDescription?: string;
|
||||||
|
date?: Date;
|
||||||
|
operator?: string;
|
||||||
|
realWeight?: number;
|
||||||
|
volumeUsed?: number;
|
||||||
|
areaPainted?: number;
|
||||||
|
wetThicknessAvg?: number;
|
||||||
|
dryThicknessCalc?: number;
|
||||||
|
method?: string;
|
||||||
|
diluentUsed?: number;
|
||||||
|
notes?: string;
|
||||||
|
items?: ApplicationRecordItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createApplicationRecord = async (data: ApplicationRecordData) => {
|
||||||
|
const result = await query(
|
||||||
|
`INSERT INTO gpi.application_records (
|
||||||
|
organization_id, created_by, project_id, coat_stage, piece_description,
|
||||||
|
date, operator, real_weight, volume_used, area_painted,
|
||||||
|
wet_thickness_avg, dry_thickness_calc, method, diluent_used, notes
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
data.organizationId, data.createdBy, data.projectId, data.coatStage, data.pieceDescription,
|
||||||
|
data.date, data.operator, data.realWeight, data.volumeUsed, data.areaPainted,
|
||||||
|
data.wetThicknessAvg, data.dryThicknessCalc, data.method, data.diluentUsed, data.notes
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
const record = result.rows[0];
|
||||||
|
|
||||||
|
if (data.items && data.items.length > 0) {
|
||||||
|
for (const item of data.items) {
|
||||||
|
await query(
|
||||||
|
'INSERT INTO gpi.application_record_items (application_record_id, part_id, quantity) VALUES ($1, $2, $3)',
|
||||||
|
[record.id, item.partId, item.quantity]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return record;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getApplicationRecordsByProject = async (projectId: string, organizationId?: string) => {
|
export const getApplicationRecordsByProject = async (projectId: string, organizationId?: string) => {
|
||||||
const filter: any = { project_id: projectId };
|
let sql = 'SELECT * FROM gpi.application_records WHERE project_id = $1';
|
||||||
if (organizationId) {
|
const params: any[] = [projectId];
|
||||||
filter.organization_id = organizationId;
|
|
||||||
}
|
if (organizationId) {
|
||||||
return await ApplicationRecord.find(filter);
|
sql += ' AND organization_id = $2';
|
||||||
|
params.push(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
sql += ' ORDER BY date DESC';
|
||||||
|
|
||||||
|
const result = await query(sql, params);
|
||||||
|
|
||||||
|
// Fetch items for each record
|
||||||
|
const records = [];
|
||||||
|
for (const row of result.rows) {
|
||||||
|
const itemsResult = await query(
|
||||||
|
'SELECT part_id as "partId", quantity FROM gpi.application_record_items WHERE application_record_id = $1',
|
||||||
|
[row.id]
|
||||||
|
);
|
||||||
|
records.push({ ...row, items: itemsResult.rows });
|
||||||
|
}
|
||||||
|
|
||||||
|
return records;
|
||||||
};
|
};
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
export const updateApplicationRecord = async (
|
||||||
export const updateApplicationRecord = async (id: string, data: any, organizationId?: string, userId?: string, userRole?: string, isDeveloper: boolean = false) => {
|
id: string,
|
||||||
const existing = await ApplicationRecord.findById(id);
|
data: Partial<ApplicationRecordData>,
|
||||||
if (!existing) return null;
|
organizationId?: string,
|
||||||
|
userId?: string,
|
||||||
|
userRole?: string,
|
||||||
|
isDeveloper: boolean = false
|
||||||
|
) => {
|
||||||
|
const checkResult = await query('SELECT * FROM gpi.application_records WHERE id = $1', [id]);
|
||||||
|
if (checkResult.rows.length === 0) return null;
|
||||||
|
const existing = checkResult.rows[0];
|
||||||
|
|
||||||
// Organization Check
|
if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
||||||
if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
return null;
|
||||||
return null;
|
}
|
||||||
|
|
||||||
|
const isPowerUser = userRole === 'admin' || isDeveloper;
|
||||||
|
if (!isPowerUser && existing.created_by && existing.created_by !== userId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields: string[] = [];
|
||||||
|
const params: any[] = [id];
|
||||||
|
let paramIdx = 2;
|
||||||
|
|
||||||
|
const updatableFields = [
|
||||||
|
'coatStage', 'pieceDescription', 'date', 'operator', 'realWeight',
|
||||||
|
'volumeUsed', 'areaPainted', 'wetThicknessAvg', 'dryThicknessCalc',
|
||||||
|
'method', 'diluentUsed', 'notes'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const key of updatableFields) {
|
||||||
|
if ((data as any)[key] !== undefined) {
|
||||||
|
// Convert camelCase to snake_case
|
||||||
|
const dbKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
||||||
|
fields.push(`${dbKey} = $${paramIdx++}`);
|
||||||
|
params.push((data as any)[key]);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Role/Ownership check
|
if (fields.length > 0) {
|
||||||
const isPowerUser = userRole === 'admin' || isDeveloper;
|
await query(
|
||||||
if (!isPowerUser && existing.created_by && existing.created_by !== userId) {
|
`UPDATE gpi.application_records SET ${fields.join(', ')}, updated_at = NOW() WHERE id = $1`,
|
||||||
console.warn(`Permission Denied: User ${userId} tried to update record ${id} created by ${existing.created_by}`);
|
params
|
||||||
return null;
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle items update (replace all)
|
||||||
|
if (data.items) {
|
||||||
|
await query('DELETE FROM gpi.application_record_items WHERE application_record_id = $1', [id]);
|
||||||
|
for (const item of data.items) {
|
||||||
|
await query(
|
||||||
|
'INSERT INTO gpi.application_record_items (application_record_id, part_id, quantity) VALUES ($1, $2, $3)',
|
||||||
|
[id, item.partId, item.quantity]
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const updateData = {
|
const finalResult = await query('SELECT * FROM gpi.application_records WHERE id = $1', [id]);
|
||||||
...data,
|
const itemsFinal = await query('SELECT part_id as "partId", quantity FROM gpi.application_record_items WHERE application_record_id = $1', [id]);
|
||||||
date: data.date ? new Date(data.date).toISOString() : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
if (organizationId && !existing.organization_id) {
|
return { ...finalResult.rows[0], items: itemsFinal.rows };
|
||||||
updateData.organization_id = organizationId;
|
|
||||||
}
|
|
||||||
|
|
||||||
return await ApplicationRecord.findByIdAndUpdate(id, updateData);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteApplicationRecord = async (id: string, organizationId?: string, userId?: string, userRole?: string, isDeveloper: boolean = false) => {
|
export const deleteApplicationRecord = async (id: string, organizationId?: string, userId?: string, userRole?: string, isDeveloper: boolean = false) => {
|
||||||
const existing = await ApplicationRecord.findById(id);
|
const checkResult = await query('SELECT * FROM gpi.application_records WHERE id = $1', [id]);
|
||||||
if (!existing) return false;
|
if (checkResult.rows.length === 0) return false;
|
||||||
|
const existing = checkResult.rows[0];
|
||||||
|
|
||||||
// Organization Check
|
if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
||||||
if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Role/Ownership check
|
const isPowerUser = userRole === 'admin' || isDeveloper;
|
||||||
const isPowerUser = userRole === 'admin' || isDeveloper;
|
if (!isPowerUser && existing.created_by && existing.created_by !== userId) {
|
||||||
if (!isPowerUser && existing.created_by && existing.created_by !== userId) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
await ApplicationRecord.findByIdAndDelete(id);
|
await query('DELETE FROM gpi.application_records WHERE id = $1', [id]);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,14 @@
|
|||||||
import {
|
import Project from '../models/Project.js';
|
||||||
Project, Inspection, ApplicationRecord, TechnicalDataSheet, PaintingScheme,
|
import Inspection from '../models/Inspection.js';
|
||||||
Part, Instrument, YieldStudy, GeometryType, StockItem, StockMovement
|
import ApplicationRecord from '../models/ApplicationRecord.js';
|
||||||
} from '../lib/compat.js';
|
import TechnicalDataSheet from '../models/TechnicalDataSheet.js';
|
||||||
|
import PaintingScheme from '../models/PaintingScheme.js';
|
||||||
|
import Part from '../models/Part.js';
|
||||||
|
import Instrument from '../models/Instrument.js';
|
||||||
|
import YieldStudy from '../models/YieldStudy.js';
|
||||||
|
import GeometryType from '../models/GeometryType.js';
|
||||||
|
import StockItem from '../models/StockItem.js';
|
||||||
|
import StockMovement from '../models/StockMovement.js';
|
||||||
|
|
||||||
interface BackupData {
|
interface BackupData {
|
||||||
version: string;
|
version: string;
|
||||||
@@ -42,17 +48,17 @@ export const backupService = {
|
|||||||
stockItems,
|
stockItems,
|
||||||
stockMovements
|
stockMovements
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
Project.find({ organizationId }),
|
Project.find({ organizationId }).lean(),
|
||||||
Inspection.find({ organizationId }),
|
Inspection.find({ organizationId }).lean(),
|
||||||
ApplicationRecord.find({ organizationId }),
|
ApplicationRecord.find({ organizationId }).lean(),
|
||||||
TechnicalDataSheet.find({ organizationId }),
|
TechnicalDataSheet.find({ organizationId }).lean(),
|
||||||
PaintingScheme.find({ organizationId }),
|
PaintingScheme.find({ organizationId }).lean(),
|
||||||
Part.find({ organizationId }),
|
Part.find({ organizationId }).lean(),
|
||||||
Instrument.find({ organizationId }),
|
Instrument.find({ organizationId }).lean(),
|
||||||
YieldStudy.find({ organizationId }),
|
YieldStudy.find({ organizationId }).lean(),
|
||||||
GeometryType.find({ organizationId }),
|
GeometryType.find({ organizationId }).lean(),
|
||||||
StockItem.find({ organizationId }),
|
StockItem.find({ organizationId }).lean(),
|
||||||
StockMovement.find({ organizationId })
|
StockMovement.find({ organizationId }).lean()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const backup: BackupData = {
|
const backup: BackupData = {
|
||||||
|
|||||||
@@ -1,141 +1,133 @@
|
|||||||
import { supabase } from '../config/supabase.js';
|
import { query } from '../config/database.js';
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
const BUCKET_NAME = 'gpi-files';
|
interface DataSheetData {
|
||||||
|
organizationId?: string;
|
||||||
export const saveFileToStorage = async (localPath: string, filename: string): Promise<string> => {
|
name: string;
|
||||||
try {
|
manufacturer?: string;
|
||||||
const fileBuffer = fs.readFileSync(localPath);
|
manufacturerCode?: string;
|
||||||
const fileExt = path.extname(filename);
|
type?: string;
|
||||||
const uniqueName = `${Date.now()}-${Math.random().toString(36).substr(2, 9)}${fileExt}`;
|
minStock?: number;
|
||||||
|
typicalApplication?: string;
|
||||||
const { data, error } = await supabase.storage
|
fileId?: string;
|
||||||
.from(BUCKET_NAME)
|
fileUrl?: string;
|
||||||
.upload(uniqueName, fileBuffer, {
|
solidsVolume?: number;
|
||||||
contentType: getContentType(fileExt)
|
density?: number;
|
||||||
});
|
mixingRatio?: string;
|
||||||
|
mixingRatioWeight?: string;
|
||||||
if (error) throw error;
|
mixingRatioVolume?: string;
|
||||||
|
wftMin?: number;
|
||||||
const { data: urlData } = supabase.storage
|
wftMax?: number;
|
||||||
.from(BUCKET_NAME)
|
dftMin?: number;
|
||||||
.getPublicUrl(uniqueName);
|
dftMax?: number;
|
||||||
|
reducer?: string;
|
||||||
try { fs.unlinkSync(localPath); } catch {}
|
yieldTheoretical?: number;
|
||||||
|
dftReference?: number;
|
||||||
return urlData.publicUrl;
|
yieldFactor?: number;
|
||||||
} catch (err) {
|
dilution?: number;
|
||||||
console.error('Failed to upload file:', err);
|
notes?: string;
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteFileFromStorage = async (fileUrl: string): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const fileName = fileUrl.split('/').pop();
|
|
||||||
if (!fileName) return false;
|
|
||||||
|
|
||||||
const { error } = await supabase.storage
|
|
||||||
.from(BUCKET_NAME)
|
|
||||||
.remove([fileName]);
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return true;
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to delete file:', err);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function getContentType(ext: string): string {
|
|
||||||
const types: Record<string, string> = {
|
|
||||||
'.pdf': 'application/pdf',
|
|
||||||
'.jpg': 'image/jpeg',
|
|
||||||
'.jpeg': 'image/jpeg',
|
|
||||||
'.png': 'image/png',
|
|
||||||
'.doc': 'application/msword',
|
|
||||||
'.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
|
||||||
};
|
|
||||||
return types[ext.toLowerCase()] || 'application/octet-stream';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const saveFileToGridFS = saveFileToStorage;
|
|
||||||
export const deleteFileFromGridFS = deleteFileFromStorage;
|
|
||||||
|
|
||||||
export const getFileStream = (fileUrl: string) => {
|
|
||||||
return fileUrl;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const migrateFilesToGridFS = async () => {
|
|
||||||
console.log('ℹ️ File migration skipped - using Supabase Storage');
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAllDataSheets = async (organizationId?: string) => {
|
export const getAllDataSheets = async (organizationId?: string) => {
|
||||||
try {
|
let sql = 'SELECT * FROM gpi.technical_data_sheets';
|
||||||
let query = supabase.from('technical_data_sheets').select('*');
|
const params: any[] = [];
|
||||||
if (organizationId) {
|
|
||||||
query = query.eq('organization_id', organizationId);
|
if (organizationId) {
|
||||||
}
|
sql += ' WHERE (organization_id = $1 OR organization_id IS NULL)';
|
||||||
const { data, error } = await query;
|
params.push(organizationId);
|
||||||
if (error && error.code !== '42P01') throw error;
|
}
|
||||||
return data || [];
|
|
||||||
} catch (err) {
|
sql += ' ORDER BY created_at DESC';
|
||||||
console.log('Error fetching datasheets:', err);
|
|
||||||
return [];
|
const result = await query(sql, params);
|
||||||
|
return result.rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const matchSheets = async (searchTerm: string, organizationId?: string) => {
|
||||||
|
let sql = `
|
||||||
|
SELECT * FROM gpi.technical_data_sheets
|
||||||
|
WHERE (name ILIKE $1 OR manufacturer ILIKE $1 OR type ILIKE $1)`;
|
||||||
|
const params: any[] = [`%${searchTerm}%`];
|
||||||
|
|
||||||
|
if (organizationId) {
|
||||||
|
sql += ' AND (organization_id = $2 OR organization_id IS NULL)';
|
||||||
|
params.push(organizationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await query(sql, params);
|
||||||
|
return result.rows;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createDataSheet = async (data: DataSheetData) => {
|
||||||
|
const result = await query(
|
||||||
|
`INSERT INTO gpi.technical_data_sheets (
|
||||||
|
organization_id, name, manufacturer, manufacturer_code, type,
|
||||||
|
min_stock, typical_application, file_id, file_url, solids_volume,
|
||||||
|
density, mixing_ratio, mixing_ratio_weight, mixing_ratio_volume,
|
||||||
|
wft_min, wft_max, dft_min, dft_max, reducer, yield_theoretical,
|
||||||
|
dft_reference, yield_factor, dilution, notes
|
||||||
|
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $22, $23, $24)
|
||||||
|
RETURNING *`,
|
||||||
|
[
|
||||||
|
data.organizationId, data.name, data.manufacturer, data.manufacturerCode, data.type,
|
||||||
|
data.minStock, data.typicalApplication, data.fileId, data.fileUrl, data.solidsVolume,
|
||||||
|
data.density, data.mixingRatio, data.mixingRatioWeight, data.mixingRatioVolume,
|
||||||
|
data.wftMin, data.wftMax, data.dftMin, data.dftMax, data.reducer, data.yieldTheoretical,
|
||||||
|
data.dftReference, data.yieldFactor, data.dilution, data.notes
|
||||||
|
]
|
||||||
|
);
|
||||||
|
return result.rows[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateDataSheet = async (id: string, data: Partial<DataSheetData>, organizationId?: string) => {
|
||||||
|
const checkResult = await query('SELECT * FROM gpi.technical_data_sheets WHERE id = $1', [id]);
|
||||||
|
if (checkResult.rows.length === 0) return null;
|
||||||
|
const existing = checkResult.rows[0];
|
||||||
|
|
||||||
|
if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const fields: string[] = [];
|
||||||
|
const params: any[] = [id];
|
||||||
|
let paramIdx = 2;
|
||||||
|
|
||||||
|
const updatableFields = [
|
||||||
|
'name', 'manufacturer', 'manufacturerCode', 'type', 'minStock',
|
||||||
|
'typicalApplication', 'fileId', 'fileUrl', 'solidsVolume', 'density',
|
||||||
|
'mixingRatio', 'mixingRatioWeight', 'mixingRatioVolume', 'wftMin',
|
||||||
|
'wftMax', 'dftMin', 'dftMax', 'reducer', 'yieldTheoretical',
|
||||||
|
'dftReference', 'yieldFactor', 'dilution', 'notes'
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const key of updatableFields) {
|
||||||
|
if ((data as any)[key] !== undefined) {
|
||||||
|
const dbKey = key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
||||||
|
fields.push(`${dbKey} = $${paramIdx++}`);
|
||||||
|
params.push((data as any)[key]);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const uploadDataSheetFile = async (file: any, organizationId: string) => {
|
if (fields.length === 0) return existing;
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('technical_data_sheets')
|
|
||||||
.insert({
|
|
||||||
organization_id: organizationId,
|
|
||||||
name: file.originalname,
|
|
||||||
file_url: file.path
|
|
||||||
})
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
const updateSql = `
|
||||||
return data;
|
UPDATE gpi.technical_data_sheets
|
||||||
};
|
SET ${fields.join(', ')}, updated_at = NOW()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING *`;
|
||||||
|
|
||||||
export const getDataSheets = async (organizationId: string) => {
|
const result = await query(updateSql, params);
|
||||||
return getAllDataSheets(organizationId);
|
return result.rows[0];
|
||||||
};
|
|
||||||
|
|
||||||
export const createDataSheet = async (data: any) => {
|
|
||||||
const { data: sheet, error } = await supabase
|
|
||||||
.from('technical_data_sheets')
|
|
||||||
.insert(data)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error) throw error;
|
|
||||||
return sheet;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateDataSheet = async (id: string, data: any, organizationId?: string) => {
|
|
||||||
const { data: sheet, error } = await supabase
|
|
||||||
.from('technical_data_sheets')
|
|
||||||
.update(data)
|
|
||||||
.eq('id', id)
|
|
||||||
.select()
|
|
||||||
.single();
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
|
||||||
return sheet;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteDataSheet = async (id: string, organizationId?: string) => {
|
export const deleteDataSheet = async (id: string, organizationId?: string) => {
|
||||||
const { error } = await supabase
|
const checkResult = await query('SELECT * FROM gpi.technical_data_sheets WHERE id = $1', [id]);
|
||||||
.from('technical_data_sheets')
|
if (checkResult.rows.length === 0) return false;
|
||||||
.delete()
|
const existing = checkResult.rows[0];
|
||||||
.eq('id', id);
|
|
||||||
|
|
||||||
if (error && error.code !== '42P01') throw error;
|
if (organizationId && existing.organization_id && existing.organization_id !== organizationId) {
|
||||||
return true;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await query('DELETE FROM gpi.technical_data_sheets WHERE id = $1', [id]);
|
||||||
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log('✅ DataSheetService loaded with Supabase Storage');
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user