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,93 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
class TaskActivitiesComponent < TudlaHubstaff::BaseComponent
|
|
3
|
+
PER_PAGE = 20
|
|
4
|
+
|
|
5
|
+
def initialize(tudla_task_id:, current_page: 1, per_page: PER_PAGE)
|
|
6
|
+
@tudla_task_id = tudla_task_id
|
|
7
|
+
@current_page = current_page.to_i
|
|
8
|
+
@per_page = per_page.to_i
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
attr_reader :tudla_task_id, :current_page, :per_page
|
|
12
|
+
|
|
13
|
+
def activities
|
|
14
|
+
@activities ||= fetch_activities
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def total_count
|
|
18
|
+
@total_count ||= activities_scope.count
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def total_pages
|
|
22
|
+
(total_count.to_f / per_page).ceil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def render?
|
|
26
|
+
tudla_task_id.present?
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def start_item
|
|
30
|
+
return 0 if total_count.zero?
|
|
31
|
+
((current_page - 1) * per_page) + 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def end_item
|
|
35
|
+
[ current_page * per_page, total_count ].min
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def show_previous?
|
|
39
|
+
current_page > 1
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_next?
|
|
43
|
+
current_page < total_pages
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def previous_page
|
|
47
|
+
current_page - 1
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def next_page
|
|
51
|
+
current_page + 1
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def format_duration(seconds)
|
|
55
|
+
return "0m" if seconds.nil? || seconds.zero?
|
|
56
|
+
|
|
57
|
+
hours = seconds / 3600
|
|
58
|
+
minutes = (seconds % 3600) / 60
|
|
59
|
+
|
|
60
|
+
if hours > 0
|
|
61
|
+
"#{hours}h #{minutes}m"
|
|
62
|
+
else
|
|
63
|
+
"#{minutes}m"
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def format_date(date_string)
|
|
68
|
+
return "" if date_string.blank?
|
|
69
|
+
Date.parse(date_string).strftime("%b %d, %Y")
|
|
70
|
+
rescue ArgumentError
|
|
71
|
+
date_string
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def fetch_activities
|
|
77
|
+
activities_scope
|
|
78
|
+
.order(last_updated_at: :desc)
|
|
79
|
+
.offset((current_page - 1) * per_page)
|
|
80
|
+
.limit(per_page)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def activities_scope
|
|
84
|
+
@activities_scope ||= begin
|
|
85
|
+
hubstaff_task_ids = TudlaHubstaff::Task
|
|
86
|
+
.where(tudla_task_id: tudla_task_id)
|
|
87
|
+
.pluck(:task_id)
|
|
88
|
+
|
|
89
|
+
TudlaHubstaff::Activity.where(task_id: hubstaff_task_ids)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<tr id="<%= dom_id %>" class="tudla_hubstaff-task-row">
|
|
2
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
3
|
+
<div class="text-sm font-medium text-gray-900"><%= task.summary %></div>
|
|
4
|
+
<div class="text-sm text-gray-500"><%= truncated_details %></div>
|
|
5
|
+
</td>
|
|
6
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
7
|
+
<div class="text-sm text-gray-900"><%= task.project_type %></div>
|
|
8
|
+
</td>
|
|
9
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
10
|
+
<%= render TudlaHubstaff::UI::StatusBadgeComponent.new(status: task.status) %>
|
|
11
|
+
</td>
|
|
12
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
13
|
+
<button type="button"
|
|
14
|
+
class="text-indigo-600 hover:text-indigo-900"
|
|
15
|
+
data-action="click->task-mapping-modal#open"
|
|
16
|
+
data-task-id="<%= task.id %>"
|
|
17
|
+
data-task-name="<%= task.summary %>"
|
|
18
|
+
data-map-url="<%= map_tudla_task_task_path(task) %>">
|
|
19
|
+
Map to Tudla Task
|
|
20
|
+
</button>
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
class TaskRowComponent < TudlaHubstaff::BaseComponent
|
|
3
|
+
def initialize(task:)
|
|
4
|
+
@task = task
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
attr_reader :task
|
|
8
|
+
|
|
9
|
+
def dom_id
|
|
10
|
+
"tudla_hubstaff_task_#{task.id}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def truncated_details
|
|
14
|
+
task.details.to_s.truncate(50)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<button type="button"
|
|
2
|
+
class="tudla_hubstaff-map-button text-indigo-600 hover:text-indigo-900"
|
|
3
|
+
data-action="click-><%= stimulus_controller %>#open"
|
|
4
|
+
data-item-id="<%= item_id %>"
|
|
5
|
+
data-item-name="<%= item_name %>"
|
|
6
|
+
data-map-url="<%= map_url %>">
|
|
7
|
+
<%= button_text %>
|
|
8
|
+
</button>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
module UI
|
|
3
|
+
class MapButtonComponent < TudlaHubstaff::BaseComponent
|
|
4
|
+
def initialize(
|
|
5
|
+
item_id:,
|
|
6
|
+
item_name:,
|
|
7
|
+
map_url:,
|
|
8
|
+
stimulus_controller:,
|
|
9
|
+
button_text: "Map"
|
|
10
|
+
)
|
|
11
|
+
@item_id = item_id
|
|
12
|
+
@item_name = item_name
|
|
13
|
+
@map_url = map_url
|
|
14
|
+
@stimulus_controller = stimulus_controller
|
|
15
|
+
@button_text = button_text
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
attr_reader :item_id, :item_name, :map_url, :stimulus_controller, :button_text
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<div id="<%= id %>"
|
|
2
|
+
class="tudla_hubstaff-mapping-modal fixed inset-0 z-50 overflow-y-auto hidden"
|
|
3
|
+
data-<%= stimulus_controller %>-target="modal">
|
|
4
|
+
<!-- Backdrop -->
|
|
5
|
+
<div class="fixed inset-0 bg-black/50 transition-opacity"
|
|
6
|
+
data-action="click-><%= stimulus_controller %>#close"></div>
|
|
7
|
+
|
|
8
|
+
<!-- Modal Content -->
|
|
9
|
+
<div class="fixed inset-0 z-10 overflow-y-auto">
|
|
10
|
+
<div class="flex min-h-full items-center justify-center p-4">
|
|
11
|
+
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all w-full max-w-lg">
|
|
12
|
+
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
13
|
+
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">
|
|
14
|
+
<%= title %>: <span data-<%= stimulus_controller %>-target="itemName"></span>
|
|
15
|
+
</h3>
|
|
16
|
+
|
|
17
|
+
<!-- Search Filter -->
|
|
18
|
+
<div class="mb-4">
|
|
19
|
+
<input type="text"
|
|
20
|
+
placeholder="<%= search_placeholder %>"
|
|
21
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
|
|
22
|
+
data-<%= stimulus_controller %>-target="searchInput"
|
|
23
|
+
data-action="input-><%= stimulus_controller %>#filterItems">
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<!-- Items List -->
|
|
27
|
+
<div class="max-h-80 overflow-y-auto">
|
|
28
|
+
<div data-<%= stimulus_controller %>-target="itemsList" class="space-y-2">
|
|
29
|
+
<!-- Items will be loaded here -->
|
|
30
|
+
</div>
|
|
31
|
+
<div data-<%= stimulus_controller %>-target="loading" class="hidden text-center py-4">
|
|
32
|
+
<span class="text-gray-500"><%= loading_text %></span>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
|
|
36
|
+
<!-- Modal Pagination -->
|
|
37
|
+
<div data-<%= stimulus_controller %>-target="pagination" class="flex justify-between items-center mt-4 pt-4 border-t border-gray-200">
|
|
38
|
+
</div>
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
|
42
|
+
<button type="button"
|
|
43
|
+
class="mt-3 inline-flex w-full justify-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50 sm:mt-0 sm:w-auto"
|
|
44
|
+
data-action="click-><%= stimulus_controller %>#close">
|
|
45
|
+
<%= cancel_text %>
|
|
46
|
+
</button>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</div>
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
module UI
|
|
3
|
+
class MappingModalComponent < TudlaHubstaff::BaseComponent
|
|
4
|
+
renders_one :search_input
|
|
5
|
+
renders_one :items_list
|
|
6
|
+
renders_one :pagination
|
|
7
|
+
|
|
8
|
+
def initialize(
|
|
9
|
+
id:,
|
|
10
|
+
title:,
|
|
11
|
+
stimulus_controller:,
|
|
12
|
+
available_items_url:,
|
|
13
|
+
search_placeholder: "Filter by name...",
|
|
14
|
+
loading_text: "Loading...",
|
|
15
|
+
cancel_text: "Cancel"
|
|
16
|
+
)
|
|
17
|
+
@id = id
|
|
18
|
+
@title = title
|
|
19
|
+
@stimulus_controller = stimulus_controller
|
|
20
|
+
@available_items_url = available_items_url
|
|
21
|
+
@search_placeholder = search_placeholder
|
|
22
|
+
@loading_text = loading_text
|
|
23
|
+
@cancel_text = cancel_text
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
attr_reader :id, :title, :stimulus_controller, :available_items_url,
|
|
27
|
+
:search_placeholder, :loading_text, :cancel_text
|
|
28
|
+
|
|
29
|
+
def target_prefix
|
|
30
|
+
stimulus_controller.gsub("-", "_").camelize(:lower)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
<div id="<%= id %>"
|
|
2
|
+
class="tudla_hubstaff-modal fixed inset-0 z-50 overflow-y-auto hidden"
|
|
3
|
+
<% if stimulus_controller %>data-<%= stimulus_controller %>-target="modal"<% end %>>
|
|
4
|
+
<!-- Backdrop -->
|
|
5
|
+
<div class="fixed inset-0 bg-black/50 transition-opacity"
|
|
6
|
+
<% if stimulus_controller %>data-action="click-><%= stimulus_controller %>#close"<% end %>></div>
|
|
7
|
+
|
|
8
|
+
<!-- Modal Content -->
|
|
9
|
+
<div class="fixed inset-0 z-10 overflow-y-auto">
|
|
10
|
+
<div class="flex min-h-full items-center justify-center p-4">
|
|
11
|
+
<div class="relative transform overflow-hidden rounded-lg bg-white text-left shadow-xl transition-all w-full max-w-lg">
|
|
12
|
+
<div class="bg-white px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
|
|
13
|
+
<% if header? %>
|
|
14
|
+
<%= header %>
|
|
15
|
+
<% elsif title %>
|
|
16
|
+
<h3 class="text-lg font-medium leading-6 text-gray-900 mb-4">
|
|
17
|
+
<%= title %>
|
|
18
|
+
</h3>
|
|
19
|
+
<% end %>
|
|
20
|
+
|
|
21
|
+
<% if body? %>
|
|
22
|
+
<%= body %>
|
|
23
|
+
<% else %>
|
|
24
|
+
<%= content %>
|
|
25
|
+
<% end %>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<% if footer? %>
|
|
29
|
+
<div class="bg-gray-50 px-4 py-3 sm:flex sm:flex-row-reverse sm:px-6">
|
|
30
|
+
<%= footer %>
|
|
31
|
+
</div>
|
|
32
|
+
<% end %>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
module UI
|
|
3
|
+
class ModalComponent < TudlaHubstaff::BaseComponent
|
|
4
|
+
renders_one :header
|
|
5
|
+
renders_one :body
|
|
6
|
+
renders_one :footer
|
|
7
|
+
|
|
8
|
+
def initialize(id:, title: nil, stimulus_controller: nil)
|
|
9
|
+
@id = id
|
|
10
|
+
@title = title
|
|
11
|
+
@stimulus_controller = stimulus_controller
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
attr_reader :id, :title, :stimulus_controller
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<nav class="tudla_hubstaff-pagination flex items-center justify-between border-t border-gray-200 px-4 py-3 sm:px-6 mt-4" aria-label="Pagination">
|
|
2
|
+
<div class="hidden sm:block">
|
|
3
|
+
<p class="text-sm text-gray-700">
|
|
4
|
+
Showing <span class="font-medium"><%= start_item %></span> to
|
|
5
|
+
<span class="font-medium"><%= end_item %></span> of
|
|
6
|
+
<span class="font-medium"><%= total_count %></span> results
|
|
7
|
+
</p>
|
|
8
|
+
</div>
|
|
9
|
+
<div class="flex flex-1 justify-between sm:justify-end gap-2">
|
|
10
|
+
<% if show_previous? %>
|
|
11
|
+
<%= link_to "Previous", previous_page_path,
|
|
12
|
+
class: "relative inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
|
13
|
+
<% end %>
|
|
14
|
+
<% if show_next? %>
|
|
15
|
+
<%= link_to "Next", next_page_path,
|
|
16
|
+
class: "relative inline-flex items-center rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" %>
|
|
17
|
+
<% end %>
|
|
18
|
+
</div>
|
|
19
|
+
</nav>
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
module UI
|
|
3
|
+
class PaginationComponent < TudlaHubstaff::BaseComponent
|
|
4
|
+
def initialize(pagy:, path_helper:, path_params: {})
|
|
5
|
+
@pagy = pagy
|
|
6
|
+
@path_helper = path_helper
|
|
7
|
+
@path_params = path_params
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
attr_reader :pagy, :path_helper, :path_params
|
|
11
|
+
|
|
12
|
+
delegate :last, :count, :from, :to, :previous, :next, to: :pagy
|
|
13
|
+
|
|
14
|
+
def render?
|
|
15
|
+
last > 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def start_item
|
|
19
|
+
from
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def end_item
|
|
23
|
+
to
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def total_count
|
|
27
|
+
count
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def previous_page_path
|
|
31
|
+
send(path_helper, path_params.merge(page: previous))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def next_page_path
|
|
35
|
+
send(path_helper, path_params.merge(page: self.next))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def show_previous?
|
|
39
|
+
previous.present?
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def show_next?
|
|
43
|
+
self.next.present?
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
module UI
|
|
3
|
+
class StatusBadgeComponent < TudlaHubstaff::BaseComponent
|
|
4
|
+
STYLES = {
|
|
5
|
+
active: "bg-green-100 text-green-800",
|
|
6
|
+
pending: "bg-gray-100 text-gray-800",
|
|
7
|
+
inactive: "bg-gray-100 text-gray-800",
|
|
8
|
+
default: "bg-gray-100 text-gray-800"
|
|
9
|
+
}.freeze
|
|
10
|
+
|
|
11
|
+
def initialize(status:)
|
|
12
|
+
@status = status.to_s.downcase
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_reader :status
|
|
16
|
+
|
|
17
|
+
def style_classes
|
|
18
|
+
STYLES[status.to_sym] || STYLES[:default]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def display_text
|
|
22
|
+
status.presence || "pending"
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
<% if rows? %>
|
|
2
|
+
<div class="tudla_hubstaff-table bg-white shadow rounded-lg overflow-hidden">
|
|
3
|
+
<table class="min-w-full divide-y divide-gray-200" <%= "id=\"#{id}\"" if id %>>
|
|
4
|
+
<% if header? %>
|
|
5
|
+
<thead class="bg-gray-50">
|
|
6
|
+
<%= header %>
|
|
7
|
+
</thead>
|
|
8
|
+
<% end %>
|
|
9
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
10
|
+
<% rows.each do |row| %>
|
|
11
|
+
<%= row %>
|
|
12
|
+
<% end %>
|
|
13
|
+
</tbody>
|
|
14
|
+
</table>
|
|
15
|
+
</div>
|
|
16
|
+
<% else %>
|
|
17
|
+
<div class="text-center py-12">
|
|
18
|
+
<p class="text-gray-500"><%= empty_message %></p>
|
|
19
|
+
</div>
|
|
20
|
+
<% end %>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
module UI
|
|
3
|
+
class TableComponent < TudlaHubstaff::BaseComponent
|
|
4
|
+
renders_one :header
|
|
5
|
+
renders_many :rows
|
|
6
|
+
|
|
7
|
+
def initialize(id: nil, empty_message: "No items found.")
|
|
8
|
+
@id = id
|
|
9
|
+
@empty_message = empty_message
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :id, :empty_message
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<%= turbo_frame_tag turbo_frame_id do %>
|
|
2
|
+
<div class="tudla_hubstaff-user-activities-list" data-controller="activity-task-mapping-modal" data-available-tasks-url="<%= available_tudla_tasks_activities_path %>">
|
|
3
|
+
<div class="flex items-center justify-between mb-4">
|
|
4
|
+
<div class="flex items-center space-x-4">
|
|
5
|
+
<%= link_to "All", filter_path(unmapped: false),
|
|
6
|
+
class: "px-3 py-1 text-sm rounded-md #{unmapped_only ? 'text-gray-600 hover:bg-gray-100' : 'bg-indigo-600 text-white'}",
|
|
7
|
+
data: { turbo_frame: turbo_frame_id } %>
|
|
8
|
+
<%= link_to "Unmapped Only", filter_path(unmapped: true),
|
|
9
|
+
class: "px-3 py-1 text-sm rounded-md #{unmapped_only ? 'bg-indigo-600 text-white' : 'text-gray-600 hover:bg-gray-100'}",
|
|
10
|
+
data: { turbo_frame: turbo_frame_id } %>
|
|
11
|
+
</div>
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
<% if activities.any? %>
|
|
15
|
+
<div class="bg-white shadow rounded-lg overflow-hidden">
|
|
16
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
17
|
+
<thead class="bg-gray-50">
|
|
18
|
+
<tr>
|
|
19
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
|
20
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Task</th>
|
|
21
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tracked</th>
|
|
22
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Activity</th>
|
|
23
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
|
|
24
|
+
</tr>
|
|
25
|
+
</thead>
|
|
26
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
27
|
+
<% activities.each do |activity| %>
|
|
28
|
+
<%= render TudlaHubstaff::ActivityRowComponent.new(activity: activity) %>
|
|
29
|
+
<% end %>
|
|
30
|
+
</tbody>
|
|
31
|
+
</table>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<% if total_pages > 1 %>
|
|
35
|
+
<div class="tudla_hubstaff-pagination flex items-center justify-between mt-4 px-4 py-3 bg-white border border-gray-200 rounded-lg sm:px-6">
|
|
36
|
+
<div class="text-sm text-gray-700">
|
|
37
|
+
Showing <span class="font-medium"><%= start_item %></span> to <span class="font-medium"><%= end_item %></span> of <span class="font-medium"><%= total_count %></span> activities
|
|
38
|
+
</div>
|
|
39
|
+
<div class="flex space-x-2">
|
|
40
|
+
<% if show_previous? %>
|
|
41
|
+
<%= link_to "Previous", pagination_path(page: previous_page),
|
|
42
|
+
class: "text-indigo-600 hover:text-indigo-900",
|
|
43
|
+
data: { turbo_frame: turbo_frame_id } %>
|
|
44
|
+
<% else %>
|
|
45
|
+
<span class="text-gray-400 cursor-not-allowed">Previous</span>
|
|
46
|
+
<% end %>
|
|
47
|
+
<% if show_next? %>
|
|
48
|
+
<%= link_to "Next", pagination_path(page: next_page),
|
|
49
|
+
class: "text-indigo-600 hover:text-indigo-900",
|
|
50
|
+
data: { turbo_frame: turbo_frame_id } %>
|
|
51
|
+
<% else %>
|
|
52
|
+
<span class="text-gray-400 cursor-not-allowed">Next</span>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
<% end %>
|
|
57
|
+
|
|
58
|
+
<%= render TudlaHubstaff::UI::MappingModalComponent.new(
|
|
59
|
+
id: "activity-task-mapping-modal",
|
|
60
|
+
title: "Map Activity Task",
|
|
61
|
+
stimulus_controller: "activity-task-mapping-modal",
|
|
62
|
+
available_items_url: available_tudla_tasks_activities_path
|
|
63
|
+
) %>
|
|
64
|
+
<% else %>
|
|
65
|
+
<div class="text-center py-12 bg-white rounded-lg border border-gray-200">
|
|
66
|
+
<p class="text-gray-500">No activities found for this user.</p>
|
|
67
|
+
</div>
|
|
68
|
+
<% end %>
|
|
69
|
+
</div>
|
|
70
|
+
<% end %>
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
class UserActivitiesListComponent < TudlaHubstaff::BaseComponent
|
|
3
|
+
PER_PAGE = 10
|
|
4
|
+
|
|
5
|
+
def initialize(user_id:, current_page: 1, per_page: PER_PAGE, unmapped_only: false)
|
|
6
|
+
@user_id = user_id
|
|
7
|
+
@current_page = current_page.to_i
|
|
8
|
+
@per_page = per_page.to_i
|
|
9
|
+
@unmapped_only = unmapped_only
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
attr_reader :user_id, :current_page, :per_page, :unmapped_only
|
|
13
|
+
|
|
14
|
+
def activities
|
|
15
|
+
@activities ||= fetch_activities
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def total_count
|
|
19
|
+
@total_count ||= activities_scope.count
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def total_pages
|
|
23
|
+
(total_count.to_f / per_page).ceil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def render?
|
|
27
|
+
user_id.present?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def start_item
|
|
31
|
+
return 0 if total_count.zero?
|
|
32
|
+
((current_page - 1) * per_page) + 1
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def end_item
|
|
36
|
+
[ current_page * per_page, total_count ].min
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def show_previous?
|
|
40
|
+
current_page > 1
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def show_next?
|
|
44
|
+
current_page < total_pages
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def previous_page
|
|
48
|
+
current_page - 1
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def next_page
|
|
52
|
+
current_page + 1
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def turbo_frame_id
|
|
56
|
+
"user_#{user_id}_activities"
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def pagination_path(page:)
|
|
60
|
+
user_activities_activities_path(user_id: user_id, page: page, unmapped_only: unmapped_only)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def filter_path(unmapped:)
|
|
64
|
+
user_activities_activities_path(user_id: user_id, page: 1, unmapped_only: unmapped)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def tudla_hubstaff_user
|
|
70
|
+
@tudla_hubstaff_user ||= TudlaHubstaff::User.find_by(tudla_user_id: user_id)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def fetch_activities
|
|
74
|
+
activities_scope
|
|
75
|
+
.order(updated_at: :desc)
|
|
76
|
+
.offset((current_page - 1) * per_page)
|
|
77
|
+
.limit(per_page)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def activities_scope
|
|
81
|
+
return TudlaHubstaff::Activity.none if tudla_hubstaff_user.blank?
|
|
82
|
+
@activities_scope ||= begin
|
|
83
|
+
scope = TudlaHubstaff::Activity.where(user_id: tudla_hubstaff_user.user_id)
|
|
84
|
+
if unmapped_only
|
|
85
|
+
unmapped_task_ids = TudlaHubstaff::Task.where(tudla_task_id: nil).pluck(:task_id)
|
|
86
|
+
scope = scope.where(task_id: unmapped_task_ids)
|
|
87
|
+
end
|
|
88
|
+
scope
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<tr id="<%= dom_id %>" class="tudla_hubstaff-user-row">
|
|
2
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
3
|
+
<div class="text-sm font-medium text-gray-900"><%= user.name %></div>
|
|
4
|
+
<div class="text-sm text-gray-500"><%= user.first_name %> <%= user.last_name %></div>
|
|
5
|
+
</td>
|
|
6
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
7
|
+
<div class="text-sm text-gray-900"><%= user.email %></div>
|
|
8
|
+
</td>
|
|
9
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
10
|
+
<%= render TudlaHubstaff::UI::StatusBadgeComponent.new(status: user.status) %>
|
|
11
|
+
</td>
|
|
12
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
13
|
+
<button type="button"
|
|
14
|
+
class="text-indigo-600 hover:text-indigo-900"
|
|
15
|
+
data-action="click->user-mapping-modal#open"
|
|
16
|
+
data-user-id="<%= user.id %>"
|
|
17
|
+
data-user-name="<%= user.name %>"
|
|
18
|
+
data-map-url="<%= map_tudla_user_user_path(user) %>">
|
|
19
|
+
Map to Tudla User
|
|
20
|
+
</button>
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|