decidim-stratified_sortitions 0.0.1

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.
Files changed (124) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE-AGPLv3.txt +661 -0
  3. data/README.md +91 -0
  4. data/Rakefile +42 -0
  5. data/app/cells/decidim/stratified_sortitions/stratified_sortition_cell.rb +21 -0
  6. data/app/cells/decidim/stratified_sortitions/stratified_sortition_l_cell.rb +22 -0
  7. data/app/cells/decidim/stratified_sortitions/stratified_sortition_metadata_cell.rb +63 -0
  8. data/app/commands/decidim/stratified_sortitions/admin/create_stratified_sortition.rb +84 -0
  9. data/app/commands/decidim/stratified_sortitions/admin/destroy_stratified_sortition.rb +49 -0
  10. data/app/commands/decidim/stratified_sortitions/admin/duplicate_stratified_sortition.rb +74 -0
  11. data/app/commands/decidim/stratified_sortitions/admin/import_sample.rb +40 -0
  12. data/app/commands/decidim/stratified_sortitions/admin/remove_uploaded_samples.rb +53 -0
  13. data/app/commands/decidim/stratified_sortitions/admin/update_stratified_sortition.rb +174 -0
  14. data/app/controllers/concerns/decidim/stratified_sortitions/orderable_stratified_sortitions.rb +37 -0
  15. data/app/controllers/concerns/decidim/stratified_sortitions/strata_charts_data.rb +91 -0
  16. data/app/controllers/decidim/stratified_sortitions/admin/application_controller.rb +19 -0
  17. data/app/controllers/decidim/stratified_sortitions/admin/samples_controller.rb +143 -0
  18. data/app/controllers/decidim/stratified_sortitions/admin/stratified_sortitions_controller.rb +235 -0
  19. data/app/controllers/decidim/stratified_sortitions/application_controller.rb +13 -0
  20. data/app/controllers/decidim/stratified_sortitions/charts_pdf_controller_helper.rb +17 -0
  21. data/app/controllers/decidim/stratified_sortitions/stratified_sortitions_controller.rb +61 -0
  22. data/app/forms/decidim/stratified_sortitions/admin/sample_upload_form.rb +15 -0
  23. data/app/forms/decidim/stratified_sortitions/admin/stratified_sortitions_form.rb +155 -0
  24. data/app/forms/decidim/stratified_sortitions/admin/stratum_form.rb +69 -0
  25. data/app/forms/decidim/stratified_sortitions/admin/substratum_form.rb +26 -0
  26. data/app/helpers/decidim/stratified_sortitions/admin/stratified_sortitions_helper.rb +18 -0
  27. data/app/helpers/decidim/stratified_sortitions/application_helper.rb +85 -0
  28. data/app/helpers/decidim/stratified_sortitions/charts_pdf_helper.rb +73 -0
  29. data/app/helpers/decidim/stratified_sortitions/stratified_sortition_cells_helper.rb +56 -0
  30. data/app/javascript/channels/sample_import_progress.js +12 -0
  31. data/app/jobs/decidim/stratified_sortitions/admin/execute_sortition_job.rb +21 -0
  32. data/app/jobs/decidim/stratified_sortitions/admin/import_sample_job.rb +170 -0
  33. data/app/jobs/decidim/stratified_sortitions/admin/remove_samples_job.rb +19 -0
  34. data/app/jobs/decidim/stratified_sortitions/admin/sortition_results_export_job.rb +28 -0
  35. data/app/mailers/decidim/stratified_sortitions/admin/import_mailer.rb +26 -0
  36. data/app/models/decidim/stratified_sortitions/application_record.rb +9 -0
  37. data/app/models/decidim/stratified_sortitions/panel_portfolio.rb +165 -0
  38. data/app/models/decidim/stratified_sortitions/sample_import.rb +14 -0
  39. data/app/models/decidim/stratified_sortitions/sample_participant.rb +12 -0
  40. data/app/models/decidim/stratified_sortitions/sample_participant_stratum.rb +11 -0
  41. data/app/models/decidim/stratified_sortitions/stratified_sortition.rb +74 -0
  42. data/app/models/decidim/stratified_sortitions/stratum.rb +22 -0
  43. data/app/models/decidim/stratified_sortitions/substratum.rb +19 -0
  44. data/app/packs/entrypoints/decidim_stratified_sortitions.js +10 -0
  45. data/app/packs/entrypoints/decidim_stratified_sortitions_admin.js +9 -0
  46. data/app/packs/entrypoints/decidim_stratified_sortitions_admin.scss +1 -0
  47. data/app/packs/images/decidim/stratified_sortitions/icon.svg +1 -0
  48. data/app/packs/src/decidim/stratified_sortitions/application.js +3 -0
  49. data/app/packs/src/decidim/stratified_sortitions/results_tabs.js +59 -0
  50. data/app/packs/src/decidim/stratified_sortitions/stratum_fields.js +302 -0
  51. data/app/packs/src/decidim/stratified_sortitions/substratum_fields.js +165 -0
  52. data/app/packs/src/decidim/stratified_sortitions/upload_sample.js +16 -0
  53. data/app/packs/stylesheets/decidim/stratified_sortitions/admin/stratified_sortitions.scss +210 -0
  54. data/app/packs/stylesheets/decidim/stratified_sortitions/charts_pdf.scss +148 -0
  55. data/app/packs/stylesheets/stratified_sortitions.scss +56 -0
  56. data/app/permissions/decidim/stratified_sortitions/admin/permissions.rb +74 -0
  57. data/app/permissions/decidim/stratified_sortitions/permissions.rb +23 -0
  58. data/app/presenters/decidim/stratified_sortitions/admin_log/stratified_sortition_presenter.rb +35 -0
  59. data/app/queries/decidim/stratified_sortitions/filtered_stratified_sortitions.rb +39 -0
  60. data/app/serializers/decidim/stratified_sortitions/sortition_result_serializer.rb +116 -0
  61. data/app/services/decidim/stratified_sortitions/charts_pdf_generator.rb +96 -0
  62. data/app/services/decidim/stratified_sortitions/fair_sortition_service.rb +247 -0
  63. data/app/services/decidim/stratified_sortitions/leximin/constraint_builder.rb +180 -0
  64. data/app/services/decidim/stratified_sortitions/leximin/distribution_solver.rb +217 -0
  65. data/app/services/decidim/stratified_sortitions/leximin/feasibility_checker.rb +222 -0
  66. data/app/services/decidim/stratified_sortitions/leximin/panel_generator.rb +149 -0
  67. data/app/services/decidim/stratified_sortitions/leximin/panel_sampler.rb +125 -0
  68. data/app/services/decidim/stratified_sortitions/leximin_selector.rb +136 -0
  69. data/app/services/decidim/stratified_sortitions/sortition_results_exporter.rb +161 -0
  70. data/app/views/decidim/admin/components/index.html.erb +54 -0
  71. data/app/views/decidim/stratified_sortitions/admin/import_mailer/import.html.erb +22 -0
  72. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_confirm_execute_modal.html.erb +19 -0
  73. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_confirm_participants_modal.html.erb +20 -0
  74. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_form.html.erb +55 -0
  75. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_navigation_menu.html.erb +10 -0
  76. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_strata.html.erb +22 -0
  77. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_stratum.html.erb +75 -0
  78. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_substrata.html.erb +34 -0
  79. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/_substratum.html.erb +54 -0
  80. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/edit.html.erb +21 -0
  81. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/execute.html.erb +155 -0
  82. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/export_charts_pdf.html.erb +38 -0
  83. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/index.html.erb +75 -0
  84. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/new.html.erb +21 -0
  85. data/app/views/decidim/stratified_sortitions/admin/stratified_sortitions/upload_sample.html.erb +117 -0
  86. data/app/views/decidim/stratified_sortitions/shared/_strata_charts.html.erb +21 -0
  87. data/app/views/decidim/stratified_sortitions/stratified_sortitions/_results_count.html.erb +15 -0
  88. data/app/views/decidim/stratified_sortitions/stratified_sortitions/_stratified_sortition.html.erb +1 -0
  89. data/app/views/decidim/stratified_sortitions/stratified_sortitions/_stratified_sortitions.html.erb +13 -0
  90. data/app/views/decidim/stratified_sortitions/stratified_sortitions/_tags.html.erb +9 -0
  91. data/app/views/decidim/stratified_sortitions/stratified_sortitions/index.html.erb +22 -0
  92. data/app/views/decidim/stratified_sortitions/stratified_sortitions/index.js.erb +5 -0
  93. data/app/views/decidim/stratified_sortitions/stratified_sortitions/show.html.erb +142 -0
  94. data/app/views/layouts/decidim/admin/_sidebar_menu.html.erb +45 -0
  95. data/app/views/layouts/decidim/stratified_sortitions/charts_pdf.html.erb +14 -0
  96. data/config/assets.rb +24 -0
  97. data/config/locales/ca.yml +361 -0
  98. data/config/locales/en.yml +360 -0
  99. data/config/locales/es.yml +360 -0
  100. data/config/locales/oc.yml +2 -0
  101. data/config/routes.rb +1 -0
  102. data/db/migrate/20251023103900_create_decidim_stratified_sortitions_stratified_sortitions.rb +18 -0
  103. data/db/migrate/20251104120000_create_stratified_sortitions_strata_and_substrata.decidim_stratified_sortitions.rb +27 -0
  104. data/db/migrate/20251219120000_create_sample_imports.rb +17 -0
  105. data/db/migrate/20251219120100_create_sample_participants.rb +19 -0
  106. data/db/migrate/20251219120200_create_sample_participant_strata.rb +16 -0
  107. data/db/migrate/20251230130000_change_value_to_text_in_substrata.rb +11 -0
  108. data/db/migrate/20260102114744_add_status_to_stratified_sortitions.rb +11 -0
  109. data/db/migrate/20260109105643_allow_null_value_in_substrata.rb +10 -0
  110. data/db/migrate/20260113121726_add_position_to_strata_and_substrata.rb +8 -0
  111. data/db/migrate/20260127100000_rename_weighing_to_max_quota_percentage.rb +12 -0
  112. data/db/migrate/20260127120000_create_panel_portfolios.rb +46 -0
  113. data/db/migrate/20260518000000_add_foreign_key_to_stratified_sortitions_component.rb +10 -0
  114. data/db/migrate/20260521000000_add_cascade_to_stratified_sortitions_foreign_keys.rb +64 -0
  115. data/db/migrate/20260616000000_add_execution_error_to_stratified_sortitions.rb +11 -0
  116. data/lib/decidim/stratified_sortitions/admin.rb +10 -0
  117. data/lib/decidim/stratified_sortitions/admin_engine.rb +49 -0
  118. data/lib/decidim/stratified_sortitions/component.rb +41 -0
  119. data/lib/decidim/stratified_sortitions/engine.rb +34 -0
  120. data/lib/decidim/stratified_sortitions/seeds.rb +24 -0
  121. data/lib/decidim/stratified_sortitions/test/factories.rb +80 -0
  122. data/lib/decidim/stratified_sortitions/version.rb +13 -0
  123. data/lib/decidim/stratified_sortitions.rb +29 -0
  124. metadata +222 -0
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ class SampleParticipant < ApplicationRecord
6
+ belongs_to :decidim_stratified_sortition, class_name: "Decidim::StratifiedSortitions::StratifiedSortition"
7
+ belongs_to :decidim_stratified_sortitions_sample_import, class_name: "Decidim::StratifiedSortitions::SampleImport", optional: true
8
+
9
+ has_many :sample_participant_strata, dependent: :destroy, foreign_key: :decidim_stratified_sortitions_sample_participant_id
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ class SampleParticipantStratum < ApplicationRecord
6
+ belongs_to :decidim_stratified_sortitions_sample_participant, class_name: "Decidim::StratifiedSortitions::SampleParticipant"
7
+ belongs_to :decidim_stratified_sortitions_stratum, class_name: "Decidim::StratifiedSortitions::Stratum"
8
+ belongs_to :decidim_stratified_sortitions_substratum, class_name: "Decidim::StratifiedSortitions::Substratum"
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ class StratifiedSortition < ApplicationRecord
6
+ include Decidim::HasComponent
7
+ include Decidim::FilterableResource
8
+ include Decidim::ScopableResource
9
+ include Decidim::Loggable
10
+ include Decidim::Resourceable
11
+ include Decidim::Searchable
12
+ include Decidim::Traceable
13
+ include Decidim::TranslatableAttributes
14
+ include Decidim::Randomable
15
+ include Decidim::HasUploadValidations
16
+
17
+ delegate :mounted_engine, :mounted_admin_engine, :mounted_params, to: :component
18
+
19
+ component_manifest_name "stratified_sortitions"
20
+
21
+ def self.log_presenter_class_for(_log)
22
+ Decidim::StratifiedSortitions::AdminLog::StratifiedSortitionPresenter
23
+ end
24
+
25
+ has_many :strata, class_name: "Decidim::StratifiedSortitions::Stratum", foreign_key: "decidim_stratified_sortition_id", dependent: :destroy
26
+ has_many :sample_imports, class_name: "Decidim::StratifiedSortitions::SampleImport", dependent: :destroy
27
+ has_many :sample_participants, class_name: "Decidim::StratifiedSortitions::SampleParticipant", foreign_key: "decidim_stratified_sortition_id", dependent: :destroy
28
+ has_one :panel_portfolio, class_name: "Decidim::StratifiedSortitions::PanelPortfolio", foreign_key: "decidim_stratified_sortitions_stratified_sortition_id",
29
+ dependent: :destroy
30
+
31
+ scope :search_text_cont, lambda { |search_text|
32
+ where("title ->> '#{I18n.locale}' ILIKE ?", "%#{search_text}%")
33
+ }
34
+
35
+ scope :with_any_state, lambda { |*values|
36
+ values = values.flatten.compact_blank
37
+ return all if values.empty? || values.include?("all")
38
+
39
+ where(status: values)
40
+ }
41
+
42
+ def self.ransackable_scopes(_auth_object = nil)
43
+ [:search_text_cont, :with_any_state]
44
+ end
45
+
46
+ searchable_fields({
47
+ participatory_space: :itself,
48
+ A: :title,
49
+ B: :description,
50
+ datetime: :created_at,
51
+ })
52
+
53
+ def strata_and_substrata_configured?
54
+ strata.any? && strata.all? { |stratum| stratum.substrata.any? }
55
+ end
56
+
57
+ def can_execute?
58
+ strata_and_substrata_configured? && !sample_participants.empty? && !executing?
59
+ end
60
+
61
+ def executed?
62
+ status == "executed"
63
+ end
64
+
65
+ def executing?
66
+ status == "executing"
67
+ end
68
+
69
+ def failed?
70
+ status == "failed"
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ class Stratum < ApplicationRecord
6
+ self.table_name = "decidim_stratified_sortitions_strata"
7
+
8
+ include Decidim::TranslatableResource
9
+ include Decidim::TranslatableAttributes
10
+
11
+ belongs_to :stratified_sortition, class_name: "Decidim::StratifiedSortitions::StratifiedSortition", foreign_key: "decidim_stratified_sortition_id"
12
+ has_many :substrata, class_name: "Decidim::StratifiedSortitions::Substratum", foreign_key: "decidim_stratified_sortitions_stratum_id", dependent: :destroy
13
+ has_many :sample_participant_strata, class_name: "Decidim::StratifiedSortitions::SampleParticipantStratum",
14
+ foreign_key: "decidim_stratified_sortitions_stratum_id",
15
+ dependent: :destroy
16
+
17
+ KINDS = %w(value numeric_range).freeze
18
+
19
+ translatable_fields :name
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Decidim
4
+ module StratifiedSortitions
5
+ class Substratum < ApplicationRecord
6
+ self.table_name = "decidim_stratified_sortitions_substrata"
7
+
8
+ include Decidim::TranslatableResource
9
+ include Decidim::TranslatableAttributes
10
+
11
+ belongs_to :stratum, class_name: "Decidim::StratifiedSortitions::Stratum", foreign_key: "decidim_stratified_sortitions_stratum_id"
12
+ has_many :sample_participant_strata, class_name: "Decidim::StratifiedSortitions::SampleParticipantStratum",
13
+ foreign_key: "decidim_stratified_sortitions_substratum_id",
14
+ dependent: :destroy
15
+
16
+ translatable_fields :name
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,10 @@
1
+ import "src/decidim/stratified_sortitions/stratum_fields"
2
+ import "src/decidim/stratified_sortitions/substratum_fields"
3
+
4
+ import "chartkick/chart.js"
5
+
6
+ // Images
7
+ require.context("../images", true)
8
+
9
+ // CSS
10
+ import "stylesheets/stratified_sortitions.scss"
@@ -0,0 +1,9 @@
1
+ import "src/decidim/stratified_sortitions/stratum_fields"
2
+ import "src/decidim/stratified_sortitions/substratum_fields"
3
+ import "src/decidim/stratified_sortitions/results_tabs"
4
+
5
+ import "chartkick/chart.js"
6
+
7
+ import "entrypoints/decidim_stratified_sortitions_admin.scss"
8
+
9
+ import "src/decidim/stratified_sortitions/upload_sample"
@@ -0,0 +1 @@
1
+ @import "stylesheets/decidim/stratified_sortitions/admin/stratified_sortitions.scss";
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="8" height="8" data-icon="aperture" version="1.1" viewBox="0 0 34 34"><path d="M17 0c9.389 0 17 7.611 17 17s-7.611 17-17 17S0 26.389 0 17 7.611 0 17 0zm0 1.889C8.654 1.889 1.889 8.654 1.889 17S8.654 32.111 17 32.111 32.111 25.346 32.111 17 25.346 1.889 17 1.889zm11.865 10.74a3.15 3.15 0 1 1-6.3 0 3.15 3.15 0 0 1 6.3 0zm-1.89 0a1.26 1.26 0 1 0-2.521 0 1.26 1.26 0 0 0 2.522 0zM15.25 19.136a.944.944 0 0 1 1.89 0v1.792a5.71 5.71 0 1 1-11.423 0v-1.792a.944.944 0 0 1 1.89 0v1.792a3.822 3.822 0 1 0 7.643 0v-1.792zm5.841-6.974l.018.019a.941.941 0 0 1-.02 1.335l-2.514 2.44a.944.944 0 0 1-1.316-1.355l.843-.818a6.237 6.237 0 0 0-6.177 6.23.944.944 0 0 1-1.889 0c0-4.492 3.653-8.12 8.12-8.12h.041l-.958-.986a.944.944 0 0 1 1.355-1.316l2.497 2.571a.941.941 0 0 0-.66-.268h-2.234z"/></svg>
@@ -0,0 +1,3 @@
1
+ import "src/decidim/stratified_sortitions/nested_fields"
2
+
3
+ import "chartkick/chart.js"
@@ -0,0 +1,59 @@
1
+ document.addEventListener("DOMContentLoaded", () => {
2
+ const tabContainer = document.querySelector("[data-results-tabs]");
3
+ if (!tabContainer) return;
4
+
5
+ const tabs = tabContainer.querySelectorAll("[data-tab-target]");
6
+ const panels = document.querySelectorAll("[data-tab-panel]");
7
+
8
+ function activateTab(targetId) {
9
+ tabs.forEach((t) => t.classList.remove("active"));
10
+ panels.forEach((p) => (p.style.display = "none"));
11
+
12
+ const tab = tabContainer.querySelector(`[data-tab-target="${targetId}"]`);
13
+ if (tab) tab.classList.add("active");
14
+
15
+ const targetPanel = document.getElementById(targetId);
16
+ if (targetPanel) targetPanel.style.display = "";
17
+ }
18
+
19
+ tabs.forEach((tab) => {
20
+ tab.addEventListener("click", () => {
21
+ const targetId = tab.dataset.tabTarget;
22
+
23
+ // If the tab opens a confirmation modal, don't switch yet
24
+ if (tab.dataset.dialogOpen) return;
25
+
26
+ activateTab(targetId);
27
+ });
28
+ });
29
+
30
+ // When the user confirms in the participants modal, switch to that tab
31
+ const confirmBtn = document.getElementById("confirm-show-participants");
32
+ if (confirmBtn) {
33
+ confirmBtn.addEventListener("click", () => {
34
+ activateTab("participants-section");
35
+
36
+ // Log the view_participants action
37
+ const logUrl = confirmBtn.dataset.logUrl;
38
+ const csrfToken = confirmBtn.dataset.authenticityToken;
39
+ if (logUrl && csrfToken) {
40
+ fetch(logUrl, {
41
+ method: "POST",
42
+ headers: {
43
+ "X-CSRF-Token": csrfToken,
44
+ "Content-Type": "application/json",
45
+ },
46
+ }).catch(() => {
47
+ // Silently ignore log errors
48
+ });
49
+ }
50
+
51
+ // Close the modal
52
+ const modal = document.getElementById("confirm-participants-modal");
53
+ if (modal) {
54
+ const closeBtn = modal.querySelector("[data-dialog-close]");
55
+ if (closeBtn) closeBtn.click();
56
+ }
57
+ });
58
+ }
59
+ });
@@ -0,0 +1,302 @@
1
+ import AutoButtonsByPositionComponent from "src/decidim/admin/auto_buttons_by_position.component"
2
+ import AutoLabelByPositionComponent from "src/decidim/admin/auto_label_by_position.component"
3
+ import createLiveTextUpdateComponent from "src/decidim/forms/admin/live_text_update.component"
4
+ import createSortList from "src/decidim/admin/sort_list.component"
5
+ import { initializeSubstrataWrapper } from "src/decidim/stratified_sortitions/substratum_fields"
6
+ import { createAccordion } from "src/decidim/a11y"
7
+
8
+ $(() => {
9
+ const wrapperSelector = ".stratified-sortition-strata";
10
+ const fieldSelector = ".stratified-sortition-stratum";
11
+
12
+ const autoLabelByPosition = new AutoLabelByPositionComponent({
13
+ listSelector: ".stratified-sortition-stratum:not(.hidden)",
14
+ labelSelector: ".card-title span:first",
15
+ onPositionComputed: (el, idx) => {
16
+ $(el).find("input[name$=\\[position\\]]").not("[name*=substrata]").first().val(idx);
17
+ }
18
+ });
19
+
20
+ const autoButtonsByPosition = new AutoButtonsByPositionComponent({
21
+ listSelector: ".stratified-sortition-stratum:not(.hidden)",
22
+ hideOnFirstSelector: ".move-up-stratum",
23
+ hideOnLastSelector: ".move-down-stratum"
24
+ });
25
+
26
+ const createSortableList = () => {
27
+ createSortList(".stratified-sortition-strata-list:not(.published)", {
28
+ handle: ".stratum-divider",
29
+ placeholder: '<div style="border-style: dashed; border-color: #000"></div>',
30
+ forcePlaceholderSize: true,
31
+ onSortUpdate: () => { autoLabelByPosition.run() }
32
+ });
33
+ };
34
+
35
+ const hideDeletedStratum = ($target) => {
36
+ const inputDeleted = $target.find("input[name$=\\[deleted\\]]").val();
37
+
38
+ if (inputDeleted === "true") {
39
+ $target.addClass("hidden");
40
+ $target.hide();
41
+ }
42
+ };
43
+
44
+ const runComponents = () => {
45
+ autoLabelByPosition.run();
46
+ autoButtonsByPosition.run();
47
+ };
48
+
49
+ const expandStratum = ($stratum) => {
50
+ const $toggle = $stratum.find('.stratum--collapse');
51
+ if ($toggle.length === 0) return;
52
+ const ctrl = $toggle.attr('data-controls');
53
+ const panel = ctrl ? document.getElementById(ctrl) : null;
54
+ const isExpanded = $toggle.attr('aria-expanded') === 'true';
55
+
56
+ if (!isExpanded) {
57
+ $toggle.trigger('click');
58
+
59
+ setTimeout(() => {
60
+ const nowExpanded = $toggle.attr('aria-expanded') === 'true';
61
+ if (!nowExpanded && panel) {
62
+ $toggle.attr('aria-expanded', 'true');
63
+ panel.setAttribute('aria-hidden', 'false');
64
+ $(panel).show();
65
+ }
66
+ }, 0);
67
+ }
68
+ };
69
+
70
+ const createCollapsibleStratum = ($target) => {
71
+ const $collapsible = $target.find(".collapsible");
72
+ if ($collapsible.length > 0) {
73
+ const collapsibleId = $collapsible.attr("id").replace("-stratum-card", "");
74
+ const toggleAttr = `${collapsibleId}-stratum-card`;
75
+
76
+ $target.find(".stratum--collapse")
77
+ .attr("data-controls", toggleAttr)
78
+ .attr("aria-controls", toggleAttr)
79
+ .each((_i, btn) => {
80
+ if (!btn.id) {
81
+ btn.id = `${toggleAttr}-toggle`;
82
+ }
83
+ if (!btn.hasAttribute("aria-expanded")) {
84
+ btn.setAttribute("aria-expanded", "false");
85
+ }
86
+ });
87
+ }
88
+ };
89
+
90
+ const createDynamicStratumTitle = (fieldId) => {
91
+ const targetSelector = `#${fieldId} .stratum-title-statement`;
92
+ const locale = $(targetSelector).data("locale");
93
+ const maxLength = $(targetSelector).data("max-length");
94
+ const omission = $(targetSelector).data("omission");
95
+ const placeholder = $(targetSelector).data("placeholder");
96
+
97
+ return createLiveTextUpdateComponent({
98
+ inputSelector: `#${fieldId} input[name$=\\[name_${locale}\\]]`,
99
+ targetSelector: targetSelector,
100
+ maxLength: maxLength,
101
+ omission: omission,
102
+ placeholder: placeholder
103
+ });
104
+ }
105
+
106
+ $(document).on("click", ".add-stratum", function(e) {
107
+ e.preventDefault();
108
+ e.stopPropagation();
109
+
110
+ const $button = $(this);
111
+ const $wrapper = $button.closest(wrapperSelector);
112
+ const $container = $wrapper.find(".stratified-sortition-strata-list");
113
+ const $template = $wrapper.find("template.decidim-template, script.decidim-template");
114
+
115
+ if ($template.length && $container.length) {
116
+
117
+ const templateContent = $template.html();
118
+ const uniqueId = new Date().getTime();
119
+ let newField = templateContent.replace(/(?<!SUB)STRATUM_ID/g, uniqueId);
120
+ newField = newField.replace(/stratified-sortition-stratum-id/g, `stratified-sortition-stratum-${uniqueId}`);
121
+ newField = newField.replace(/substrata-\d+/g, `substrata-${uniqueId}`);
122
+ $container.append(newField);
123
+
124
+ const $newField = $container.find(fieldSelector).last();
125
+ $newField.find(".stratified-sortition-substrata").each((idx, wrapperEl) => {
126
+ if (!wrapperEl.id) {
127
+ wrapperEl.id = `substrata-${uniqueId}`;
128
+ }
129
+ initializeSubstrataWrapper(wrapperEl);
130
+ });
131
+
132
+ $newField.find('[data-tabs]').each(function() {
133
+ new Foundation.Tabs($(this));
134
+ });
135
+
136
+ updateSubstratumFieldsVisibility($newField);
137
+ createCollapsibleStratum($newField);
138
+ createDynamicStratumTitle($newField.attr("id"));
139
+
140
+ const $newPanel = $newField.find('.collapsible');
141
+ const $newToggle = $newField.find('.stratum--collapse');
142
+ if ($newPanel.length && $newToggle.length) {
143
+ $newToggle.attr('aria-expanded', 'true');
144
+ $newPanel.attr('aria-hidden', 'false').show();
145
+ }
146
+
147
+ createSortableList();
148
+ runComponents();
149
+
150
+ const newAccordion = $newField[0].querySelector('[data-component="accordion"]');
151
+ if (newAccordion) {
152
+ try { createAccordion(newAccordion); } catch (_e) {}
153
+ }
154
+
155
+ if (document && typeof document.dispatchEvent === "function") {
156
+ try {
157
+ document.dispatchEvent(new CustomEvent("ajax:loaded", { detail: $newField[0] }));
158
+ } catch (_e) {}
159
+ }
160
+
161
+ }
162
+ });
163
+
164
+ $(document).on("click", ".remove-stratum", function(e) {
165
+ e.preventDefault();
166
+ e.stopPropagation();
167
+
168
+ const $field = $(this).closest(fieldSelector);
169
+
170
+ $field.find("input[name$=\\[deleted\\]]").val("true");
171
+ $field.addClass("hidden");
172
+ $field.hide();
173
+
174
+ runComponents();
175
+ });
176
+
177
+ $(document).on("click", ".move-up-stratum", function(e) {
178
+ e.preventDefault();
179
+ e.stopPropagation();
180
+
181
+ const $field = $(this).closest(fieldSelector);
182
+ const $prev = $field.prevAll(fieldSelector + ":not(.hidden)").first();
183
+
184
+ if ($prev.length) {
185
+ $field.insertBefore($prev);
186
+ runComponents();
187
+ }
188
+ });
189
+
190
+ $(document).on("click", ".move-down-stratum", function(e) {
191
+ e.preventDefault();
192
+ e.stopPropagation();
193
+
194
+ const $field = $(this).closest(fieldSelector);
195
+ const $next = $field.nextAll(fieldSelector + ":not(.hidden)").first();
196
+
197
+ if ($next.length) {
198
+ $field.insertAfter($next);
199
+ runComponents();
200
+ }
201
+ });
202
+
203
+ const updateSubstratumFieldsVisibility = ($stratum) => {
204
+ const kind = $stratum.find("select[id$='_kind']").val();
205
+ if (kind === "value") {
206
+ $stratum.find(".substratum-value-field").show();
207
+ $stratum.find(".substratum-range-field").hide();
208
+ } else {
209
+ $stratum.find(".substratum-value-field").hide();
210
+ $stratum.find(".substratum-range-field").show();
211
+ }
212
+ };
213
+
214
+ $(document).on("change", "select[id$='_kind']", function() {
215
+ const $stratum = $(this).closest(fieldSelector);
216
+ updateSubstratumFieldsVisibility($stratum);
217
+ });
218
+
219
+ const makeRequiredCatalanFields = () => {
220
+ $(fieldSelector).find(".stratum-fields input[id$='_name_ca']").attr("required", true);
221
+ };
222
+
223
+ $("form").on("submit", function(e) {
224
+ let hasEmptyName = false;
225
+ let $firstInvalidStratum = null;
226
+
227
+ $(fieldSelector).each((idx, el) => {
228
+ const $stratum = $(el);
229
+ const isDeleted = $stratum.find("input[name$='[deleted]']").val() === "true";
230
+ const $nameField = $stratum.find(".stratum-fields input[id$='_name_ca']");
231
+
232
+ $nameField.removeClass("is-invalid-input");
233
+
234
+ if (!isDeleted) {
235
+ const nameValue = $nameField.val();
236
+ if (!nameValue || nameValue.trim() === "") {
237
+ hasEmptyName = true;
238
+ $nameField.addClass("is-invalid-input");
239
+
240
+ expandStratum($stratum);
241
+
242
+ if (!$firstInvalidStratum) {
243
+ $firstInvalidStratum = $stratum;
244
+ }
245
+ }
246
+ }
247
+ });
248
+
249
+ if (hasEmptyName) {
250
+ e.preventDefault();
251
+ if ($firstInvalidStratum) {
252
+ $firstInvalidStratum[0].scrollIntoView({ behavior: "smooth", block: "center" });
253
+ }
254
+ return false;
255
+ }
256
+ });
257
+
258
+ createSortableList();
259
+
260
+ $(fieldSelector).each((idx, el) => {
261
+ const $target = $(el);
262
+ hideDeletedStratum($target);
263
+ updateSubstratumFieldsVisibility($target);
264
+ createCollapsibleStratum($target);
265
+ createDynamicStratumTitle($target.attr("id"));
266
+ });
267
+
268
+ makeRequiredCatalanFields();
269
+
270
+ runComponents();
271
+
272
+ if (document && typeof document.dispatchEvent === "function") {
273
+ try {
274
+ const $root = $(wrapperSelector).first();
275
+ if ($root.length) {
276
+ document.dispatchEvent(new CustomEvent("ajax:loaded", { detail: $root[0] }));
277
+ }
278
+ } catch (_e) {}
279
+ }
280
+ })
281
+
282
+ $(document).on("click", ".stratified-sortition-stratum .stratum--collapse", function(e) {
283
+ const btn = this;
284
+
285
+ if (btn.hasAttribute("role")) return;
286
+
287
+ const ctrl = btn.getAttribute("data-controls");
288
+ if (!ctrl) return;
289
+ const panel = document.getElementById(ctrl);
290
+ if (!panel) return;
291
+
292
+ const expanded = btn.getAttribute("aria-expanded") === "true";
293
+ const next = !expanded;
294
+ btn.setAttribute("aria-expanded", next ? "true" : "false");
295
+ panel.setAttribute("aria-hidden", next ? "false" : "true");
296
+
297
+ if (next) {
298
+ $(panel).show();
299
+ } else {
300
+ $(panel).hide();
301
+ }
302
+ });