Compare commits
16 Commits
96ea8e21ef
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d3ed824d81 | |||
| 45fdf7110e | |||
| b4eee298a8 | |||
| e8ecac05d8 | |||
| 0dace9ee00 | |||
| a927c01269 | |||
| 210a5c69f9 | |||
| 31d602bb1b | |||
| 58343be771 | |||
| 34f60b25e6 | |||
| 242d67c509 | |||
| 9a3874bd61 | |||
| 2ddc8b886a | |||
| e1453ada14 | |||
| dd06fd1196 | |||
| 5c24783320 |
@@ -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
57
bulk_migration_final.sql
Normal 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
31
create_public_views.sql
Normal 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
337
full_schema.sql
Normal 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
147
migrate.js
Normal 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
143
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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
1
reset_gpi.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP SCHEMA IF EXISTS gpi CASCADE;
|
||||||
@@ -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,64 +30,80 @@ 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 />} />
|
||||||
<Route path="/schemes" element={<SchemesList />} />
|
<Route path="/schemes" element={<SchemesList />} />
|
||||||
<Route path="/inspections" element={<InspectionsList />} />
|
<Route path="/inspections" element={<InspectionsList />} />
|
||||||
<Route path="/library" element={
|
<Route path="/library" element={
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
||||||
<DataSheetLibrary />
|
<DataSheetLibrary />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/instruments" element={
|
||||||
|
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
||||||
|
<InstrumentList />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/yield-study" element={
|
||||||
|
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
||||||
|
<YieldStudyDashboard />
|
||||||
|
</ProtectedRoute>
|
||||||
|
} />
|
||||||
|
<Route path="/calculators" element={<CalculatorDashboard />} />
|
||||||
|
<Route
|
||||||
|
path="/admin"
|
||||||
|
element={
|
||||||
|
<ProtectedRoute allowedRoles={['admin']}>
|
||||||
|
<AdminDashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
}
|
||||||
<Route path="/instruments" element={
|
/>
|
||||||
|
<Route
|
||||||
|
path="/stock"
|
||||||
|
element={
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
||||||
<InstrumentList />
|
<StockDashboard />
|
||||||
</ProtectedRoute>
|
</ProtectedRoute>
|
||||||
} />
|
}
|
||||||
<Route path="/yield-study" element={
|
/>
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin']}>
|
<Route
|
||||||
<YieldStudyDashboard />
|
path="/developer"
|
||||||
</ProtectedRoute>
|
element={
|
||||||
} />
|
<DeveloperRoute>
|
||||||
<Route path="/calculators" element={<CalculatorDashboard />} />
|
<DeveloperDashboard />
|
||||||
<Route
|
</DeveloperRoute>
|
||||||
path="/admin"
|
}
|
||||||
element={
|
/>
|
||||||
<ProtectedRoute allowedRoles={['admin']}>
|
<Route path="*" element={<Navigate to="/" replace />} />
|
||||||
<AdminDashboard />
|
</Routes>
|
||||||
</ProtectedRoute>
|
</Layout>
|
||||||
}
|
</NotificationProvider>
|
||||||
/>
|
</SystemSettingsProvider>
|
||||||
<Route
|
|
||||||
path="/stock"
|
|
||||||
element={
|
|
||||||
<ProtectedRoute allowedRoles={['user', 'admin', 'guest']}>
|
|
||||||
<StockDashboard />
|
|
||||||
</ProtectedRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Route
|
|
||||||
path="/developer"
|
|
||||||
element={
|
|
||||||
<DeveloperRoute>
|
|
||||||
<DeveloperDashboard />
|
|
||||||
</DeveloperRoute>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Routes>
|
|
||||||
</Layout>
|
|
||||||
</NotificationProvider>
|
|
||||||
</SystemSettingsProvider>
|
|
||||||
</AuthProvider>
|
|
||||||
</ToastProvider>
|
</ToastProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -94,7 +111,9 @@ const AppContent: React.FC = () => {
|
|||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Router>
|
<Router>
|
||||||
<AppContent />
|
<AuthProvider>
|
||||||
|
<AppContent />
|
||||||
|
</AuthProvider>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
</div>
|
||||||
|
|
||||||
{/* Login Button - Logto */}
|
{/* Login Form */}
|
||||||
<div className="w-full bg-surface rounded-[2rem] border border-border/40 shadow-2xl shadow-primary/5 p-8 animate-in slide-in-from-bottom-8 duration-1000">
|
<div className="w-full bg-surface rounded-[2.5rem] border border-border/40 shadow-2xl shadow-primary/5 p-10 backdrop-blur-sm">
|
||||||
<button
|
<div className="flex items-center gap-3 mb-8 text-primary">
|
||||||
onClick={handleLogin}
|
<Lock size={20} className="opacity-70" />
|
||||||
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"
|
<h2 className="text-lg font-bold uppercase tracking-tight">Chave de Acesso</h2>
|
||||||
>
|
</div>
|
||||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<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"/>
|
<form onSubmit={handleSubmit} className="space-y-6">
|
||||||
<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"/>
|
<div className="space-y-2">
|
||||||
<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"/>
|
<input
|
||||||
<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"/>
|
type="password"
|
||||||
</svg>
|
placeholder="Digite a senha mestra..."
|
||||||
Continuar com Google
|
value={password}
|
||||||
</button>
|
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>
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
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"
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<div className="w-5 h-5 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<ShieldCheck size={20} />
|
||||||
|
Entrar no Sistema
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
</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>
|
||||||
|
|||||||
@@ -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); }}
|
||||||
|
|||||||
@@ -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"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -572,19 +572,19 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
const hasRealSV = sheet?.solidsVolume && sheet.solidsVolume > 0;
|
const hasRealSV = sheet?.solidsVolume && sheet.solidsVolume > 0;
|
||||||
let sv = sheet?.solidsVolume || 60;
|
let sv = sheet?.solidsVolume || 60;
|
||||||
if (sv <= 1) sv *= 100;
|
if (sv <= 1) sv *= 100;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className={`text-[9px] font-black uppercase tracking-wider ${hasRealSV ? 'text-success' : 'text-amber-500'}`}>
|
<span className={`text-[9px] font-black uppercase tracking-wider ${hasRealSV ? 'text-success' : 'text-amber-500'}`}>
|
||||||
SV da Tinta {hasRealSV ? '✓' : '⚠️'}
|
SV da Tinta {hasRealSV ? '✓' : '⚠️'}
|
||||||
</span>
|
</span>
|
||||||
<div className={`text-2xl font-black leading-none ${hasRealSV ? 'text-text-main' : 'text-amber-500'}`}>
|
<div className={`text-2xl font-black leading-none ${hasRealSV ? 'text-text-main' : 'text-amber-500'}`}>
|
||||||
{sv.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)'}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
})()}
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -617,13 +617,13 @@ export const YieldStudyDashboard: React.FC = () => {
|
|||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-[10px] font-bold text-text-muted uppercase">Taxa Média</span>
|
<span className="text-[10px] font-bold text-text-muted uppercase">Taxa Média</span>
|
||||||
<span className="text-sm font-black text-text-main">
|
<span className="text-sm font-black text-text-main">
|
||||||
{selectedStudy.totalWeight > 0 ? ((selectedStudy.estimatedPaintVolume / 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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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: [] });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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([]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
@@ -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;
|
||||||
|
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) {
|
||||||
|
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();
|
res.status(204).send();
|
||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
res.status(204).send();
|
res.status(500).json({ error: error instanceof Error ? error.message : 'Unknown error' });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
36
src/server/utils/caseMapper.ts
Normal file
36
src/server/utils/caseMapper.ts
Normal 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;
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user