tudla_hubstaff 0.1.0

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 (88) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +270 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/stylesheets/tudla_hubstaff/application.css +15 -0
  6. data/app/assets/tailwind/tudla_hubstaff/engine.css +32 -0
  7. data/app/components/tudla_hubstaff/activity_row_component.html.erb +46 -0
  8. data/app/components/tudla_hubstaff/activity_row_component.rb +67 -0
  9. data/app/components/tudla_hubstaff/base_component.rb +10 -0
  10. data/app/components/tudla_hubstaff/project_row_component.html.erb +22 -0
  11. data/app/components/tudla_hubstaff/project_row_component.rb +21 -0
  12. data/app/components/tudla_hubstaff/task_activities_component.html.erb +82 -0
  13. data/app/components/tudla_hubstaff/task_activities_component.rb +93 -0
  14. data/app/components/tudla_hubstaff/task_row_component.html.erb +22 -0
  15. data/app/components/tudla_hubstaff/task_row_component.rb +17 -0
  16. data/app/components/tudla_hubstaff/ui/map_button_component.html.erb +8 -0
  17. data/app/components/tudla_hubstaff/ui/map_button_component.rb +21 -0
  18. data/app/components/tudla_hubstaff/ui/mapping_modal_component.html.erb +51 -0
  19. data/app/components/tudla_hubstaff/ui/mapping_modal_component.rb +34 -0
  20. data/app/components/tudla_hubstaff/ui/modal_component.html.erb +36 -0
  21. data/app/components/tudla_hubstaff/ui/modal_component.rb +17 -0
  22. data/app/components/tudla_hubstaff/ui/pagination_component.html.erb +19 -0
  23. data/app/components/tudla_hubstaff/ui/pagination_component.rb +47 -0
  24. data/app/components/tudla_hubstaff/ui/status_badge_component.html.erb +3 -0
  25. data/app/components/tudla_hubstaff/ui/status_badge_component.rb +26 -0
  26. data/app/components/tudla_hubstaff/ui/table_component.html.erb +20 -0
  27. data/app/components/tudla_hubstaff/ui/table_component.rb +15 -0
  28. data/app/components/tudla_hubstaff/ui.rb +4 -0
  29. data/app/components/tudla_hubstaff/user_activities_list_component.html.erb +70 -0
  30. data/app/components/tudla_hubstaff/user_activities_list_component.rb +92 -0
  31. data/app/components/tudla_hubstaff/user_row_component.html.erb +22 -0
  32. data/app/components/tudla_hubstaff/user_row_component.rb +13 -0
  33. data/app/controllers/tudla_hubstaff/activities_controller.rb +88 -0
  34. data/app/controllers/tudla_hubstaff/application_controller.rb +14 -0
  35. data/app/controllers/tudla_hubstaff/projects_controller.rb +75 -0
  36. data/app/controllers/tudla_hubstaff/tasks_controller.rb +75 -0
  37. data/app/controllers/tudla_hubstaff/users_controller.rb +75 -0
  38. data/app/helpers/tudla_hubstaff/application_helper.rb +4 -0
  39. data/app/javascript/tudla_hubstaff/application.js +21 -0
  40. data/app/javascript/tudla_hubstaff/controllers/activity_task_mapping_modal_controller.js +183 -0
  41. data/app/javascript/tudla_hubstaff/controllers/index.js +15 -0
  42. data/app/javascript/tudla_hubstaff/controllers/project_mapping_modal_controller.js +180 -0
  43. data/app/javascript/tudla_hubstaff/controllers/task_mapping_modal_controller.js +181 -0
  44. data/app/javascript/tudla_hubstaff/controllers/user_mapping_modal_controller.js +181 -0
  45. data/app/jobs/tudla_hubstaff/application_job.rb +4 -0
  46. data/app/jobs/tudla_hubstaff/fetch_updated_activities_job.rb +40 -0
  47. data/app/mailers/tudla_hubstaff/application_mailer.rb +6 -0
  48. data/app/models/tudla_hubstaff/activity.rb +6 -0
  49. data/app/models/tudla_hubstaff/application_record.rb +5 -0
  50. data/app/models/tudla_hubstaff/config.rb +8 -0
  51. data/app/models/tudla_hubstaff/organization_update.rb +5 -0
  52. data/app/models/tudla_hubstaff/project.rb +14 -0
  53. data/app/models/tudla_hubstaff/task.rb +9 -0
  54. data/app/models/tudla_hubstaff/user.rb +16 -0
  55. data/app/services/tudla_hubstaff/api_client.rb +64 -0
  56. data/app/services/tudla_hubstaff/api_connection.rb +54 -0
  57. data/app/services/tudla_hubstaff/concerns/activity_syncing.rb +46 -0
  58. data/app/services/tudla_hubstaff/concerns/task_syncing.rb +32 -0
  59. data/app/services/tudla_hubstaff/fetch_updated_activities_service.rb +28 -0
  60. data/app/services/tudla_hubstaff/get_task_service.rb +24 -0
  61. data/app/services/tudla_hubstaff/sync_activities_service.rb +28 -0
  62. data/app/services/tudla_hubstaff/sync_projects_service.rb +51 -0
  63. data/app/services/tudla_hubstaff/sync_tasks_service.rb +31 -0
  64. data/app/services/tudla_hubstaff/sync_users_service.rb +53 -0
  65. data/app/views/layouts/tudla_hubstaff/application.html.erb +19 -0
  66. data/app/views/tudla_hubstaff/projects/unmapped.html.erb +75 -0
  67. data/app/views/tudla_hubstaff/tasks/unmapped.html.erb +75 -0
  68. data/app/views/tudla_hubstaff/users/map_tudla_user.turbo_stream.erb +1 -0
  69. data/app/views/tudla_hubstaff/users/unmapped.html.erb +75 -0
  70. data/config/importmap.rb +7 -0
  71. data/config/routes.rb +41 -0
  72. data/db/migrate/20251230220607_create_tudla_hubstaff_users.rb +16 -0
  73. data/db/migrate/20251230222524_create_tudla_hubstaff_tasks.rb +21 -0
  74. data/db/migrate/20251230223731_create_tudla_hubstaff_projects.rb +15 -0
  75. data/db/migrate/20251230225408_add_hubstaff_ids_to_models.rb +14 -0
  76. data/db/migrate/20251231041753_add_last_updated_at_to_models.rb +7 -0
  77. data/db/migrate/20251231043338_add_tudla_ids_to_models.rb +14 -0
  78. data/db/migrate/20251231045209_create_tudla_hubstaff_activities.rb +30 -0
  79. data/db/migrate/20251231074018_create_tudla_hubstaff_configs.rb +15 -0
  80. data/db/migrate/20251231074556_create_tudla_hubstaff_organization_updates.rb +12 -0
  81. data/lib/generators/tudla_hubstaff/components_generator.rb +68 -0
  82. data/lib/generators/tudla_hubstaff/views_generator.rb +54 -0
  83. data/lib/tasks/tudla_hubstaff_tasks.rake +18 -0
  84. data/lib/tudla_hubstaff/engine.rb +88 -0
  85. data/lib/tudla_hubstaff/provider.rb +12 -0
  86. data/lib/tudla_hubstaff/version.rb +3 -0
  87. data/lib/tudla_hubstaff.rb +49 -0
  88. metadata +442 -0
@@ -0,0 +1,88 @@
1
+ module TudlaHubstaff
2
+ class ActivitiesController < ApplicationController
3
+ skip_forgery_protection only: :map_activity_task
4
+
5
+ TUDLA_TASKS_PER_PAGE = 10
6
+
7
+ def user_activities
8
+ user_id = params[:user_id]
9
+ page = params[:page] || 1
10
+ unmapped_only = ActiveModel::Type::Boolean.new.cast(params[:unmapped_only])
11
+
12
+ render TudlaHubstaff::UserActivitiesListComponent.new(
13
+ user_id: user_id,
14
+ current_page: page,
15
+ unmapped_only: unmapped_only
16
+ )
17
+ end
18
+
19
+ def available_tudla_tasks
20
+ page = [ params[:page].to_i, 1 ].max
21
+ name_filter = params[:name].to_s.strip.downcase
22
+
23
+ all_tasks = host_interface.available_tasks_for_user(current_user)
24
+
25
+ filtered_tasks = if name_filter.present?
26
+ all_tasks.select { |t| t.name.downcase.include?(name_filter) }
27
+ else
28
+ all_tasks
29
+ end
30
+
31
+ total_count = filtered_tasks.size
32
+ offset = (page - 1) * TUDLA_TASKS_PER_PAGE
33
+ paginated_tasks = filtered_tasks.slice(offset, TUDLA_TASKS_PER_PAGE) || []
34
+
35
+ render json: {
36
+ tasks: paginated_tasks.map { |t| { id: t.id, name: t.name, project_name: t.project_name } },
37
+ current_page: page,
38
+ total_pages: (total_count.to_f / TUDLA_TASKS_PER_PAGE).ceil,
39
+ total_count: total_count
40
+ }
41
+ end
42
+
43
+ def map_activity_task
44
+ @activity = Activity.find(params[:id])
45
+ hubstaff_task = Task.find_by(task_id: @activity.task_id)
46
+ tudla_task_id = params[:tudla_task_id]
47
+
48
+ if hubstaff_task.nil?
49
+ respond_with_error("Hubstaff task not found for this activity.")
50
+ return
51
+ end
52
+
53
+ if tudla_task_id.present? && hubstaff_task.update(tudla_task_id: tudla_task_id)
54
+ respond_to do |format|
55
+ format.html do
56
+ if request.xhr?
57
+ head :ok
58
+ else
59
+ redirect_back fallback_location: root_path, notice: "Task mapped successfully."
60
+ end
61
+ end
62
+ format.turbo_stream
63
+ end
64
+ else
65
+ respond_with_error("Failed to map task.")
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def host_interface
72
+ TudlaHubstaff.host_interface
73
+ end
74
+
75
+ def respond_with_error(message)
76
+ respond_to do |format|
77
+ format.html do
78
+ if request.xhr?
79
+ head :unprocessable_entity
80
+ else
81
+ redirect_back fallback_location: root_path, alert: message
82
+ end
83
+ end
84
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("flash", partial: "shared/flash", locals: { alert: message }) }
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,14 @@
1
+ module TudlaHubstaff
2
+ class ApplicationController < ::ApplicationController
3
+ include Pagy::Method
4
+
5
+ layout -> { TudlaHubstaff::Engine.config.tudla_hubstaff.layout }
6
+ end
7
+
8
+ protected
9
+
10
+ # we explicitly need the host to provide a current_user method
11
+ def current_user
12
+ super
13
+ end
14
+ end
@@ -0,0 +1,75 @@
1
+ module TudlaHubstaff
2
+ class ProjectsController < ApplicationController
3
+ skip_forgery_protection only: :map_tudla_project
4
+
5
+ PER_PAGE = 20
6
+ TUDLA_PROJECTS_PER_PAGE = 10
7
+
8
+ def unmapped
9
+ @pagy, @projects = pagy(
10
+ :offset,
11
+ Project.where(tudla_project_id: nil).order(:name),
12
+ limit: PER_PAGE
13
+ )
14
+ end
15
+
16
+ def available_tudla_projects
17
+ page = [ params[:page].to_i, 1 ].max
18
+ name_filter = params[:name].to_s.strip.downcase
19
+
20
+ all_projects = host_interface.available_projects_for_user(current_user)
21
+
22
+ filtered_projects = if name_filter.present?
23
+ all_projects.select { |p| p.name.downcase.include?(name_filter) }
24
+ else
25
+ all_projects
26
+ end
27
+
28
+ total_count = filtered_projects.size
29
+ offset = (page - 1) * TUDLA_PROJECTS_PER_PAGE
30
+ paginated_projects = filtered_projects.slice(offset, TUDLA_PROJECTS_PER_PAGE) || []
31
+
32
+ render json: {
33
+ projects: paginated_projects.map { |p| { id: p.id, name: p.name } },
34
+ current_page: page,
35
+ total_pages: (total_count.to_f / TUDLA_PROJECTS_PER_PAGE).ceil,
36
+ total_count: total_count
37
+ }
38
+ end
39
+
40
+ def map_tudla_project
41
+ @project = Project.find(params[:id])
42
+ tudla_project_id = params[:tudla_project_id]
43
+
44
+ if tudla_project_id.present? && @project.update(tudla_project_id: tudla_project_id)
45
+ respond_to do |format|
46
+ format.html do
47
+ if request.xhr?
48
+ head :ok
49
+ else
50
+ redirect_to unmapped_projects_path, notice: "Project mapped successfully."
51
+ end
52
+ end
53
+ format.turbo_stream
54
+ end
55
+ else
56
+ respond_to do |format|
57
+ format.html do
58
+ if request.xhr?
59
+ head :unprocessable_entity
60
+ else
61
+ redirect_to unmapped_projects_path, alert: "Failed to map project."
62
+ end
63
+ end
64
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("flash", partial: "shared/flash", locals: { alert: "Failed to map project." }) }
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def host_interface
72
+ TudlaHubstaff.host_interface
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,75 @@
1
+ module TudlaHubstaff
2
+ class TasksController < ApplicationController
3
+ skip_forgery_protection only: :map_tudla_task
4
+
5
+ PER_PAGE = 20
6
+ TUDLA_TASKS_PER_PAGE = 10
7
+
8
+ def unmapped
9
+ @pagy, @tasks = pagy(
10
+ :offset,
11
+ Task.where(tudla_task_id: nil).order(:summary),
12
+ limit: PER_PAGE
13
+ )
14
+ end
15
+
16
+ def available_tudla_tasks
17
+ page = [ params[:page].to_i, 1 ].max
18
+ name_filter = params[:name].to_s.strip.downcase
19
+
20
+ all_tasks = host_interface.available_tasks_for_user(current_user)
21
+
22
+ filtered_tasks = if name_filter.present?
23
+ all_tasks.select { |t| t.name.downcase.include?(name_filter) }
24
+ else
25
+ all_tasks
26
+ end
27
+
28
+ total_count = filtered_tasks.size
29
+ offset = (page - 1) * TUDLA_TASKS_PER_PAGE
30
+ paginated_tasks = filtered_tasks.slice(offset, TUDLA_TASKS_PER_PAGE) || []
31
+
32
+ render json: {
33
+ tasks: paginated_tasks.map { |t| { id: t.id, name: t.name, project_name: t.project_name } },
34
+ current_page: page,
35
+ total_pages: (total_count.to_f / TUDLA_TASKS_PER_PAGE).ceil,
36
+ total_count: total_count
37
+ }
38
+ end
39
+
40
+ def map_tudla_task
41
+ @task = Task.find(params[:id])
42
+ tudla_task_id = params[:tudla_task_id]
43
+
44
+ if tudla_task_id.present? && @task.update(tudla_task_id: tudla_task_id)
45
+ respond_to do |format|
46
+ format.html do
47
+ if request.xhr?
48
+ head :ok
49
+ else
50
+ redirect_to unmapped_tasks_path, notice: "Task mapped successfully."
51
+ end
52
+ end
53
+ format.turbo_stream
54
+ end
55
+ else
56
+ respond_to do |format|
57
+ format.html do
58
+ if request.xhr?
59
+ head :unprocessable_entity
60
+ else
61
+ redirect_to unmapped_tasks_path, alert: "Failed to map task."
62
+ end
63
+ end
64
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("flash", partial: "shared/flash", locals: { alert: "Failed to map task." }) }
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def host_interface
72
+ TudlaHubstaff.host_interface
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,75 @@
1
+ module TudlaHubstaff
2
+ class UsersController < ApplicationController
3
+ skip_forgery_protection only: :map_tudla_user
4
+
5
+ PER_PAGE = 20
6
+ TUDLA_USERS_PER_PAGE = 10
7
+
8
+ def unmapped
9
+ @pagy, @users = pagy(
10
+ :offset,
11
+ User.where(tudla_user_id: nil).order(:name),
12
+ limit: PER_PAGE
13
+ )
14
+ end
15
+
16
+ def available_tudla_users
17
+ page = [ params[:page].to_i, 1 ].max
18
+ name_filter = params[:name].to_s.strip.downcase
19
+
20
+ all_users = host_interface.available_users_for_user(current_user)
21
+
22
+ filtered_users = if name_filter.present?
23
+ all_users.select { |u| u.name.downcase.include?(name_filter) }
24
+ else
25
+ all_users
26
+ end
27
+
28
+ total_count = filtered_users.size
29
+ offset = (page - 1) * TUDLA_USERS_PER_PAGE
30
+ paginated_users = filtered_users.slice(offset, TUDLA_USERS_PER_PAGE) || []
31
+
32
+ render json: {
33
+ users: paginated_users.map { |u| { id: u.id, name: u.name, email: u.email } },
34
+ current_page: page,
35
+ total_pages: (total_count.to_f / TUDLA_USERS_PER_PAGE).ceil,
36
+ total_count: total_count
37
+ }
38
+ end
39
+
40
+ def map_tudla_user
41
+ @user = User.find(params[:id])
42
+ tudla_user_id = params[:tudla_user_id]
43
+
44
+ if tudla_user_id.present? && @user.update(tudla_user_id: tudla_user_id)
45
+ respond_to do |format|
46
+ format.html do
47
+ if request.xhr?
48
+ head :ok
49
+ else
50
+ redirect_to unmapped_users_path, notice: "User mapped successfully."
51
+ end
52
+ end
53
+ format.turbo_stream
54
+ end
55
+ else
56
+ respond_to do |format|
57
+ format.html do
58
+ if request.xhr?
59
+ head :unprocessable_entity
60
+ else
61
+ redirect_to unmapped_users_path, alert: "Failed to map user."
62
+ end
63
+ end
64
+ format.turbo_stream { render turbo_stream: turbo_stream.replace("flash", partial: "shared/flash", locals: { alert: "Failed to map user." }) }
65
+ end
66
+ end
67
+ end
68
+
69
+ private
70
+
71
+ def host_interface
72
+ TudlaHubstaff.host_interface
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,4 @@
1
+ module TudlaHubstaff
2
+ module ApplicationHelper
3
+ end
4
+ end
@@ -0,0 +1,21 @@
1
+ // TudlaHubstaff Stimulus Integration
2
+ // This module provides controller registration for host applications.
3
+ // The host app is responsible for initializing Stimulus (Application.start()).
4
+ // This engine registers its controllers with the host's Stimulus instance.
5
+
6
+ import { Application } from "@hotwired/stimulus"
7
+
8
+ // Get the host application's Stimulus instance, or create one if it doesn't exist
9
+ function getApplication() {
10
+ if (window.Stimulus) {
11
+ return window.Stimulus
12
+ }
13
+ // Fallback: start a new application if host hasn't initialized one
14
+ const application = Application.start()
15
+ window.Stimulus = application
16
+ return application
17
+ }
18
+
19
+ const application = getApplication()
20
+
21
+ export { application }
@@ -0,0 +1,183 @@
1
+ import { Controller } from "@hotwired/stimulus"
2
+
3
+ export default class extends Controller {
4
+ static targets = ["modal", "itemName", "itemsList", "loading", "pagination", "searchInput"]
5
+
6
+ connect() {
7
+ this.currentPage = 1
8
+ this.currentActivityId = null
9
+ this.currentHubstaffTaskId = null
10
+ this.searchTimeout = null
11
+ }
12
+
13
+ open(event) {
14
+ this.currentActivityId = event.currentTarget.dataset.activityId
15
+ this.currentHubstaffTaskId = event.currentTarget.dataset.hubstaffTaskId
16
+ this._mapUrl = event.currentTarget.dataset.mapUrl
17
+ const taskName = event.currentTarget.dataset.taskName
18
+
19
+ this.itemNameTarget.textContent = taskName
20
+ this.searchInputTarget.value = ""
21
+ this.currentPage = 1
22
+
23
+ this.modalTarget.classList.remove("hidden")
24
+ this.loadItems()
25
+ }
26
+
27
+ close() {
28
+ this.modalTarget.classList.add("hidden")
29
+ this.itemsListTarget.innerHTML = ""
30
+ this.paginationTarget.innerHTML = ""
31
+ }
32
+
33
+ filterItems() {
34
+ clearTimeout(this.searchTimeout)
35
+ this.searchTimeout = setTimeout(() => {
36
+ this.currentPage = 1
37
+ this.loadItems()
38
+ }, 300)
39
+ }
40
+
41
+ async loadItems() {
42
+ this.loadingTarget.classList.remove("hidden")
43
+ this.itemsListTarget.innerHTML = ""
44
+
45
+ const nameFilter = this.searchInputTarget.value
46
+ const url = new URL(this.availableItemsUrl, window.location.origin)
47
+ url.searchParams.set("page", this.currentPage)
48
+ if (nameFilter) {
49
+ url.searchParams.set("name", nameFilter)
50
+ }
51
+
52
+ try {
53
+ const response = await fetch(url, {
54
+ headers: {
55
+ "Accept": "application/json",
56
+ "X-CSRF-Token": this.csrfToken
57
+ }
58
+ })
59
+
60
+ if (!response.ok) throw new Error("Failed to load tasks")
61
+
62
+ const data = await response.json()
63
+ this.renderItems(data.tasks)
64
+ this.renderPagination(data.current_page, data.total_pages)
65
+ } catch (error) {
66
+ this.itemsListTarget.innerHTML = `<p class="text-red-500 text-center py-4">Failed to load tasks</p>`
67
+ } finally {
68
+ this.loadingTarget.classList.add("hidden")
69
+ }
70
+ }
71
+
72
+ renderItems(tasks) {
73
+ if (tasks.length === 0) {
74
+ this.itemsListTarget.innerHTML = `<p class="text-gray-500 text-center py-4">No tasks found</p>`
75
+ return
76
+ }
77
+
78
+ this.itemsListTarget.innerHTML = tasks.map(task => `
79
+ <button type="button"
80
+ class="w-full text-left px-4 py-3 rounded-md border border-gray-200 hover:bg-indigo-50 hover:border-indigo-300 transition-colors"
81
+ data-action="click->activity-task-mapping-modal#selectItem"
82
+ data-tudla-task-id="${task.id}">
83
+ <div class="font-medium text-gray-900">${this.escapeHtml(task.name)}</div>
84
+ <div class="text-sm text-gray-500">${this.escapeHtml(task.project_name || "")}</div>
85
+ </button>
86
+ `).join("")
87
+ }
88
+
89
+ renderPagination(currentPage, totalPages) {
90
+ if (totalPages <= 1) {
91
+ this.paginationTarget.innerHTML = ""
92
+ return
93
+ }
94
+
95
+ let html = `<span class="text-sm text-gray-500">Page ${currentPage} of ${totalPages}</span><div class="flex gap-2">`
96
+
97
+ if (currentPage > 1) {
98
+ html += `<button type="button"
99
+ class="px-3 py-1 text-sm rounded-md bg-white border border-gray-300 hover:bg-gray-50"
100
+ data-action="click->activity-task-mapping-modal#prevPage">
101
+ Previous
102
+ </button>`
103
+ }
104
+
105
+ if (currentPage < totalPages) {
106
+ html += `<button type="button"
107
+ class="px-3 py-1 text-sm rounded-md bg-white border border-gray-300 hover:bg-gray-50"
108
+ data-action="click->activity-task-mapping-modal#nextPage">
109
+ Next
110
+ </button>`
111
+ }
112
+
113
+ html += "</div>"
114
+ this.paginationTarget.innerHTML = html
115
+ }
116
+
117
+ prevPage() {
118
+ if (this.currentPage > 1) {
119
+ this.currentPage--
120
+ this.loadItems()
121
+ }
122
+ }
123
+
124
+ nextPage() {
125
+ this.currentPage++
126
+ this.loadItems()
127
+ }
128
+
129
+ selectItem(event) {
130
+ event.preventDefault()
131
+ event.stopPropagation()
132
+
133
+ const tudlaTaskId = event.currentTarget.dataset.tudlaTaskId
134
+ const mapUrl = this._mapUrl
135
+
136
+ if (!mapUrl) {
137
+ alert("Error: Map URL not set. Please try again.")
138
+ return
139
+ }
140
+
141
+ const xhr = new XMLHttpRequest()
142
+ const fullUrl = window.location.origin + mapUrl
143
+ const csrfToken = this.csrfToken
144
+
145
+ xhr.open("PATCH", fullUrl, true)
146
+ xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded")
147
+ xhr.setRequestHeader("Accept", "text/html, application/xhtml+xml")
148
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest")
149
+ if (csrfToken) {
150
+ xhr.setRequestHeader("X-CSRF-Token", csrfToken)
151
+ }
152
+
153
+ xhr.onload = () => {
154
+ if (xhr.status >= 200 && xhr.status < 300) {
155
+ alert("Success! Task mapped.")
156
+ window.location.reload()
157
+ } else {
158
+ alert("Failed to map task. Status: " + xhr.status)
159
+ }
160
+ }
161
+
162
+ xhr.onerror = () => {
163
+ alert("Network error mapping task.")
164
+ }
165
+
166
+ xhr.send(`tudla_task_id=${encodeURIComponent(tudlaTaskId)}`)
167
+ this.close()
168
+ }
169
+
170
+ escapeHtml(text) {
171
+ const div = document.createElement("div")
172
+ div.textContent = text
173
+ return div.innerHTML
174
+ }
175
+
176
+ get availableItemsUrl() {
177
+ return this.element.dataset.availableTasksUrl || "/tudla_hubstaff/activities/available_tudla_tasks"
178
+ }
179
+
180
+ get csrfToken() {
181
+ return document.querySelector('meta[name="csrf-token"]')?.content || ""
182
+ }
183
+ }
@@ -0,0 +1,15 @@
1
+ // TudlaHubstaff Stimulus Controllers
2
+ // Registers engine controllers with the host application's Stimulus instance.
3
+ // We import from the host's "application" module to ensure Stimulus is initialized first.
4
+
5
+ import { application } from "application"
6
+
7
+ import UserMappingModalController from "tudla_hubstaff/controllers/user_mapping_modal_controller"
8
+ import TaskMappingModalController from "tudla_hubstaff/controllers/task_mapping_modal_controller"
9
+ import ProjectMappingModalController from "tudla_hubstaff/controllers/project_mapping_modal_controller"
10
+ import ActivityTaskMappingModalController from "tudla_hubstaff/controllers/activity_task_mapping_modal_controller"
11
+
12
+ application.register("user-mapping-modal", UserMappingModalController)
13
+ application.register("task-mapping-modal", TaskMappingModalController)
14
+ application.register("project-mapping-modal", ProjectMappingModalController)
15
+ application.register("activity-task-mapping-modal", ActivityTaskMappingModalController)