coati-payroll 0.0.12__py3-none-any.whl → 0.0.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of coati-payroll might be problematic. Click here for more details.

Files changed (244) hide show
  1. coati_payroll/__init__.py +7 -17
  2. coati_payroll/app.py +2 -13
  3. coati_payroll/audit_helpers.py +2 -13
  4. coati_payroll/auth.py +2 -13
  5. coati_payroll/cli.py +17 -14
  6. coati_payroll/config.py +2 -13
  7. coati_payroll/demo_data.py +2 -13
  8. coati_payroll/enums.py +2 -13
  9. coati_payroll/forms.py +7 -13
  10. coati_payroll/formula_engine/__init__.py +14 -13
  11. coati_payroll/formula_engine/ast/__init__.py +16 -1
  12. coati_payroll/formula_engine/ast/ast_visitor.py +15 -17
  13. coati_payroll/formula_engine/ast/expression_evaluator.py +12 -14
  14. coati_payroll/formula_engine/ast/safe_operators.py +16 -15
  15. coati_payroll/formula_engine/ast/type_converter.py +17 -17
  16. coati_payroll/formula_engine/data_sources.py +3 -13
  17. coati_payroll/formula_engine/engine.py +12 -13
  18. coati_payroll/formula_engine/exceptions.py +13 -13
  19. coati_payroll/formula_engine/execution/__init__.py +15 -13
  20. coati_payroll/formula_engine/execution/execution_context.py +12 -13
  21. coati_payroll/formula_engine/execution/step_executor.py +12 -14
  22. coati_payroll/formula_engine/execution/variable_store.py +13 -13
  23. coati_payroll/formula_engine/novelty_codes.py +3 -13
  24. coati_payroll/formula_engine/results/__init__.py +14 -13
  25. coati_payroll/formula_engine/results/execution_result.py +47 -35
  26. coati_payroll/formula_engine/steps/__init__.py +14 -14
  27. coati_payroll/formula_engine/steps/assignment_step.py +12 -13
  28. coati_payroll/formula_engine/steps/base_step.py +13 -13
  29. coati_payroll/formula_engine/steps/calculation_step.py +12 -13
  30. coati_payroll/formula_engine/steps/conditional_step.py +12 -13
  31. coati_payroll/formula_engine/steps/step_factory.py +11 -13
  32. coati_payroll/formula_engine/steps/tax_lookup_step.py +12 -13
  33. coati_payroll/formula_engine/tables/__init__.py +14 -13
  34. coati_payroll/formula_engine/tables/bracket_calculator.py +12 -13
  35. coati_payroll/formula_engine/tables/table_lookup.py +12 -14
  36. coati_payroll/formula_engine/tables/tax_table.py +13 -13
  37. coati_payroll/formula_engine/validation/__init__.py +14 -13
  38. coati_payroll/formula_engine/validation/schema_validator.py +12 -14
  39. coati_payroll/formula_engine/validation/security_validator.py +12 -13
  40. coati_payroll/formula_engine/validation/tax_table_validator.py +12 -13
  41. coati_payroll/formula_engine_examples.py +2 -13
  42. coati_payroll/i18n.py +2 -13
  43. coati_payroll/initial_data.py +2 -13
  44. coati_payroll/interes_engine.py +2 -13
  45. coati_payroll/liquidacion_engine/__init__.py +2 -13
  46. coati_payroll/liquidacion_engine/engine.py +2 -13
  47. coati_payroll/locale_config.py +2 -13
  48. coati_payroll/migrations/20260125_032900_initial_migration.py +2 -14
  49. coati_payroll/migrations/__init__.py +2 -13
  50. coati_payroll/model.py +83 -82
  51. coati_payroll/nomina_engine/__init__.py +2 -13
  52. coati_payroll/nomina_engine/calculators/__init__.py +2 -13
  53. coati_payroll/nomina_engine/calculators/benefit_calculator.py +2 -13
  54. coati_payroll/nomina_engine/calculators/concept_calculator.py +2 -1
  55. coati_payroll/nomina_engine/calculators/deduction_calculator.py +2 -13
  56. coati_payroll/nomina_engine/calculators/exchange_rate_calculator.py +2 -13
  57. coati_payroll/nomina_engine/calculators/perception_calculator.py +2 -13
  58. coati_payroll/nomina_engine/calculators/salary_calculator.py +2 -13
  59. coati_payroll/nomina_engine/domain/__init__.py +2 -13
  60. coati_payroll/nomina_engine/domain/calculation_items.py +2 -13
  61. coati_payroll/nomina_engine/domain/employee_calculation.py +2 -13
  62. coati_payroll/nomina_engine/domain/payroll_context.py +2 -13
  63. coati_payroll/nomina_engine/engine.py +2 -13
  64. coati_payroll/nomina_engine/processors/__init__.py +2 -13
  65. coati_payroll/nomina_engine/processors/accounting_processor.py +2 -13
  66. coati_payroll/nomina_engine/processors/accumulation_processor.py +2 -13
  67. coati_payroll/nomina_engine/processors/loan_processor.py +2 -13
  68. coati_payroll/nomina_engine/processors/novelty_processor.py +2 -13
  69. coati_payroll/nomina_engine/processors/vacation_processor.py +2 -13
  70. coati_payroll/nomina_engine/repositories/__init__.py +2 -13
  71. coati_payroll/nomina_engine/repositories/acumulado_repository.py +2 -13
  72. coati_payroll/nomina_engine/repositories/base_repository.py +2 -13
  73. coati_payroll/nomina_engine/repositories/config_repository.py +2 -13
  74. coati_payroll/nomina_engine/repositories/employee_repository.py +2 -13
  75. coati_payroll/nomina_engine/repositories/exchange_rate_repository.py +2 -13
  76. coati_payroll/nomina_engine/repositories/novelty_repository.py +2 -13
  77. coati_payroll/nomina_engine/repositories/planilla_repository.py +2 -13
  78. coati_payroll/nomina_engine/results/__init__.py +2 -13
  79. coati_payroll/nomina_engine/results/error_result.py +2 -13
  80. coati_payroll/nomina_engine/results/payroll_result.py +2 -13
  81. coati_payroll/nomina_engine/results/validation_result.py +2 -13
  82. coati_payroll/nomina_engine/services/__init__.py +2 -13
  83. coati_payroll/nomina_engine/services/accounting_voucher_service.py +2 -13
  84. coati_payroll/nomina_engine/services/employee_processing_service.py +2 -13
  85. coati_payroll/nomina_engine/services/payroll_execution_service.py +2 -13
  86. coati_payroll/nomina_engine/validators/__init__.py +2 -13
  87. coati_payroll/nomina_engine/validators/base_validator.py +2 -13
  88. coati_payroll/nomina_engine/validators/currency_validator.py +2 -13
  89. coati_payroll/nomina_engine/validators/employee_validator.py +2 -13
  90. coati_payroll/nomina_engine/validators/period_validator.py +2 -13
  91. coati_payroll/nomina_engine/validators/planilla_validator.py +2 -13
  92. coati_payroll/queue/__init__.py +2 -13
  93. coati_payroll/queue/driver.py +2 -13
  94. coati_payroll/queue/drivers/__init__.py +2 -13
  95. coati_payroll/queue/drivers/dramatiq_driver.py +9 -16
  96. coati_payroll/queue/drivers/huey_driver.py +2 -13
  97. coati_payroll/queue/drivers/noop_driver.py +2 -13
  98. coati_payroll/queue/selector.py +2 -13
  99. coati_payroll/queue/tasks.py +13 -20
  100. coati_payroll/rate_limiting.py +2 -13
  101. coati_payroll/rbac.py +12 -19
  102. coati_payroll/report_engine.py +2 -13
  103. coati_payroll/report_export.py +2 -13
  104. coati_payroll/schema_validator.py +2 -13
  105. coati_payroll/security.py +2 -13
  106. coati_payroll/static/schema_editor.js +862 -0
  107. coati_payroll/static/styles.css +5 -0
  108. coati_payroll/system_reports.py +2 -13
  109. coati_payroll/templates/auth/login.html +5 -0
  110. coati_payroll/templates/base.html +5 -1
  111. coati_payroll/templates/index.html +5 -0
  112. coati_payroll/templates/macros.html +5 -0
  113. coati_payroll/templates/modules/calculation_rule/form.html +5 -0
  114. coati_payroll/templates/modules/calculation_rule/index.html +5 -0
  115. coati_payroll/templates/modules/calculation_rule/schema_editor.html +292 -974
  116. coati_payroll/templates/modules/carga_inicial_prestacion/form.html +5 -0
  117. coati_payroll/templates/modules/carga_inicial_prestacion/index.html +5 -0
  118. coati_payroll/templates/modules/carga_inicial_prestacion/reporte.html +5 -0
  119. coati_payroll/templates/modules/config_calculos/index.html +5 -0
  120. coati_payroll/templates/modules/configuracion/index.html +24 -27
  121. coati_payroll/templates/modules/currency/form.html +5 -0
  122. coati_payroll/templates/modules/currency/index.html +5 -0
  123. coati_payroll/templates/modules/custom_field/form.html +5 -0
  124. coati_payroll/templates/modules/custom_field/index.html +5 -0
  125. coati_payroll/templates/modules/deduccion/form.html +5 -0
  126. coati_payroll/templates/modules/deduccion/index.html +5 -0
  127. coati_payroll/templates/modules/employee/form.html +5 -0
  128. coati_payroll/templates/modules/employee/index.html +5 -0
  129. coati_payroll/templates/modules/empresa/form.html +5 -0
  130. coati_payroll/templates/modules/empresa/index.html +5 -0
  131. coati_payroll/templates/modules/exchange_rate/form.html +5 -0
  132. coati_payroll/templates/modules/exchange_rate/import.html +5 -0
  133. coati_payroll/templates/modules/exchange_rate/index.html +5 -0
  134. coati_payroll/templates/modules/liquidacion/index.html +5 -0
  135. coati_payroll/templates/modules/liquidacion/nueva.html +5 -0
  136. coati_payroll/templates/modules/liquidacion/ver.html +5 -0
  137. coati_payroll/templates/modules/payroll_concepts/audit_log.html +5 -0
  138. coati_payroll/templates/modules/percepcion/form.html +5 -0
  139. coati_payroll/templates/modules/percepcion/index.html +5 -0
  140. coati_payroll/templates/modules/planilla/config.html +5 -0
  141. coati_payroll/templates/modules/planilla/config_deducciones.html +5 -0
  142. coati_payroll/templates/modules/planilla/config_empleados.html +5 -0
  143. coati_payroll/templates/modules/planilla/config_percepciones.html +5 -0
  144. coati_payroll/templates/modules/planilla/config_prestaciones.html +5 -0
  145. coati_payroll/templates/modules/planilla/config_reglas.html +5 -0
  146. coati_payroll/templates/modules/planilla/ejecutar_nomina.html +5 -0
  147. coati_payroll/templates/modules/planilla/form.html +5 -0
  148. coati_payroll/templates/modules/planilla/index.html +5 -0
  149. coati_payroll/templates/modules/planilla/listar_nominas.html +5 -0
  150. coati_payroll/templates/modules/planilla/log_nomina.html +5 -0
  151. coati_payroll/templates/modules/planilla/novedades/form.html +5 -0
  152. coati_payroll/templates/modules/planilla/novedades/index.html +5 -0
  153. coati_payroll/templates/modules/planilla/ver_nomina.html +5 -0
  154. coati_payroll/templates/modules/planilla/ver_nomina_empleado.html +5 -0
  155. coati_payroll/templates/modules/plugins/index.html +5 -0
  156. coati_payroll/templates/modules/prestacion/form.html +5 -0
  157. coati_payroll/templates/modules/prestacion/index.html +5 -0
  158. coati_payroll/templates/modules/prestacion_management/dashboard.html +5 -0
  159. coati_payroll/templates/modules/prestacion_management/initial_balance_bulk.html +5 -0
  160. coati_payroll/templates/modules/prestamo/approve.html +5 -0
  161. coati_payroll/templates/modules/prestamo/condonacion.html +5 -0
  162. coati_payroll/templates/modules/prestamo/detail.html +5 -0
  163. coati_payroll/templates/modules/prestamo/form.html +5 -0
  164. coati_payroll/templates/modules/prestamo/index.html +5 -0
  165. coati_payroll/templates/modules/prestamo/pago_extraordinario.html +5 -0
  166. coati_payroll/templates/modules/prestamo/tabla_pago_pdf.html +5 -0
  167. coati_payroll/templates/modules/report/admin_index.html +5 -0
  168. coati_payroll/templates/modules/report/detail.html +5 -0
  169. coati_payroll/templates/modules/report/execute.html +5 -0
  170. coati_payroll/templates/modules/report/index.html +5 -0
  171. coati_payroll/templates/modules/report/permissions.html +5 -0
  172. coati_payroll/templates/modules/settings/index.html +5 -0
  173. coati_payroll/templates/modules/shared/concept_form.html +19 -12
  174. coati_payroll/templates/modules/shared/concept_index.html +39 -27
  175. coati_payroll/templates/modules/tipo_planilla/form.html +5 -0
  176. coati_payroll/templates/modules/tipo_planilla/index.html +5 -0
  177. coati_payroll/templates/modules/user/form.html +5 -0
  178. coati_payroll/templates/modules/user/index.html +5 -0
  179. coati_payroll/templates/modules/user/profile.html +5 -0
  180. coati_payroll/templates/modules/vacation/account_detail.html +5 -0
  181. coati_payroll/templates/modules/vacation/account_form.html +5 -0
  182. coati_payroll/templates/modules/vacation/account_index.html +5 -0
  183. coati_payroll/templates/modules/vacation/dashboard.html +5 -0
  184. coati_payroll/templates/modules/vacation/initial_balance_bulk.html +5 -0
  185. coati_payroll/templates/modules/vacation/initial_balance_form.html +5 -0
  186. coati_payroll/templates/modules/vacation/leave_request_detail.html +5 -0
  187. coati_payroll/templates/modules/vacation/leave_request_form.html +5 -0
  188. coati_payroll/templates/modules/vacation/leave_request_index.html +5 -0
  189. coati_payroll/templates/modules/vacation/policy_detail.html +5 -0
  190. coati_payroll/templates/modules/vacation/policy_form.html +5 -0
  191. coati_payroll/templates/modules/vacation/policy_index.html +5 -0
  192. coati_payroll/templates/modules/vacation/register_taken_form.html +5 -0
  193. coati_payroll/translations/en/LC_MESSAGES/messages.mo +0 -0
  194. coati_payroll/translations/en/LC_MESSAGES/messages.po +2963 -3561
  195. coati_payroll/vacation_service.py +2 -13
  196. coati_payroll/version.py +3 -14
  197. coati_payroll/vistas/__init__.py +2 -13
  198. coati_payroll/vistas/calculation_rule.py +14 -24
  199. coati_payroll/vistas/carga_inicial_prestacion.py +2 -13
  200. coati_payroll/vistas/config_calculos.py +2 -13
  201. coati_payroll/vistas/configuracion.py +29 -39
  202. coati_payroll/vistas/constants.py +2 -13
  203. coati_payroll/vistas/currency.py +2 -13
  204. coati_payroll/vistas/custom_field.py +2 -13
  205. coati_payroll/vistas/employee.py +2 -13
  206. coati_payroll/vistas/empresa.py +2 -13
  207. coati_payroll/vistas/exchange_rate.py +2 -13
  208. coati_payroll/vistas/liquidacion.py +2 -13
  209. coati_payroll/vistas/payroll_concepts.py +2 -13
  210. coati_payroll/vistas/planilla/__init__.py +2 -13
  211. coati_payroll/vistas/planilla/association_routes.py +8 -17
  212. coati_payroll/vistas/planilla/config_routes.py +2 -13
  213. coati_payroll/vistas/planilla/export_routes.py +28 -35
  214. coati_payroll/vistas/planilla/helpers/__init__.py +2 -13
  215. coati_payroll/vistas/planilla/helpers/association_helpers.py +2 -13
  216. coati_payroll/vistas/planilla/helpers/excel_helpers.py +2 -13
  217. coati_payroll/vistas/planilla/helpers/form_helpers.py +2 -13
  218. coati_payroll/vistas/planilla/nomina_routes.py +2 -13
  219. coati_payroll/vistas/planilla/novedad_routes.py +2 -13
  220. coati_payroll/vistas/planilla/routes.py +2 -13
  221. coati_payroll/vistas/planilla/services/__init__.py +2 -13
  222. coati_payroll/vistas/planilla/services/export_service.py +4 -15
  223. coati_payroll/vistas/planilla/services/nomina_service.py +2 -13
  224. coati_payroll/vistas/planilla/services/novedad_service.py +3 -16
  225. coati_payroll/vistas/planilla/services/planilla_service.py +2 -13
  226. coati_payroll/vistas/planilla/validators/__init__.py +2 -13
  227. coati_payroll/vistas/planilla/validators/planilla_validators.py +2 -13
  228. coati_payroll/vistas/prestacion.py +2 -13
  229. coati_payroll/vistas/prestamo.py +2 -13
  230. coati_payroll/vistas/report.py +2 -13
  231. coati_payroll/vistas/settings.py +2 -13
  232. coati_payroll/vistas/tipo_planilla.py +2 -13
  233. coati_payroll/vistas/user.py +2 -13
  234. coati_payroll/vistas/vacation.py +15 -13
  235. coati_payroll/wsgi_server.py +37 -0
  236. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/METADATA +11 -4
  237. coati_payroll-0.0.13.dist-info/RECORD +248 -0
  238. coati_payroll/translations/es/LC_MESSAGES/messages.mo +0 -0
  239. coati_payroll/translations/es/LC_MESSAGES/messages.po +0 -7374
  240. coati_payroll-0.0.12.dist-info/RECORD +0 -248
  241. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/LICENSE +0 -0
  242. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/WHEEL +0 -0
  243. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/entry_points.txt +0 -0
  244. {coati_payroll-0.0.12.dist-info → coati_payroll-0.0.13.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,862 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // SPDX-FileCopyrightText: 2025 - 2026 BMO Soluciones, S.A.
3
+
4
+ // Note: The following variables are initialized in the HTML template with Jinja2:
5
+ // - schema: initialized from {{ schema_data|tojson }}
6
+ // - exampleSchema: initialized from {{ example_schema_data|tojson }}
7
+ // - availableSources: initialized from {{ available_sources_data|tojson }}
8
+
9
+ // Counter for unique IDs
10
+ let inputCounter = 0;
11
+ let stepCounter = 0;
12
+ let tableCounter = 0;
13
+
14
+ // Sortable instances
15
+ let inputsSortable = null;
16
+ let stepsSortable = null;
17
+
18
+ function initializeSortable() {
19
+ // Initialize sortable for inputs
20
+ const inputsContainer = document.getElementById('inputs-container');
21
+ if (inputsContainer && typeof Sortable !== 'undefined') {
22
+ inputsSortable = Sortable.create(inputsContainer, {
23
+ animation: 150,
24
+ handle: '.drag-handle',
25
+ ghostClass: 'sortable-ghost',
26
+ dragClass: 'sortable-drag',
27
+ onEnd: function() {
28
+ updateJsonPreview();
29
+ }
30
+ });
31
+ }
32
+
33
+ // Initialize sortable for steps
34
+ const stepsContainer = document.getElementById('steps-container');
35
+ if (stepsContainer && typeof Sortable !== 'undefined') {
36
+ stepsSortable = Sortable.create(stepsContainer, {
37
+ animation: 150,
38
+ handle: '.drag-handle',
39
+ ghostClass: 'sortable-ghost',
40
+ dragClass: 'sortable-drag',
41
+ onEnd: function() {
42
+ updateJsonPreview();
43
+ }
44
+ });
45
+ }
46
+ }
47
+
48
+ function loadSchemaToEditor() {
49
+ // Load meta
50
+ if (schema.meta) {
51
+ document.getElementById('meta-name').value = schema.meta.name || '';
52
+ document.getElementById('meta-reference-currency').value = schema.meta.reference_currency || schema.meta.currency || '';
53
+ document.getElementById('meta-description').value = schema.meta.description || '';
54
+ }
55
+
56
+ // Load inputs
57
+ const inputsContainer = document.getElementById('inputs-container');
58
+ inputsContainer.innerHTML = '';
59
+ if (schema.inputs && schema.inputs.length > 0) {
60
+ schema.inputs.forEach(input => addInput(input));
61
+ }
62
+
63
+ // Load steps
64
+ const stepsContainer = document.getElementById('steps-container');
65
+ stepsContainer.innerHTML = '';
66
+ if (schema.steps && schema.steps.length > 0) {
67
+ schema.steps.forEach(step => addStep(step.type, step));
68
+ }
69
+
70
+ // Load tax tables
71
+ const tablesContainer = document.getElementById('tax-tables-container');
72
+ tablesContainer.innerHTML = '';
73
+ if (schema.tax_tables) {
74
+ Object.entries(schema.tax_tables).forEach(([name, brackets]) => {
75
+ addTaxTable(name, brackets);
76
+ });
77
+ }
78
+
79
+ // Load output
80
+ updateOutputSelect();
81
+ document.getElementById('output-variable').value = schema.output || '';
82
+ }
83
+
84
+ function addInput(data = null) {
85
+ const container = document.getElementById('inputs-container');
86
+ const id = inputCounter++;
87
+
88
+ // Build source options grouped by category
89
+ let sourceOptionsHtml = '<option value="">Select source or enter value...</option>';
90
+ let currentCategory = '';
91
+ availableSources.forEach(source => {
92
+ if (source.category !== currentCategory) {
93
+ if (currentCategory !== '') {
94
+ sourceOptionsHtml += '</optgroup>';
95
+ }
96
+ sourceOptionsHtml += `<optgroup label="${source.category}">`;
97
+ currentCategory = source.category;
98
+ }
99
+ sourceOptionsHtml += `<option value="${source.value}" title="${source.description}">${source.label}</option>`;
100
+ });
101
+ if (currentCategory !== '') {
102
+ sourceOptionsHtml += '</optgroup>';
103
+ }
104
+
105
+ const currentSourceValue = data?.source || data?.default || '';
106
+
107
+ const html = `
108
+ <div class="input-item" id="input-${id}">
109
+ <div class="drag-handle">
110
+ <i class="bi bi-grip-vertical"></i>
111
+ </div>
112
+ <div class="reorder-buttons">
113
+ <button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveInputUp(${id})" title="Move up">
114
+ <i class="bi bi-arrow-up"></i>
115
+ </button>
116
+ <button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveInputDown(${id})" title="Move down">
117
+ <i class="bi bi-arrow-down"></i>
118
+ </button>
119
+ </div>
120
+ <button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeInput(${id})">
121
+ <i class="bi bi-x"></i>
122
+ </button>
123
+ <div class="row">
124
+ <div class="col-md-4">
125
+ <label class="form-label small">Name</label>
126
+ <input type="text" class="form-control form-control-sm input-name"
127
+ value="${data?.name || ''}" placeholder="Ex: salary_monthly"
128
+ onchange="updateJsonPreview()">
129
+ </div>
130
+ <div class="col-md-3">
131
+ <label class="form-label small">Type</label>
132
+ <select class="form-select form-select-sm input-type" onchange="updateJsonPreview()">
133
+ <option value="decimal" ${data?.type === 'decimal' ? 'selected' : ''}>Decimal</option>
134
+ <option value="integer" ${data?.type === 'integer' ? 'selected' : ''}>Integer</option>
135
+ <option value="string" ${data?.type === 'string' ? 'selected' : ''}>Text</option>
136
+ <option value="boolean" ${data?.type === 'boolean' ? 'selected' : ''}>Boolean</option>
137
+ <option value="date" ${data?.type === 'date' ? 'selected' : ''}>Date</option>
138
+ </select>
139
+ </div>
140
+ <div class="col-md-5">
141
+ <label class="form-label small">Data Source</label>
142
+ <select class="form-select form-select-sm input-source-select"
143
+ onchange="handleSourceChange(this, ${id}); updateJsonPreview()">
144
+ ${sourceOptionsHtml}
145
+ <option value="__custom__">Custom value...</option>
146
+ </select>
147
+ </div>
148
+ </div>
149
+ <div class="row mt-2">
150
+ <div class="col-md-6">
151
+ <input type="text" class="form-control form-control-sm input-source-custom"
152
+ value="${currentSourceValue}"
153
+ placeholder="Source (employee.field) or default value"
154
+ onchange="updateJsonPreview()"
155
+ style="${currentSourceValue && !availableSources.find(s => s.value === currentSourceValue) ? '' : 'display:none'}">
156
+ </div>
157
+ <div class="col-md-6">
158
+ <input type="text" class="form-control form-control-sm input-description"
159
+ value="${data?.description || ''}"
160
+ placeholder="Description (optional)"
161
+ onchange="updateJsonPreview()">
162
+ </div>
163
+ </div>
164
+ </div>
165
+ `;
166
+ container.insertAdjacentHTML('beforeend', html);
167
+
168
+ // Set the select value if it matches an available source
169
+ const selectEl = document.querySelector(`#input-${id} .input-source-select`);
170
+ if (currentSourceValue) {
171
+ const matchingSource = availableSources.find(s => s.value === currentSourceValue);
172
+ if (matchingSource) {
173
+ selectEl.value = currentSourceValue;
174
+ } else {
175
+ selectEl.value = '__custom__';
176
+ document.querySelector(`#input-${id} .input-source-custom`).style.display = '';
177
+ }
178
+ }
179
+
180
+ updateJsonPreview();
181
+ }
182
+
183
+ function handleSourceChange(selectEl, inputId) {
184
+ const customInput = document.querySelector(`#input-${inputId} .input-source-custom`);
185
+ if (selectEl.value === '__custom__') {
186
+ customInput.style.display = '';
187
+ customInput.focus();
188
+ } else if (selectEl.value === '') {
189
+ customInput.style.display = 'none';
190
+ customInput.value = '';
191
+ } else {
192
+ customInput.style.display = 'none';
193
+ customInput.value = selectEl.value;
194
+ }
195
+ }
196
+
197
+ function removeInput(id) {
198
+ document.getElementById(`input-${id}`).remove();
199
+ updateJsonPreview();
200
+ }
201
+
202
+ function moveInputUp(id) {
203
+ const element = document.getElementById(`input-${id}`);
204
+ const prev = element.previousElementSibling;
205
+ if (prev) {
206
+ element.parentNode.insertBefore(element, prev);
207
+ updateJsonPreview();
208
+ }
209
+ }
210
+
211
+ function moveInputDown(id) {
212
+ const element = document.getElementById(`input-${id}`);
213
+ const next = element.nextElementSibling;
214
+ if (next) {
215
+ element.parentNode.insertBefore(next, element);
216
+ updateJsonPreview();
217
+ }
218
+ }
219
+
220
+ function addStep(type, data = null) {
221
+ const container = document.getElementById('steps-container');
222
+ const id = stepCounter++;
223
+
224
+ let typeHtml = '';
225
+ let typeBadgeClass = '';
226
+
227
+ switch(type) {
228
+ case 'calculation':
229
+ typeBadgeClass = 'bg-primary';
230
+ typeHtml = `
231
+ <div class="mb-2">
232
+ <label class="form-label small">Fórmula</label>
233
+ <input type="text" class="form-control form-control-sm step-formula"
234
+ value="${data?.formula || ''}"
235
+ placeholder="Ej: salario_mensual * 12"
236
+ onchange="updateJsonPreview()">
237
+ </div>
238
+ `;
239
+ break;
240
+ case 'conditional':
241
+ typeBadgeClass = 'bg-warning text-dark';
242
+ typeHtml = `
243
+ <div class="mb-2">
244
+ <label class="form-label small">Condición</label>
245
+ <div class="row g-2">
246
+ <div class="col-4">
247
+ <input type="text" class="form-control form-control-sm step-cond-left"
248
+ value="${data?.condition?.left || ''}" placeholder="Variable"
249
+ onchange="updateJsonPreview()">
250
+ </div>
251
+ <div class="col-2">
252
+ <select class="form-select form-select-sm step-cond-op" onchange="updateJsonPreview()">
253
+ <option value=">" ${ data?.condition?.operator === '>' ? 'selected' : '' }>&gt;</option>
254
+ <option value=">=" ${ data?.condition?.operator === '>=' ? 'selected' : '' }>&gt;=</option>
255
+ <option value="<" ${ data?.condition?.operator === '<' ? 'selected' : '' }>&lt;</option>
256
+ <option value="<=" ${ data?.condition?.operator === '<=' ? 'selected' : '' }>&lt;=</option>
257
+ <option value="==" ${ data?.condition?.operator === '==' ? 'selected' : '' }>==</option>
258
+ <option value="!=" ${ data?.condition?.operator === '!=' ? 'selected' : '' }>!=</option>
259
+ </select>
260
+ </div>
261
+ <div class="col-4">
262
+ <input type="text" class="form-control form-control-sm step-cond-right"
263
+ value="${data?.condition?.right || ''}" placeholder="Valor"
264
+ onchange="updateJsonPreview()">
265
+ </div>
266
+ </div>
267
+ </div>
268
+ <div class="row g-2">
269
+ <div class="col-6">
270
+ <label class="form-label small">Si verdadero</label>
271
+ <input type="text" class="form-control form-control-sm step-if-true"
272
+ value="${data?.if_true || ''}" placeholder="Fórmula si cumple"
273
+ onchange="updateJsonPreview()">
274
+ </div>
275
+ <div class="col-6">
276
+ <label class="form-label small">Si falso</label>
277
+ <input type="text" class="form-control form-control-sm step-if-false"
278
+ value="${data?.if_false || ''}" placeholder="Fórmula si no cumple"
279
+ onchange="updateJsonPreview()">
280
+ </div>
281
+ </div>
282
+ `;
283
+ break;
284
+ case 'tax_lookup':
285
+ typeBadgeClass = 'bg-danger';
286
+ typeHtml = `
287
+ <div class="row g-2">
288
+ <div class="col-6">
289
+ <label class="form-label small">Tabla de impuestos</label>
290
+ <input type="text" class="form-control form-control-sm step-table"
291
+ value="${data?.table || ''}" placeholder="Nombre de la tabla"
292
+ onchange="updateJsonPreview()">
293
+ </div>
294
+ <div class="col-6">
295
+ <label class="form-label small">Variable de entrada</label>
296
+ <input type="text" class="form-control form-control-sm step-input"
297
+ value="${data?.input || ''}" placeholder="Variable a buscar"
298
+ onchange="updateJsonPreview()">
299
+ </div>
300
+ </div>
301
+ `;
302
+ break;
303
+ case 'assignment':
304
+ typeBadgeClass = 'bg-info';
305
+ typeHtml = `
306
+ <div class="mb-2">
307
+ <label class="form-label small">Valor</label>
308
+ <input type="text" class="form-control form-control-sm step-value"
309
+ value="${data?.value || ''}" placeholder="Variable o valor a asignar"
310
+ onchange="updateJsonPreview()">
311
+ </div>
312
+ `;
313
+ break;
314
+ }
315
+
316
+ const html = `
317
+ <div class="step-item" id="step-${id}" data-type="${type}">
318
+ <div class="drag-handle">
319
+ <i class="bi bi-grip-vertical"></i>
320
+ </div>
321
+ <div class="reorder-buttons">
322
+ <button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveStepUp(${id})" title="Subir">
323
+ <i class="bi bi-arrow-up"></i>
324
+ </button>
325
+ <button type="button" class="btn btn-outline-secondary btn-sm" onclick="moveStepDown(${id})" title="Bajar">
326
+ <i class="bi bi-arrow-down"></i>
327
+ </button>
328
+ </div>
329
+ <button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeStep(${id})">
330
+ <i class="bi bi-x"></i>
331
+ </button>
332
+ <div class="d-flex align-items-center mb-2">
333
+ <span class="badge ${typeBadgeClass} step-type-badge me-2">${type}</span>
334
+ <input type="text" class="form-control form-control-sm step-name"
335
+ value="${data?.name || ''}" placeholder="Nombre del paso"
336
+ style="max-width: 200px;" onchange="updateJsonPreview()">
337
+ </div>
338
+ ${typeHtml}
339
+ <div class="mt-2">
340
+ <input type="text" class="form-control form-control-sm step-description"
341
+ value="${data?.description || ''}" placeholder="Descripción (opcional)"
342
+ onchange="updateJsonPreview()">
343
+ </div>
344
+ </div>
345
+ `;
346
+ container.insertAdjacentHTML('beforeend', html);
347
+ updateJsonPreview();
348
+ }
349
+
350
+ function removeStep(id) {
351
+ document.getElementById(`step-${id}`).remove();
352
+ updateJsonPreview();
353
+ }
354
+
355
+ function moveStepUp(id) {
356
+ const element = document.getElementById(`step-${id}`);
357
+ const prev = element.previousElementSibling;
358
+ if (prev) {
359
+ element.parentNode.insertBefore(element, prev);
360
+ updateJsonPreview();
361
+ }
362
+ }
363
+
364
+ function moveStepDown(id) {
365
+ const element = document.getElementById(`step-${id}`);
366
+ const next = element.nextElementSibling;
367
+ if (next) {
368
+ element.parentNode.insertBefore(next, element);
369
+ updateJsonPreview();
370
+ }
371
+ }
372
+
373
+ function addTaxTable(name = null, brackets = null) {
374
+ const container = document.getElementById('tax-tables-container');
375
+ const id = tableCounter++;
376
+
377
+ const html = `
378
+ <div class="tax-table-item" id="table-${id}">
379
+ <button type="button" class="btn btn-danger btn-sm remove-btn" onclick="removeTaxTable(${id})">
380
+ <i class="bi bi-x"></i>
381
+ </button>
382
+ <div class="mb-3">
383
+ <label class="form-label small">Nombre de la tabla</label>
384
+ <input type="text" class="form-control form-control-sm table-name"
385
+ value="${name || ''}" placeholder="Ej: income_tax_brackets"
386
+ onchange="updateJsonPreview()">
387
+ </div>
388
+ <div class="brackets-container" id="brackets-${id}">
389
+ <!-- Brackets will be added here -->
390
+ </div>
391
+ <button type="button" class="btn btn-sm btn-outline-primary mt-2" onclick="addBracket(${id})">
392
+ <i class="bi bi-plus-lg me-1"></i>Agregar Tramo
393
+ </button>
394
+ </div>
395
+ `;
396
+ container.insertAdjacentHTML('beforeend', html);
397
+
398
+ // Add existing brackets
399
+ if (brackets && brackets.length > 0) {
400
+ brackets.forEach(bracket => addBracket(id, bracket));
401
+ }
402
+
403
+ updateJsonPreview();
404
+ }
405
+
406
+ function removeTaxTable(id) {
407
+ document.getElementById(`table-${id}`).remove();
408
+ updateJsonPreview();
409
+ }
410
+
411
+ function addBracket(tableId, data = null) {
412
+ const container = document.getElementById(`brackets-${tableId}`);
413
+ const html = `
414
+ <div class="bracket-row">
415
+ <div class="row g-2 align-items-center">
416
+ <div class="col">
417
+ <input type="number" class="form-control form-control-sm bracket-min"
418
+ value="${data?.min ?? ''}" placeholder="Mín"
419
+ onchange="updateJsonPreview()">
420
+ </div>
421
+ <div class="col">
422
+ <input type="number" class="form-control form-control-sm bracket-max"
423
+ value="${data?.max ?? ''}" placeholder="Máx"
424
+ onchange="updateJsonPreview()">
425
+ </div>
426
+ <div class="col">
427
+ <input type="number" class="form-control form-control-sm bracket-rate"
428
+ value="${data?.rate ?? ''}" placeholder="Tasa" step="0.01"
429
+ onchange="updateJsonPreview()">
430
+ </div>
431
+ <div class="col">
432
+ <input type="number" class="form-control form-control-sm bracket-fixed"
433
+ value="${data?.fixed ?? ''}" placeholder="Fijo"
434
+ onchange="updateJsonPreview()">
435
+ </div>
436
+ <div class="col">
437
+ <input type="number" class="form-control form-control-sm bracket-over"
438
+ value="${data?.over ?? ''}" placeholder="Sobre"
439
+ onchange="updateJsonPreview()">
440
+ </div>
441
+ <div class="col-auto">
442
+ <button type="button" class="btn btn-sm btn-outline-danger" onclick="this.closest('.bracket-row').remove(); updateJsonPreview();">
443
+ <i class="bi bi-x"></i>
444
+ </button>
445
+ </div>
446
+ </div>
447
+ </div>
448
+ `;
449
+ container.insertAdjacentHTML('beforeend', html);
450
+ updateJsonPreview();
451
+ }
452
+
453
+ function updateOutputSelect() {
454
+ const select = document.getElementById('output-variable');
455
+ const currentValue = select.value;
456
+ select.innerHTML = '<option value="">Seleccionar variable de resultado...</option>';
457
+
458
+ // Add all input names
459
+ document.querySelectorAll('.input-name').forEach(input => {
460
+ if (input.value) {
461
+ select.innerHTML += `<option value="${input.value}">${input.value} (input)</option>`;
462
+ }
463
+ });
464
+
465
+ // Add all step names
466
+ document.querySelectorAll('.step-name').forEach(input => {
467
+ if (input.value) {
468
+ select.innerHTML += `<option value="${input.value}">${input.value} (step)</option>`;
469
+ }
470
+ });
471
+
472
+ select.value = currentValue;
473
+ }
474
+
475
+ function collectSchemaFromEditor() {
476
+ const newSchema = {
477
+ meta: {
478
+ name: document.getElementById('meta-name').value,
479
+ reference_currency: document.getElementById('meta-reference-currency').value,
480
+ description: document.getElementById('meta-description').value
481
+ },
482
+ inputs: [],
483
+ steps: [],
484
+ tax_tables: {},
485
+ output: document.getElementById('output-variable').value
486
+ };
487
+
488
+ // Collect inputs
489
+ document.querySelectorAll('.input-item').forEach(item => {
490
+ const input = {
491
+ name: item.querySelector('.input-name').value,
492
+ type: item.querySelector('.input-type').value
493
+ };
494
+ // Get source from either the select dropdown or the custom input field
495
+ const selectEl = item.querySelector('.input-source-select');
496
+ const customEl = item.querySelector('.input-source-custom');
497
+ let source = '';
498
+ if (selectEl && selectEl.value && selectEl.value !== '__custom__' && selectEl.value !== '') {
499
+ source = selectEl.value;
500
+ } else if (customEl) {
501
+ source = customEl.value;
502
+ }
503
+ if (source) {
504
+ if (source.includes('.')) {
505
+ input.source = source;
506
+ } else if (!isNaN(source)) {
507
+ input.default = parseFloat(source);
508
+ } else {
509
+ input.default = source;
510
+ }
511
+ }
512
+ const desc = item.querySelector('.input-description').value;
513
+ if (desc) input.description = desc;
514
+
515
+ if (input.name) newSchema.inputs.push(input);
516
+ });
517
+
518
+ // Collect steps
519
+ document.querySelectorAll('.step-item').forEach(item => {
520
+ const type = item.dataset.type;
521
+ const step = {
522
+ name: item.querySelector('.step-name').value,
523
+ type: type
524
+ };
525
+
526
+ switch(type) {
527
+ case 'calculation':
528
+ step.formula = item.querySelector('.step-formula').value;
529
+ break;
530
+ case 'conditional':
531
+ step.condition = {
532
+ left: item.querySelector('.step-cond-left').value,
533
+ operator: item.querySelector('.step-cond-op').value,
534
+ right: item.querySelector('.step-cond-right').value
535
+ };
536
+ // Try to parse right value as number
537
+ if (!isNaN(step.condition.right)) {
538
+ step.condition.right = parseFloat(step.condition.right);
539
+ }
540
+ step.if_true = item.querySelector('.step-if-true').value;
541
+ step.if_false = item.querySelector('.step-if-false').value;
542
+ break;
543
+ case 'tax_lookup':
544
+ step.table = item.querySelector('.step-table').value;
545
+ step.input = item.querySelector('.step-input').value;
546
+ break;
547
+ case 'assignment':
548
+ step.value = item.querySelector('.step-value').value;
549
+ break;
550
+ }
551
+
552
+ const desc = item.querySelector('.step-description').value;
553
+ if (desc) step.description = desc;
554
+
555
+ if (step.name) newSchema.steps.push(step);
556
+ });
557
+
558
+ // Collect tax tables
559
+ document.querySelectorAll('.tax-table-item').forEach(item => {
560
+ const tableName = item.querySelector('.table-name').value;
561
+ if (!tableName) return;
562
+
563
+ const brackets = [];
564
+ item.querySelectorAll('.bracket-row').forEach(row => {
565
+ const bracket = {};
566
+ const min = row.querySelector('.bracket-min').value;
567
+ const max = row.querySelector('.bracket-max').value;
568
+ const rate = row.querySelector('.bracket-rate').value;
569
+ const fixed = row.querySelector('.bracket-fixed').value;
570
+ const over = row.querySelector('.bracket-over').value;
571
+
572
+ if (min !== '') bracket.min = parseFloat(min);
573
+ if (max !== '') bracket.max = parseFloat(max);
574
+ else bracket.max = null;
575
+ if (rate !== '') bracket.rate = parseFloat(rate);
576
+ if (fixed !== '') bracket.fixed = parseFloat(fixed);
577
+ if (over !== '') bracket.over = parseFloat(over);
578
+
579
+ if (Object.keys(bracket).length > 0) brackets.push(bracket);
580
+ });
581
+
582
+ if (brackets.length > 0) {
583
+ newSchema.tax_tables[tableName] = brackets;
584
+ }
585
+ });
586
+
587
+ return newSchema;
588
+ }
589
+
590
+ function updateJsonPreview() {
591
+ schema = collectSchemaFromEditor();
592
+ document.getElementById('json-preview').value = JSON.stringify(schema, null, 2);
593
+ updateOutputSelect();
594
+ generateTestInputs();
595
+ }
596
+
597
+ function generateTestInputs() {
598
+ const container = document.getElementById('test-inputs-container');
599
+ container.innerHTML = '';
600
+
601
+ schema.inputs.forEach(input => {
602
+ const html = `
603
+ <div class="mb-2">
604
+ <label class="form-label small">${input.name}</label>
605
+ <input type="number" class="form-control form-control-sm test-input"
606
+ data-name="${input.name}"
607
+ value="${input.default || 0}" step="0.01">
608
+ </div>
609
+ `;
610
+ container.insertAdjacentHTML('beforeend', html);
611
+ });
612
+ }
613
+
614
+ function copyJson() {
615
+ const textarea = document.getElementById('json-preview');
616
+ textarea.select();
617
+ document.execCommand('copy');
618
+ alert('JSON copied to clipboard');
619
+ }
620
+
621
+ function formatJson() {
622
+ const textarea = document.getElementById('json-preview');
623
+ try {
624
+ const json = JSON.parse(textarea.value);
625
+ textarea.value = JSON.stringify(json, null, 2);
626
+ } catch (e) {
627
+ alert('Invalid JSON');
628
+ }
629
+ }
630
+
631
+ function loadExample() {
632
+ if (confirm('Load progressive tax example? This will replace the current schema.')) {
633
+ schema = JSON.parse(JSON.stringify(exampleSchema));
634
+ loadSchemaToEditor();
635
+ updateJsonPreview();
636
+ alert('Example loaded successfully');
637
+ }
638
+ }
639
+
640
+ async function saveSchema() {
641
+ try {
642
+ const schemaToSave = collectSchemaFromEditor();
643
+ const ruleId = document.body.dataset.ruleId;
644
+
645
+ const response = await fetch(`/calculation_rule/${ruleId}/save_schema`, {
646
+ method: 'POST',
647
+ headers: {
648
+ 'Content-Type': 'application/json',
649
+ },
650
+ body: JSON.stringify({ schema: schemaToSave })
651
+ });
652
+
653
+ const result = await response.json();
654
+
655
+ if (result.success) {
656
+ alert('Esquema guardado exitosamente');
657
+ } else {
658
+ alert('Error: ' + result.error);
659
+ }
660
+ } catch (e) {
661
+ alert('Error al guardar: ' + e.message);
662
+ }
663
+ }
664
+
665
+ async function testCalculation() {
666
+ try {
667
+ const testInputs = {};
668
+ document.querySelectorAll('.test-input').forEach(input => {
669
+ testInputs[input.dataset.name] = parseFloat(input.value) || 0;
670
+ });
671
+
672
+ const ruleId = document.body.dataset.ruleId;
673
+
674
+ const response = await fetch(`/calculation_rule/${ruleId}/test_schema`, {
675
+ method: 'POST',
676
+ headers: {
677
+ 'Content-Type': 'application/json',
678
+ },
679
+ body: JSON.stringify({
680
+ schema: collectSchemaFromEditor(),
681
+ inputs: testInputs
682
+ })
683
+ });
684
+
685
+ const result = await response.json();
686
+
687
+ const resultDiv = document.getElementById('test-result');
688
+ const resultContent = document.getElementById('test-result-content');
689
+
690
+ resultDiv.style.display = 'block';
691
+
692
+ if (result.success) {
693
+ // Format result with 2 decimal places
694
+ const formattedResult = formatResultWithDecimals(result.result);
695
+ resultContent.innerHTML = JSON.stringify(formattedResult, null, 2);
696
+ resultContent.classList.remove('text-danger');
697
+ resultContent.classList.add('text-success');
698
+ } else {
699
+ resultContent.innerHTML = 'Error: ' + result.error;
700
+ resultContent.classList.remove('text-success');
701
+ resultContent.classList.add('text-danger');
702
+ }
703
+ } catch (e) {
704
+ alert('Error al probar: ' + e.message);
705
+ }
706
+ }
707
+
708
+ function formatResultWithDecimals(obj) {
709
+ if (typeof obj === 'number') {
710
+ return parseFloat(obj.toFixed(2));
711
+ } else if (typeof obj === 'object' && obj !== null) {
712
+ if (Array.isArray(obj)) {
713
+ return obj.map(item => formatResultWithDecimals(item));
714
+ } else {
715
+ const formatted = {};
716
+ for (const key in obj) {
717
+ formatted[key] = formatResultWithDecimals(obj[key]);
718
+ }
719
+ return formatted;
720
+ }
721
+ }
722
+ return obj;
723
+ }
724
+
725
+ async function loadJsonFile(event) {
726
+ const file = event.target.files[0];
727
+ if (!file) return;
728
+
729
+ // Check file type
730
+ if (!file.name.endsWith('.json')) {
731
+ alert('Please select a valid JSON file');
732
+ event.target.value = '';
733
+ return;
734
+ }
735
+
736
+ try {
737
+ const text = await file.text();
738
+ let jsonData;
739
+
740
+ // Parse JSON
741
+ try {
742
+ jsonData = JSON.parse(text);
743
+ } catch (e) {
744
+ alert('Error: The file does not contain valid JSON\n' + e.message);
745
+ event.target.value = '';
746
+ return;
747
+ }
748
+
749
+ // Validate schema structure before loading
750
+ const validationResult = await validateJsonSchema(jsonData);
751
+
752
+ if (!validationResult.valid) {
753
+ alert('Validation error:\n\n' + validationResult.error);
754
+ event.target.value = '';
755
+ return;
756
+ }
757
+
758
+ // Show confirmation dialog
759
+ if (confirm('Load this schema? This will replace the current schema.')) {
760
+ schema = jsonData;
761
+ loadSchemaToEditor();
762
+ updateJsonPreview();
763
+ alert('Schema loaded successfully');
764
+ }
765
+
766
+ } catch (e) {
767
+ alert('Error reading file: ' + e.message);
768
+ } finally {
769
+ event.target.value = '';
770
+ }
771
+ }
772
+
773
+ async function validateJsonSchema(jsonData) {
774
+ // Basic structure validation
775
+ if (!jsonData || typeof jsonData !== 'object') {
776
+ return { valid: false, error: 'The schema must be a JSON object' };
777
+ }
778
+
779
+ // Check for required sections
780
+ if (!jsonData.steps || !Array.isArray(jsonData.steps)) {
781
+ return { valid: false, error: 'The schema must contain a \'steps\' section with an array of steps' };
782
+ }
783
+
784
+ // Validate steps structure
785
+ for (let i = 0; i < jsonData.steps.length; i++) {
786
+ const step = jsonData.steps[i];
787
+ if (!step.name) {
788
+ return { valid: false, error: `Step ${i + 1} must have a 'name' field` };
789
+ }
790
+ if (!step.type) {
791
+ return { valid: false, error: `Step ${i + 1} must have a 'type' field` };
792
+ }
793
+
794
+ // Validate step type
795
+ const validTypes = ['calculation', 'conditional', 'tax_lookup', 'assignment'];
796
+ if (!validTypes.includes(step.type)) {
797
+ return {
798
+ valid: false,
799
+ error: `Step ${i + 1} has an invalid type: '${step.type}'. Allowed types: ${validTypes.join(', ')}`
800
+ };
801
+ }
802
+
803
+ // Validate step-specific fields
804
+ if (step.type === 'calculation' && !step.formula) {
805
+ return { valid: false, error: `Calculation step '${step.name}' must have a 'formula' field` };
806
+ }
807
+ if (step.type === 'conditional' && !step.condition) {
808
+ return { valid: false, error: `Conditional step '${step.name}' must have a 'condition' field` };
809
+ }
810
+ if (step.type === 'tax_lookup' && (!step.table || !step.input)) {
811
+ return { valid: false, error: `Tax lookup step '${step.name}' must have 'table' and 'input' fields` };
812
+ }
813
+ if (step.type === 'assignment' && step.value === undefined) {
814
+ return { valid: false, error: `Assignment step '${step.name}' must have a 'value' field` };
815
+ }
816
+ }
817
+
818
+ // Validate with backend FormulaEngine
819
+ try {
820
+ const ruleId = document.body.dataset.ruleId;
821
+ const response = await fetch(`/calculation_rule/${ruleId}/validate_schema_api`, {
822
+ method: 'POST',
823
+ headers: {
824
+ 'Content-Type': 'application/json',
825
+ },
826
+ body: JSON.stringify({ schema: jsonData })
827
+ });
828
+
829
+ const result = await response.json();
830
+
831
+ if (!result.success) {
832
+ return { valid: false, error: result.error };
833
+ }
834
+
835
+ return { valid: true };
836
+ } catch (e) {
837
+ // If backend validation fails, still allow if basic validation passed
838
+ console.warn('Backend validation failed:', e);
839
+ return { valid: true };
840
+ }
841
+ }
842
+
843
+ // Make functions globally accessible
844
+ window.copyJson = copyJson;
845
+ window.formatJson = formatJson;
846
+ window.moveInputUp = moveInputUp;
847
+ window.moveInputDown = moveInputDown;
848
+ window.removeInput = removeInput;
849
+ window.moveStepUp = moveStepUp;
850
+ window.moveStepDown = moveStepDown;
851
+ window.removeStep = removeStep;
852
+ window.removeTaxTable = removeTaxTable;
853
+ window.addBracket = addBracket;
854
+ window.loadExample = loadExample;
855
+ window.saveSchema = saveSchema;
856
+ window.testCalculation = testCalculation;
857
+ window.loadJsonFile = loadJsonFile;
858
+ window.addInput = addInput;
859
+ window.addStep = addStep;
860
+ window.addTaxTable = addTaxTable;
861
+ window.handleSourceChange = handleSourceChange;
862
+ window.updateJsonPreview = updateJsonPreview;