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
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: d4618d21ce8eea610d7b0c2b25ca7f4188b4044ddac344ae30a5518e154db21c
|
|
4
|
+
data.tar.gz: 44c08c47579cd47c0ac320f02c95b3bb58e36b2c5a0d6ebe051941bf3f962c0c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 91f7745c87bfd703b0384ab62f898c7d5d3f313ec4e65848241bcb90947f9a43702988e5338c954052a22cd709e25fe5d253a17722cecad18357171816b3ddad
|
|
7
|
+
data.tar.gz: 750b594080e755fc669bf6f9469b0a9ee689eedc1ca7d1162b85a88f388b46cad2988fdc27b8e9aeac18c1097cad86e9acb0a4c5119db84104eb38b823b62e71
|
data/MIT-LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Copyright Tibo Mogul
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
4
|
+
a copy of this software and associated documentation files (the
|
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
9
|
+
the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be
|
|
12
|
+
included in all copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
# TudlaHubstaff
|
|
2
|
+
|
|
3
|
+
`TudlaHubstaff` is a Rails 8.1 mountable engine that provides a seamless integration with the **Hubstaff API v2**. It is built with the "Solid trifecta" (Solid Queue, Solid Cache, and Solid Cable) and integrates into the `TudlaContracts` ecosystem.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Hubstaff API v2 Integration**: Fetch daily activities, members, and tasks.
|
|
8
|
+
- **Automated OAuth2**: Handles token refreshing and discovery automatically via `OpenIDConnect`.
|
|
9
|
+
- **Solid Stack Integration**: SQL-backed background jobs, caching, and pub/sub.
|
|
10
|
+
- **TudlaContracts Compatible**: Registers as a `time_sheet` provider.
|
|
11
|
+
- **ViewComponent Architecture**: Modular, testable UI components with slot-based composition.
|
|
12
|
+
- **Unmapped Entity Management**: Built-in views for mapping Users, Tasks, and Projects to Tudla entities.
|
|
13
|
+
- **Customizable Views**: Generator-based ejection pattern for host app customization.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add the gem to your application's Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
gem "tudla_contracts"
|
|
21
|
+
gem "tudla_hubstaff"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
And then execute:
|
|
25
|
+
```bash
|
|
26
|
+
$ bundle install
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### Mounting the Engine
|
|
30
|
+
|
|
31
|
+
Mount the engine in your application's `config/routes.rb`:
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
Rails.application.routes.draw do
|
|
35
|
+
mount TudlaHubstaff::Engine => "/tudla_hubstaff"
|
|
36
|
+
end
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Installing Migrations
|
|
40
|
+
|
|
41
|
+
Copy the engine's migrations to your application:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
$ bin/rails tudla_hubstaff:install:migrations
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Then run the migrations:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
$ bin/rails db:migrate
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Generating the engine's Tailwind import file
|
|
54
|
+
|
|
55
|
+
We are using`tailwindcss-rails` experiment support https://github.com/rails/tailwindcss-rails?tab=readme-ov-file#rails-engines-support-experimental
|
|
56
|
+
|
|
57
|
+
Run the following command to generate the engine's Tailwind import file `app/assets/builds/tailwind/tudla_hubstaff.css`:
|
|
58
|
+
```bash
|
|
59
|
+
$ bin/rails tailwindcss:engines
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Edit your `app/assets/tailwind/application.css` to import the engine's Tailwind import file:
|
|
63
|
+
```css
|
|
64
|
+
@import "../builds/tailwind/tudla_hubstaff";
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Importing the engine's JavaScript
|
|
68
|
+
|
|
69
|
+
Edit your `app/javascript/application.js` to import the engine's JavaScript:
|
|
70
|
+
```javascript
|
|
71
|
+
import "tudla_hubstaff/controllers/index"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Database Setup
|
|
75
|
+
|
|
76
|
+
This engine uses the Solid trifecta. Ensure your host application is configured with the necessary databases (primary, queue, cache, cable) as described in the [Solid Rails documentation](https://github.com/rails/solid_queue).
|
|
77
|
+
|
|
78
|
+
## Configuration
|
|
79
|
+
|
|
80
|
+
The engine registers itself automatically with `TudlaContracts`. You typically interact with it via the `TudlaHubstaff::Provider`.
|
|
81
|
+
|
|
82
|
+
To initialize a connection, you need a Hubstaff Personal Access Token (used as the initial refresh token):
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
config = TudlaHubstaff::Config.new
|
|
86
|
+
config.tudla_organization = Organization.first
|
|
87
|
+
config.personal_access_token = ENV["HUBSTAFF_PAT"]
|
|
88
|
+
config.organization_id = ENV["HUBSTAFF_VALUEPRO_ID"].to_i
|
|
89
|
+
config.save
|
|
90
|
+
|
|
91
|
+
# provider = TudlaHubstaff::Provider.new(config)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Host Interface Configuration
|
|
95
|
+
|
|
96
|
+
The engine requires a host interface class to provide available Tudla entities for mapping. Configure this in an initializer:
|
|
97
|
+
|
|
98
|
+
```ruby
|
|
99
|
+
# config/initializers/tudla_hubstaff.rb
|
|
100
|
+
TudlaHubstaff.host_interface_class = "::HubstaffHostInterface"
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Your class must inherit from `TudlaContracts::Integrations::HostInterface` and implement:
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
class YourApp::HubstaffHostInterface < TudlaContracts::Integrations::HostInterface
|
|
107
|
+
def available_users_for_user(current_user)
|
|
108
|
+
# Return array of objects with :id, :name, :email
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def available_tasks_for_user(current_user)
|
|
112
|
+
# Return array of objects with :id, :name, :project_name
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def available_projects_for_user(current_user)
|
|
116
|
+
# Return array of objects with :id, :name
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Layout Configuration
|
|
122
|
+
|
|
123
|
+
By default, the engine uses its own layout. To use your application's layout:
|
|
124
|
+
|
|
125
|
+
```ruby
|
|
126
|
+
# config/initializers/tudla_hubstaff.rb
|
|
127
|
+
TudlaHubstaff::Engine.config.tudla_hubstaff.layout = "application"
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Usage
|
|
131
|
+
|
|
132
|
+
### Fetching Activities
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
activities = provider.daily_activities
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
### Advanced API Access
|
|
139
|
+
|
|
140
|
+
For more granular control, use the `ApiClient`:
|
|
141
|
+
|
|
142
|
+
```ruby
|
|
143
|
+
connection = TudlaHubstaff::ApiConnection.new("your_pat")
|
|
144
|
+
client = TudlaHubstaff::ApiClient.new("your_org_id", connection)
|
|
145
|
+
|
|
146
|
+
# Fetch members
|
|
147
|
+
members = client.members
|
|
148
|
+
|
|
149
|
+
# Fetch tasks
|
|
150
|
+
tasks = client.tasks(status: "active")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Scheduling Background Jobs
|
|
154
|
+
|
|
155
|
+
The engine includes a `FetchUpdatedActivitiesJob` that syncs activity updates from Hubstaff. **The host application must schedule this job to run periodically.**
|
|
156
|
+
|
|
157
|
+
### Using Solid Queue (Recommended)
|
|
158
|
+
|
|
159
|
+
Add a recurring job in your host application's `config/recurring.yml`:
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
production:
|
|
163
|
+
fetch_hubstaff_activities:
|
|
164
|
+
class: TudlaHubstaff::FetchUpdatedActivitiesJob
|
|
165
|
+
schedule: every 15 minutes
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Using Cron or Other Schedulers
|
|
169
|
+
|
|
170
|
+
If you prefer cron or another scheduler, enqueue the job periodically:
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
TudlaHubstaff::FetchUpdatedActivitiesJob.perform_later
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
**Note:** Before the job can process an organization, you must create an `OrganizationUpdate` record with a `last_updated_at` timestamp. Records with `nil` timestamps are skipped.
|
|
177
|
+
|
|
178
|
+
## Architecture
|
|
179
|
+
|
|
180
|
+
- **`TudlaHubstaff::Provider`**: The main entry point for the `TudlaContracts` interface.
|
|
181
|
+
- **`TudlaHubstaff::ApiClient`**: Handles Hubstaff v2 API endpoints.
|
|
182
|
+
- **`TudlaHubstaff::ApiConnection`**: Manages Faraday connections and OAuth2 token lifecycle.
|
|
183
|
+
- **Caching**: Access tokens and refresh tokens are stored in `Rails.cache` (backed by `SolidCache`).
|
|
184
|
+
|
|
185
|
+
### ViewComponent Architecture
|
|
186
|
+
|
|
187
|
+
The engine uses [ViewComponent](https://viewcomponent.org/) for modular, testable UI components:
|
|
188
|
+
|
|
189
|
+
- **`TudlaHubstaff::BaseComponent`**: Base class with engine route helpers
|
|
190
|
+
- **`TudlaHubstaff::UI::ModalComponent`**: Reusable modal with slots (header, body, footer)
|
|
191
|
+
- **`TudlaHubstaff::UI::TableComponent`**: Table with header and rows slots
|
|
192
|
+
- **`TudlaHubstaff::UI::PaginationComponent`**: Pagination with configurable path helper
|
|
193
|
+
- **`TudlaHubstaff::UI::StatusBadgeComponent`**: Status badges with color coding
|
|
194
|
+
- **`TudlaHubstaff::UI::MappingModalComponent`**: Specialized modal for entity mapping
|
|
195
|
+
- **`TudlaHubstaff::UI::MapButtonComponent`**: Button to trigger mapping modal
|
|
196
|
+
|
|
197
|
+
## Customizing Views
|
|
198
|
+
|
|
199
|
+
The engine provides generators to copy views and components to your application for customization.
|
|
200
|
+
|
|
201
|
+
### Copy All Views
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
rails g tudla_hubstaff:views
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Copy Scoped Views
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# Copy only user views
|
|
211
|
+
rails g tudla_hubstaff:views users
|
|
212
|
+
|
|
213
|
+
# Copy only task views
|
|
214
|
+
rails g tudla_hubstaff:views tasks
|
|
215
|
+
|
|
216
|
+
# Copy only project views
|
|
217
|
+
rails g tudla_hubstaff:views projects
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### Copy Components
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
# Copy all components (Ruby classes and templates)
|
|
224
|
+
rails g tudla_hubstaff:components
|
|
225
|
+
|
|
226
|
+
# Copy only component templates (recommended for customization)
|
|
227
|
+
rails g tudla_hubstaff:components --templates_only
|
|
228
|
+
|
|
229
|
+
# Copy specific component scope
|
|
230
|
+
rails g tudla_hubstaff:components ui
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
**Note:** When copying only templates, the engine retains control over the component logic while you customize the presentation.
|
|
234
|
+
|
|
235
|
+
## Testing
|
|
236
|
+
|
|
237
|
+
The engine uses RSpec for testing. A dummy Rails application is located in `spec/dummy`.
|
|
238
|
+
|
|
239
|
+
### Database Setup for Tests
|
|
240
|
+
|
|
241
|
+
**Important:** Rails 8 with the Solid trifecta uses multiple databases (primary, queue, cache, cable). When resetting the test database, you must load each schema separately.
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
# One-time setup or when you need a complete refresh
|
|
245
|
+
RAILS_ENV=test bundle exec rails db:reset
|
|
246
|
+
|
|
247
|
+
# Load Solid gem schemas (required after db:reset)
|
|
248
|
+
RAILS_ENV=test bundle exec rails app:db:schema:load:cache
|
|
249
|
+
RAILS_ENV=test bundle exec rails app:db:schema:load:queue
|
|
250
|
+
RAILS_ENV=test bundle exec rails app:db:schema:load:cable
|
|
251
|
+
|
|
252
|
+
# Run the specs
|
|
253
|
+
bundle exec rspec
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Quick workflow for ongoing development:**
|
|
257
|
+
|
|
258
|
+
```bash
|
|
259
|
+
# When adding new migrations
|
|
260
|
+
RAILS_ENV=test bundle exec rails db:migrate
|
|
261
|
+
|
|
262
|
+
# Run tests
|
|
263
|
+
bundle exec rspec
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
**Note:** `db:reset` only loads the primary database schema. The Solid gem databases (cache, queue, cable) have separate schema files that must be loaded explicitly.
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This is a manifest file that'll be compiled into application.css, which will include all the files
|
|
3
|
+
* listed below.
|
|
4
|
+
*
|
|
5
|
+
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
|
|
6
|
+
* or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path.
|
|
7
|
+
*
|
|
8
|
+
* You're free to add application-wide styles to this file and they'll appear at the bottom of the
|
|
9
|
+
* compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS
|
|
10
|
+
* files in this directory. Styles in this file should be added after the last require_* statement.
|
|
11
|
+
* It is generally better to create a new file per style scope.
|
|
12
|
+
*
|
|
13
|
+
*= require_tree .
|
|
14
|
+
*= require_self
|
|
15
|
+
*/
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/* Engine CSS */
|
|
2
|
+
@import "tailwindcss";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
@source Directive:
|
|
6
|
+
Explicitly tells the CLI where to look for class usage within the engine.
|
|
7
|
+
Relative path resolution is key here.
|
|
8
|
+
Assuming the file is in app/assets/tailwind/my_engine/engine.css:
|
|
9
|
+
.. -> my_engine
|
|
10
|
+
.. -> tailwind
|
|
11
|
+
.. -> assets
|
|
12
|
+
.. -> app
|
|
13
|
+
../views -> Target Directory
|
|
14
|
+
*/
|
|
15
|
+
@source "../../../views";
|
|
16
|
+
@source "../../../components";
|
|
17
|
+
|
|
18
|
+
/*
|
|
19
|
+
@plugin Directive:
|
|
20
|
+
If the engine relies on specific plugins (e.g., typography, forms),
|
|
21
|
+
they must be declared here if not inheriting from the host.
|
|
22
|
+
*/
|
|
23
|
+
/*
|
|
24
|
+
@plugin "@tailwindcss/typography";
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/* Custom Theme Variables specific to the engine */
|
|
28
|
+
/*
|
|
29
|
+
@theme {
|
|
30
|
+
--color-engine-brand: oklch(0.65 0.2 30);
|
|
31
|
+
}
|
|
32
|
+
*/
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<tr id="<%= dom_id %>" class="tudla_hubstaff-activity-row">
|
|
2
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
3
|
+
<div class="text-sm text-gray-900"><%= format_date(activity.date) %></div>
|
|
4
|
+
</td>
|
|
5
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
6
|
+
<div class="text-sm font-medium text-gray-900"><%= task_name %></div>
|
|
7
|
+
<% if mapped? %>
|
|
8
|
+
<div class="text-xs text-green-600">Mapped</div>
|
|
9
|
+
<% elsif hubstaff_task.present? %>
|
|
10
|
+
<div class="text-xs text-gray-500">Hubstaff Task</div>
|
|
11
|
+
<% end %>
|
|
12
|
+
</td>
|
|
13
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
14
|
+
<div class="text-sm text-gray-900"><%= format_duration(activity.tracked) %></div>
|
|
15
|
+
<% if activity.billable.to_i > 0 %>
|
|
16
|
+
<div class="text-xs text-green-600">Billable: <%= format_duration(activity.billable) %></div>
|
|
17
|
+
<% end %>
|
|
18
|
+
</td>
|
|
19
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
20
|
+
<div class="flex items-center space-x-2">
|
|
21
|
+
<div class="text-sm text-gray-500">
|
|
22
|
+
<span title="Overall activity"><%= activity.overall %>%</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div class="w-16 bg-gray-200 rounded-full h-2">
|
|
25
|
+
<div class="bg-indigo-600 h-2 rounded-full" style="width: <%= [activity.overall.to_i, 100].min %>%"></div>
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</td>
|
|
29
|
+
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
|
30
|
+
<% if show_map_button? %>
|
|
31
|
+
<button type="button"
|
|
32
|
+
class="text-indigo-600 hover:text-indigo-900"
|
|
33
|
+
data-action="click->activity-task-mapping-modal#open"
|
|
34
|
+
data-activity-id="<%= activity.id %>"
|
|
35
|
+
data-hubstaff-task-id="<%= hubstaff_task.id %>"
|
|
36
|
+
data-task-name="<%= hubstaff_task.summary %>"
|
|
37
|
+
data-map-url="<%= map_url %>">
|
|
38
|
+
Map
|
|
39
|
+
</button>
|
|
40
|
+
<% elsif mapped? %>
|
|
41
|
+
<span class="text-green-600">✓</span>
|
|
42
|
+
<% else %>
|
|
43
|
+
<span class="text-gray-400">-</span>
|
|
44
|
+
<% end %>
|
|
45
|
+
</td>
|
|
46
|
+
</tr>
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
class ActivityRowComponent < TudlaHubstaff::BaseComponent
|
|
3
|
+
def initialize(activity:)
|
|
4
|
+
@activity = activity
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
attr_reader :activity
|
|
8
|
+
|
|
9
|
+
def dom_id
|
|
10
|
+
"activity_#{activity.id}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def hubstaff_task
|
|
14
|
+
@hubstaff_task ||= TudlaHubstaff::Task.find_by(task_id: activity.task_id) if activity.task_id.present?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def tudla_task_id
|
|
18
|
+
hubstaff_task&.tudla_task_id
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def mapped?
|
|
22
|
+
tudla_task_id.present?
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def task_name
|
|
26
|
+
if mapped?
|
|
27
|
+
tudla_task&.name || "Task ##{tudla_task_id}"
|
|
28
|
+
elsif hubstaff_task.present?
|
|
29
|
+
hubstaff_task.summary
|
|
30
|
+
else
|
|
31
|
+
"No task"
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def tudla_task
|
|
36
|
+
@tudla_task ||= TudlaHubstaff.host_interface.find_task(tudla_task_id) if tudla_task_id.present?
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def show_map_button?
|
|
40
|
+
hubstaff_task.present? && !mapped?
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def map_url
|
|
44
|
+
map_activity_task_activity_path(activity) if hubstaff_task.present?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def format_duration(seconds)
|
|
48
|
+
return "0m" if seconds.nil? || seconds.zero?
|
|
49
|
+
|
|
50
|
+
hours = seconds / 3600
|
|
51
|
+
minutes = (seconds % 3600) / 60
|
|
52
|
+
|
|
53
|
+
if hours > 0
|
|
54
|
+
"#{hours}h #{minutes}m"
|
|
55
|
+
else
|
|
56
|
+
"#{minutes}m"
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def format_date(date_string)
|
|
61
|
+
return "" if date_string.blank?
|
|
62
|
+
Date.parse(date_string).strftime("%b %d, %Y")
|
|
63
|
+
rescue ArgumentError
|
|
64
|
+
date_string
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<tr id="<%= dom_id %>" class="tudla_hubstaff-project-row">
|
|
2
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
3
|
+
<div class="text-sm font-medium text-gray-900"><%= project.name %></div>
|
|
4
|
+
<div class="text-sm text-gray-500"><%= truncated_description %></div>
|
|
5
|
+
</td>
|
|
6
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
7
|
+
<div class="text-sm text-gray-900"><%= formatted_project_type %></div>
|
|
8
|
+
</td>
|
|
9
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
10
|
+
<%= render TudlaHubstaff::UI::StatusBadgeComponent.new(status: project.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->project-mapping-modal#open"
|
|
16
|
+
data-project-id="<%= project.id %>"
|
|
17
|
+
data-project-name="<%= project.name %>"
|
|
18
|
+
data-map-url="<%= map_tudla_project_project_path(project) %>">
|
|
19
|
+
Map to Tudla Project
|
|
20
|
+
</button>
|
|
21
|
+
</td>
|
|
22
|
+
</tr>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module TudlaHubstaff
|
|
2
|
+
class ProjectRowComponent < TudlaHubstaff::BaseComponent
|
|
3
|
+
def initialize(project:)
|
|
4
|
+
@project = project
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
attr_reader :project
|
|
8
|
+
|
|
9
|
+
def dom_id
|
|
10
|
+
"tudla_hubstaff_project_#{project.id}"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def truncated_description
|
|
14
|
+
project.description.to_s.truncate(50)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def formatted_project_type
|
|
18
|
+
project.project_type.titleize
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
<div class="tudla_hubstaff-task-activities">
|
|
2
|
+
<% if activities.any? %>
|
|
3
|
+
<div class="bg-white shadow rounded-lg overflow-hidden">
|
|
4
|
+
<table class="min-w-full divide-y divide-gray-200">
|
|
5
|
+
<thead class="bg-gray-50">
|
|
6
|
+
<tr>
|
|
7
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Date</th>
|
|
8
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">User</th>
|
|
9
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tracked</th>
|
|
10
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Activity</th>
|
|
11
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Updated</th>
|
|
12
|
+
</tr>
|
|
13
|
+
</thead>
|
|
14
|
+
<tbody class="bg-white divide-y divide-gray-200">
|
|
15
|
+
<% activities.each do |activity| %>
|
|
16
|
+
<tr class="tudla_hubstaff-activity-row">
|
|
17
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
18
|
+
<div class="text-sm text-gray-900"><%= format_date(activity.date) %></div>
|
|
19
|
+
</td>
|
|
20
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
21
|
+
<div class="text-sm text-gray-900"><%= activity.user_id %></div>
|
|
22
|
+
</td>
|
|
23
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
24
|
+
<div class="text-sm text-gray-900"><%= format_duration(activity.tracked) %></div>
|
|
25
|
+
<% if activity.billable.to_i > 0 %>
|
|
26
|
+
<div class="text-xs text-green-600">Billable: <%= format_duration(activity.billable) %></div>
|
|
27
|
+
<% end %>
|
|
28
|
+
</td>
|
|
29
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
30
|
+
<div class="flex items-center space-x-2">
|
|
31
|
+
<div class="text-sm text-gray-500">
|
|
32
|
+
<span title="Overall activity"><%= activity.overall %>%</span>
|
|
33
|
+
</div>
|
|
34
|
+
<div class="w-16 bg-gray-200 rounded-full h-2">
|
|
35
|
+
<div class="bg-indigo-600 h-2 rounded-full" style="width: <%= [activity.overall.to_i, 100].min %>%"></div>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</td>
|
|
39
|
+
<td class="px-6 py-4 whitespace-nowrap">
|
|
40
|
+
<div class="text-sm text-gray-500">
|
|
41
|
+
<% if activity.last_updated_at.present? %>
|
|
42
|
+
<%= activity.last_updated_at.strftime("%b %d, %Y %H:%M") %>
|
|
43
|
+
<% else %>
|
|
44
|
+
-
|
|
45
|
+
<% end %>
|
|
46
|
+
</div>
|
|
47
|
+
</td>
|
|
48
|
+
</tr>
|
|
49
|
+
<% end %>
|
|
50
|
+
</tbody>
|
|
51
|
+
</table>
|
|
52
|
+
</div>
|
|
53
|
+
|
|
54
|
+
<% if total_pages > 1 %>
|
|
55
|
+
<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">
|
|
56
|
+
<div class="text-sm text-gray-700">
|
|
57
|
+
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
|
|
58
|
+
</div>
|
|
59
|
+
<div class="flex space-x-2">
|
|
60
|
+
<% if show_previous? %>
|
|
61
|
+
<span class="text-indigo-600 hover:text-indigo-900 cursor-pointer" data-page="<%= previous_page %>">
|
|
62
|
+
Previous
|
|
63
|
+
</span>
|
|
64
|
+
<% else %>
|
|
65
|
+
<span class="text-gray-400 cursor-not-allowed">Previous</span>
|
|
66
|
+
<% end %>
|
|
67
|
+
<% if show_next? %>
|
|
68
|
+
<span class="text-indigo-600 hover:text-indigo-900 cursor-pointer" data-page="<%= next_page %>">
|
|
69
|
+
Next
|
|
70
|
+
</span>
|
|
71
|
+
<% else %>
|
|
72
|
+
<span class="text-gray-400 cursor-not-allowed">Next</span>
|
|
73
|
+
<% end %>
|
|
74
|
+
</div>
|
|
75
|
+
</div>
|
|
76
|
+
<% end %>
|
|
77
|
+
<% else %>
|
|
78
|
+
<div class="text-center py-12 bg-white rounded-lg border border-gray-200">
|
|
79
|
+
<p class="text-gray-500">No activities found for this task.</p>
|
|
80
|
+
</div>
|
|
81
|
+
<% end %>
|
|
82
|
+
</div>
|