Compare commits

..

16 Commits

Author SHA1 Message Date
d3ed824d81 docs: implement Global User Rules for automation and Cronos monitoring 2026-04-03 21:07:47 +00:00
45fdf7110e 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:42:45 2026-04-03 20:42:45 +00:00
b4eee298a8 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:42:16 2026-04-03 20:42:16 +00:00
e8ecac05d8 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:38:03 2026-04-03 20:38:03 +00:00
0dace9ee00 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:31:33 2026-04-03 20:31:33 +00:00
a927c01269 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:30:43 2026-04-03 20:30:43 +00:00
210a5c69f9 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:24:53 2026-04-03 20:24:53 +00:00
31d602bb1b 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:14:10 2026-04-03 20:14:10 +00:00
58343be771 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:06:02 2026-04-03 20:06:02 +00:00
34f60b25e6 🚀 Auto-deploy: GPI atualizado em 03/04/2026 20:05:48 2026-04-03 20:05:48 +00:00
242d67c509 🚀 Auto-deploy: GPI atualizado em 03/04/2026 19:58:11 2026-04-03 19:58:11 +00:00
9a3874bd61 🚀 Auto-deploy: GPI atualizado em 03/04/2026 19:56:31 2026-04-03 19:56:31 +00:00
2ddc8b886a fix: global camelCase mapping for all technical data tables 2026-04-03 18:55:39 +00:00
e1453ada14 migracao 2026-04-03 18:34:19 +00:00
dd06fd1196 Finalizando migração de dados do MongoDB Atlas para Supabase: corrigindo esquemas de campos decimais e garantindo integridade de relacionamento de projetos 2026-04-03 18:15:32 +00:00
5c24783320 banco reestabelecido 2026-04-03 18:09:28 +00:00
27 changed files with 1293 additions and 319 deletions

View File

@@ -75,6 +75,15 @@ 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:

57
bulk_migration_final.sql Normal file
View File

@@ -0,0 +1,57 @@
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);

31
create_public_views.sql Normal file
View File

@@ -0,0 +1,31 @@
-- 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;

337
full_schema.sql Normal file
View File

@@ -0,0 +1,337 @@
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;

147
migrate.js Normal file
View File

@@ -0,0 +1,147 @@
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();

143
package-lock.json generated
View File

@@ -24,6 +24,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"jose": "^5.2.0", "jose": "^5.2.0",
"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",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",
@@ -2389,6 +2390,15 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/@mongodb-js/saslprep": {
"version": "1.4.6",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.6.tgz",
"integrity": "sha512-y+x3H1xBZd38n10NZF/rEBlvDOOMQ6LKUTHqr8R9VkJ+mmQOYtJFxIlkkK8fZrtOiL6VixbOBWMbZGBdal3Z1g==",
"license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"dev": true, "dev": true,
@@ -3595,6 +3605,21 @@
"version": "10.0.0", "version": "10.0.0",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
"license": "MIT"
},
"node_modules/@types/whatwg-url": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
"integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
"license": "MIT",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/@types/ws": { "node_modules/@types/ws": {
"version": "8.18.1", "version": "8.18.1",
"license": "MIT", "license": "MIT",
@@ -5022,6 +5047,15 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
} }
}, },
"node_modules/bson": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-7.2.0.tgz",
"integrity": "sha512-YCEo7KjMlbNlyHhz7zAZNDpIpQbd+wOEHJYezv0nMYTn4x31eIUM2yomNNubclAt63dObUzKHWsBLJ9QcZNSnQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/buffer-from": { "node_modules/buffer-from": {
"version": "1.1.2", "version": "1.1.2",
"license": "MIT" "license": "MIT"
@@ -8010,6 +8044,12 @@
"node": ">= 0.8" "node": ">= 0.8"
} }
}, },
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"license": "MIT"
},
"node_modules/merge-descriptors": { "node_modules/merge-descriptors": {
"version": "2.0.0", "version": "2.0.0",
"license": "MIT", "license": "MIT",
@@ -8133,6 +8173,65 @@
"mkdirp": "bin/cmd.js" "mkdirp": "bin/cmd.js"
} }
}, },
"node_modules/mongodb": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.1.1.tgz",
"integrity": "sha512-067DXiMjcpYQl6bGjWQoTUEE9UoRViTtKFcoqX7z08I+iDZv/emH1g8XEFiO3qiDfXAheT5ozl1VffDTKhIW/w==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.3.0",
"bson": "^7.1.1",
"mongodb-connection-string-url": "^7.0.0"
},
"engines": {
"node": ">=20.19.0"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.806.0",
"@mongodb-js/zstd": "^7.0.0",
"gcp-metadata": "^7.0.1",
"kerberos": "^7.0.0",
"mongodb-client-encryption": ">=7.0.0 <7.1.0",
"snappy": "^7.3.2",
"socks": "^2.8.6"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.1.tgz",
"integrity": "sha512-h0AZ9A7IDVwwHyMxmdMXKy+9oNlF0zFoahHiX3vQ8e3KFcSP3VmsmfvtRSuLPxmyv2vjIDxqty8smTgie/SNRQ==",
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^13.0.0",
"whatwg-url": "^14.1.0"
},
"engines": {
"node": ">=20.19.0"
}
},
"node_modules/mri": { "node_modules/mri": {
"version": "1.2.0", "version": "1.2.0",
"dev": true, "dev": true,
@@ -8769,7 +8868,6 @@
}, },
"node_modules/punycode": { "node_modules/punycode": {
"version": "2.3.1", "version": "2.3.1",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6" "node": ">=6"
@@ -9695,6 +9793,15 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"license": "MIT",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/statuses": { "node_modules/statuses": {
"version": "2.0.2", "version": "2.0.2",
"license": "MIT", "license": "MIT",
@@ -10084,6 +10191,18 @@
"nodetouch": "bin/nodetouch.js" "nodetouch": "bin/nodetouch.js"
} }
}, },
"node_modules/tr46": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.1"
},
"engines": {
"node": ">=18"
}
},
"node_modules/tree-kill": { "node_modules/tree-kill": {
"version": "1.2.2", "version": "1.2.2",
"dev": true, "dev": true,
@@ -11126,6 +11245,28 @@
"version": "1.8.0", "version": "1.8.0",
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "14.2.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
"license": "MIT",
"dependencies": {
"tr46": "^5.1.0",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=18"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"license": "ISC", "license": "ISC",

View File

@@ -33,6 +33,7 @@
"express": "^5.2.1", "express": "^5.2.1",
"jose": "^5.2.0", "jose": "^5.2.0",
"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",
"prop-types": "^15.8.1", "prop-types": "^15.8.1",

1
reset_gpi.sql Normal file
View File

@@ -0,0 +1 @@
DROP SCHEMA IF EXISTS gpi CASCADE;

View File

@@ -1,4 +1,4 @@
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'; import { BrowserRouter as Router, Routes, Route, Navigate, useLocation } from 'react-router-dom';
import { AuthProvider } from './context/AuthContext'; import { AuthProvider } from './context/AuthContext';
import { useAuth } from './context/useAuth'; import { useAuth } from './context/useAuth';
import { SystemSettingsProvider } from './context/SystemSettingsContext'; import { SystemSettingsProvider } from './context/SystemSettingsContext';
@@ -17,6 +17,7 @@ import { DeveloperDashboard } from './pages/DeveloperDashboard';
import { CalculatorDashboard } from './pages/CalculatorDashboard'; 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 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 }) => {
@@ -29,14 +30,30 @@ const DeveloperRoute: React.FC<{ children: React.ReactNode }> = ({ children }) =
}; };
const AppContent: React.FC = () => { const AppContent: React.FC = () => {
const { isSignedIn, isLoading } = useAuth();
const location = useLocation();
if (isLoading) {
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
if (!isSignedIn && location.pathname !== '/callback') {
return <Login />;
}
return ( return (
<ToastProvider> <ToastProvider>
<AuthProvider>
<SystemSettingsProvider> <SystemSettingsProvider>
<NotificationProvider> <NotificationProvider>
<Layout> <Layout>
<Routes> <Routes>
<Route path="/" element={<ProjectList />} /> <Route path="/" element={<ProjectList />} />
<Route path="/login" element={<Login />} />
<Route path="/guest-dashboard" element={<GuestDashboard />} /> <Route path="/guest-dashboard" element={<GuestDashboard />} />
<Route path="/projects" element={<ProjectList />} /> <Route path="/projects" element={<ProjectList />} />
<Route path="/project/:id" element={<ProjectDetails />} /> <Route path="/project/:id" element={<ProjectDetails />} />
@@ -82,11 +99,11 @@ const AppContent: React.FC = () => {
</DeveloperRoute> </DeveloperRoute>
} }
/> />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes> </Routes>
</Layout> </Layout>
</NotificationProvider> </NotificationProvider>
</SystemSettingsProvider> </SystemSettingsProvider>
</AuthProvider>
</ToastProvider> </ToastProvider>
); );
}; };
@@ -94,7 +111,9 @@ const AppContent: React.FC = () => {
function App() { function App() {
return ( return (
<Router> <Router>
<AuthProvider>
<AppContent /> <AppContent />
</AuthProvider>
</Router> </Router>
); );
} }

View File

@@ -48,7 +48,10 @@ export const StockModal: React.FC<StockModalProps> = ({ isOpen, onClose, onSucce
if (isOpen) { if (isOpen) {
fetchDataSheets(); fetchDataSheets();
if (initialData) { if (initialData) {
setDataSheetId(typeof initialData.dataSheetId === 'object' ? initialData.dataSheetId._id : initialData.dataSheetId); const dsId = (typeof initialData.dataSheetId === 'object')
? (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 || '');
@@ -108,7 +111,8 @@ export const StockModal: React.FC<StockModalProps> = ({ isOpen, onClose, onSucce
try { try {
if (initialData) { if (initialData) {
await stockService.update(initialData._id!, payload); const itemId = initialData.id || initialData._id;
await stockService.update(itemId!, payload);
} else { } else {
await stockService.create(payload); await stockService.create(payload);
} }
@@ -147,12 +151,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 === val); const ds = dataSheets.find(d => (d.id || 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 }))} options={filteredDataSheets.map(ds => ({ label: `${ds.name} - ${ds.manufacturer}`, value: ds.id || ds._id }))}
disabled={!!initialData} // Lock product on edit disabled={!!initialData} // Lock product on edit
/> />

View File

@@ -1,7 +1,6 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import type { AppUser, UserRole } from '../types'; import type { AppUser } from '../types';
import { AuthContext } from './AuthContextType'; import { AuthContext } from './AuthContextType';
import { getUser } from '../main';
import { setApiOrganizationId } from '../services/api'; import { setApiOrganizationId } from '../services/api';
interface AuthProviderProps { interface AuthProviderProps {
@@ -25,27 +24,45 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
const [appUser, setAppUser] = useState<AppUser | null>(null); const [appUser, setAppUser] = useState<AppUser | null>(null);
useEffect(() => { useEffect(() => {
const storedUser = getUser(); const storedUser = localStorage.getItem('gpi_user');
if (storedUser) { if (storedUser) {
setAppUser({ ...defaultUser, ...storedUser, role: storedUser.role as UserRole }); try {
} else { setAppUser(JSON.parse(storedUser));
setAppUser(defaultUser); } catch (e) {
console.error("Error parsing stored user", e);
}
} }
setApiOrganizationId(DEFAULT_ORGANIZATION_ID, DEFAULT_ORGANIZATION_NAME); setApiOrganizationId(DEFAULT_ORGANIZATION_ID, DEFAULT_ORGANIZATION_NAME);
}, []); }, []);
const isDeveloper = useCallback(() => false, []); const signInWithPassword = async (password: string): Promise<boolean> => {
const isAdmin = useCallback(() => true, []); if (password === '@@Gi05Br;;') {
const isUser = useCallback(() => true, []); const adminUser: AppUser = {
const isGuest = useCallback(() => false, []); ...defaultUser,
const canEdit = useCallback(() => true, []); id: 'admin-001',
email: 'admtracksteel@gmail.com',
name: 'Administrator / DEV',
role: 'admin'
};
setAppUser(adminUser);
localStorage.setItem('gpi_user', JSON.stringify(adminUser));
return true;
}
return false;
};
const isDeveloper = useCallback(() => appUser?.email === 'admtracksteel@gmail.com', [appUser]);
const isAdmin = useCallback(() => appUser?.role === 'admin' || appUser?.email === 'admtracksteel@gmail.com', [appUser]);
const isUser = useCallback(() => !!appUser, [appUser]);
const isGuest = useCallback(() => !appUser, [appUser]);
const canEdit = useCallback(() => isAdmin(), [isAdmin]);
const refetchUser = useCallback(async () => {}, []); const refetchUser = useCallback(async () => {}, []);
const value = useMemo(() => ({ const value = useMemo(() => ({
appUser, appUser,
isLoading: false, isLoading: false,
isSignedIn: true, isSignedIn: !!appUser,
error: null, error: null,
isAdmin, isAdmin,
isUser, isUser,
@@ -53,7 +70,8 @@ export const AuthProvider: React.FC<AuthProviderProps> = ({ children }) => {
isDeveloper, isDeveloper,
canEdit, canEdit,
refetchUser, refetchUser,
}), [appUser, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser]); signInWithPassword
}), [appUser, isAdmin, isUser, isGuest, isDeveloper, canEdit, refetchUser, signInWithPassword]);
return ( return (
<AuthContext.Provider value={value}> <AuthContext.Provider value={value}>

View File

@@ -12,6 +12,7 @@ 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); export const AuthContext = createContext<AuthContextType | undefined>(undefined);

View File

@@ -1,13 +1,32 @@
import { Hammer } from "lucide-react"; import React, { useState } from 'react';
import { useLogto } from "@logto/react"; import { Hammer, Lock, ShieldCheck } from "lucide-react";
import { useAuth } from '../context/useAuth';
const CALLBACK_URL = import.meta.env.VITE_LOGTO_CALLBACK_URL || `${window.location.origin}/callback`; import { useNavigate } from 'react-router-dom';
export const Login = () => { export const Login = () => {
const { signIn } = useLogto(); const [password, setPassword] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const { signInWithPassword } = useAuth();
const navigate = useNavigate();
const handleLogin = () => { const handleSubmit = async (e: React.FormEvent) => {
signIn(CALLBACK_URL); e.preventDefault();
setError('');
setLoading(true);
try {
const success = await signInWithPassword(password);
if (success) {
navigate('/');
} else {
setError('Senha incorreta. Acesso negado.');
}
} catch (err) {
setError('Erro ao processar login.');
} finally {
setLoading(false);
}
}; };
return ( return (
@@ -19,32 +38,54 @@ 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 animate-in zoom-in duration-700"> <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">
G G
</div> </div>
<h1 className="text-3xl font-bold text-text-main tracking-tight mb-1">GPI</h1> <h1 className="text-3xl font-bold text-text-main tracking-tight mb-1">GPI RESTRICT</h1>
<p className="text-text-muted text-sm font-medium uppercase tracking-widest">Gestão de Pintura Industrial</p> <p className="text-text-muted text-[10px] font-black uppercase tracking-[0.3em]">Ambiente de Desenvolvimento</p>
</div>
{/* 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="flex items-center gap-3 mb-8 text-primary">
<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">
<div className="space-y-2">
<input
type="password"
placeholder="Digite a senha mestra..."
value={password}
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"
required
autoFocus
/>
{error && <p className="text-error text-[10px] font-bold uppercase text-center mt-2 tracking-wider">{error}</p>}
</div> </div>
{/* Login Button - Logto */}
<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">
<button <button
onClick={handleLogin} type="submit"
className="w-full flex items-center justify-center gap-3 px-6 py-4 bg-primary hover:bg-primary/90 text-white font-bold rounded-xl transition-all shadow-lg shadow-primary/20" 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"
> >
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> {loading ? (
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/> <div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/> ) : (
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/> <>
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/> <ShieldCheck size={20} />
</svg> Entrar no Sistema
Continuar com Google </>
)}
</button> </button>
</form>
</div> </div>
<div className="mt-8 flex items-center gap-2 text-text-muted/60 text-xs font-medium"> <div className="mt-8 flex items-center gap-2 text-text-muted/60 text-[10px] font-black uppercase tracking-widest">
<Hammer size={14} /> <Hammer size={12} />
<span>© 2026 GPI - Eficiência Industrial</span> <span>Desenvolvimento Ativo</span>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -10,6 +10,7 @@ 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);
@@ -17,8 +18,6 @@ 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();
@@ -135,7 +134,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
@@ -151,7 +150,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); }}

View File

@@ -79,11 +79,12 @@ export const StockDashboard: React.FC = () => {
await Promise.all( await Promise.all(
items.map(async (item) => { items.map(async (item) => {
try { try {
const movements = await stockService.getMovements(item._id!); const itemId = item.id || (item as any)._id;
movementsMap.set(item._id!, movements); if (!itemId) return;
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!, []);
} }
}) })
); );
@@ -117,8 +118,8 @@ export const StockDashboard: React.FC = () => {
const manufacturer = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).manufacturer : ''; const manufacturer = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).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)
); );
@@ -130,7 +131,8 @@ export const StockDashboard: React.FC = () => {
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 as any).name : 'Unknown';
const manufacturer = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).manufacturer : ''; const manufacturer = typeof item.dataSheetId === 'object' ? (item.dataSheetId as any).manufacturer : '';
const key = `${(item.dataSheetId as any)._id || item.dataSheetId}-${item.color}`; const dsId = (item.dataSheetId as any).id || (item.dataSheetId as any)._id || item.dataSheetId;
const key = `${dsId}-${item.color}`;
if (!groups.has(key)) { if (!groups.has(key)) {
groups.set(key, { groups.set(key, {
@@ -295,7 +297,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.toFixed(1)} {group.unit} {(group.totalQty || 0).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">
@@ -313,11 +315,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={item._id} className="bg-surface-soft/50 hover:bg-surface-hover/80 transition-colors border-l-4 border-l-primary/20"> <tr key={itemId} 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">
@@ -371,7 +373,7 @@ export const StockDashboard: React.FC = () => {
<Edit size={16} /> <Edit size={16} />
</button> </button>
<button <button
onClick={(e) => { e.stopPropagation(); handleDelete(item._id!); }} onClick={(e) => { e.stopPropagation(); handleDelete(itemId!); }}
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"
> >

View File

@@ -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 * 10000 / svFactor).toFixed(1)) ? Number((((study.targetDft || 0) * 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.toFixed(2)), litrosPeso: Number((litrosPeso || 0).toFixed(2)),
litrosArea: litrosArea > 0 ? Number(litrosArea.toFixed(2)) : undefined litrosArea: litrosArea > 0 ? Number((litrosArea || 0).toFixed(2)) : undefined
}; };
}); });
@@ -332,10 +332,10 @@ export const YieldStudyDashboard: React.FC = () => {
...study, ...study,
categories: updatedCategories, categories: updatedCategories,
totalWeight, totalWeight,
estimatedPaintVolume: Number(totalVolumeByWeight.toFixed(2)), estimatedPaintVolume: Number((totalVolumeByWeight || 0).toFixed(2)),
estimatedReducerVolume: Number(reducerVolByWeight.toFixed(2)), estimatedReducerVolume: Number((reducerVolByWeight || 0).toFixed(2)),
estimatedPaintVolumeByArea: Number(totalVolumeByArea.toFixed(2)), estimatedPaintVolumeByArea: Number((totalVolumeByArea || 0).toFixed(2)),
estimatedReducerVolumeByArea: Number(reducerVolByArea.toFixed(2)), estimatedReducerVolumeByArea: Number((reducerVolByArea || 0).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.8).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 0.8).toFixed(1)), label: `-20%` }, { dft: ((selectedStudy.targetDft || 0) * 0.8).toFixed(0), vol: Number(((selectedStudy.estimatedPaintVolume || 0) * 0.8).toFixed(1)), label: `-20%` },
{ dft: (selectedStudy.targetDft * 0.9).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 0.9).toFixed(1)), label: `-10%` }, { dft: ((selectedStudy.targetDft || 0) * 0.9).toFixed(0), vol: Number(((selectedStudy.estimatedPaintVolume || 0) * 0.9).toFixed(1)), label: `-10%` },
{ dft: selectedStudy.targetDft.toFixed(0), vol: selectedStudy.estimatedPaintVolume, label: 'ALVO' }, { dft: (selectedStudy.targetDft || 0).toFixed(0), vol: (selectedStudy.estimatedPaintVolume || 0), label: 'ALVO' },
{ dft: (selectedStudy.targetDft * 1.1).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 1.1).toFixed(1)), label: '+10%' }, { dft: ((selectedStudy.targetDft || 0) * 1.1).toFixed(0), vol: Number(((selectedStudy.estimatedPaintVolume || 0) * 1.1).toFixed(1)), label: '+10%' },
{ dft: (selectedStudy.targetDft * 1.3).toFixed(0), vol: Number((selectedStudy.estimatedPaintVolume * 1.3).toFixed(1)), label: '+30%' }, { dft: ((selectedStudy.targetDft || 0) * 1.3).toFixed(0), vol: Number(((selectedStudy.estimatedPaintVolume || 0) * 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.toFixed(1)} t</span> <span className="text-sm font-black text-text-main">{(study.totalWeight || 0).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} <span className="text-[10px] text-text-muted">μm</span></span> <span className="text-sm font-black text-text-main">{(study.targetDft || 0)} <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; const dilFactor = 100 - (selectedStudy.dilutionPercent || 0);
const svFactor = sv * dilFactor; const svFactor = sv * dilFactor;
return svFactor > 0 ? (selectedStudy.targetDft * 10000 / svFactor).toFixed(1) : '0'; return svFactor > 0 ? ((selectedStudy.targetDft || 0) * 10000 / svFactor).toFixed(1) : '0';
})() })()
} <span className="text-xs">µm</span> } <span className="text-xs">µm</span>
</div> </div>
@@ -578,7 +578,7 @@ export const YieldStudyDashboard: React.FC = () => {
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.toFixed(0)} <span className="text-xs">%</span> {(sv || 0).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)'}
@@ -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 / selectedStudy.totalWeight).toFixed(2)) : '0.00'} <span className="text-[10px] text-text-muted">L/t</span> {selectedStudy.totalWeight > 0 ? (((selectedStudy.estimatedPaintVolume || 0) / 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.toFixed(2)} TON {(selectedStudy.totalWeight || 0).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.toFixed(2)}</div> <div className="text-xl font-black">{(selectedStudy.totalWeight || 0).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 / 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 || 0) / 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.toFixed(2)}</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-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>

View File

@@ -1,17 +1,23 @@
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 { IAppUser } from '../middleware/authMiddleware.js';
export const getAllDataSheets = async (req: Request, res: Response) => { interface AuthRequest extends Request {
appUser?: IAppUser;
}
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(sheets); res.json(toCamelCase(sheets));
} catch (error: unknown) { } catch (error: unknown) {
res.json([]); res.json([]);
} }
}; };
export const extractData = async (req: Request, res: Response) => { export const extractData = async (req: AuthRequest, res: Response) => {
try { try {
const file = req.file; const file = req.file;
if (!file) { if (!file) {
@@ -23,40 +29,38 @@ export const extractData = async (req: Request, res: Response) => {
} }
}; };
export const createDataSheet = async (req: Request, res: Response) => { export const createDataSheet = async (req: AuthRequest, res: Response) => {
try { try {
const organizationId = req.appUser?.organizationId; const organizationId = req.appUser?.organizationId;
const newSheet = await dataSheetService.createDataSheet({ const payload = { ...req.body, organization_id: organizationId };
...req.body, const newSheet = await dataSheetService.createDataSheet(toSnakeCase(payload));
organization_id: organizationId res.status(201).json(toCamelCase(newSheet));
});
res.status(201).json(newSheet);
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.status(400).json({ error: (error as any).message });
} }
}; };
export const deleteDataSheet = async (req: Request, 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); await dataSheetService.deleteDataSheet(id as string);
res.status(204).send(); res.status(204).send();
} catch (error: unknown) { } catch (error: unknown) {
res.status(204).send(); res.status(500).json({ error: (error as any).message });
} }
}; };
export const updateDataSheet = async (req: Request, 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, req.body); const updatedSheet = await dataSheetService.updateDataSheet(id, toSnakeCase(req.body));
res.json(updatedSheet || req.body); res.json(toCamelCase(updatedSheet || req.body));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.status(400).json({ error: (error as any).message });
} }
}; };
export const getFile = async (req: Request, res: Response) => { export const getFile = async (req: AuthRequest, res: Response) => {
try { try {
res.status(404).json({ error: 'File not found' }); res.status(404).json({ error: 'File not found' });
} catch (error: unknown) { } catch (error: unknown) {

View File

@@ -1,26 +1,27 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { supabase } from '../config/supabase.js'; import { supabase } from '../config/supabase.js';
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
const DEFAULT_TYPES = [ const DEFAULT_TYPES = [
{ name: 'Guarda-corpo/escada', efficiency_loss: 20 }, { name: 'Guarda-corpo/escada', efficiencyLoss: 20 },
{ name: 'Vigas leves', efficiency_loss: 20 }, { name: 'Vigas leves', efficiencyLoss: 20 },
{ name: 'Vigas médias', efficiency_loss: 20 }, { name: 'Vigas médias', efficiencyLoss: 20 },
{ name: 'Vigas pesadas', efficiency_loss: 20 }, { name: 'Vigas pesadas', efficiencyLoss: 20 },
{ name: 'Chaparia comum', efficiency_loss: 20 }, { name: 'Chaparia comum', efficiencyLoss: 20 },
{ name: 'Chapas de pisos (>0,5m²)', efficiency_loss: 20 }, { name: 'Chapas de pisos (>0,5m²)', efficiencyLoss: 20 },
{ name: 'Calhas', efficiency_loss: 20 }, { name: 'Calhas', efficiencyLoss: 20 },
{ name: 'Cantoneiras', efficiency_loss: 20 }, { name: 'Cantoneiras', efficiencyLoss: 20 },
{ name: 'Telhas', efficiency_loss: 20 }, { name: 'Telhas', efficiencyLoss: 20 },
{ name: 'Tubulações (ret/red) <100mm', efficiency_loss: 20 }, { name: 'Tubulações (ret/red) <100mm', efficiencyLoss: 20 },
{ name: 'Tubulações (ret/red) >100mm', efficiency_loss: 20 }, { name: 'Tubulações (ret/red) >100mm', efficiencyLoss: 20 },
{ name: 'Peças diversas (outras)', efficiency_loss: 20 } { name: 'Peças diversas (outras)', efficiencyLoss: 20 }
]; ];
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 { data, error } = await supabase.from('geometry_types').select('*');
if (error && error.code !== '42P01') throw error; if (error && error.code !== '42P01') throw error;
res.json(data || []); res.json(toCamelCase(data || []));
} catch (error: unknown) { } catch (error: unknown) {
res.json(DEFAULT_TYPES); res.json(DEFAULT_TYPES);
} }
@@ -36,13 +37,18 @@ export const restoreDefaults = async (req: Request, res: Response) => {
export const createType = async (req: Request, res: Response) => { export const createType = async (req: Request, res: Response) => {
try { try {
const payload = toSnakeCase({
...req.body,
organizationId: (req as any).appUser?.organizationId
});
const { data, error } = await supabase const { data, error } = await supabase
.from('geometry_types') .from('geometry_types')
.insert({ ...req.body, organization_id: req.appUser?.organizationId }) .insert(payload)
.select() .select()
.single(); .single();
if (error) throw error; if (error) throw error;
res.status(201).json(data); res.status(201).json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.json(req.body);
} }
@@ -52,12 +58,12 @@ export const updateType = async (req: Request, res: Response) => {
try { try {
const { data, error } = await supabase const { data, error } = await supabase
.from('geometry_types') .from('geometry_types')
.update(req.body) .update(toSnakeCase(req.body))
.eq('id', req.params.id) .eq('id', req.params.id)
.select() .select()
.single(); .single();
if (error) throw error; if (error) throw error;
res.json(data); res.json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.json(req.body);
} }

View File

@@ -1,18 +1,21 @@
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';
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?.email || 'guest';
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 { try {
await notificationService.create({ await notificationService.create({
@@ -25,7 +28,7 @@ export const createInspection = async (req: Request, res: Response) => {
} catch (e) { /* ignore notification errors */ } } catch (e) { /* ignore notification errors */ }
} }
res.status(201).json(inspection); res.status(201).json(toCamelCase(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 });
@@ -37,7 +40,7 @@ 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(inspections); res.json(toCamelCase(inspections || []));
} catch (error: unknown) { } catch (error: unknown) {
res.json([]); res.json([]);
} }
@@ -47,10 +50,10 @@ export const updateInspection = async (req: Request, res: Response) => {
try { try {
const inspection = await inspectionService.updateInspection( const inspection = await inspectionService.updateInspection(
req.params.id as string, req.params.id as string,
req.body toSnakeCase(req.body)
); );
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(inspection); res.json(toCamelCase(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 });
@@ -59,7 +62,7 @@ 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 {
const success = await inspectionService.deleteInspection(req.params.id as string); await inspectionService.deleteInspection(req.params.id as string);
res.status(204).send(); res.status(204).send();
} catch (error: unknown) { } catch (error: unknown) {
res.status(204).send(); res.status(204).send();
@@ -72,7 +75,7 @@ export const getAllInspections = async (req: Request, res: Response) => {
const inspections = organizationId const inspections = organizationId
? await inspectionService.getInspectionsByOrganization(organizationId) ? await inspectionService.getInspectionsByOrganization(organizationId)
: await inspectionService.getInspectionStats(); : await inspectionService.getInspectionStats();
res.json(inspections); res.json(toCamelCase(inspections || []));
} catch (error: unknown) { } catch (error: unknown) {
res.json({ total: 0, inspections: [] }); res.json({ total: 0, inspections: [] });
} }

View File

@@ -1,13 +1,15 @@
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 scheme = await paintingSchemeService.createPaintingScheme({ ...req.body, organizationId }); const schemeData = toSnakeCase({ ...req.body, organizationId });
res.status(201).json(scheme); const scheme = await paintingSchemeService.createPaintingScheme(schemeData);
res.status(201).json(toCamelCase(scheme));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.status(400).json({ error: (error as any).message });
} }
}; };
@@ -15,7 +17,7 @@ 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 schemes = await paintingSchemeService.getPaintingSchemesByProject(projectId as string);
res.json(schemes); res.json(toCamelCase(schemes || []));
} catch (error: unknown) { } catch (error: unknown) {
res.json([]); res.json([]);
} }
@@ -23,10 +25,10 @@ export const getPaintingSchemesByProject = async (req: Request, res: Response) =
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, req.body); const scheme = await paintingSchemeService.updatePaintingScheme(req.params.id as string, toSnakeCase(req.body));
res.json(scheme || req.body); res.json(toCamelCase(scheme || req.body));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.status(400).json({ error: (error as any).message });
} }
}; };
@@ -35,7 +37,7 @@ export const deletePaintingScheme = async (req: Request, res: Response) => {
await paintingSchemeService.deletePaintingScheme(req.params.id as string); await paintingSchemeService.deletePaintingScheme(req.params.id as string);
res.status(204).send(); res.status(204).send();
} catch (error: unknown) { } catch (error: unknown) {
res.status(204).send(); res.status(500).json({ error: (error as any).message });
} }
}; };
@@ -43,7 +45,7 @@ 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(schemes); res.json(toCamelCase(schemes || []));
} catch (error: unknown) { } catch (error: unknown) {
res.json([]); res.json([]);
} }

View File

@@ -1,6 +1,7 @@
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/authMiddleware.js';
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
interface AuthRequest extends Request { interface AuthRequest extends Request {
appUser?: IAppUser; appUser?: IAppUser;
@@ -8,14 +9,12 @@ 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 part = await partService.createPart({ ...req.body, organizationId }); const payload = toSnakeCase({ ...req.body, organizationId });
console.log('[CREATE PART] Success:', part); const part = await partService.createPart(payload);
res.status(201).json(part); res.status(201).json(toCamelCase(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 });
} }
}; };
@@ -26,7 +25,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(parts); res.json(toCamelCase(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 });
@@ -37,8 +36,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, req.body, organizationId, isGlobalAdmin); const part = await partService.updatePart(req.params.id as string, toSnakeCase(req.body), organizationId, isGlobalAdmin);
res.json(part); res.json(toCamelCase(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 });
@@ -47,9 +46,7 @@ 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 {
const organizationId = req.appUser?.organizationId; await partService.deletePart(req.params.id as string);
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';
@@ -60,9 +57,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;
const isGlobalAdmin = req.appUser?.email === 'admtracksteel@gmail.com'; if (!organizationId) return res.json([]);
const parts = await partService.getAllParts(organizationId, isGlobalAdmin); const parts = await partService.getPartsByOrganization(organizationId);
res.json(parts); res.json(toCamelCase(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 });

View File

@@ -2,6 +2,7 @@ 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/authMiddleware.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;
@@ -11,7 +12,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(project); res.status(201).json(toCamelCase(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 });
@@ -23,14 +24,11 @@ export const getAllProjects = 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 { status } = req.query; const { status } = req.query;
console.log('getAllProjects controller:', { organizationId, isGlobalAdmin, status });
const projects = await projectService.getAllProjects(organizationId, isGlobalAdmin, status as string); const projects = await projectService.getAllProjects(organizationId, isGlobalAdmin, status as string);
console.log('getAllProjects result:', projects?.length); res.json(toCamelCase(projects));
res.json(projects);
} catch (error: unknown) { } catch (error: unknown) {
console.error('Error in getAllProjects controller:', error); console.error('Error in getAllProjects controller:', error);
const message = error instanceof Error ? error.message : JSON.stringify(error); const message = error instanceof Error ? error.message : JSON.stringify(error);
console.log('Sending error response:', message);
res.status(500).json({ error: message }); res.status(500).json({ error: message });
} }
}; };
@@ -40,7 +38,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(project); res.json(toCamelCase(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 });
@@ -51,7 +49,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(projects); res.json(toCamelCase(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 });
@@ -61,7 +59,7 @@ 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 project = await projectService.getProjectById(req.params.id as string);
res.json(project); res.json(toCamelCase(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 });
@@ -83,7 +81,7 @@ export const updateProject = async (req: AuthRequest, res: Response) => {
}); });
} }
res.json(project); res.json(toCamelCase(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 });

View File

@@ -1,83 +1,99 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { supabase } from '../config/supabase.js'; import { supabase } from '../config/supabase.js';
import { toCamelCase, toSnakeCase } from '../utils/caseMapper.js';
import { IAppUser } from '../middleware/authMiddleware.js';
export const getStockItems = async (req: Request, res: Response) => { interface AuthRequest extends Request {
appUser?: IAppUser;
}
export const getStockItems = async (req: AuthRequest, res: Response) => {
try { try {
const { data, error } = await supabase.from('stock_items').select('*'); const { data: items, error: itemsError } = await supabase.from('stock_items').select('*');
if (error && error.code !== '42P01') throw error; if (itemsError && itemsError.code !== '42P01') throw itemsError;
res.json(data || []); 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) { } catch (error: unknown) {
console.error('Error fetching stock items:', error);
res.json([]); res.json([]);
} }
}; };
export const getStockItemById = async (req: Request, res: Response) => { export const getStockItemById = async (req: AuthRequest, res: Response) => {
try { try {
const { data, error } = await supabase.from('stock_items').select('*').eq('id', req.params.id).single(); const { data, error } = await supabase.from('stock_items').select('*').eq('id', req.params.id).single();
if (error) throw error; if (error) throw error;
res.json(data); res.json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.json(null); res.json(null);
} }
}; };
export const getStockMovements = async (req: Request, res: Response) => { export const getStockMovements = async (req: AuthRequest, res: Response) => {
try { try {
const { data, error } = await supabase.from('stock_movements').select('*').eq('stock_item_id', req.params.id); const { data, error } = await supabase.from('stock_movements').select('*').eq('stock_item_id', req.params.id);
if (error && error.code !== '42P01') throw error; if (error && error.code !== '42P01') throw error;
res.json(data || []); res.json(toCamelCase(data || []));
} catch (error: unknown) { } catch (error: unknown) {
res.json([]); res.json([]);
} }
}; };
export const getStockAuditLogs = async (req: Request, res: Response) => { export const getStockAuditLogs = async (req: AuthRequest, res: Response) => {
try { try {
const { data, error } = await supabase.from('stock_audit_logs').select('*').eq('stock_item_id', req.params.id); const { data, error } = await supabase.from('stock_audit_logs').select('*').eq('stock_item_id', req.params.id);
if (error && error.code !== '42P01') throw error; if (error && error.code !== '42P01') throw error;
res.json(data || []); res.json(toCamelCase(data || []));
} catch (error: unknown) { } catch (error: unknown) {
res.json([]); res.json([]);
} }
}; };
export const createStockItem = async (req: Request, res: Response) => { export const createStockItem = async (req: AuthRequest, res: Response) => {
try { try {
const { data, error } = await supabase.from('stock_items').insert({ ...req.body, organization_id: req.appUser?.organizationId }).select().single(); const itemData = toSnakeCase({ ...req.body, organizationId: req.appUser?.organizationId });
const { data, error } = await supabase.from('stock_items').insert(itemData).select().single();
if (error) throw error; if (error) throw error;
res.status(201).json(data); res.status(201).json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' }); res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
} }
}; };
export const updateStockItem = async (req: Request, res: Response) => { export const updateStockItem = async (req: AuthRequest, res: Response) => {
try { try {
const { data, error } = await supabase.from('stock_items').update(req.body).eq('id', req.params.id).select().single(); const updateData = toSnakeCase(req.body);
const { data, error } = await supabase.from('stock_items').update(updateData).eq('id', req.params.id).select().single();
if (error) throw error; if (error) throw error;
res.json(data); res.json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
} }
}; };
export const adjustStock = async (req: Request, res: Response) => { export const deleteStockItem = async (req: AuthRequest, res: Response) => {
try {
res.json({ success: true });
} catch (error: unknown) {
res.json({ success: true });
}
};
export const consumeStock = async (req: Request, res: Response) => {
try {
res.json({ success: true });
} catch (error: unknown) {
res.json({ success: true });
}
};
export const deleteStockItem = async (req: Request, res: Response) => {
try { try {
await supabase.from('stock_items').delete().eq('id', req.params.id); await supabase.from('stock_items').delete().eq('id', req.params.id);
res.status(204).send(); res.status(204).send();
@@ -85,22 +101,52 @@ export const deleteStockItem = async (req: Request, res: Response) => {
res.status(204).send(); res.status(204).send();
} }
}; };
export const adjustStock = async (req: AuthRequest, res: Response) => {
export const updateStockMovement = async (req: Request, res: Response) => {
try { try {
const { data, error } = await supabase.from('stock_movements').update(req.body).eq('id', req.params.id).select().single(); const { id } = req.params;
const updateData = toSnakeCase(req.body);
const { data, error } = await supabase.from('stock_items').update(updateData).eq('id', id).select().single();
if (error) throw error; if (error) throw error;
res.json(data); res.json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
} }
}; };
export const deleteStockMovement = async (req: Request, res: Response) => { export const consumeStock = async (req: AuthRequest, res: Response) => {
try { try {
await supabase.from('stock_movements').delete().eq('id', req.params.id); const { id } = req.params;
res.status(204).send(); const { quantity } = req.body;
const { data: item, error: fetchError } = await supabase.from('stock_items').select('quantity').eq('id', id).single();
if (fetchError) throw fetchError;
const newQuantity = (item.quantity || 0) - (quantity || 0);
const { data, error } = await supabase.from('stock_items').update({ quantity: newQuantity }).eq('id', id).select().single();
if (error) throw error;
res.json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.status(204).send(); res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
}
};
export const updateStockMovement = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
const { data, error } = await supabase.from('stock_movements').update(req.body).eq('id', id).select().single();
if (error) throw error;
res.json(toCamelCase(data));
} catch (error: unknown) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
}
};
export const deleteStockMovement = async (req: AuthRequest, res: Response) => {
try {
const { id } = req.params;
await supabase.from('stock_movements').delete().eq('id', id);
res.status(204).send();
} catch (error: unknown) {
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
} }
}; };

View File

@@ -1,11 +1,12 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { supabase } from '../config/supabase.js'; import { supabase } from '../config/supabase.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 { data, error } = await supabase.from('yield_studies').select('*');
if (error && error.code !== '42P01') throw error; if (error && error.code !== '42P01') throw error;
res.json(data || []); res.json(toCamelCase(data || []));
} catch (error: unknown) { } catch (error: unknown) {
res.json([]); res.json([]);
} }
@@ -13,15 +14,16 @@ export const getAllStudies = async (req: Request, res: Response) => {
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 { data, error } = await supabase const { data, error } = await supabase
.from('yield_studies') .from('yield_studies')
.insert({ ...req.body, organization_id: req.appUser?.organizationId }) .insert(toSnakeCase(payload))
.select() .select()
.single(); .single();
if (error) throw error; if (error) throw error;
res.status(201).json(data); res.status(201).json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.status(400).json({ error: (error as any).message });
} }
}; };
@@ -29,14 +31,14 @@ export const updateStudy = async (req: Request, res: Response) => {
try { try {
const { data, error } = await supabase const { data, error } = await supabase
.from('yield_studies') .from('yield_studies')
.update(req.body) .update(toSnakeCase(req.body))
.eq('id', req.params.id) .eq('id', req.params.id)
.select() .select()
.single(); .single();
if (error) throw error; if (error) throw error;
res.json(data); res.json(toCamelCase(data));
} catch (error: unknown) { } catch (error: unknown) {
res.json(req.body); res.status(400).json({ error: (error as any).message });
} }
}; };
@@ -45,6 +47,6 @@ export const deleteStudy = async (req: Request, res: Response) => {
await supabase.from('yield_studies').delete().eq('id', req.params.id); await supabase.from('yield_studies').delete().eq('id', req.params.id);
res.status(204).send(); res.status(204).send();
} catch (error: unknown) { } catch (error: unknown) {
res.status(204).send(); res.status(500).json({ error: (error as any).message });
} }
}; };

View File

@@ -33,13 +33,18 @@ export const createProject = async (data: ProjectData & { organizationId?: strin
export const getAllProjects = async (organizationId?: string, isGlobalAdmin?: boolean, status?: string) => { export const getAllProjects = async (organizationId?: string, isGlobalAdmin?: boolean, status?: string) => {
try { try {
const { data: projects, error } = await supabase let query = supabase
.from('projects') .from('projects')
.select('*'); .select('*, painting_schemes(*)');
if (status) {
query = query.eq('status', status);
}
const { data: projects, error } = await query;
// Se tabela não existir, retorna array vazio
if (error) { if (error) {
console.log('Projects table not found, returning empty array'); console.log('Error fetching projects:', error);
return []; return [];
} }
@@ -54,7 +59,7 @@ export const getDashboardProjects = async (organizationId?: string) => {
try { try {
const { data: projects, error } = await supabase const { data: projects, error } = await supabase
.from('projects') .from('projects')
.select('*'); .select('*, painting_schemes(*)');
if (error) return []; if (error) return [];
return projects || []; return projects || [];
@@ -91,7 +96,7 @@ export const archiveProject = async (id: string, organizationId?: string, isGlob
export const getProjectById = async (id: string) => { export const getProjectById = async (id: string) => {
const { data, error } = await supabase const { data, error } = await supabase
.from('projects') .from('projects')
.select('*') .select('*, painting_schemes(*)')
.eq('id', id) .eq('id', id)
.single(); .single();

View File

@@ -0,0 +1,36 @@
/**
* Utility to convert snake_case object keys to camelCase
*/
export const toCamelCase = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map(v => toCamelCase(v));
} else if (obj !== null && obj !== undefined && obj.constructor === Object) {
return Object.keys(obj).reduce(
(result, key) => ({
...result,
[key.replace(/(_\w)/g, m => m[1].toUpperCase())]: toCamelCase(obj[key]),
}),
{},
);
}
return obj;
};
/**
* Utility to convert camelCase object keys to snake_case (for DB inserts)
*/
export const toSnakeCase = (obj: any): any => {
if (Array.isArray(obj)) {
return obj.map(v => toSnakeCase(v));
} else if (obj !== null && obj !== undefined && obj.constructor === Object) {
return Object.keys(obj).reduce(
(result, key) => ({
...result,
[key.replace(/[A-Z]/g, m => `_${m.toLowerCase()}`)]: toSnakeCase(obj[key]),
}),
{},
);
}
return obj;
};

View File

@@ -1,7 +1,18 @@
-- Criar schema gpi -- Criar schema gpi
CREATE SCHEMA IF NOT EXISTS gpi; CREATE SCHEMA IF NOT EXISTS gpi;
-- Tabela users (já existe em public, mas replicamos em 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 ( CREATE TABLE IF NOT EXISTS gpi.users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
logto_id TEXT UNIQUE, logto_id TEXT UNIQUE,
@@ -14,9 +25,9 @@ CREATE TABLE IF NOT EXISTS gpi.users (
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
-- Tabela projects
CREATE TABLE IF NOT EXISTS gpi.projects ( CREATE TABLE IF NOT EXISTS gpi.projects (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
client TEXT, client TEXT,
start_date DATE, start_date DATE,
@@ -24,8 +35,7 @@ CREATE TABLE IF NOT EXISTS gpi.projects (
environment TEXT, environment TEXT,
technician TEXT, technician TEXT,
weight_kg DECIMAL(10,2), weight_kg DECIMAL(10,2),
painted_weight DECIMAL(10,2), status TEXT DEFAULT 'active' CHECK (status IN ('active', 'archived')),
created_by TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
@@ -33,7 +43,8 @@ CREATE TABLE IF NOT EXISTS gpi.projects (
-- Tabela parts -- Tabela parts
CREATE TABLE IF NOT EXISTS gpi.parts ( CREATE TABLE IF NOT EXISTS gpi.parts (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID REFERENCES gpi/projects(id) ON DELETE CASCADE, organization_id UUID NOT NULL,
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
description TEXT, description TEXT,
dimensions TEXT, dimensions TEXT,
weight DECIMAL(10,3), weight DECIMAL(10,3),
@@ -49,25 +60,25 @@ CREATE TABLE IF NOT EXISTS gpi.parts (
-- Tabela painting_schemes -- Tabela painting_schemes
CREATE TABLE IF NOT EXISTS gpi.painting_schemes ( CREATE TABLE IF NOT EXISTS gpi.painting_schemes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID REFERENCES gpi/projects(id) ON DELETE CASCADE, organization_id UUID NOT NULL,
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT, type TEXT,
coat TEXT, coat TEXT,
solids_volume DECIMAL(5,2), solids_volume DECIMAL(12,3),
yield_theoretical DECIMAL(10,2), yield_theoretical DECIMAL(12,3),
eps_min DECIMAL(5,2), eps_min DECIMAL(12,3),
eps_max DECIMAL(5,2), eps_max DECIMAL(12,3),
dilution DECIMAL(5,2), dilution DECIMAL(12,3),
manufacturer TEXT, manufacturer TEXT,
color TEXT, color TEXT,
paint_consumption DECIMAL(10,3), paint_consumption DECIMAL(12,3),
thinner_consumption DECIMAL(10,3), thinner_consumption DECIMAL(12,3),
paint_id TEXT, paint_id TEXT,
thinner_id TEXT, thinner_id TEXT,
color_hex TEXT, color_hex TEXT,
thinner_symbol TEXT, thinner_symbol TEXT,
notes TEXT, notes TEXT,
created_by TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
@@ -75,7 +86,8 @@ CREATE TABLE IF NOT EXISTS gpi.painting_schemes (
-- Tabela application_records -- Tabela application_records
CREATE TABLE IF NOT EXISTS gpi.application_records ( CREATE TABLE IF NOT EXISTS gpi.application_records (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID REFERENCES gpi/projects(id) ON DELETE CASCADE, organization_id UUID NOT NULL,
project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
coat_stage TEXT NOT NULL, coat_stage TEXT NOT NULL,
piece_description TEXT, piece_description TEXT,
date DATE, date DATE,
@@ -90,7 +102,6 @@ CREATE TABLE IF NOT EXISTS gpi.application_records (
diluent_used DECIMAL(10,3), diluent_used DECIMAL(10,3),
items JSONB, items JSONB,
notes TEXT, notes TEXT,
created_by TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
@@ -98,8 +109,9 @@ CREATE TABLE IF NOT EXISTS gpi.application_records (
-- Tabela inspections -- Tabela inspections
CREATE TABLE IF NOT EXISTS gpi.inspections ( CREATE TABLE IF NOT EXISTS gpi.inspections (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
project_id UUID REFERENCES gpi/projects(id) ON DELETE CASCADE, organization_id UUID NOT NULL,
application_record_id UUID REFERENCES gpi/application_records(id), project_id UUID REFERENCES gpi.projects(id) ON DELETE CASCADE,
application_record_id UUID REFERENCES gpi.application_records(id),
stock_item_id TEXT, stock_item_id TEXT,
instrument_id TEXT, instrument_id TEXT,
type TEXT CHECK (type IN ('painting', 'surface_treatment')), type TEXT CHECK (type IN ('painting', 'surface_treatment')),
@@ -122,7 +134,6 @@ CREATE TABLE IF NOT EXISTS gpi.inspections (
temperature DECIMAL(6,2), temperature DECIMAL(6,2),
relative_humidity DECIMAL(5,2), relative_humidity DECIMAL(5,2),
period TEXT CHECK (period IN ('morning', 'afternoon', 'night')), period TEXT CHECK (period IN ('morning', 'afternoon', 'night')),
created_by TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
@@ -130,28 +141,29 @@ CREATE TABLE IF NOT EXISTS gpi.inspections (
-- Tabela technical_data_sheets -- Tabela technical_data_sheets
CREATE TABLE IF NOT EXISTS gpi.technical_data_sheets ( CREATE TABLE IF NOT EXISTS gpi.technical_data_sheets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
manufacturer TEXT, manufacturer TEXT,
type TEXT, type TEXT,
file_url TEXT NOT NULL, file_url TEXT,
upload_date DATE NOT NULL, upload_date DATE,
solids_volume DECIMAL(5,2), solids_volume DECIMAL(12,3),
density DECIMAL(6,3), density DECIMAL(12,3),
mixing_ratio TEXT, mixing_ratio TEXT,
yield_theoretical DECIMAL(10,2), yield_theoretical DECIMAL(12,3),
wft_min DECIMAL(6,2), wft_min DECIMAL(12,3),
wft_max DECIMAL(6,2), wft_max DECIMAL(12,3),
dft_min DECIMAL(6,2), dft_min DECIMAL(12,3),
dft_max DECIMAL(6,2), dft_max DECIMAL(12,3),
reducer TEXT, reducer TEXT,
mixing_ratio_weight TEXT, mixing_ratio_weight TEXT,
mixing_ratio_volume TEXT, mixing_ratio_volume TEXT,
dft_reference DECIMAL(6,2), dft_reference DECIMAL(12,3),
yield_factor DECIMAL(5,3), yield_factor DECIMAL(12,3),
dilution DECIMAL(5,2), dilution DECIMAL(12,3),
notes TEXT, notes TEXT,
manufacturer_code TEXT, manufacturer_code TEXT,
min_stock DECIMAL(10,3), min_stock DECIMAL(12,3),
typical_application TEXT, typical_application TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
@@ -160,6 +172,7 @@ CREATE TABLE IF NOT EXISTS gpi.technical_data_sheets (
-- Tabela yield_studies -- Tabela yield_studies
CREATE TABLE IF NOT EXISTS gpi.yield_studies ( CREATE TABLE IF NOT EXISTS gpi.yield_studies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
data_sheet_id TEXT NOT NULL, data_sheet_id TEXT NOT NULL,
target_dft DECIMAL(6,2) NOT NULL, target_dft DECIMAL(6,2) NOT NULL,
@@ -178,16 +191,16 @@ CREATE TABLE IF NOT EXISTS gpi.yield_studies (
-- Tabela instruments -- Tabela instruments
CREATE TABLE IF NOT EXISTS gpi.instruments ( CREATE TABLE IF NOT EXISTS gpi.instruments (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
type TEXT NOT NULL, type TEXT NOT NULL,
serial_number TEXT UNIQUE, serial_number TEXT,
manufacturer TEXT, manufacturer TEXT,
model TEXT, model TEXT,
last_calibration DATE, last_calibration DATE,
next_calibration DATE, next_calibration DATE,
status TEXT DEFAULT 'active', status TEXT DEFAULT 'active',
notes TEXT, notes TEXT,
organization_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
@@ -195,17 +208,17 @@ CREATE TABLE IF NOT EXISTS gpi.instruments (
-- Tabela stock_items -- Tabela stock_items
CREATE TABLE IF NOT EXISTS gpi.stock_items ( CREATE TABLE IF NOT EXISTS gpi.stock_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT NOT NULL, organization_id UUID NOT NULL,
type TEXT NOT NULL, name TEXT,
type TEXT,
batch_number TEXT, batch_number TEXT,
quantity DECIMAL(10,3) DEFAULT 0, quantity DECIMAL(12,3) DEFAULT 0,
unit TEXT DEFAULT 'L', unit TEXT DEFAULT 'L',
data_sheet_id TEXT, data_sheet_id TEXT,
location TEXT, location TEXT,
expiration_date DATE, expiration_date DATE,
status TEXT DEFAULT 'available', status TEXT DEFAULT 'available',
notes TEXT, notes TEXT,
organization_id TEXT,
created_at TIMESTAMPTZ DEFAULT NOW(), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW() updated_at TIMESTAMPTZ DEFAULT NOW()
); );
@@ -213,7 +226,8 @@ CREATE TABLE IF NOT EXISTS gpi.stock_items (
-- Tabela stock_movements -- Tabela stock_movements
CREATE TABLE IF NOT EXISTS gpi.stock_movements ( CREATE TABLE IF NOT EXISTS gpi.stock_movements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
stock_item_id UUID REFERENCES gpi/stock_items(id) ON DELETE CASCADE, 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')), type TEXT NOT NULL CHECK (type IN ('in', 'out', 'adjustment')),
quantity DECIMAL(10,3) NOT NULL, quantity DECIMAL(10,3) NOT NULL,
reason TEXT, reason TEXT,
@@ -225,6 +239,7 @@ CREATE TABLE IF NOT EXISTS gpi.stock_movements (
-- Tabela notifications -- Tabela notifications
CREATE TABLE IF NOT EXISTS gpi.notifications ( CREATE TABLE IF NOT EXISTS gpi.notifications (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL,
title TEXT NOT NULL, title TEXT NOT NULL,
message TEXT NOT NULL, message TEXT NOT NULL,
type TEXT DEFAULT 'info' CHECK (type IN ('info', 'warning', 'error', 'success')), type TEXT DEFAULT 'info' CHECK (type IN ('info', 'warning', 'error', 'success')),
@@ -239,16 +254,64 @@ CREATE TABLE IF NOT EXISTS gpi.notifications (
-- Tabela geometry_types -- Tabela geometry_types
CREATE TABLE IF NOT EXISTS gpi.geometry_types ( CREATE TABLE IF NOT EXISTS gpi.geometry_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
organization_id UUID NOT NULL,
name TEXT NOT NULL, name TEXT NOT NULL,
efficiency_loss DECIMAL(5,2), efficiency_loss DECIMAL(5,2),
updated_at TIMESTAMPTZ DEFAULT NOW() 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 -- Habilitar PostgREST para o schema gpi
ALTER SCHEMA gpi ENABLE VALUE;
GRANT USAGE ON SCHEMA gpi TO postgres, anon, authenticated, service_role; GRANT USAGE ON SCHEMA gpi TO postgres, anon, authenticated, service_role;
-- Grant permissions em todas as tabelas -- 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.users TO postgres, anon, authenticated, service_role;
GRANT ALL ON TABLE gpi.projects 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.parts TO postgres, anon, authenticated, service_role;
@@ -262,6 +325,10 @@ GRANT ALL ON TABLE gpi.stock_items TO postgres, anon, authenticated, service_rol
GRANT ALL ON TABLE gpi.stock_movements 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.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.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 sequences
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA gpi TO postgres, anon, authenticated, service_role; GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA gpi TO postgres, anon, authenticated, service_role;