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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +270 -0
- data/Rakefile +6 -0
- data/app/assets/stylesheets/tudla_hubstaff/application.css +15 -0
- data/app/assets/tailwind/tudla_hubstaff/engine.css +32 -0
- data/app/components/tudla_hubstaff/activity_row_component.html.erb +46 -0
- data/app/components/tudla_hubstaff/activity_row_component.rb +67 -0
- data/app/components/tudla_hubstaff/base_component.rb +10 -0
- data/app/components/tudla_hubstaff/project_row_component.html.erb +22 -0
- data/app/components/tudla_hubstaff/project_row_component.rb +21 -0
- data/app/components/tudla_hubstaff/task_activities_component.html.erb +82 -0
- data/app/components/tudla_hubstaff/task_activities_component.rb +93 -0
- data/app/components/tudla_hubstaff/task_row_component.html.erb +22 -0
- data/app/components/tudla_hubstaff/task_row_component.rb +17 -0
- data/app/components/tudla_hubstaff/ui/map_button_component.html.erb +8 -0
- data/app/components/tudla_hubstaff/ui/map_button_component.rb +21 -0
- data/app/components/tudla_hubstaff/ui/mapping_modal_component.html.erb +51 -0
- data/app/components/tudla_hubstaff/ui/mapping_modal_component.rb +34 -0
- data/app/components/tudla_hubstaff/ui/modal_component.html.erb +36 -0
- data/app/components/tudla_hubstaff/ui/modal_component.rb +17 -0
- data/app/components/tudla_hubstaff/ui/pagination_component.html.erb +19 -0
- data/app/components/tudla_hubstaff/ui/pagination_component.rb +47 -0
- data/app/components/tudla_hubstaff/ui/status_badge_component.html.erb +3 -0
- data/app/components/tudla_hubstaff/ui/status_badge_component.rb +26 -0
- data/app/components/tudla_hubstaff/ui/table_component.html.erb +20 -0
- data/app/components/tudla_hubstaff/ui/table_component.rb +15 -0
- data/app/components/tudla_hubstaff/ui.rb +4 -0
- data/app/components/tudla_hubstaff/user_activities_list_component.html.erb +70 -0
- data/app/components/tudla_hubstaff/user_activities_list_component.rb +92 -0
- data/app/components/tudla_hubstaff/user_row_component.html.erb +22 -0
- data/app/components/tudla_hubstaff/user_row_component.rb +13 -0
- data/app/controllers/tudla_hubstaff/activities_controller.rb +88 -0
- data/app/controllers/tudla_hubstaff/application_controller.rb +14 -0
- data/app/controllers/tudla_hubstaff/projects_controller.rb +75 -0
- data/app/controllers/tudla_hubstaff/tasks_controller.rb +75 -0
- data/app/controllers/tudla_hubstaff/users_controller.rb +75 -0
- data/app/helpers/tudla_hubstaff/application_helper.rb +4 -0
- data/app/javascript/tudla_hubstaff/application.js +21 -0
- data/app/javascript/tudla_hubstaff/controllers/activity_task_mapping_modal_controller.js +183 -0
- data/app/javascript/tudla_hubstaff/controllers/index.js +15 -0
- data/app/javascript/tudla_hubstaff/controllers/project_mapping_modal_controller.js +180 -0
- data/app/javascript/tudla_hubstaff/controllers/task_mapping_modal_controller.js +181 -0
- data/app/javascript/tudla_hubstaff/controllers/user_mapping_modal_controller.js +181 -0
- data/app/jobs/tudla_hubstaff/application_job.rb +4 -0
- data/app/jobs/tudla_hubstaff/fetch_updated_activities_job.rb +40 -0
- data/app/mailers/tudla_hubstaff/application_mailer.rb +6 -0
- data/app/models/tudla_hubstaff/activity.rb +6 -0
- data/app/models/tudla_hubstaff/application_record.rb +5 -0
- data/app/models/tudla_hubstaff/config.rb +8 -0
- data/app/models/tudla_hubstaff/organization_update.rb +5 -0
- data/app/models/tudla_hubstaff/project.rb +14 -0
- data/app/models/tudla_hubstaff/task.rb +9 -0
- data/app/models/tudla_hubstaff/user.rb +16 -0
- data/app/services/tudla_hubstaff/api_client.rb +64 -0
- data/app/services/tudla_hubstaff/api_connection.rb +54 -0
- data/app/services/tudla_hubstaff/concerns/activity_syncing.rb +46 -0
- data/app/services/tudla_hubstaff/concerns/task_syncing.rb +32 -0
- data/app/services/tudla_hubstaff/fetch_updated_activities_service.rb +28 -0
- data/app/services/tudla_hubstaff/get_task_service.rb +24 -0
- data/app/services/tudla_hubstaff/sync_activities_service.rb +28 -0
- data/app/services/tudla_hubstaff/sync_projects_service.rb +51 -0
- data/app/services/tudla_hubstaff/sync_tasks_service.rb +31 -0
- data/app/services/tudla_hubstaff/sync_users_service.rb +53 -0
- data/app/views/layouts/tudla_hubstaff/application.html.erb +19 -0
- data/app/views/tudla_hubstaff/projects/unmapped.html.erb +75 -0
- data/app/views/tudla_hubstaff/tasks/unmapped.html.erb +75 -0
- data/app/views/tudla_hubstaff/users/map_tudla_user.turbo_stream.erb +1 -0
- data/app/views/tudla_hubstaff/users/unmapped.html.erb +75 -0
- data/config/importmap.rb +7 -0
- data/config/routes.rb +41 -0
- data/db/migrate/20251230220607_create_tudla_hubstaff_users.rb +16 -0
- data/db/migrate/20251230222524_create_tudla_hubstaff_tasks.rb +21 -0
- data/db/migrate/20251230223731_create_tudla_hubstaff_projects.rb +15 -0
- data/db/migrate/20251230225408_add_hubstaff_ids_to_models.rb +14 -0
- data/db/migrate/20251231041753_add_last_updated_at_to_models.rb +7 -0
- data/db/migrate/20251231043338_add_tudla_ids_to_models.rb +14 -0
- data/db/migrate/20251231045209_create_tudla_hubstaff_activities.rb +30 -0
- data/db/migrate/20251231074018_create_tudla_hubstaff_configs.rb +15 -0
- data/db/migrate/20251231074556_create_tudla_hubstaff_organization_updates.rb +12 -0
- data/lib/generators/tudla_hubstaff/components_generator.rb +68 -0
- data/lib/generators/tudla_hubstaff/views_generator.rb +54 -0
- data/lib/tasks/tudla_hubstaff_tasks.rake +18 -0
- data/lib/tudla_hubstaff/engine.rb +88 -0
- data/lib/tudla_hubstaff/provider.rb +12 -0
- data/lib/tudla_hubstaff/version.rb +3 -0
- data/lib/tudla_hubstaff.rb +49 -0
- 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,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)
|