From 56f30907e7326ab133db08ab343abc18993b8ffc Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 27 Feb 2025 13:50:44 +0100 Subject: [PATCH 01/88] [#59376] Separate time tracking module with calendar view for logged time with start and finish time https://community.openproject.org/work_packages/59376 From ce4325d9f58fdac60bed9f6ed7700d42931f6493 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 3 Mar 2025 14:56:53 +0100 Subject: [PATCH 02/88] add stubs for controller methods --- .../my/time_tracking_controller.rb | 69 +++++++++++++++++++ .../app/controllers/my/timer_controller.rb | 2 + .../app/views/my/time_tracking/day.html.erb | 6 ++ .../app/views/my/time_tracking/month.html.erb | 6 ++ .../app/views/my/time_tracking/week.html.erb | 6 ++ modules/costs/config/routes.rb | 8 ++- modules/costs/lib/costs/engine.rb | 30 ++++++-- 7 files changed, 121 insertions(+), 6 deletions(-) create mode 100644 modules/costs/app/controllers/my/time_tracking_controller.rb create mode 100644 modules/costs/app/views/my/time_tracking/day.html.erb create mode 100644 modules/costs/app/views/my/time_tracking/month.html.erb create mode 100644 modules/costs/app/views/my/time_tracking/week.html.erb diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb new file mode 100644 index 00000000000..9ceaefbd6b3 --- /dev/null +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module My + class TimeTrackingController < ApplicationController + before_action :require_login + + no_authorization_required!(:day, :week, :month) + + current_menu_item do |ctrl| + if ctrl.params[:action] == "day" && ctrl.current_day == Time.zone.today + :my_time_tracking_today + else + :my_time_tracking + end + end + + layout "global" + + def day; end + + def week; end + + def month; end + + def default_breadcrumb + "my time" + end + + def current_day + @current_day ||= if params[:date].present? + begin + Date.parse(params[:date]) + rescue StandardError + Time.zone.today + end + else + Time.zone.today + end + end + end +end diff --git a/modules/costs/app/controllers/my/timer_controller.rb b/modules/costs/app/controllers/my/timer_controller.rb index d3bde9cd283..f9d070ae722 100644 --- a/modules/costs/app/controllers/my/timer_controller.rb +++ b/modules/costs/app/controllers/my/timer_controller.rb @@ -33,6 +33,8 @@ module My before_action :require_login before_action :find_active_timer, only: %i[show] + no_authorization_required! :show + def show render layout: nil end diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb new file mode 100644 index 00000000000..4f04629f9b9 --- /dev/null +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -0,0 +1,6 @@ +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { "my time: day view" } + header.with_breadcrumbs(["my time"]) + end +%> diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb new file mode 100644 index 00000000000..88a3b74e667 --- /dev/null +++ b/modules/costs/app/views/my/time_tracking/month.html.erb @@ -0,0 +1,6 @@ +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { "my time: month" } + header.with_breadcrumbs(["my time"]) + end +%> diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb new file mode 100644 index 00000000000..f9b68609a16 --- /dev/null +++ b/modules/costs/app/views/my/time_tracking/week.html.erb @@ -0,0 +1,6 @@ +<%= + render Primer::OpenProject::PageHeader.new do |header| + header.with_title { "my time week" } + header.with_breadcrumbs(["my time"]) + end +%> diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index dc129df6c34..3f4d3d70702 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -47,8 +47,12 @@ Rails.application.routes.draw do get "/time_entries/dialog" => "time_entries#dialog" end - scope "my" do - get "/timer" => "my/timer#show", as: "my_timers" + namespace "my" do + get "/timer" => "timer#show", as: "timers" + + get "/time-tracking/day" => "time_tracking#day" + get "/time-tracking/week" => "time_tracking#week" + get "/time-tracking/month" => "time_tracking#month" end scope "projects/:project_id", as: "project", module: "projects" do diff --git a/modules/costs/lib/costs/engine.rb b/modules/costs/lib/costs/engine.rb index 32538e828fe..18838251cb8 100644 --- a/modules/costs/lib/costs/engine.rb +++ b/modules/costs/lib/costs/engine.rb @@ -34,10 +34,7 @@ module Costs include OpenProject::Plugins::ActsAsOpEngine - register "costs", - author_url: "https://www.openproject.org", - bundled: true, - settings: { menu_item: :costs_settings } do + register "costs", author_url: "https://www.openproject.org", bundled: true, settings: { menu_item: :costs_settings } do project_module :costs do permission :view_time_entries, {}, @@ -144,6 +141,31 @@ module Costs if: ->(*) { User.current.admin? }, parent: :admin_costs, caption: :enumeration_activities + + menu :global_menu, + :my_time_tracking, + { controller: "/my/time_tracking", action: "day" }, + after: :my_page, + caption: :label_my_time_tracking, + icon: :stopwatch + + menu :global_menu, + :my_time_tracking_today, + { controller: "/my/time_tracking", action: "day" }, + parent: :my_time_tracking, + caption: :label_today + + menu :global_menu, + :my_time_tracking_this_week, + { controller: "/my/time_tracking", action: "week" }, + parent: :my_time_tracking, + caption: :label_this_week + + menu :global_menu, + :my_time_tracking_this_month, + { controller: "/my/time_tracking", action: "month" }, + parent: :my_time_tracking, + caption: :label_this_month end initializer "costs.settings" do From d0029deb2039d95c7e526bf63c74f6d5ce86a122 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 3 Mar 2025 15:30:18 +0100 Subject: [PATCH 03/88] add more checking for the date --- .../my/time_tracking_controller.rb | 50 +++++++++++++++---- .../app/views/my/time_tracking/day.html.erb | 7 ++- modules/costs/config/locales/en.yml | 1 + 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 9ceaefbd6b3..e81b777133c 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -35,8 +35,12 @@ module My no_authorization_required!(:day, :week, :month) current_menu_item do |ctrl| - if ctrl.params[:action] == "day" && ctrl.current_day == Time.zone.today + if ctrl.params[:action] == "day" && ctrl.today? :my_time_tracking_today + elsif ctrl.params[:action] == "week" && ctrl.this_week? + :my_time_tracking_this_week + elsif ctrl.params[:action] == "month" && ctrl.this_month? + :my_time_tracking_this_month else :my_time_tracking end @@ -44,6 +48,8 @@ module My layout "global" + helper_method :current_day, :today?, :this_week?, :this_month? + def day; end def week; end @@ -54,16 +60,40 @@ module My "my time" end + def today? + current_day == Time.zone.today + end + + def this_week? + current_day == Time.zone.today.beginning_of_week + end + + def this_month? + current_day == Time.zone.today.beginning_of_month + end + + private + def current_day - @current_day ||= if params[:date].present? - begin - Date.parse(params[:date]) - rescue StandardError - Time.zone.today - end - else - Time.zone.today - end + return @current_day if defined?(@current_day) + + parsed_date = if params[:date].present? + begin + Date.iso8601(params[:date]) + rescue StandardError + nil + end + end + + @current_day = parsed_date || current_date + end + + def current_date + case params[:action].to_sym + when :day then Time.zone.today + when :week then Time.zone.today.beginning_of_week + when :month then Time.zone.today.beginning_of_month + end end end end diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb index 4f04629f9b9..7f32dd9c59d 100644 --- a/modules/costs/app/views/my/time_tracking/day.html.erb +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -1,6 +1,11 @@ <%= render Primer::OpenProject::PageHeader.new do |header| header.with_title { "my time: day view" } - header.with_breadcrumbs(["my time"]) + header.with_breadcrumbs( + [ + { title: "label_my_time_tracking", href: my_time_tracking_week_path }, + "day view" + ] + ) end %> diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index dd77b2529e8..f1750245023 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -164,6 +164,7 @@ en: label_cost: "Cost" label_costs: "Costs" label_mandatory_fields: "Mandatory fields" + label_my_time_tracking: "My time tracking" placeholder_activity_select_work_package_first: Work package selection is required first From 96acca07a44dde78149c8a9eb8809a8b33b3d563 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 4 Mar 2025 17:28:57 +0100 Subject: [PATCH 04/88] Add the layout for the week view --- .../app/views/my/time_tracking/day.html.erb | 6 +- .../app/views/my/time_tracking/month.html.erb | 9 ++- .../app/views/my/time_tracking/week.html.erb | 63 ++++++++++++++++++- modules/costs/lib/costs/engine.rb | 2 +- 4 files changed, 72 insertions(+), 8 deletions(-) diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb index 7f32dd9c59d..8d731f456fa 100644 --- a/modules/costs/app/views/my/time_tracking/day.html.erb +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -1,10 +1,10 @@ <%= render Primer::OpenProject::PageHeader.new do |header| - header.with_title { "my time: day view" } + header.with_title { I18n.t(:label_today) } header.with_breadcrumbs( [ - { title: "label_my_time_tracking", href: my_time_tracking_week_path }, - "day view" + { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_week_path }, + I18n.t(:label_today) ] ) end diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb index 88a3b74e667..f18bf60a872 100644 --- a/modules/costs/app/views/my/time_tracking/month.html.erb +++ b/modules/costs/app/views/my/time_tracking/month.html.erb @@ -1,6 +1,11 @@ <%= render Primer::OpenProject::PageHeader.new do |header| - header.with_title { "my time: month" } - header.with_breadcrumbs(["my time"]) + header.with_title { I18n.t(:label_this_month) } + header.with_breadcrumbs( + [ + { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_week_path }, + I18n.t(:label_this_month) + ] + ) end %> diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb index f9b68609a16..965396b6b34 100644 --- a/modules/costs/app/views/my/time_tracking/week.html.erb +++ b/modules/costs/app/views/my/time_tracking/week.html.erb @@ -1,6 +1,65 @@ <%= render Primer::OpenProject::PageHeader.new do |header| - header.with_title { "my time week" } - header.with_breadcrumbs(["my time"]) + header.with_title(variant: :large) { I18n.t(:label_this_week) } + header.with_breadcrumbs( + [ + { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_week_path }, + I18n.t(:label_this_week) + ] + ) + + btn = lambda do |button| + button.with_leading_visual_icon(icon: :calendar) + button.with_trailing_action_icon(icon: :"triangle-down") + "Calendar" + end + + header.with_action_menu(menu_arguments: { anchor_align: :end }, button_arguments: { button_block: btn }) do |menu| + menu.with_item(label: "Calendar") do |item| + item.with_leading_visual_icon(icon: :calendar) + end + menu.with_item(label: "List") do |item| + item.with_leading_visual_icon(icon: "list-unordered") + end + end + + header.with_action_zen_mode_button + header.with_action_icon_button(icon: "ellipsis", mobile_icon: "ellipsis", label: "more") end %> + +<%= render(Primer::OpenProject::SubHeader.new) do |component| + component.with_filter_component do + concat(render(Primer::Beta::IconButton.new(icon: "search", aria: { label: "Search" }))) + + concat( + render(Primer::Beta::Button.new) do |button| + button.with_leading_visual_icon(icon: :project) + button.with_trailing_action_icon(icon: :"triangle-down") + "Project" + end + ) + + concat( + render(Primer::Beta::Button.new) do |button| + button.with_leading_visual_icon(icon: :log) + button.with_trailing_action_icon(icon: :"triangle-down") + "Activity" + end + ) + end + + component.with_text do + "#{Time.zone.now.beginning_of_week.day} - #{Time.zone.now.end_of_week.day}, #{Time.zone.now.strftime('%B %Y')}" + end + component.with_action_component do + render(Primer::Beta::ButtonGroup.new) do |group| + group.with_button(icon: "arrow-left", aria: { label: "Previous week" }) + group.with_button(icon: "arrow-right", aria: { label: "Next week" }) + end + end + + component.with_action_button { "Today" } + + component.with_action_button(scheme: :primary) { "Log Time" } + end %> diff --git a/modules/costs/lib/costs/engine.rb b/modules/costs/lib/costs/engine.rb index 18838251cb8..a321326683d 100644 --- a/modules/costs/lib/costs/engine.rb +++ b/modules/costs/lib/costs/engine.rb @@ -144,7 +144,7 @@ module Costs menu :global_menu, :my_time_tracking, - { controller: "/my/time_tracking", action: "day" }, + { controller: "/my/time_tracking", action: "week" }, after: :my_page, caption: :label_my_time_tracking, icon: :stopwatch From 45da824bdaa7f3635c22fdfd6f4b53dfb9b45ce0 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 5 Mar 2025 15:09:49 +0100 Subject: [PATCH 05/88] implement stimulus controller to render FullCalendar --- .../dynamic/my/time-tracking.controller.ts | 50 +++++++++++++++++++ .../my/time_tracking_controller.rb | 45 ++++++++++++++--- .../views/my/time_tracking/_calendar.html.erb | 45 +++++++++++++++++ .../app/views/my/time_tracking/day.html.erb | 2 + .../app/views/my/time_tracking/month.html.erb | 2 + .../app/views/my/time_tracking/week.html.erb | 36 +------------ 6 files changed, 139 insertions(+), 41 deletions(-) create mode 100644 frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts create mode 100644 modules/costs/app/views/my/time_tracking/_calendar.html.erb diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts new file mode 100644 index 00000000000..ebeb18459ae --- /dev/null +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -0,0 +1,50 @@ +import { Controller } from '@hotwired/stimulus'; +import { Calendar } from '@fullcalendar/core'; +import timeGridPlugin from '@fullcalendar/timegrid'; +import dayGridPlugin from '@fullcalendar/daygrid'; + +export default class MyTimeTrackingController extends Controller { + static targets = ['calendar']; + + static values = { + mode: String, + timeEntries: Array, + initialDate: String, + }; + + declare readonly calendarTarget:HTMLElement; + declare readonly modeValue:string; + declare readonly timeEntriesValue:object[]; + declare readonly initialDateValue:string; + + private calendar:Calendar; + + connect() { + // styling for the calendar entries can be stolen from the team planner + + this.calendar = new Calendar(this.calendarTarget, { + plugins: [timeGridPlugin, dayGridPlugin], + initialView: this.calendarView(), + firstDay: 1, // get from settings + locale: 'de', // also get from settings + events: this.timeEntriesValue, + headerToolbar: false, + initialDate: this.initialDateValue, + }); + + this.calendar.render(); + } + + calendarView():string { + switch (this.modeValue) { + case 'week': + return 'timeGridWeek'; + case 'month': + return 'dayGridMonth'; + case 'day': + return 'timeGridDay'; + default: + return 'timeGridWeek'; + } + } +} diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index e81b777133c..80c66fb8d09 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -48,16 +48,36 @@ module My layout "global" - helper_method :current_day, :today?, :this_week?, :this_month? + helper_method :current_day, :today?, :this_week?, :this_month?, :time_entries_json - def day; end + def day + @time_entries = TimeEntry + .includes(:project, :activity, :work_package) + .where(user: User.current, spent_on: current_day) - def week; end + # TODO: At some point the filters will reduce the list, so we need to load them seperately + @project_filters = @time_entries.map(&:project).uniq + @activity_filters = @time_entries.map(&:activity).uniq + end - def month; end + def week + @time_entries = TimeEntry + .includes(:project, :activity, :work_package) + .where(user: User.current, spent_on: current_day.all_week) - def default_breadcrumb - "my time" + # TODO: At some point the filters will reduce the list, so we need to load them seperately + @project_filters = @time_entries.map(&:project).uniq + @activity_filters = @time_entries.map(&:activity).uniq + end + + def month + @time_entries = TimeEntry + .includes(:project, :activity, :work_package) + .where(user: User.current, spent_on: current_day.all_month) + + # TODO: At some point the filters will reduce the list, so we need to load them seperately + @project_filters = @time_entries.map(&:project).uniq + @activity_filters = @time_entries.map(&:activity).uniq end def today? @@ -74,6 +94,19 @@ module My private + def time_entries_json + @time_entries.map do |time_entry| + { + id: time_entry.id.to_s, + title: "#{time_entry.project.name}: ##{time_entry.work_package.id} #{time_entry.work_package.subject}", + start: time_entry.start_timestamp || time_entry.spent_on, + end: time_entry.end_timestamp || time_entry.spent_on, + allDay: time_entry.start_time.blank? + + } + end.to_json + end + def current_day return @current_day if defined?(@current_day) diff --git a/modules/costs/app/views/my/time_tracking/_calendar.html.erb b/modules/costs/app/views/my/time_tracking/_calendar.html.erb new file mode 100644 index 00000000000..4ee728acd98 --- /dev/null +++ b/modules/costs/app/views/my/time_tracking/_calendar.html.erb @@ -0,0 +1,45 @@ +
+<%= render(Primer::OpenProject::SubHeader.new) do |component| + component.with_filter_component do + concat(render(Primer::Beta::IconButton.new(icon: "search", aria: { label: "Search" }))) + + concat( + render(Primer::Beta::Button.new) do |button| + button.with_leading_visual_icon(icon: :project) + button.with_trailing_action_icon(icon: :"triangle-down") + "Project" + end + ) + + concat( + render(Primer::Beta::Button.new) do |button| + button.with_leading_visual_icon(icon: :log) + button.with_trailing_action_icon(icon: :"triangle-down") + "Activity" + end + ) + end + + component.with_text do + "#{current_day.beginning_of_week.day}-#{current_day.end_of_week.day}. #{current_day.strftime('%B %Y')}" + end + + component.with_action_component do + render(Primer::Beta::ButtonGroup.new) do |group| + group.with_button(icon: "arrow-left", aria: { label: "Previous week" }, tag: :a, href: my_time_tracking_week_path(date: current_day - 1.week)) + group.with_button(icon: "arrow-right", aria: { label: "Next week" }, tag: :a, href: my_time_tracking_week_path(date: current_day + 1.week)) + end + end + + component.with_action_button(tag: :a, href: my_time_tracking_week_path(date: Time.zone.today)) { "Today" } + + component.with_action_button(scheme: :primary) { "Log Time" } + end %> + +
+
diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb index 8d731f456fa..f3c7c50cfd3 100644 --- a/modules/costs/app/views/my/time_tracking/day.html.erb +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -9,3 +9,5 @@ ) end %> + +<%= render(partial: "calendar", locals: { mode: "day" }) %> diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb index f18bf60a872..d1e4bda3bac 100644 --- a/modules/costs/app/views/my/time_tracking/month.html.erb +++ b/modules/costs/app/views/my/time_tracking/month.html.erb @@ -9,3 +9,5 @@ ) end %> + +<%= render(partial: "calendar", locals: { mode: "month" }) %> diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb index 965396b6b34..ce5662e0afc 100644 --- a/modules/costs/app/views/my/time_tracking/week.html.erb +++ b/modules/costs/app/views/my/time_tracking/week.html.erb @@ -28,38 +28,4 @@ end %> -<%= render(Primer::OpenProject::SubHeader.new) do |component| - component.with_filter_component do - concat(render(Primer::Beta::IconButton.new(icon: "search", aria: { label: "Search" }))) - - concat( - render(Primer::Beta::Button.new) do |button| - button.with_leading_visual_icon(icon: :project) - button.with_trailing_action_icon(icon: :"triangle-down") - "Project" - end - ) - - concat( - render(Primer::Beta::Button.new) do |button| - button.with_leading_visual_icon(icon: :log) - button.with_trailing_action_icon(icon: :"triangle-down") - "Activity" - end - ) - end - - component.with_text do - "#{Time.zone.now.beginning_of_week.day} - #{Time.zone.now.end_of_week.day}, #{Time.zone.now.strftime('%B %Y')}" - end - component.with_action_component do - render(Primer::Beta::ButtonGroup.new) do |group| - group.with_button(icon: "arrow-left", aria: { label: "Previous week" }) - group.with_button(icon: "arrow-right", aria: { label: "Next week" }) - end - end - - component.with_action_button { "Today" } - - component.with_action_button(scheme: :primary) { "Log Time" } - end %> +<%= render(partial: "calendar", locals: { mode: "week" }) %> From f0b3fdb88fe225fa203f6c04322d10d669eea6a8 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 7 Mar 2025 11:58:07 +0100 Subject: [PATCH 06/88] add an ActiveModel for full calendar events --- lib/full_calendar/event.rb | 64 +++++++++++++++++++ .../my/time_tracking_controller.rb | 13 ++-- 2 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 lib/full_calendar/event.rb diff --git a/lib/full_calendar/event.rb b/lib/full_calendar/event.rb new file mode 100644 index 00000000000..1505745a156 --- /dev/null +++ b/lib/full_calendar/event.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module FullCalendar + class Event + include ActiveModel::Model + include ActiveModel::Attributes + + attribute :id, :string + attribute :group_id, :string + attribute :all_day, :boolean, default: false + attribute :starts_at, :datetime + attribute :ends_at, :datetime + attribute :title, :string + attribute :url, :string + attribute :class_names, array: true, default: [] + attribute :editable, :boolean, default: false + + def as_json + { + "id" => id, + "groupId" => group_id, + "allDay" => all_day, + "start" => starts_at, + "end" => ends_at, + "title" => title, + "url" => url, + "classNames" => class_names, + "editable" => editable + }.compact_blank.as_json + end + + def to_json(*) + as_json.to_json(*) + end + end +end diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 80c66fb8d09..1ceed938efd 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -96,14 +96,13 @@ module My def time_entries_json @time_entries.map do |time_entry| - { - id: time_entry.id.to_s, + FullCalendar::Event.new( + id: time_entry.id, title: "#{time_entry.project.name}: ##{time_entry.work_package.id} #{time_entry.work_package.subject}", - start: time_entry.start_timestamp || time_entry.spent_on, - end: time_entry.end_timestamp || time_entry.spent_on, - allDay: time_entry.start_time.blank? - - } + starts_at: time_entry.start_timestamp || time_entry.spent_on, + ends_at: time_entry.end_timestamp || time_entry.spent_on, + all_day: time_entry.start_time.blank? + ) end.to_json end From 776d354161d6da13ed108ba4645baad97ebd5378 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 7 Mar 2025 12:07:43 +0100 Subject: [PATCH 07/88] DRY out controller --- .../controllers/my/time_tracking_controller.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 1ceed938efd..c6f5e00ac4b 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -51,9 +51,7 @@ module My helper_method :current_day, :today?, :this_week?, :this_month?, :time_entries_json def day - @time_entries = TimeEntry - .includes(:project, :activity, :work_package) - .where(user: User.current, spent_on: current_day) + load_time_entries(current_day) # TODO: At some point the filters will reduce the list, so we need to load them seperately @project_filters = @time_entries.map(&:project).uniq @@ -61,9 +59,7 @@ module My end def week - @time_entries = TimeEntry - .includes(:project, :activity, :work_package) - .where(user: User.current, spent_on: current_day.all_week) + load_time_entries(current_day.all_week) # TODO: At some point the filters will reduce the list, so we need to load them seperately @project_filters = @time_entries.map(&:project).uniq @@ -71,9 +67,7 @@ module My end def month - @time_entries = TimeEntry - .includes(:project, :activity, :work_package) - .where(user: User.current, spent_on: current_day.all_month) + load_time_entries(current_day.all_month) # TODO: At some point the filters will reduce the list, so we need to load them seperately @project_filters = @time_entries.map(&:project).uniq @@ -127,5 +121,11 @@ module My when :month then Time.zone.today.beginning_of_month end end + + def load_time_entries(time_scope) + @time_entries = TimeEntry + .includes(:project, :activity, { work_package: :status }) + .where(user: User.current, spent_on: time_scope) + end end end From 3a3d3148706d8171ddec5b967a88cc0189d093da Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 12 Mar 2025 12:43:48 +0100 Subject: [PATCH 08/88] Start with custom view implementation for calendar --- .../dynamic/my/time-tracking.controller.ts | 7 +++ lib/full_calendar/event.rb | 16 +++++- modules/costs/app/components/_index.sass | 1 + .../time_entry_event_component.html.erb | 3 ++ .../time_entry_event_component.rb | 35 ++++++++++++ .../time_entry_event_component.sass | 24 +++++++++ .../my/time_tracking_controller.rb | 8 +-- .../lib/full_calendar/time_entry_event.rb | 54 +++++++++++++++++++ 8 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 modules/costs/app/components/time_tracking/time_entry_event_component.html.erb create mode 100644 modules/costs/app/components/time_tracking/time_entry_event_component.rb create mode 100644 modules/costs/app/components/time_tracking/time_entry_event_component.sass create mode 100644 modules/costs/lib/full_calendar/time_entry_event.rb diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index ebeb18459ae..2b2c8baaff5 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -30,6 +30,13 @@ export default class MyTimeTrackingController extends Controller { events: this.timeEntriesValue, headerToolbar: false, initialDate: this.initialDateValue, + eventClassNames: ['calendar-time-entry-event'], + eventContent: (arg) => { + if (arg.event.extendedProps.hasOwnProperty('customEventView')) { + return { html: arg.event.extendedProps.customEventView }; + } + return null; + }, }); this.calendar.render(); diff --git a/lib/full_calendar/event.rb b/lib/full_calendar/event.rb index 1505745a156..897d4a7c5de 100644 --- a/lib/full_calendar/event.rb +++ b/lib/full_calendar/event.rb @@ -53,12 +53,26 @@ module FullCalendar "title" => title, "url" => url, "classNames" => class_names, - "editable" => editable + "editable" => editable, + "customEventView" => rendered_event_content }.compact_blank.as_json end def to_json(*) as_json.to_json(*) end + + def event_content_view_component + # override in subclasses + nil + end + + private + + def rendered_event_content + if event_content_view_component + ApplicationController.render(event_content_view_component, layout: false) + end + end end end diff --git a/modules/costs/app/components/_index.sass b/modules/costs/app/components/_index.sass index c2d6cebea24..baf9ffc4943 100644 --- a/modules/costs/app/components/_index.sass +++ b/modules/costs/app/components/_index.sass @@ -1 +1,2 @@ @import "time_entries/entry_dialog_component" +@import "time_tracking/time_entry_event_component" diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb new file mode 100644 index 00000000000..38d0d65701f --- /dev/null +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb @@ -0,0 +1,3 @@ +
+ +
<%= time_entry.work_package.subject %>
diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.rb b/modules/costs/app/components/time_tracking/time_entry_event_component.rb new file mode 100644 index 00000000000..fa5b74bfdf2 --- /dev/null +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module TimeTracking + class TimeEntryEventComponent < ApplicationComponent + options :time_entry + end +end diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.sass b/modules/costs/app/components/time_tracking/time_entry_event_component.sass new file mode 100644 index 00000000000..0b5d4f3682d --- /dev/null +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.sass @@ -0,0 +1,24 @@ +@import 'helpers' +@import '../../app/spot/styles/sass/variables' + +.calendar-time-entry-event + display: flex + user-select: none + // Leave space for the shadow to be displayed + width: calc(100% - 2px) + border: 1px solid var(--borderColor-default) + border-radius: 2px + padding: 10px + position: relative + box-shadow: 1px 1px 3px 0px var(--borderColor-default) + background: var(--body-background) + color: var(--body-font-color) + font-size: var(--body-font-size) + + &--highlighting + width: 100% + height: 2px + position: absolute + top: 0 + left: 0 + border-radius: 2px 2px 0 0 diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index c6f5e00ac4b..d87a65edc67 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -90,13 +90,7 @@ module My def time_entries_json @time_entries.map do |time_entry| - FullCalendar::Event.new( - id: time_entry.id, - title: "#{time_entry.project.name}: ##{time_entry.work_package.id} #{time_entry.work_package.subject}", - starts_at: time_entry.start_timestamp || time_entry.spent_on, - ends_at: time_entry.end_timestamp || time_entry.spent_on, - all_day: time_entry.start_time.blank? - ) + FullCalendar::TimeEntryEvent.from_time_entry(time_entry) end.to_json end diff --git a/modules/costs/lib/full_calendar/time_entry_event.rb b/modules/costs/lib/full_calendar/time_entry_event.rb new file mode 100644 index 00000000000..a65471f8dfd --- /dev/null +++ b/modules/costs/lib/full_calendar/time_entry_event.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +module FullCalendar + class TimeEntryEvent < Event + attr_accessor :time_entry + + class << self + def from_time_entry(time_entry) + event = new( + id: time_entry.id, + starts_at: time_entry.start_timestamp || time_entry.spent_on, + ends_at: time_entry.end_timestamp || time_entry.spent_on, + all_day: time_entry.start_time.blank?, + title: "#{time_entry.project.name}: ##{time_entry.work_package.id} #{time_entry.work_package.subject}" + ) + event.time_entry = time_entry + + event + end + end + + def event_content_view_component + TimeTracking::TimeEntryEventComponent.new(time_entry: time_entry) + end + end +end From 9d1a11ca730aba48424acdaca9a8b160676825a6 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 12 Mar 2025 14:09:12 +0100 Subject: [PATCH 09/88] copy entire style from wp-single-card and fill complete html of calendar event --- .../dynamic/my/time-tracking.controller.ts | 16 +- .../time_entry_event_component.html.erb | 5 +- .../time_entry_event_component.sass | 161 ++++++++++++++++++ 3 files changed, 174 insertions(+), 8 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 2b2c8baaff5..0bf362785f7 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -30,12 +30,16 @@ export default class MyTimeTrackingController extends Controller { events: this.timeEntriesValue, headerToolbar: false, initialDate: this.initialDateValue, - eventClassNames: ['calendar-time-entry-event'], - eventContent: (arg) => { - if (arg.event.extendedProps.hasOwnProperty('customEventView')) { - return { html: arg.event.extendedProps.customEventView }; - } - return null; + height: 800, + contentHeight: 780, + aspectRatio: 3, + eventClassNames: [ + 'calendar-time-entry-event', + 'calendar-time-entry-event_inline ', + ], + eventDidMount(info) { + //eslint-disable-next-line + info.el.innerHTML = info.event.extendedProps.customEventView; }, }); diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb index 38d0d65701f..3ce7cb483bc 100644 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb @@ -1,3 +1,4 @@
- -
<%= time_entry.work_package.subject %>
+
+
<%= time_entry.work_package.subject %>
+
diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.sass b/modules/costs/app/components/time_tracking/time_entry_event_component.sass index 0b5d4f3682d..49fdc819676 100644 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.sass +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.sass @@ -15,6 +15,130 @@ color: var(--body-font-color) font-size: var(--body-font-size) + &:hover + .calendar-time-entry-event--inline-buttons + opacity: 1 + z-index: 2 + box-shadow: -4px -4px 10px 4px var(--body-background) + + &_new + padding-right: 25px + + &_ghosted + opacity: 0.5 + + &_selected + background-color: var(--selection-bgColor) + .op-icon--wrapper + background: var(--selection-bgColor) + &:hover + .calendar-time-entry-event--inline-buttons + box-shadow: none + + &_closed:not(&_selected) + background-color: var(--display-gray-bgColor-muted) + .op-icon--wrapper + background: var(--display-gray-bgColor-muted) + &:hover + .calendar-time-entry-event--inline-buttons + box-shadow: -4px -4px 10px 4px var(--display-gray-bgColor-muted) + + &_horizontal + height: 100% + + &_vertical + margin-top: 10px + // Take care that the shadow of the last element is still visible + &:last-of-type + margin-bottom: 5px + + &_inline &--content + grid-template-columns: max-content max-content auto auto 1fr + grid-template-rows: auto max-content + grid-template-areas: "status id project project project" "subject subject subject subject subject" + + &-assignee + display: none + &-subject-line + display: flex + &-subject + flex-grow: 1 + @include text-shortener + + // Style shadow element while dragging + wp-single-card:host.gu-transit & + @include modifying--placeholder + + &--content + + &:not(.-new) + display: grid + grid-template-columns: max-content max-content auto auto 1fr + grid-template-rows: auto 1fr max-content auto + grid-row-gap: 5px + grid-column-gap: 5px + grid-template-areas: "status id project project project" "subject subject subject subject subject" "image image image image image" "avatar avatar avatar dates dates" + overflow: hidden + flex-grow: 1 + font-size: var(--body-font-size) + line-height: 16px + + &-project-name + grid-area: project + font-style: italic + color: var(--fgColor-muted) + font-size: 12px + @include text-shortener + &-type + grid-area: type + margin-right: 4px + &-subject-line + grid-area: subject + @include text-shortener(false) + &-assignee + margin-right: auto + grid-area: avatar + max-width: 140px + color: var(--fgColor-muted) + font-size: 12px + &-id + grid-area: id + color: var(--fgColor-muted) + font-size: 12px + &-status-baseline + grid-area: status + margin-right: 5px + max-width: 6.5rem + display: flex + &-status + overflow: hidden + &-baseline + margin-left: $spot-spacing-0_5 + + &-tooltip + width: 6.5rem + + &-cover-image + grid-area: image + max-height: 350px + justify-self: center + + @media screen and (max-width: $breakpoint-sm) + max-height: 250px + + &-dates + grid-area: dates + place-self: center end + white-space: nowrap + color: var(--fgColor-muted) + font-size: 12px + + &-inline-date + font-size: 12px + color: var(--fgColor-muted) + margin: 0 8px + white-space: nowrap + &--highlighting width: 100% height: 2px @@ -22,3 +146,40 @@ top: 0 left: 0 border-radius: 2px 2px 0 0 + + &--card-actions + display: flex + position: absolute + top: 9px + right: 0 + + &--card-action + z-index: 2 + + &_closed + color: $spot-color-basic-gray-2 + + &--inline-buttons + opacity: 0 + padding-right: 4px + white-space: nowrap + + // For selected cards the padding needs to be smaller + // Otherwise the left-side color shading can overflow the card (e.g. in small cards in the team planner) + .calendar-time-entry-event_selected & + padding-left: 15px + + .op-icon--wrapper:not(&_selected) + background: var(--body-background) + + &.-show + opacity: 1 + + &--inline-cancel-button + color: var(--warn) + + @media only screen and (max-width: $breakpoint-sm) + &_shrink + border: none + box-shadow: none + padding: 15px From 7c681df0ee62478c5e9ac03bbfa16273b370ae4d Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 12 Mar 2025 14:27:13 +0100 Subject: [PATCH 10/88] update view to make it like figma design --- .../time_entry_event_component.html.erb | 8 +- .../time_entry_event_component.sass | 129 +++--------------- 2 files changed, 27 insertions(+), 110 deletions(-) diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb index 3ce7cb483bc..fc78640574a 100644 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb @@ -1,4 +1,10 @@
+
-
<%= time_entry.work_package.subject %>
+ +
<%= time_entry.work_package.subject %>
+
<%= time_entry.project.name %>
diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.sass b/modules/costs/app/components/time_tracking/time_entry_event_component.sass index 49fdc819676..1cd1bb38bd4 100644 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.sass +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.sass @@ -5,9 +5,9 @@ display: flex user-select: none // Leave space for the shadow to be displayed - width: calc(100% - 2px) + width: calc(100% - 4px) border: 1px solid var(--borderColor-default) - border-radius: 2px + border-radius: 5px padding: 10px position: relative box-shadow: 1px 1px 3px 0px var(--borderColor-default) @@ -21,67 +21,17 @@ z-index: 2 box-shadow: -4px -4px 10px 4px var(--body-background) - &_new - padding-right: 25px - - &_ghosted - opacity: 0.5 - - &_selected - background-color: var(--selection-bgColor) - .op-icon--wrapper - background: var(--selection-bgColor) - &:hover - .calendar-time-entry-event--inline-buttons - box-shadow: none - - &_closed:not(&_selected) - background-color: var(--display-gray-bgColor-muted) - .op-icon--wrapper - background: var(--display-gray-bgColor-muted) - &:hover - .calendar-time-entry-event--inline-buttons - box-shadow: -4px -4px 10px 4px var(--display-gray-bgColor-muted) - - &_horizontal - height: 100% - - &_vertical - margin-top: 10px - // Take care that the shadow of the last element is still visible - &:last-of-type - margin-bottom: 5px - - &_inline &--content - grid-template-columns: max-content max-content auto auto 1fr - grid-template-rows: auto max-content - grid-template-areas: "status id project project project" "subject subject subject subject subject" - - &-assignee - display: none - &-subject-line - display: flex - &-subject - flex-grow: 1 - @include text-shortener - - // Style shadow element while dragging - wp-single-card:host.gu-transit & - @include modifying--placeholder - &--content - - &:not(.-new) - display: grid - grid-template-columns: max-content max-content auto auto 1fr - grid-template-rows: auto 1fr max-content auto - grid-row-gap: 5px - grid-column-gap: 5px - grid-template-areas: "status id project project project" "subject subject subject subject subject" "image image image image image" "avatar avatar avatar dates dates" - overflow: hidden - flex-grow: 1 - font-size: var(--body-font-size) - line-height: 16px + display: grid + grid-template-columns: max-content max-content auto auto 1fr + grid-template-rows: auto 1fr max-content auto + grid-row-gap: 5px + grid-column-gap: 5px + grid-template-areas: "dates" "subject" "project" + overflow: hidden + flex-grow: 1 + font-size: var(--body-font-size) + line-height: 16px &-project-name grid-area: project @@ -89,18 +39,22 @@ color: var(--fgColor-muted) font-size: 12px @include text-shortener + &-type grid-area: type margin-right: 4px + &-subject-line grid-area: subject @include text-shortener(false) + &-assignee margin-right: auto grid-area: avatar max-width: 140px color: var(--fgColor-muted) font-size: 12px + &-id grid-area: id color: var(--fgColor-muted) @@ -110,22 +64,16 @@ margin-right: 5px max-width: 6.5rem display: flex + &-status overflow: hidden + &-baseline margin-left: $spot-spacing-0_5 &-tooltip width: 6.5rem - &-cover-image - grid-area: image - max-height: 350px - justify-self: center - - @media screen and (max-width: $breakpoint-sm) - max-height: 250px - &-dates grid-area: dates place-self: center end @@ -141,45 +89,8 @@ &--highlighting width: 100% - height: 2px + height: 4px position: absolute top: 0 left: 0 - border-radius: 2px 2px 0 0 - - &--card-actions - display: flex - position: absolute - top: 9px - right: 0 - - &--card-action - z-index: 2 - - &_closed - color: $spot-color-basic-gray-2 - - &--inline-buttons - opacity: 0 - padding-right: 4px - white-space: nowrap - - // For selected cards the padding needs to be smaller - // Otherwise the left-side color shading can overflow the card (e.g. in small cards in the team planner) - .calendar-time-entry-event_selected & - padding-left: 15px - - .op-icon--wrapper:not(&_selected) - background: var(--body-background) - - &.-show - opacity: 1 - - &--inline-cancel-button - color: var(--warn) - - @media only screen and (max-width: $breakpoint-sm) - &_shrink - border: none - box-shadow: none - padding: 15px + border-radius: 5px 5px 0 0 From 810160f615546972211b3afcc40e9112e4a46746 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 12 Mar 2025 14:38:31 +0100 Subject: [PATCH 11/88] remove more unused CSS from copying --- .../time_entry_event_component.sass | 30 ------------------- 1 file changed, 30 deletions(-) diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.sass b/modules/costs/app/components/time_tracking/time_entry_event_component.sass index 1cd1bb38bd4..0ee491c93ef 100644 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.sass +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.sass @@ -48,44 +48,14 @@ grid-area: subject @include text-shortener(false) - &-assignee - margin-right: auto - grid-area: avatar - max-width: 140px - color: var(--fgColor-muted) - font-size: 12px - - &-id - grid-area: id - color: var(--fgColor-muted) - font-size: 12px - &-status-baseline - grid-area: status - margin-right: 5px - max-width: 6.5rem - display: flex - - &-status - overflow: hidden - - &-baseline - margin-left: $spot-spacing-0_5 - - &-tooltip - width: 6.5rem - &-dates grid-area: dates place-self: center end white-space: nowrap color: var(--fgColor-muted) font-size: 12px - - &-inline-date - font-size: 12px color: var(--fgColor-muted) margin: 0 8px - white-space: nowrap &--highlighting width: 100% From 9efb90418b5f1f6a9f590467e10ed0ddfe926543 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 13 Mar 2025 10:29:45 +0100 Subject: [PATCH 12/88] Add an action to redirect to the correct view --- .../costs/app/controllers/my/time_tracking_controller.rb | 7 ++++++- modules/costs/config/routes.rb | 1 + modules/costs/lib/costs/engine.rb | 2 +- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index d87a65edc67..54248c0c85f 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -32,7 +32,7 @@ module My class TimeTrackingController < ApplicationController before_action :require_login - no_authorization_required!(:day, :week, :month) + no_authorization_required!(:calendar, :day, :week, :month) current_menu_item do |ctrl| if ctrl.params[:action] == "day" && ctrl.today? @@ -50,6 +50,11 @@ module My helper_method :current_day, :today?, :this_week?, :this_month?, :time_entries_json + def calendar + # TODO: on mobile, we should show the day view + redirect_to action: :week + end + def day load_time_entries(current_day) diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index 3f4d3d70702..92d16421f7f 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -50,6 +50,7 @@ Rails.application.routes.draw do namespace "my" do get "/timer" => "timer#show", as: "timers" + get "/time-tracking" => "time_tracking#calendar" get "/time-tracking/day" => "time_tracking#day" get "/time-tracking/week" => "time_tracking#week" get "/time-tracking/month" => "time_tracking#month" diff --git a/modules/costs/lib/costs/engine.rb b/modules/costs/lib/costs/engine.rb index a321326683d..5366b3c45bd 100644 --- a/modules/costs/lib/costs/engine.rb +++ b/modules/costs/lib/costs/engine.rb @@ -144,7 +144,7 @@ module Costs menu :global_menu, :my_time_tracking, - { controller: "/my/time_tracking", action: "week" }, + { controller: "/my/time_tracking", action: "calendar" }, after: :my_page, caption: :label_my_time_tracking, icon: :stopwatch From 07ea7f8a0d731c43511f6b613f26c5bf1fdac420 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 13 Mar 2025 10:58:41 +0100 Subject: [PATCH 13/88] Show "full-day" events correctly --- .../time_tracking/time_entry_event_component.html.erb | 9 +++++++-- modules/costs/app/models/time_entry.rb | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb index fc78640574a..463dccf5257 100644 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb @@ -2,8 +2,13 @@
<%= time_entry.work_package.subject %>
<%= time_entry.project.name %>
diff --git a/modules/costs/app/models/time_entry.rb b/modules/costs/app/models/time_entry.rb index c0ab8b203b5..552d007f068 100644 --- a/modules/costs/app/models/time_entry.rb +++ b/modules/costs/app/models/time_entry.rb @@ -147,6 +147,10 @@ class TimeEntry < ApplicationRecord (user_id == usr.id && usr.allowed_in_project?(:view_own_hourly_rate, project)) end + def has_start_and_end_time? + start_time.present? + end + def start_timestamp # rubocop:disable Metrics/AbcSize return nil if start_time.blank? return nil if time_zone.blank? From db10c8738f7b390c2b596fdc65616310b07a4275 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 13 Mar 2025 10:59:08 +0100 Subject: [PATCH 14/88] add redirect to day page on mobile --- .../costs/app/controllers/my/time_tracking_controller.rb | 7 +++++-- modules/costs/app/views/my/time_tracking/day.html.erb | 2 +- modules/costs/app/views/my/time_tracking/month.html.erb | 2 +- modules/costs/app/views/my/time_tracking/week.html.erb | 2 +- 4 files changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 54248c0c85f..dc8d4697d53 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -51,8 +51,11 @@ module My helper_method :current_day, :today?, :this_week?, :this_month?, :time_entries_json def calendar - # TODO: on mobile, we should show the day view - redirect_to action: :week + if browser.device.mobile? + redirect_to action: :day + else + redirect_to action: :week + end end def day diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb index f3c7c50cfd3..bb66833433c 100644 --- a/modules/costs/app/views/my/time_tracking/day.html.erb +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -3,7 +3,7 @@ header.with_title { I18n.t(:label_today) } header.with_breadcrumbs( [ - { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_week_path }, + { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path }, I18n.t(:label_today) ] ) diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb index d1e4bda3bac..09c750b46fd 100644 --- a/modules/costs/app/views/my/time_tracking/month.html.erb +++ b/modules/costs/app/views/my/time_tracking/month.html.erb @@ -3,7 +3,7 @@ header.with_title { I18n.t(:label_this_month) } header.with_breadcrumbs( [ - { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_week_path }, + { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path }, I18n.t(:label_this_month) ] ) diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb index ce5662e0afc..3ed0044c3dd 100644 --- a/modules/costs/app/views/my/time_tracking/week.html.erb +++ b/modules/costs/app/views/my/time_tracking/week.html.erb @@ -3,7 +3,7 @@ header.with_title(variant: :large) { I18n.t(:label_this_week) } header.with_breadcrumbs( [ - { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_week_path }, + { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path }, I18n.t(:label_this_week) ] ) From 8ac99ea46b6fca786be1546ef4b7754d4ec63def Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 13 Mar 2025 10:59:23 +0100 Subject: [PATCH 15/88] remove quick filters --- .../views/my/time_tracking/_calendar.html.erb | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/modules/costs/app/views/my/time_tracking/_calendar.html.erb b/modules/costs/app/views/my/time_tracking/_calendar.html.erb index 4ee728acd98..f5496ea4e49 100644 --- a/modules/costs/app/views/my/time_tracking/_calendar.html.erb +++ b/modules/costs/app/views/my/time_tracking/_calendar.html.erb @@ -5,26 +5,6 @@ data-my--time-tracking-time-entries-value="<%= time_entries_json %>" data-my--time-tracking-initial-date-value="<%= current_day %>"> <%= render(Primer::OpenProject::SubHeader.new) do |component| - component.with_filter_component do - concat(render(Primer::Beta::IconButton.new(icon: "search", aria: { label: "Search" }))) - - concat( - render(Primer::Beta::Button.new) do |button| - button.with_leading_visual_icon(icon: :project) - button.with_trailing_action_icon(icon: :"triangle-down") - "Project" - end - ) - - concat( - render(Primer::Beta::Button.new) do |button| - button.with_leading_visual_icon(icon: :log) - button.with_trailing_action_icon(icon: :"triangle-down") - "Activity" - end - ) - end - component.with_text do "#{current_day.beginning_of_week.day}-#{current_day.end_of_week.day}. #{current_day.strftime('%B %Y')}" end From 503bedaaaf3210638240e057693f5a358a7f21f1 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 13 Mar 2025 11:19:55 +0100 Subject: [PATCH 16/88] enable the log time button --- modules/costs/app/views/my/time_tracking/_calendar.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/costs/app/views/my/time_tracking/_calendar.html.erb b/modules/costs/app/views/my/time_tracking/_calendar.html.erb index f5496ea4e49..e6fe6c3d031 100644 --- a/modules/costs/app/views/my/time_tracking/_calendar.html.erb +++ b/modules/costs/app/views/my/time_tracking/_calendar.html.erb @@ -18,7 +18,7 @@ component.with_action_button(tag: :a, href: my_time_tracking_week_path(date: Time.zone.today)) { "Today" } - component.with_action_button(scheme: :primary) { "Log Time" } + component.with_action_button(scheme: :primary, data: { "turbo-stream" => true }, tag: :a, href: dialog_time_entries_path(onlyMe: true)) { "Log Time" } end %>
From c3df2561ab6d24c1bff1dd931a76d8579ffdee1e Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 13 Mar 2025 13:27:24 +0100 Subject: [PATCH 17/88] add a generic color class that we can use with different style classes --- app/helpers/colors_helper.rb | 98 +++++++++++++++++++-------- app/views/highlighting/styles.css.erb | 3 + 2 files changed, 71 insertions(+), 30 deletions(-) diff --git a/app/helpers/colors_helper.rb b/app/helpers/colors_helper.rb index 96af0e1c73a..e19c8b33a20 100644 --- a/app/helpers/colors_helper.rb +++ b/app/helpers/colors_helper.rb @@ -48,6 +48,14 @@ module ColorsHelper colored_thing.color_id end + def concat(line) + if Rails.env.development? + super("#{line}\n") + else + super + end + end + # # Styles to display colors itself (e.g. for the colors autocompleter) ## @@ -71,7 +79,7 @@ module ColorsHelper color = entry.color if color.nil? - concat ".#{hl_inline_class(name, entry)}::before { display: none }\n" + concat ".#{hl_inline_class(name, entry)}::before { display: none }" return end @@ -82,6 +90,13 @@ module ColorsHelper end set_background_colors_for(class_name: ".#{hl_background_class(name, entry)}", color:) + + # generic class for color + set_generic_color_for(class_name: ".#{hl_color_class(name, entry)}", color:) + end + + def hl_color_class(name, model) + "__hl_#{name}_#{model.id}" end def hl_inline_class(name, model) @@ -115,6 +130,13 @@ module ColorsHelper DesignColor.find_by(variable:)&.hexcode end + def set_generic_color_for(class_name:, color:) + mode = User.current.pref.theme.split("_", 2)[0] + mode_variables = mode == "dark" ? default_variables_dark : default_variables_light + + concat "#{class_name} { #{default_color_styles(color.hexcode)} #{mode_variables} }" + end + def set_background_colors_for(class_name:, color:) mode = User.current.pref.theme.split("_", 2)[0] @@ -141,66 +163,82 @@ module ColorsHelper end end - # rubocop:disable Layout/LineLength def default_color_styles(hex) color = ColorConversion::Color.new(hex) rgb = color.rgb hsl = color.hsl - "--color-r: #{rgb[:r]}; - --color-g: #{rgb[:g]}; - --color-b: #{rgb[:b]}; - --color-h: #{hsl[:h]}; - --color-s: #{hsl[:s]}; - --color-l: #{hsl[:l]}; - --perceived-lightness: calc( ((var(--color-r) * 0.2126) + (var(--color-g) * 0.7152) + (var(--color-b) * 0.0722)) / 255 ); - --lightness-switch: max(0, min(calc((1/(var(--lightness-threshold) - var(--perceived-lightness)))), 1));" + <<~CSS.squish + --color-r: #{rgb[:r]}; + --color-g: #{rgb[:g]}; + --color-b: #{rgb[:b]}; + --color-h: #{hsl[:h]}; + --color-s: #{hsl[:s]}; + --color-l: #{hsl[:l]}; + --perceived-lightness: calc( ((var(--color-r) * 0.2126) + (var(--color-g) * 0.7152) + (var(--color-b) * 0.0722)) / 255 ); + --lightness-switch: max(0, min(calc((1/(var(--lightness-threshold) - var(--perceived-lightness)))), 1)); + CSS end def default_variables_dark - "--lightness-threshold: 0.6; - --background-alpha: 0.18; - --lighten-by: calc(((var(--lightness-threshold) - var(--perceived-lightness)) * 100) * var(--lightness-switch));" + <<~CSS.squish + --lightness-threshold: 0.6; + --background-alpha: 0.18; + --lighten-by: calc(((var(--lightness-threshold) - var(--perceived-lightness)) * 100) * var(--lightness-switch)); + CSS end def default_variables_light - "--lightness-threshold: 0.453;" + <<~CSS.squish + --lightness-threshold: 0.453; + CSS end def highlighted_background_dark - "color: hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) + var(--lighten-by)) * 1%)) !important; - background: rgba(var(--color-r), var(--color-g), var(--color-b), var(--background-alpha)) !important; - border: 1px solid hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) + var(--lighten-by)) * 1%)) !important;" + <<~CSS.squish + color: hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) + var(--lighten-by)) * 1%)) !important; + background: rgba(var(--color-r), var(--color-g), var(--color-b), var(--background-alpha)) !important; + border: 1px solid hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) + var(--lighten-by)) * 1%)) !important; + CSS end def highlighted_background_light - style = "color: hsl(0deg, 0%, calc(var(--lightness-switch) * 100%)) !important; - background: rgb(var(--color-r), var(--color-g), var(--color-b)) !important;" + style = <<~CSS.squish + color: hsl(0deg, 0%, calc(var(--lightness-switch) * 100%)) !important; + background: rgb(var(--color-r), var(--color-g), var(--color-b)) !important; + CSS mode = User.current.pref.theme - style += - if mode == "light_high_contrast" - "border: 1px solid hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 75) * 1%), 1) !important;" - else - "border: 1px solid hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 15) * 1%)) !important;" - end + style += if mode == "light_high_contrast" + <<~CSS.squish + border: 1px solid hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 75) * 1%), 1) !important; + CSS + else + <<~CSS.squish + border: 1px solid hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 15) * 1%)) !important; + CSS + end style end def highlighted_foreground_dark - "color: hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) + var(--lighten-by)) * 1%), 1) !important;" + <<~CSS.squish + color: hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) + var(--lighten-by)) * 1%), 1) !important; + CSS end def highlighted_foreground_light mode = User.current.pref.theme if mode == "light_high_contrast" - "color: hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - (var(--color-l) * 0.5)) * 1%), 1) !important;" + <<~CSS.squish + color: hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - (var(--color-l) * 0.5)) * 1%), 1) !important; + CSS else - "color: hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - (var(--color-l) * 0.22)) * 1%), 1) !important;" + <<~CSS.squish + color: hsla(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - (var(--color-l) * 0.22)) * 1%), 1) !important; + CSS end end - - # rubocop:enable Layout/LineLength end diff --git a/app/views/highlighting/styles.css.erb b/app/views/highlighting/styles.css.erb index 2582f25c686..cfd9dc5e4b3 100644 --- a/app/views/highlighting/styles.css.erb +++ b/app/views/highlighting/styles.css.erb @@ -11,6 +11,9 @@ <%= color_css %> +<%# usage classes %> +.__hl_border_top { border-top: 4px solid hsl(var(--color-h), calc(var(--color-s) * 1%), calc((var(--color-l) - 15) * 1%)) !important; } } + <%# Overdue tasks %> .__hl_date_due_today { color: #F76707 !important; } .__hl_date_overdue { color: #C92A2A !important; font-weight: var(--base-text-weight-bold); } From cad64f06d2bae994dbc08f0cb1452009074ac649 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 13 Mar 2025 13:28:25 +0100 Subject: [PATCH 18/88] use the new status class instead of a div --- .../dynamic/my/time-tracking.controller.ts | 11 +++++++---- .../time_tracking/time_entry_event_component.html.erb | 2 -- modules/costs/lib/full_calendar/time_entry_event.rb | 6 ++++++ 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 0bf362785f7..5764aab86b9 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -33,10 +33,13 @@ export default class MyTimeTrackingController extends Controller { height: 800, contentHeight: 780, aspectRatio: 3, - eventClassNames: [ - 'calendar-time-entry-event', - 'calendar-time-entry-event_inline ', - ], + eventClassNames(arg) { + return [ + 'calendar-time-entry-event', + `__hl_status_${arg.event.extendedProps.statusId}`, + '__hl_border_top', + ]; + }, eventDidMount(info) { //eslint-disable-next-line info.el.innerHTML = info.event.extendedProps.customEventView; diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb index 463dccf5257..4a9b905bed3 100644 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb +++ b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb @@ -1,5 +1,3 @@ -
-
` }); +
`, + }; }, select: (info) => { let dialogParams = 'onlyMe=true'; @@ -111,7 +118,9 @@ export default class MyTimeTrackingController extends Controller { const startMoment = moment(info.event.startStr); const endMoment = moment(info.event.endStr); - const newEventHours = info.event.allDay ? info.event.extendedProps.hours as number : moment.duration(endMoment.diff(startMoment)).asHours(); + const newEventHours = info.event.allDay + ? (info.event.extendedProps.hours as number) + : moment.duration(endMoment.diff(startMoment)).asHours(); info.event.setExtendedProp('hours', newEventHours); @@ -128,10 +137,23 @@ export default class MyTimeTrackingController extends Controller { // When dragging from all day into the calendar set the defaultTimedEventDuration to the hours of the event so // that we display it correctly in the calendar. Will be reset in the drop event if (info.event.allDay) { - this.calendar.setOption('defaultTimedEventDuration', moment.duration(info.event.extendedProps.hours as number, 'hours').asMilliseconds()); + this.calendar.setOption( + 'defaultTimedEventDuration', + moment + .duration(info.event.extendedProps.hours as number, 'hours') + .asMilliseconds(), + ); } }, + eventAllow: (dropInfo) => { + if (dropInfo.allDay && this.forceTimesValue) { + return false; + } + + return true; + }, + eventDrop: (info) => { const startMoment = moment(info.event.startStr); @@ -144,14 +166,23 @@ export default class MyTimeTrackingController extends Controller { ); if (!info.event.allDay) { - info.event.setEnd(startMoment.add(info.event.extendedProps.hours as number, 'hours').toDate()); + info.event.setEnd( + startMoment + .add(info.event.extendedProps.hours as number, 'hours') + .toDate(), + ); } - this.calendar.setOption('defaultTimedEventDuration', DEFAULT_TIMED_EVENT_DURATION); + this.calendar.setOption( + 'defaultTimedEventDuration', + DEFAULT_TIMED_EVENT_DURATION, + ); }, eventClick: (info) => { // check if we clicked on a link tag, if so exit early as we don't want to show the modal - if (info.jsEvent.target instanceof HTMLAnchorElement) { return; } + if (info.jsEvent.target instanceof HTMLAnchorElement) { + return; + } void this.turboRequests.request( `${this.pathHelper.timeEntryEditDialog(info.event.id)}?onlyMe=true`, @@ -163,33 +194,43 @@ export default class MyTimeTrackingController extends Controller { this.calendar.render(); } - updateTimeEntry(timeEntryId:string, spentOn:string, startTime:string | null, hours:number, revertFunction:() => void) { - const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; - fetch( - this.pathHelper.timeEntryUpdate(timeEntryId), - { - method: 'PATCH', - headers: { - 'Content-Type': 'application/json', - 'X-CSRF-Token': csrfToken, - }, - body: JSON.stringify({ - time_entry: { - spent_on: spentOn, - start_time: startTime, - hours, - }, - }), + updateTimeEntry( + timeEntryId:string, + spentOn:string, + startTime:string | null, + hours:number, + revertFunction:() => void, + ) { + const csrfToken = document.querySelector('meta[name="csrf-token"]') + ?.content || ''; + fetch(this.pathHelper.timeEntryUpdate(timeEntryId), { + method: 'PATCH', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrfToken, }, - ).then((response) => { - if (response.ok) { - void response.text().then((html) => { - renderStreamMessage(html); - }); - } else if (revertFunction) { revertFunction(); } - }).catch(() => { - if (revertFunction) { revertFunction(); } - }); + body: JSON.stringify({ + time_entry: { + spent_on: spentOn, + start_time: startTime, + hours, + }, + }), + }) + .then((response) => { + if (response.ok) { + void response.text().then((html) => { + renderStreamMessage(html); + }); + } else if (revertFunction) { + revertFunction(); + } + }) + .catch(() => { + if (revertFunction) { + revertFunction(); + } + }); } displayDuration(duration:number):string { diff --git a/modules/costs/app/views/my/time_tracking/_calendar.html.erb b/modules/costs/app/views/my/time_tracking/_calendar.html.erb index e24f9f6666b..05b9ef528f7 100644 --- a/modules/costs/app/views/my/time_tracking/_calendar.html.erb +++ b/modules/costs/app/views/my/time_tracking/_calendar.html.erb @@ -5,7 +5,8 @@ data-my--time-tracking-time-entries-value="<%= time_entries_json %>" data-my--time-tracking-initial-date-value="<%= current_day %>" data-my--time-tracking-can-create-value="<%= User.current.allowed_in_any_project?(:log_own_time) %>" - data-my--time-tracking-can-edit-value="<%= User.current.allowed_in_any_project?(:edit_own_time_entries) %>"> + data-my--time-tracking-can-edit-value="<%= User.current.allowed_in_any_project?(:edit_own_time_entries) %>" + data-my--time-tracking-force-times-value="<%= TimeEntry.must_track_start_and_end_time? %>"> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_text do "#{current_day.beginning_of_week.day}-#{current_day.end_of_week.day}. #{current_day.strftime('%B %Y')}" From 7b9357fd39edb91ba9f40a87756b2f8ea42ecab9 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 27 Mar 2025 11:14:40 +0100 Subject: [PATCH 29/88] remove custom rendering for event from a component, will do in JS --- lib/full_calendar/event.rb | 23 ++----- modules/costs/app/components/_index.sass | 1 - .../time_entry_event_component.html.erb | 14 ---- .../time_entry_event_component.rb | 35 ---------- .../time_entry_event_component.sass | 66 ------------------- .../lib/full_calendar/time_entry_event.rb | 10 +-- 6 files changed, 10 insertions(+), 139 deletions(-) delete mode 100644 modules/costs/app/components/time_tracking/time_entry_event_component.html.erb delete mode 100644 modules/costs/app/components/time_tracking/time_entry_event_component.rb delete mode 100644 modules/costs/app/components/time_tracking/time_entry_event_component.sass diff --git a/lib/full_calendar/event.rb b/lib/full_calendar/event.rb index 1d56c563109..ee1c5e37ab9 100644 --- a/lib/full_calendar/event.rb +++ b/lib/full_calendar/event.rb @@ -42,6 +42,11 @@ module FullCalendar attribute :url, :string attribute :class_names, array: true, default: [] + # override in subclasses to add more fields to the JSON + def additional_attributes + {} + end + def as_json { "id" => id, @@ -51,26 +56,12 @@ module FullCalendar "end" => ends_at, "title" => title, "url" => url, - "classNames" => class_names, - "customEventView" => rendered_event_content - }.compact_blank.as_json + "classNames" => class_names + }.merge(additional_attributes).compact_blank.as_json end def to_json(*) as_json.to_json(*) end - - def event_content_view_component - # override in subclasses - nil - end - - private - - def rendered_event_content - if event_content_view_component - ApplicationController.render(event_content_view_component, layout: false) - end - end end end diff --git a/modules/costs/app/components/_index.sass b/modules/costs/app/components/_index.sass index baf9ffc4943..c2d6cebea24 100644 --- a/modules/costs/app/components/_index.sass +++ b/modules/costs/app/components/_index.sass @@ -1,2 +1 @@ @import "time_entries/entry_dialog_component" -@import "time_tracking/time_entry_event_component" diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb b/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb deleted file mode 100644 index 4879bc8cd2b..00000000000 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.html.erb +++ /dev/null @@ -1,14 +0,0 @@ -
- -
<%= time_entry.work_package.subject %>
-
<%= time_entry.project.name %>
-
diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.rb b/modules/costs/app/components/time_tracking/time_entry_event_component.rb deleted file mode 100644 index fa5b74bfdf2..00000000000 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -# -- copyright -# OpenProject is an open source project management software. -# Copyright (C) the OpenProject GmbH -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License version 3. -# -# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: -# Copyright (C) 2006-2013 Jean-Philippe Lang -# Copyright (C) 2010-2013 the ChiliProject Team -# -# This program is free software; you can redistribute it and/or -# modify it under the terms of the GNU General Public License -# as published by the Free Software Foundation; either version 2 -# of the License, or (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program; if not, write to the Free Software -# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. -# -# See COPYRIGHT and LICENSE files for more details. -# ++ - -module TimeTracking - class TimeEntryEventComponent < ApplicationComponent - options :time_entry - end -end diff --git a/modules/costs/app/components/time_tracking/time_entry_event_component.sass b/modules/costs/app/components/time_tracking/time_entry_event_component.sass deleted file mode 100644 index 0ee491c93ef..00000000000 --- a/modules/costs/app/components/time_tracking/time_entry_event_component.sass +++ /dev/null @@ -1,66 +0,0 @@ -@import 'helpers' -@import '../../app/spot/styles/sass/variables' - -.calendar-time-entry-event - display: flex - user-select: none - // Leave space for the shadow to be displayed - width: calc(100% - 4px) - border: 1px solid var(--borderColor-default) - border-radius: 5px - padding: 10px - position: relative - box-shadow: 1px 1px 3px 0px var(--borderColor-default) - background: var(--body-background) - color: var(--body-font-color) - font-size: var(--body-font-size) - - &:hover - .calendar-time-entry-event--inline-buttons - opacity: 1 - z-index: 2 - box-shadow: -4px -4px 10px 4px var(--body-background) - - &--content - display: grid - grid-template-columns: max-content max-content auto auto 1fr - grid-template-rows: auto 1fr max-content auto - grid-row-gap: 5px - grid-column-gap: 5px - grid-template-areas: "dates" "subject" "project" - overflow: hidden - flex-grow: 1 - font-size: var(--body-font-size) - line-height: 16px - - &-project-name - grid-area: project - font-style: italic - color: var(--fgColor-muted) - font-size: 12px - @include text-shortener - - &-type - grid-area: type - margin-right: 4px - - &-subject-line - grid-area: subject - @include text-shortener(false) - - &-dates - grid-area: dates - place-self: center end - white-space: nowrap - color: var(--fgColor-muted) - font-size: 12px - color: var(--fgColor-muted) - margin: 0 8px - - &--highlighting - width: 100% - height: 4px - position: absolute - top: 0 - left: 0 - border-radius: 5px 5px 0 0 diff --git a/modules/costs/lib/full_calendar/time_entry_event.rb b/modules/costs/lib/full_calendar/time_entry_event.rb index ba404b22e61..72bac191a54 100644 --- a/modules/costs/lib/full_calendar/time_entry_event.rb +++ b/modules/costs/lib/full_calendar/time_entry_event.rb @@ -47,19 +47,15 @@ module FullCalendar end end - def as_json(*) - super.merge( + def additional_attributes + { hours: time_entry.hours, statusId: time_entry.work_package.status_id, workPackageId: time_entry.work_package.id, workPackageSubject: time_entry.work_package.subject, projectId: time_entry.project.id, projectName: time_entry.project.name - ) - end - - def event_content_view_component - TimeTracking::TimeEntryEventComponent.new(time_entry: time_entry) + } end end end From 3cf42a9399afe442e5ccd5cbc227f975169ad874 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 27 Mar 2025 11:45:44 +0100 Subject: [PATCH 30/88] re add a style for the calendar --- modules/costs/app/components/_index.sass | 1 + .../costs/app/components/my/time_tracking/calendar.sass | 8 ++++++++ 2 files changed, 9 insertions(+) create mode 100644 modules/costs/app/components/my/time_tracking/calendar.sass diff --git a/modules/costs/app/components/_index.sass b/modules/costs/app/components/_index.sass index c2d6cebea24..9e62662f576 100644 --- a/modules/costs/app/components/_index.sass +++ b/modules/costs/app/components/_index.sass @@ -1 +1,2 @@ @import "time_entries/entry_dialog_component" +@import "my/time_tracking/calendar" diff --git a/modules/costs/app/components/my/time_tracking/calendar.sass b/modules/costs/app/components/my/time_tracking/calendar.sass new file mode 100644 index 00000000000..aa0392402be --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/calendar.sass @@ -0,0 +1,8 @@ +.calendar-time-entry-event + display: flex + user-select: none + border: 1px solid var(--borderColor-default) + border-radius: 5px + background: var(--body-background) + color: var(--body-font-color) + font-size: var(--body-font-size) From 8ab02a3535a5bf4de30f64dd8e9297b30fc2c332 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 27 Mar 2025 15:40:13 +0100 Subject: [PATCH 31/88] move calendar into a component --- .../time_tracking/calendar_component.html.erb | 25 ++++ .../my/time_tracking/calendar_component.rb | 115 ++++++++++++++++++ .../my/time_tracking_controller.rb | 6 - .../views/my/time_tracking/_calendar.html.erb | 52 -------- .../app/views/my/time_tracking/day.html.erb | 2 +- .../app/views/my/time_tracking/month.html.erb | 2 +- .../app/views/my/time_tracking/week.html.erb | 2 +- 7 files changed, 143 insertions(+), 61 deletions(-) create mode 100644 modules/costs/app/components/my/time_tracking/calendar_component.html.erb create mode 100644 modules/costs/app/components/my/time_tracking/calendar_component.rb delete mode 100644 modules/costs/app/views/my/time_tracking/_calendar.html.erb diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb new file mode 100644 index 00000000000..2f77e251365 --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -0,0 +1,25 @@ +<%= component_wrapper(tag: "div", data: wrapper_data) do %> + <%= render(Primer::OpenProject::SubHeader.new) do |component| + component.with_text { title } + + component.with_action_component do + render(Primer::Beta::ButtonGroup.new) do |group| + group.with_button(icon: "arrow-left", tag: :a, **previous_attrs) + group.with_button(icon: "arrow-right", tag: :a, **next_attrs) + end + end + + component.with_action_button(tag: :a, href: today_href) { "Today" } + + component.with_action_button( + scheme: :primary, + data: { + "turbo-stream" => true + }, + tag: :a, + href: dialog_time_entries_path(onlyMe: true) + ) { "Log Time" } + end %> + +
+<%- end %> diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb new file mode 100644 index 00000000000..c0a47b806af --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -0,0 +1,115 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module My + module TimeTracking + class CalendarComponent < ApplicationComponent + include OpTurbo::Streamable + + options time_entries: [], + mode: :week, + date: Date.current + + private + + def wrapper_data + { + "controller" => "my--time-tracking", + "application-target" => "dynamic", + "my--time-tracking-mode-value" => mode, + "my--time-tracking-time-entries-value" => time_entries_json, + "my--time-tracking-initial-date-value" => date.iso8601, + "my--time-tracking-can-create-value" => User.current.allowed_in_any_project?(:log_own_time), + "my--time-tracking-can-edit-value" => User.current.allowed_in_any_project?(:edit_own_time_entries), + "my--time-tracking-force-times-value" => TimeEntry.must_track_start_and_end_time? + } + end + + def time_entries_json + time_entries.map do |time_entry| + FullCalendar::TimeEntryEvent.from_time_entry(time_entry) + end.to_json + end + + def today_href + case mode + when :day + my_time_tracking_day_path(date: Date.current) + when :week + my_time_tracking_week_path(date: Date.current) + when :month + my_time_tracking_month_path(date: Date.current) + end + end + + def title # rubocop:disable Metrics/AbcSize + case mode + when :day + I18n.l(date, format: :long) + when :week + bow = date.beginning_of_week + eow = date.end_of_week + + if bow.year == eow.year && bow.month == eow.month + "#{bow.strftime('%d.')} - #{eow.strftime('%d. %B %Y')}" + elsif bow.year == eow.year + "#{bow.strftime('%d. %B')} - #{eow.strftime('%d. %B %Y')}" + else + "#{bow.strftime('%d. %B %Y')} - #{eow.strftime('%d. %B %Y')}" + end + when :month + date.strftime("%B %Y") + end + end + + def previous_attrs + case mode + when :day + { href: my_time_tracking_day_path(date: date - 1.day), aria: { label: I18n.t(:label_previous_day) } } + when :week + { href: my_time_tracking_week_path(date: date - 1.week), aria: { label: I18n.t(:label_previous_week) } } + when :month + { href: my_time_tracking_month_path(date: date - 1.month), aria: { label: I18n.t(:label_previous_month) } } + end + end + + def next_attrs + case mode + when :day + { href: my_time_tracking_day_path(date: date + 1.day), aria: { label: I18n.t(:label_next_day) } } + when :week + { href: my_time_tracking_week_path(date: date + 1.week), aria: { label: I18n.t(:label_next_week) } } + when :month + { href: my_time_tracking_month_path(date: date + 1.month), aria: { label: I18n.t(:label_next_month) } } + end + end + end + end +end diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index dc8d4697d53..b736f134092 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -96,12 +96,6 @@ module My private - def time_entries_json - @time_entries.map do |time_entry| - FullCalendar::TimeEntryEvent.from_time_entry(time_entry) - end.to_json - end - def current_day return @current_day if defined?(@current_day) diff --git a/modules/costs/app/views/my/time_tracking/_calendar.html.erb b/modules/costs/app/views/my/time_tracking/_calendar.html.erb deleted file mode 100644 index 05b9ef528f7..00000000000 --- a/modules/costs/app/views/my/time_tracking/_calendar.html.erb +++ /dev/null @@ -1,52 +0,0 @@ -
- <%= render(Primer::OpenProject::SubHeader.new) do |component| - component.with_text do - "#{current_day.beginning_of_week.day}-#{current_day.end_of_week.day}. #{current_day.strftime('%B %Y')}" - end - - component.with_action_component do - render(Primer::Beta::ButtonGroup.new) do |group| - group.with_button( - icon: "arrow-left", - aria: { - label: "Previous week" - }, - tag: :a, - href: my_time_tracking_week_path(date: current_day - 1.week) - ) - group.with_button( - icon: "arrow-right", - aria: { - label: "Next week" - }, - tag: :a, - href: my_time_tracking_week_path(date: current_day + 1.week) - ) - end - end - - component.with_action_button( - tag: :a, - href: my_time_tracking_week_path(date: Time.zone.today) - ) { "Today" } - - component.with_action_button( - scheme: :primary, - data: { - "turbo-stream" => true - }, - tag: :a, - href: dialog_time_entries_path(onlyMe: true) - ) { "Log Time" } - end %> - -
-
diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb index bb66833433c..5a12f3e2a20 100644 --- a/modules/costs/app/views/my/time_tracking/day.html.erb +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -10,4 +10,4 @@ end %> -<%= render(partial: "calendar", locals: { mode: "day" }) %> +<%= render(My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: :day, date: current_day)) %> diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb index 09c750b46fd..cc81cb00474 100644 --- a/modules/costs/app/views/my/time_tracking/month.html.erb +++ b/modules/costs/app/views/my/time_tracking/month.html.erb @@ -10,4 +10,4 @@ end %> -<%= render(partial: "calendar", locals: { mode: "month" }) %> +<%= render(My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: :month, date: current_day)) %> diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb index 3ed0044c3dd..06b96d87ea0 100644 --- a/modules/costs/app/views/my/time_tracking/week.html.erb +++ b/modules/costs/app/views/my/time_tracking/week.html.erb @@ -28,4 +28,4 @@ end %> -<%= render(partial: "calendar", locals: { mode: "week" }) %> +<%= render(My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: :week, date: current_day)) %> From 5acefc9e9279397dcd2e44d3d6e5092ceb6e9357 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 27 Mar 2025 16:09:34 +0100 Subject: [PATCH 32/88] Start implementing table view --- .../my/time_tracking/table_component.html.erb | 8 ++ .../my/time_tracking/table_component.rb | 96 +++++++++++++++++++ .../my/time_tracking_controller.rb | 11 ++- .../app/views/my/time_tracking/day.html.erb | 2 +- .../app/views/my/time_tracking/month.html.erb | 2 +- .../app/views/my/time_tracking/week.html.erb | 17 ++-- modules/costs/config/routes.rb | 12 ++- 7 files changed, 136 insertions(+), 12 deletions(-) create mode 100644 modules/costs/app/components/my/time_tracking/table_component.html.erb create mode 100644 modules/costs/app/components/my/time_tracking/table_component.rb diff --git a/modules/costs/app/components/my/time_tracking/table_component.html.erb b/modules/costs/app/components/my/time_tracking/table_component.html.erb new file mode 100644 index 00000000000..1a5757b9205 --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/table_component.html.erb @@ -0,0 +1,8 @@ +

We are a table component

+

<%= title %>

+ +
    + <%- time_entries.each do |te| %> +
  • <%= te.spent_on %> <%= te.hours %>h - <%= te.work_package.subject %>
  • + <%- end %> +
diff --git a/modules/costs/app/components/my/time_tracking/table_component.rb b/modules/costs/app/components/my/time_tracking/table_component.rb new file mode 100644 index 00000000000..adf3f3e80a3 --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/table_component.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module My + module TimeTracking + class TableComponent < ApplicationComponent + include OpTurbo::Streamable + + options time_entries: [], + mode: :week, + date: Date.current + + private + + def today_href + case mode + when :day + my_time_tracking_day_path(date: Date.current) + when :week + my_time_tracking_week_path(date: Date.current) + when :month + my_time_tracking_month_path(date: Date.current) + end + end + + def title # rubocop:disable Metrics/AbcSize + case mode + when :day + I18n.l(date, format: :long) + when :week + bow = date.beginning_of_week + eow = date.end_of_week + + if bow.year == eow.year && bow.month == eow.month + "#{bow.strftime('%d.')} - #{eow.strftime('%d. %B %Y')}" + elsif bow.year == eow.year + "#{bow.strftime('%d. %B')} - #{eow.strftime('%d. %B %Y')}" + else + "#{bow.strftime('%d. %B %Y')} - #{eow.strftime('%d. %B %Y')}" + end + when :month + date.strftime("%B %Y") + end + end + + def previous_attrs + case mode + when :day + { href: my_time_tracking_day_path(date: date - 1.day), aria: { label: I18n.t(:label_previous_day) } } + when :week + { href: my_time_tracking_week_path(date: date - 1.week), aria: { label: I18n.t(:label_previous_week) } } + when :month + { href: my_time_tracking_month_path(date: date - 1.month), aria: { label: I18n.t(:label_previous_month) } } + end + end + + def next_attrs + case mode + when :day + { href: my_time_tracking_day_path(date: date + 1.day), aria: { label: I18n.t(:label_next_day) } } + when :week + { href: my_time_tracking_week_path(date: date + 1.week), aria: { label: I18n.t(:label_next_week) } } + when :month + { href: my_time_tracking_month_path(date: date + 1.month), aria: { label: I18n.t(:label_next_month) } } + end + end + end + end +end diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index b736f134092..7c2f1594d9a 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -48,7 +48,7 @@ module My layout "global" - helper_method :current_day, :today?, :this_week?, :this_month?, :time_entries_json + helper_method :current_day, :today?, :this_week?, :this_month?, :list_view_component def calendar if browser.device.mobile? @@ -122,6 +122,15 @@ module My @time_entries = TimeEntry .includes(:project, :activity, { work_package: :status }) .where(user: User.current, spent_on: time_scope) + .order(:spent_on, :start_time, :hours) + end + + def list_view_component + if params[:view_mode] == "list" + My::TimeTracking::TableComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) + else + My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) + end end end end diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb index 5a12f3e2a20..141f7b39ece 100644 --- a/modules/costs/app/views/my/time_tracking/day.html.erb +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -10,4 +10,4 @@ end %> -<%= render(My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: :day, date: current_day)) %> +<%= render(list_view_component) %> diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb index cc81cb00474..6f55ca5bce5 100644 --- a/modules/costs/app/views/my/time_tracking/month.html.erb +++ b/modules/costs/app/views/my/time_tracking/month.html.erb @@ -10,4 +10,4 @@ end %> -<%= render(My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: :month, date: current_day)) %> +<%= render(list_view_component) %> diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb index 06b96d87ea0..4aea1d1b5fa 100644 --- a/modules/costs/app/views/my/time_tracking/week.html.erb +++ b/modules/costs/app/views/my/time_tracking/week.html.erb @@ -8,24 +8,29 @@ ] ) - btn = lambda do |button| + cal_button = lambda do |button| button.with_leading_visual_icon(icon: :calendar) button.with_trailing_action_icon(icon: :"triangle-down") "Calendar" end - header.with_action_menu(menu_arguments: { anchor_align: :end }, button_arguments: { button_block: btn }) do |menu| - menu.with_item(label: "Calendar") do |item| + list_button = lambda do |button| + button.with_leading_visual_icon(icon: "list-unordered") + button.with_trailing_action_icon(icon: :"triangle-down") + "List" + end + + header.with_action_menu(menu_arguments: { anchor_align: :end }, button_arguments: { button_block: params[:view_mode] == "list" ? list_button : cal_button }) do |menu| + menu.with_item(label: "Calendar", tag: :a, href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "calendar"))) do |item| item.with_leading_visual_icon(icon: :calendar) end - menu.with_item(label: "List") do |item| + menu.with_item(label: "List", tag: :a, href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "list"))) do |item| item.with_leading_visual_icon(icon: "list-unordered") end end header.with_action_zen_mode_button - header.with_action_icon_button(icon: "ellipsis", mobile_icon: "ellipsis", label: "more") end %> -<%= render(My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: :week, date: current_day)) %> +<%= render(list_view_component) %> diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index 92d16421f7f..7a9e24d2ecb 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -51,9 +51,15 @@ Rails.application.routes.draw do get "/timer" => "timer#show", as: "timers" get "/time-tracking" => "time_tracking#calendar" - get "/time-tracking/day" => "time_tracking#day" - get "/time-tracking/week" => "time_tracking#week" - get "/time-tracking/month" => "time_tracking#month" + get "/time-tracking/day(-:view_mode)" => "time_tracking#day", + defaults: { view_mode: "calendar" }, + as: :time_tracking_day + get "/time-tracking/week(-:view_mode)" => "time_tracking#week", + defaults: { view_mode: "calendar" }, + as: :time_tracking_week + get "/time-tracking/month(-view_mode)" => "time_tracking#month", + defaults: { view_mode: "calendar" }, + as: :time_tracking_month end scope "projects/:project_id", as: "project", module: "projects" do From 4a98bcaaffa5b3af01493145962bf8ca9466817b Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 31 Mar 2025 12:06:17 +0200 Subject: [PATCH 33/88] show totals for the time range under the calendar --- lib/chronic_duration.rb | 7 ++++++- .../my/time_tracking/calendar_component.html.erb | 2 ++ .../my/time_tracking/calendar_component.rb | 7 +++++++ modules/costs/config/locales/en.yml | 5 +++++ spec/lib/chronic_duration_spec.rb | 13 ++++++++++++- 5 files changed, 32 insertions(+), 2 deletions(-) diff --git a/lib/chronic_duration.rb b/lib/chronic_duration.rb index 94500124811..5e97dc49de6 100644 --- a/lib/chronic_duration.rb +++ b/lib/chronic_duration.rb @@ -102,7 +102,7 @@ module ChronicDuration month = days_per_month * day year = SECONDS_PER_YEAR - if opts[:format] == :hours_only + if %i[hours_only hours_and_minutes].include?(opts[:format]) hours = seconds / 3600.0 seconds = 0 elsif seconds >= SECONDS_PER_YEAR && seconds % year < seconds % month @@ -184,6 +184,11 @@ module ChronicDuration hours = hours.round(2) hours_int = hours.to_i hours = hours_int if hours - hours_int == 0 # if hours end with .0 + when :hours_and_minutes + dividers = { hours: "h", minutes: "m", keep_zero: false } + + minutes = ((hours % 1) * 60).round + hours = hours.floor when :chrono dividers = { years: ":", months: ":", weeks: ":", days: ":", hours: ":", minutes: ":", seconds: ":", keep_zero: true diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index 2f77e251365..fd8861bdbae 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -22,4 +22,6 @@ end %>
+ +
<%= total_hours %>
<%- end %> diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb index c0a47b806af..489bb5c706c 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.rb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -110,6 +110,13 @@ module My { href: my_time_tracking_month_path(date: date + 1.month), aria: { label: I18n.t(:label_next_month) } } end end + + def total_hours + total_hours = time_entries.sum(&:hours).round(2) + total_str = DurationConverter.output(total_hours, format: :hours_and_minutes) + + I18n.t(mode, scope: "total_times", hours: total_str) + end end end end diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index f1750245023..d9908438e7b 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -204,6 +204,11 @@ en: week: "week" + total_times: + week: "Week Total: %{hours}" + month: "Month Total: %{hours}" + day: "Day Total: %{hours}" + api_v3: errors: validation: diff --git a/spec/lib/chronic_duration_spec.rb b/spec/lib/chronic_duration_spec.rb index b47039765c5..a1f5f9d75ce 100644 --- a/spec/lib/chronic_duration_spec.rb +++ b/spec/lib/chronic_duration_spec.rb @@ -31,7 +31,7 @@ require "rspec_helper" require "chronic_duration" -INACCURATE_FORMATS = %i[days_and_hours hours_only].freeze +INACCURATE_FORMATS = %i[days_and_hours hours_only hours_and_minutes].freeze RSpec.describe ChronicDuration do describe ".parse" do @@ -151,6 +151,7 @@ RSpec.describe ChronicDuration do long: "1 minute 20 seconds", days_and_hours: "0.02h", hours_only: "0.02h", + hours_and_minutes: "1m", chrono: "1:20" }, (60 + 20.51) => @@ -161,6 +162,7 @@ RSpec.describe ChronicDuration do long: "1 minute 20.51 seconds", days_and_hours: "0.02h", hours_only: "0.02h", + hours_and_minutes: "1m", chrono: "1:20.51" }, (60 + 20.51928) => @@ -171,6 +173,7 @@ RSpec.describe ChronicDuration do long: "1 minute 20.51928 seconds", days_and_hours: "0.02h", hours_only: "0.02h", + hours_and_minutes: "1m", chrono: "1:20.51928" }, ((4 * 3600) + 60 + 1) => @@ -181,6 +184,7 @@ RSpec.describe ChronicDuration do long: "4 hours 1 minute 1 second", days_and_hours: "4.02h", hours_only: "4.02h", + hours_and_minutes: "4h 1m", chrono: "4:01:01" }, ((2 * 3600) + (20 * 60)) => @@ -191,6 +195,7 @@ RSpec.describe ChronicDuration do long: "2 hours 20 minutes", days_and_hours: "2.33h", hours_only: "2.33h", + hours_and_minutes: "2h 20m", chrono: "2:20:00" }, ((8 * 24 * 3600) + (3 * 3600) + (30 * 60)) => @@ -201,6 +206,7 @@ RSpec.describe ChronicDuration do long: "8 days 3 hours 30 minutes", days_and_hours: "8d 3.5h", hours_only: "195.5h", + hours_and_minutes: "195h 30m", chrono: "8:03:30:00" }, ((6 * 30 * 24 * 3600) + (24 * 3600)) => @@ -211,6 +217,7 @@ RSpec.describe ChronicDuration do long: "6 months 1 day", days_and_hours: "181d 0h", hours_only: "4344h", + hours_and_minutes: "4344h", chrono: "6:01:00:00:00" # Yuck. FIXME }, ((365.25 * 24 * 3600) + (24 * 3600)).to_i => @@ -221,6 +228,7 @@ RSpec.describe ChronicDuration do long: "1 year 1 day", days_and_hours: "366d 0h", hours_only: "8790h", + hours_and_minutes: "8790h", chrono: "1:00:01:00:00:00" }, ((3 * 365.25 * 24 * 3600) + (24 * 3600)).to_i => @@ -231,6 +239,7 @@ RSpec.describe ChronicDuration do long: "3 years 1 day", days_and_hours: "1096d 0h", hours_only: "26322h", + hours_and_minutes: "26322h", chrono: "3:00:01:00:00:00" }, ((6 * 365.25 * 24 * 3600) + (3 * 3600)).to_i => @@ -241,6 +250,7 @@ RSpec.describe ChronicDuration do long: "6 years 3 hours", days_and_hours: "2191d 3h", hours_only: "52599h", + hours_and_minutes: "52599h", chrono: "6:00:00:03:00:00" }, (3600 * 24 * 30 * 18) => @@ -251,6 +261,7 @@ RSpec.describe ChronicDuration do long: "18 months", days_and_hours: "540d 0h", hours_only: "12960h", + hours_and_minutes: "12960h", chrono: "18:00:00:00:00" } } From 4ff9d35a2f697f9906eff737d7a3ee7ddfe62af2 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 31 Mar 2025 15:46:57 +0200 Subject: [PATCH 34/88] do not globally disable rubocop rules --- lib/chronic_duration.rb | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/lib/chronic_duration.rb b/lib/chronic_duration.rb index 5e97dc49de6..f3a03d6ef8a 100644 --- a/lib/chronic_duration.rb +++ b/lib/chronic_duration.rb @@ -28,8 +28,6 @@ # Changes to this file should be kept in sync with # frontend/src/app/shared/helpers/chronic_duration.js. -# rubocop:disable Metrics/AbcSize -# rubocop:disable Metrics/PerceivedComplexity module ChronicDuration extend self @@ -81,7 +79,7 @@ module ChronicDuration # Given an integer and an optional format, # returns a formatted string representing elapsed time # rubocop:disable Lint/UselessAssignment - def output(seconds, opts = {}) + def output(seconds, opts = {}) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity int = seconds.to_i seconds = int if seconds - int == 0 # if seconds end with .0 @@ -286,7 +284,7 @@ module ChronicDuration end # Parse 3:41:59 and return 3 hours 41 minutes 59 seconds - def filter_by_type(string) + def filter_by_type(string) # rubocop:disable Metrics/AbcSize chrono_units_list = duration_units_list.reject { |v| v == "weeks" } if string.delete(" ")&.match?(time_matcher) @@ -313,7 +311,7 @@ module ChronicDuration # Get rid of unknown words and map found # words to defined time units - def filter_through_white_list(string, opts) + def filter_through_white_list(string, opts) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity res = [] string.split.each do |word| if word&.match?(float_matcher) @@ -374,5 +372,3 @@ module ChronicDuration %w[and with plus] end end -# rubocop:enable Metrics/AbcSize -# rubocop:enable Metrics/PerceivedComplexity From 39e62f7d8afdf9c1f71183d1df4e371d4519cfe5 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 31 Mar 2025 16:24:24 +0200 Subject: [PATCH 35/88] Add navigation etc to table --- .../my/time_tracking/calendar_component.rb | 54 +---------- .../my/time_tracking/component_navigation.rb | 95 +++++++++++++++++++ .../my/time_tracking/table_component.html.erb | 40 ++++++-- .../my/time_tracking/table_component.rb | 55 +++-------- modules/costs/config/routes.rb | 2 +- 5 files changed, 142 insertions(+), 104 deletions(-) create mode 100644 modules/costs/app/components/my/time_tracking/component_navigation.rb diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb index 489bb5c706c..ec126f2ae92 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.rb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -32,6 +32,7 @@ module My module TimeTracking class CalendarComponent < ApplicationComponent include OpTurbo::Streamable + include ComponentNavigation options time_entries: [], mode: :week, @@ -58,59 +59,6 @@ module My end.to_json end - def today_href - case mode - when :day - my_time_tracking_day_path(date: Date.current) - when :week - my_time_tracking_week_path(date: Date.current) - when :month - my_time_tracking_month_path(date: Date.current) - end - end - - def title # rubocop:disable Metrics/AbcSize - case mode - when :day - I18n.l(date, format: :long) - when :week - bow = date.beginning_of_week - eow = date.end_of_week - - if bow.year == eow.year && bow.month == eow.month - "#{bow.strftime('%d.')} - #{eow.strftime('%d. %B %Y')}" - elsif bow.year == eow.year - "#{bow.strftime('%d. %B')} - #{eow.strftime('%d. %B %Y')}" - else - "#{bow.strftime('%d. %B %Y')} - #{eow.strftime('%d. %B %Y')}" - end - when :month - date.strftime("%B %Y") - end - end - - def previous_attrs - case mode - when :day - { href: my_time_tracking_day_path(date: date - 1.day), aria: { label: I18n.t(:label_previous_day) } } - when :week - { href: my_time_tracking_week_path(date: date - 1.week), aria: { label: I18n.t(:label_previous_week) } } - when :month - { href: my_time_tracking_month_path(date: date - 1.month), aria: { label: I18n.t(:label_previous_month) } } - end - end - - def next_attrs - case mode - when :day - { href: my_time_tracking_day_path(date: date + 1.day), aria: { label: I18n.t(:label_next_day) } } - when :week - { href: my_time_tracking_week_path(date: date + 1.week), aria: { label: I18n.t(:label_next_week) } } - when :month - { href: my_time_tracking_month_path(date: date + 1.month), aria: { label: I18n.t(:label_next_month) } } - end - end - def total_hours total_hours = time_entries.sum(&:hours).round(2) total_str = DurationConverter.output(total_hours, format: :hours_and_minutes) diff --git a/modules/costs/app/components/my/time_tracking/component_navigation.rb b/modules/costs/app/components/my/time_tracking/component_navigation.rb new file mode 100644 index 00000000000..175d1fdcb8c --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/component_navigation.rb @@ -0,0 +1,95 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ +# +module My + module TimeTracking + module ComponentNavigation + extend ActiveSupport::Concern + def title # rubocop:disable Metrics/AbcSize + case mode + when :day + I18n.l(date, format: :long) + when :week + bow = date.beginning_of_week + eow = date.end_of_week + + if bow.year == eow.year && bow.month == eow.month + "#{bow.strftime('%d.')} - #{eow.strftime('%d. %B %Y')}" + elsif bow.year == eow.year + "#{bow.strftime('%d. %B')} - #{eow.strftime('%d. %B %Y')}" + else + "#{bow.strftime('%d. %B %Y')} - #{eow.strftime('%d. %B %Y')}" + end + when :month + date.strftime("%B %Y") + end + end + + def today_href + case mode + when :day + my_time_tracking_day_path(date: Date.current, view_mode: params[:view_mode]) + when :week + my_time_tracking_week_path(date: Date.current, view_mode: params[:view_mode]) + when :month + my_time_tracking_month_path(date: Date.current, view_mode: params[:view_mode]) + end + end + + def previous_attrs # rubocop:disable Metrics/AbcSize + case mode + when :day + { href: my_time_tracking_day_path(date: date - 1.day, view_mode: params[:view_mode]), + aria: { label: I18n.t(:label_previous_day) } } + when :week + { href: my_time_tracking_week_path(date: date - 1.week, view_mode: params[:view_mode]), + aria: { label: I18n.t(:label_previous_week) } } + when :month + { href: my_time_tracking_month_path(date: date - 1.month, view_mode: params[:view_mode]), + aria: { label: I18n.t(:label_previous_month) } } + end + end + + def next_attrs # rubocop:disable Metrics/AbcSize + case mode + when :day + { href: my_time_tracking_day_path(date: date + 1.day, view_mode: params[:view_mode]), + aria: { label: I18n.t(:label_next_day) } } + when :week + { href: my_time_tracking_week_path(date: date + 1.week, view_mode: params[:view_mode]), + aria: { label: I18n.t(:label_next_week) } } + when :month + { href: my_time_tracking_month_path(date: date + 1.month, view_mode: params[:view_mode]), + aria: { label: I18n.t(:label_next_month) } } + end + end + end + end +end diff --git a/modules/costs/app/components/my/time_tracking/table_component.html.erb b/modules/costs/app/components/my/time_tracking/table_component.html.erb index 1a5757b9205..620d41e1554 100644 --- a/modules/costs/app/components/my/time_tracking/table_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/table_component.html.erb @@ -1,8 +1,36 @@ -

We are a table component

-

<%= title %>

+<%= component_wrapper(tag: "div") do %> + <%= render(Primer::OpenProject::SubHeader.new) do |component| + component.with_text { title } -
    - <%- time_entries.each do |te| %> -
  • <%= te.spent_on %> <%= te.hours %>h - <%= te.work_package.subject %>
  • + component.with_action_component do + render(Primer::Beta::ButtonGroup.new) do |group| + group.with_button(icon: "arrow-left", tag: :a, **previous_attrs) + group.with_button(icon: "arrow-right", tag: :a, **next_attrs) + end + end + + component.with_action_button(tag: :a, href: today_href) { "Today" } + + component.with_action_button( + scheme: :primary, + data: { + "turbo-stream" => true + }, + tag: :a, + href: dialog_time_entries_path(onlyMe: true) + ) { "Log Time" } + end %> + +
      + <%- range.each do |date| %> +
    • +

      <%= date %> (<%= time_entries_by_day[date].count %> entries, <%= total_hours_per_day(date) %>)

      +
        + <%- time_entries_by_day[date].each do |te| %> +
      • <%= te.spent_on %> <%= te.hours %>h - <%= te.work_package.subject %>
      • + <%- end %> +
      +
    • <%- end %> -
    +
+<%- end %> diff --git a/modules/costs/app/components/my/time_tracking/table_component.rb b/modules/costs/app/components/my/time_tracking/table_component.rb index adf3f3e80a3..4e5375789d2 100644 --- a/modules/costs/app/components/my/time_tracking/table_component.rb +++ b/modules/costs/app/components/my/time_tracking/table_component.rb @@ -32,6 +32,7 @@ module My module TimeTracking class TableComponent < ApplicationComponent include OpTurbo::Streamable + include ComponentNavigation options time_entries: [], mode: :week, @@ -39,57 +40,23 @@ module My private - def today_href + def range case mode - when :day - my_time_tracking_day_path(date: Date.current) - when :week - my_time_tracking_week_path(date: Date.current) - when :month - my_time_tracking_month_path(date: Date.current) + when :day then [date] + when :week then date.all_week + when :month then date.all_month end end - def title # rubocop:disable Metrics/AbcSize - case mode - when :day - I18n.l(date, format: :long) - when :week - bow = date.beginning_of_week - eow = date.end_of_week - - if bow.year == eow.year && bow.month == eow.month - "#{bow.strftime('%d.')} - #{eow.strftime('%d. %B %Y')}" - elsif bow.year == eow.year - "#{bow.strftime('%d. %B')} - #{eow.strftime('%d. %B %Y')}" - else - "#{bow.strftime('%d. %B %Y')} - #{eow.strftime('%d. %B %Y')}" - end - when :month - date.strftime("%B %Y") + def time_entries_by_day + @time_entries_by_day ||= time_entries.group_by(&:spent_on).tap do |hash| + hash.default_proc = ->(h, k) { h[k] = [] } end end - def previous_attrs - case mode - when :day - { href: my_time_tracking_day_path(date: date - 1.day), aria: { label: I18n.t(:label_previous_day) } } - when :week - { href: my_time_tracking_week_path(date: date - 1.week), aria: { label: I18n.t(:label_previous_week) } } - when :month - { href: my_time_tracking_month_path(date: date - 1.month), aria: { label: I18n.t(:label_previous_month) } } - end - end - - def next_attrs - case mode - when :day - { href: my_time_tracking_day_path(date: date + 1.day), aria: { label: I18n.t(:label_next_day) } } - when :week - { href: my_time_tracking_week_path(date: date + 1.week), aria: { label: I18n.t(:label_next_week) } } - when :month - { href: my_time_tracking_month_path(date: date + 1.month), aria: { label: I18n.t(:label_next_month) } } - end + def total_hours_per_day(date) + total_hours = time_entries_by_day[date].sum(&:hours).round(2) + DurationConverter.output(total_hours, format: :hours_and_minutes) end end end diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index 7a9e24d2ecb..08529386bbe 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -57,7 +57,7 @@ Rails.application.routes.draw do get "/time-tracking/week(-:view_mode)" => "time_tracking#week", defaults: { view_mode: "calendar" }, as: :time_tracking_week - get "/time-tracking/month(-view_mode)" => "time_tracking#month", + get "/time-tracking/month(-:view_mode)" => "time_tracking#month", defaults: { view_mode: "calendar" }, as: :time_tracking_month end From f3fcc730055254326c8c656d320082cee46cad69 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 1 Apr 2025 13:52:18 +0200 Subject: [PATCH 36/88] Implement table component --- .../my/time_tracking/daily_entries_table.rb | 60 +++++++++++++ .../my/time_tracking/table_component.html.erb | 14 +-- .../my/time_tracking/time_entry_row.rb | 87 +++++++++++++++++++ 3 files changed, 150 insertions(+), 11 deletions(-) create mode 100644 modules/costs/app/components/my/time_tracking/daily_entries_table.rb create mode 100644 modules/costs/app/components/my/time_tracking/time_entry_row.rb diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb new file mode 100644 index 00000000000..f2b9ded2c4e --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module My + module TimeTracking + class DailyEntriesTable < OpPrimer::BorderBoxTableComponent + columns :time, :hours, :subject, :project, :activity, :comments + + options :date, :time_entries + + def row_class + TimeEntryRow + end + + def mobile_title + TimeEntry.model_name.human(count: 2) + end + + def has_actions? = true + + def headers + [ + [:time, { caption: "Time" }], + [:hours, { caption: "Hours" }], + [:subject, { caption: "Subject" }], + [:project, { caption: "Project" }], + [:activity, { caption: "Activity" }], + [:comments, { caption: "Comments" }] + ] + end + end + end +end diff --git a/modules/costs/app/components/my/time_tracking/table_component.html.erb b/modules/costs/app/components/my/time_tracking/table_component.html.erb index 620d41e1554..cd7bb90c3fe 100644 --- a/modules/costs/app/components/my/time_tracking/table_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/table_component.html.erb @@ -21,16 +21,8 @@ ) { "Log Time" } end %> -
    <%- range.each do |date| %> -
  • -

    <%= date %> (<%= time_entries_by_day[date].count %> entries, <%= total_hours_per_day(date) %>)

    -
      - <%- time_entries_by_day[date].each do |te| %> -
    • <%= te.spent_on %> <%= te.hours %>h - <%= te.work_package.subject %>
    • - <%- end %> -
    -
  • - <%- end %> -
+

<%= date %>

+ <%= render(My::TimeTracking::DailyEntriesTable.new(rows: time_entries_by_day[date])) %> + <%- end %> <%- end %> diff --git a/modules/costs/app/components/my/time_tracking/time_entry_row.rb b/modules/costs/app/components/my/time_tracking/time_entry_row.rb new file mode 100644 index 00000000000..9097653131b --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/time_entry_row.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module My + module TimeTracking + class TimeEntryRow < OpPrimer::BorderBoxRowComponent + def button_links + [ + action_menu + ] + end + + def action_menu + render(Primer::Alpha::ActionMenu.new) do |menu| + menu.with_show_button(icon: "kebab-horizontal", "aria-label": t("label_more"), scheme: :invisible) + menu.with_item( + content_arguments: { + data: { + "turbo-stream" => true + } + }, + tag: :a, + label: t("label_edit"), + href: dialog_time_entry_path(time_entry, onlyMe: true) + ) do |item| + item.with_leading_visual_icon(icon: :pencil) + end + end + end + + def time + time_entry.start_timestamp || "" + end + + def hours + DurationConverter.output(time_entry.hours, format: :hours_and_minutes) + end + + def subject + "##{time_entry.work_package.id} - #{time_entry.work_package.subject}" + end + + def project + time_entry.project.name + end + + def activity + time_entry.activity&.name + end + + delegate :comments, to: :time_entry + + private + + def time_entry + model + end + end + end +end From 5acf95c16df75b173f040801527cb725d7c97d4b Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 1 Apr 2025 16:44:53 +0200 Subject: [PATCH 37/88] add reload to the table view when adding new time entry --- .../generic-dialog-close.controller.ts | 51 +++++++++++++++++++ .../dynamic/my/time-tracking.controller.ts | 34 ++++++------- .../my/time_tracking/table_component.html.erb | 4 +- .../my/time_tracking/table_component.rb | 7 +++ 4 files changed, 75 insertions(+), 21 deletions(-) create mode 100644 frontend/src/stimulus/controllers/dynamic/generic-dialog-close.controller.ts diff --git a/frontend/src/stimulus/controllers/dynamic/generic-dialog-close.controller.ts b/frontend/src/stimulus/controllers/dynamic/generic-dialog-close.controller.ts new file mode 100644 index 00000000000..f8c9e08edb8 --- /dev/null +++ b/frontend/src/stimulus/controllers/dynamic/generic-dialog-close.controller.ts @@ -0,0 +1,51 @@ +/* + * -- copyright + * OpenProject is an open source project management software. + * Copyright (C) the OpenProject GmbH + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License version 3. + * + * OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: + * Copyright (C) 2006-2013 Jean-Philippe Lang + * Copyright (C) 2010-2013 the ChiliProject Team + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * See COPYRIGHT and LICENSE files for more details. + * ++ + */ + +import { Controller } from '@hotwired/stimulus'; + +export default class GenericDialogCloseController extends Controller { + // TODO: Add something that we can only refresh a certain turbo frame, etc + connect():void { + // handle dialog close event + document.addEventListener('dialog:close', this.dialogCloseListener); + } + + disconnect():void { + document.removeEventListener('dialog:close', this.dialogCloseListener); + } + + dialogCloseListener(this:void, event:CustomEvent):void { + const { detail: { dialog, submitted } } = event as { detail:{ dialog:HTMLDialogElement; submitted:boolean } }; + + if (dialog.id === 'time-entry-dialog' && submitted) { + window.location.reload(); + } + } +} diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 52004d6d3e6..972b4f49802 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -39,16 +39,7 @@ export default class MyTimeTrackingController extends Controller { this.pathHelper = context.services.pathHelperService; // handle dialog close event - document.addEventListener('dialog:close', (event:CustomEvent) => { - const { - detail: { dialog, submitted }, - } = event as { - detail:{ dialog:HTMLDialogElement; submitted:boolean }; - }; - if (dialog.id === 'time-entry-dialog' && submitted) { - window.location.reload(); - } - }); + document.addEventListener('dialog:close', this.dialogCloseListener); const DEFAULT_TIMED_EVENT_DURATION = '01:00'; @@ -194,15 +185,13 @@ export default class MyTimeTrackingController extends Controller { this.calendar.render(); } - updateTimeEntry( - timeEntryId:string, - spentOn:string, - startTime:string | null, - hours:number, - revertFunction:() => void, - ) { - const csrfToken = document.querySelector('meta[name="csrf-token"]') - ?.content || ''; + disconnect():void { + document.removeEventListener('dialog:close', this.dialogCloseListener); + } + + updateTimeEntry(timeEntryId:string, spentOn:string, startTime:string | null, hours:number, revertFunction:() => void) { + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; + fetch(this.pathHelper.timeEntryUpdate(timeEntryId), { method: 'PATCH', headers: { @@ -255,4 +244,11 @@ export default class MyTimeTrackingController extends Controller { return 'timeGridWeek'; } } + + dialogCloseListener(this:void, event:CustomEvent):void { + const { detail: { dialog, submitted } } = event as { detail:{ dialog:HTMLDialogElement; submitted:boolean }; }; + if (dialog.id === 'time-entry-dialog' && submitted) { + window.location.reload(); + } + } } diff --git a/modules/costs/app/components/my/time_tracking/table_component.html.erb b/modules/costs/app/components/my/time_tracking/table_component.html.erb index cd7bb90c3fe..1a3d8a8f6e7 100644 --- a/modules/costs/app/components/my/time_tracking/table_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/table_component.html.erb @@ -1,4 +1,4 @@ -<%= component_wrapper(tag: "div") do %> +<%= component_wrapper(tag: "div", data: wrapper_data) do %> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_text { title } @@ -22,7 +22,7 @@ end %> <%- range.each do |date| %> -

<%= date %>

+ <%= render(Primer::OpenProject::Heading.new(tag: :h2)) { date.strftime("%A %d") } %> <%= render(My::TimeTracking::DailyEntriesTable.new(rows: time_entries_by_day[date])) %> <%- end %> <%- end %> diff --git a/modules/costs/app/components/my/time_tracking/table_component.rb b/modules/costs/app/components/my/time_tracking/table_component.rb index 4e5375789d2..fa4056bd8a5 100644 --- a/modules/costs/app/components/my/time_tracking/table_component.rb +++ b/modules/costs/app/components/my/time_tracking/table_component.rb @@ -40,6 +40,13 @@ module My private + def wrapper_data + { + "controller" => "generic-dialog-close", + "application-target" => "dynamic" + } + end + def range case mode when :day then [date] From de377402374db04e989eae1a15480d2adc5daf24 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 2 Apr 2025 11:25:55 +0200 Subject: [PATCH 38/88] Allow adding content to the actions column in a BorderBoxTableComponent --- .../op_primer/border_box_table_component.html.erb | 6 +++--- app/components/op_primer/border_box_table_component.rb | 4 ++++ app/components/op_primer/border_box_table_component.sass | 5 +++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/app/components/op_primer/border_box_table_component.html.erb b/app/components/op_primer/border_box_table_component.html.erb index 9efdd2d5ec6..927e3f9d997 100644 --- a/app/components/op_primer/border_box_table_component.html.erb +++ b/app/components/op_primer/border_box_table_component.html.erb @@ -55,10 +55,10 @@ See COPYRIGHT and LICENSE files for more details. if has_actions? concat render( Primer::BaseComponent.new( - classes: heading_class, - tag: :div + classes: header_classes(:actions), + tag: :span ) - ) + ) { action_row_header_content } end end diff --git a/app/components/op_primer/border_box_table_component.rb b/app/components/op_primer/border_box_table_component.rb index 1e116079096..f8d447a54b8 100644 --- a/app/components/op_primer/border_box_table_component.rb +++ b/app/components/op_primer/border_box_table_component.rb @@ -138,6 +138,10 @@ module OpPrimer nil end + def action_row_header_content + nil + end + def footer raise ArgumentError, "Need to provide footer content" end diff --git a/app/components/op_primer/border_box_table_component.sass b/app/components/op_primer/border_box_table_component.sass index 34902845700..8d20b309126 100644 --- a/app/components/op_primer/border_box_table_component.sass +++ b/app/components/op_primer/border_box_table_component.sass @@ -23,6 +23,11 @@ &:not(:last-child) padding-right: 6px + &:last-child + justify-content: flex-end !important + display: flex !important + flex-direction: row !important + &--mobile-heading, &--row-label display: none From 1efcc0210ee2303721ad29fc1d9ac7e4b4ca68f3 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 2 Apr 2025 11:26:27 +0200 Subject: [PATCH 39/88] add the log time button --- .../my/time_tracking/daily_entries_table.rb | 16 ++++++++++++++-- .../my/time_tracking/table_component.html.erb | 6 +++--- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb index f2b9ded2c4e..b163492d5f6 100644 --- a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb +++ b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb @@ -33,8 +33,6 @@ module My class DailyEntriesTable < OpPrimer::BorderBoxTableComponent columns :time, :hours, :subject, :project, :activity, :comments - options :date, :time_entries - def row_class TimeEntryRow end @@ -45,6 +43,20 @@ module My def has_actions? = true + def action_row_header_content + render(Primer::Beta::IconButton.new( + icon: "plus", + scheme: :invisible, + size: :small, + tag: :a, + tooltip_direction: :e, + href: dialog_time_entries_path(onlyMe: true, date: options[:date]), + data: { "turbo-stream" => true }, + label: t("label_log_time"), + aria: { label: t("label_log_time") } + )) + end + def headers [ [:time, { caption: "Time" }], diff --git a/modules/costs/app/components/my/time_tracking/table_component.html.erb b/modules/costs/app/components/my/time_tracking/table_component.html.erb index 1a3d8a8f6e7..3bd72e72d76 100644 --- a/modules/costs/app/components/my/time_tracking/table_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/table_component.html.erb @@ -21,8 +21,8 @@ ) { "Log Time" } end %> - <%- range.each do |date| %> - <%= render(Primer::OpenProject::Heading.new(tag: :h2)) { date.strftime("%A %d") } %> - <%= render(My::TimeTracking::DailyEntriesTable.new(rows: time_entries_by_day[date])) %> + <%- range.each_with_index do |date, i| %> + <%= render(Primer::OpenProject::Heading.new(tag: :h2, classes: class_names("mt-3" => !i.zero?))) { date.strftime("%A %d") } %> + <%= render(My::TimeTracking::DailyEntriesTable.new(rows: time_entries_by_day[date], date: date)) %> <%- end %> <%- end %> From 7b6c28292b110fd1810e230aaa4054b297c5b1dd Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 2 Apr 2025 14:34:02 +0200 Subject: [PATCH 40/88] add method to allow skipping columns --- app/components/table_component.rb | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/app/components/table_component.rb b/app/components/table_component.rb index fd5d0ba28d0..e88c9db9593 100644 --- a/app/components/table_component.rb +++ b/app/components/table_component.rb @@ -31,8 +31,8 @@ ## # Abstract view component. Subclass this for a concrete table. class TableComponent < ApplicationComponent - def initialize(rows: [], **options) - super(rows, **options) + def initialize(rows: [], **) + super(rows, **) end class << self @@ -107,7 +107,7 @@ class TableComponent < ApplicationComponent end def sortable_columns_correlation - sortable_columns.to_h { [_1.to_s, _1.to_s] } + sortable_columns.to_h { [it.to_s, it.to_s] } .with_indifferent_access end @@ -153,20 +153,18 @@ class TableComponent < ApplicationComponent model end - def row_class - self.class.row_class - end + delegate :row_class, to: :class def container_class nil end def columns - self.class.columns + self.class.columns.reject { skip_column?(it) } end def sortable_columns - self.class.sortable_columns + self.class.sortable_columns.reject { skip_column?(it) } end def render_collection(rows) @@ -193,6 +191,10 @@ class TableComponent < ApplicationComponent true end + def skip_column?(_column) + false + end + def sortable_column?(column) sortable? && sortable_columns.include?(column.to_sym) end From 8b8e6c3f71ff2e41d043674c5222d639be1d819a Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 2 Apr 2025 15:53:57 +0200 Subject: [PATCH 41/88] show everything in the table like we have it in the figma --- .../my/time_tracking/daily_entries_table.rb | 12 ++++++++-- .../my/time_tracking/time_entry_row.rb | 23 +++++++++++++++---- 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb index b163492d5f6..427cf7ee09e 100644 --- a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb +++ b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb @@ -59,13 +59,21 @@ module My def headers [ - [:time, { caption: "Time" }], + TimeEntry.can_track_start_and_end_time? ? [:time, { caption: "Time" }] : nil, [:hours, { caption: "Hours" }], [:subject, { caption: "Subject" }], [:project, { caption: "Project" }], [:activity, { caption: "Activity" }], [:comments, { caption: "Comments" }] - ] + ].compact + end + + def skip_column?(column) + if column == :time + !TimeEntry.can_track_start_and_end_time? + else + false + end end end end diff --git a/modules/costs/app/components/my/time_tracking/time_entry_row.rb b/modules/costs/app/components/my/time_tracking/time_entry_row.rb index 9097653131b..4c4215b446d 100644 --- a/modules/costs/app/components/my/time_tracking/time_entry_row.rb +++ b/modules/costs/app/components/my/time_tracking/time_entry_row.rb @@ -55,8 +55,18 @@ module My end end - def time - time_entry.start_timestamp || "" + def time # rubocop:disable Metrics/AbcSize + return if time_entry.start_time.blank? + + times = [I18n.l(time_entry.start_timestamp, format: :time)] + + times << if time_entry.start_timestamp.to_date == time_entry.end_timestamp.to_date + I18n.l(time_entry.end_timestamp, format: :time) + else + I18n.l(time_entry.end_timestamp, format: :short) + end + + times.join(" - ") end def hours @@ -64,11 +74,16 @@ module My end def subject - "##{time_entry.work_package.id} - #{time_entry.work_package.subject}" + render(Primer::Beta::Link.new(href: project_work_package_path(time_entry.project, time_entry.work_package), + underline: false)) do + "##{time_entry.work_package.id}" + end + " - #{time_entry.work_package.subject}" end def project - time_entry.project.name + render(Primer::Beta::Link.new(href: project_path(time_entry.project), underline: false)) do + time_entry.project.name + end end def activity From 35f1a4d793e61a05a6dab0b73982d6fa6525d38c Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 2 Apr 2025 16:19:40 +0200 Subject: [PATCH 42/88] add main columns --- .../costs/app/components/my/time_tracking/daily_entries_table.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb index 427cf7ee09e..cdcdb70162c 100644 --- a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb +++ b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb @@ -32,6 +32,7 @@ module My module TimeTracking class DailyEntriesTable < OpPrimer::BorderBoxTableComponent columns :time, :hours, :subject, :project, :activity, :comments + main_column :time, :subject, :project def row_class TimeEntryRow From e024f07d580274152972257b19fed7734485ebf8 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 3 Apr 2025 16:12:28 +0200 Subject: [PATCH 43/88] show today/yesterday --- ..._component.html.erb => list_component.html.erb} | 4 +++- .../{table_component.rb => list_component.rb} | 14 +++++++++++++- .../app/controllers/my/time_tracking_controller.rb | 2 +- 3 files changed, 17 insertions(+), 3 deletions(-) rename modules/costs/app/components/my/time_tracking/{table_component.html.erb => list_component.html.erb} (91%) rename modules/costs/app/components/my/time_tracking/{table_component.rb => list_component.rb} (84%) diff --git a/modules/costs/app/components/my/time_tracking/table_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb similarity index 91% rename from modules/costs/app/components/my/time_tracking/table_component.html.erb rename to modules/costs/app/components/my/time_tracking/list_component.html.erb index 3bd72e72d76..13dd2c0c42c 100644 --- a/modules/costs/app/components/my/time_tracking/table_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -22,7 +22,9 @@ end %> <%- range.each_with_index do |date, i| %> - <%= render(Primer::OpenProject::Heading.new(tag: :h2, classes: class_names("mt-3" => !i.zero?))) { date.strftime("%A %d") } %> + <%= render(Primer::OpenProject::Heading.new(tag: :h2, classes: class_names("mt-3" => !i.zero?))) do + date_title(date) + end %> <%= render(My::TimeTracking::DailyEntriesTable.new(rows: time_entries_by_day[date], date: date)) %> <%- end %> <%- end %> diff --git a/modules/costs/app/components/my/time_tracking/table_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb similarity index 84% rename from modules/costs/app/components/my/time_tracking/table_component.rb rename to modules/costs/app/components/my/time_tracking/list_component.rb index fa4056bd8a5..701057a3cbc 100644 --- a/modules/costs/app/components/my/time_tracking/table_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -30,7 +30,7 @@ module My module TimeTracking - class TableComponent < ApplicationComponent + class ListComponent < ApplicationComponent include OpTurbo::Streamable include ComponentNavigation @@ -65,6 +65,18 @@ module My total_hours = time_entries_by_day[date].sum(&:hours).round(2) DurationConverter.output(total_hours, format: :hours_and_minutes) end + + def date_title(date) + result = [date.strftime("%A %d")] + + if date.today? + result << render(Primer::Beta::Text.new(color: :muted)) { t("label_today") } + elsif date.yesterday? + result << render(Primer::Beta::Text.new(color: :muted)) { t("label_yesterday") } + end + + safe_join(result, " ") + end end end end diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 7c2f1594d9a..cf596ae1623 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -127,7 +127,7 @@ module My def list_view_component if params[:view_mode] == "list" - My::TimeTracking::TableComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) + My::TimeTracking::ListComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) else My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) end From ea0e97f95d23658b37be96dfb414665a3aa6cca3 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 3 Apr 2025 16:24:17 +0200 Subject: [PATCH 44/88] add times and counts --- .../components/my/time_tracking/list_component.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 701057a3cbc..885763cbba4 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -66,8 +66,10 @@ module My DurationConverter.output(total_hours, format: :hours_and_minutes) end - def date_title(date) - result = [date.strftime("%A %d")] + def date_title(date) # rubocop:disable Metrics/AbcSize + result = [ + render(Primer::Beta::Text.new) { date.strftime("%A %d") } + ] if date.today? result << render(Primer::Beta::Text.new(color: :muted)) { t("label_today") } @@ -75,6 +77,13 @@ module My result << render(Primer::Beta::Text.new(color: :muted)) { t("label_yesterday") } end + result << render(Primer::Beta::Text.new(color: :muted)) do + count = time_entries_by_day[date].count + "#{count} #{TimeEntry.model_name.human(count: count)}" + end + + result << render(Primer::Beta::Text.new) { total_hours_per_day(date) } + safe_join(result, " ") end end From 02c86454fefc48a24aa0f73e016ed27e64cec7c1 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 4 Apr 2025 16:49:37 +0200 Subject: [PATCH 45/88] WIP add a footer row --- .../controllers/dynamic/my/time-tracking.controller.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 972b4f49802..515f1e4c489 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -84,9 +84,7 @@ export default class MyTimeTrackingController extends Controller { - +
${arg.event.extendedProps.projectName}
`, }; @@ -180,6 +178,11 @@ export default class MyTimeTrackingController extends Controller { { method: 'GET' }, ); }, + + viewDidMount: (info) => { + const content = ''; + info.el.querySelector('.fc-timegrid .fc-scrollgrid tbody')?.insertAdjacentHTML('beforeend', content); + }, }); this.calendar.render(); From f8fcddcd8e649c28ddafbaed87da5e3447ada44e Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 7 Apr 2025 15:25:56 +0200 Subject: [PATCH 46/88] add totals column --- .../dynamic/my/time-tracking.controller.ts | 157 +++++++++++++++--- .../time_tracking/calendar_component.html.erb | 4 +- .../my/time_tracking/calendar_component.rb | 2 +- modules/costs/config/locales/en.yml | 7 + 4 files changed, 149 insertions(+), 21 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 515f1e4c489..e35bb3cda22 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -68,25 +68,24 @@ export default class MyTimeTrackingController extends Controller { ]; }, eventContent: (arg) => { - let time = ''; + let timeDetails = ''; - if (arg.event.allDay) { - time = `${this.displayDuration(arg.event.extendedProps.hours as number)}`; - } else { - time = `${moment(arg.event.start).format('HH:mm')}-${moment(arg.event.end).format('HH:mm')} (${this.displayDuration(arg.event.extendedProps.hours as number)})`; + if (!arg.event.allDay) { + timeDetails = `
${moment(arg.event.start).format('HH:mm')} - ${moment(arg.event.end).format('HH:mm')}
`; } return { html: ` -
-
${time}
-
- -
${arg.event.extendedProps.projectName}
-
-
`, +
+
${this.displayDuration(arg.event.extendedProps.hours as number)}
+
+ +
${arg.event.extendedProps.projectName}
+ ${timeDetails} +
+
`, }; }, select: (info) => { @@ -178,10 +177,14 @@ export default class MyTimeTrackingController extends Controller { { method: 'GET' }, ); }, - - viewDidMount: (info) => { - const content = ''; - info.el.querySelector('.fc-timegrid .fc-scrollgrid tbody')?.insertAdjacentHTML('beforeend', content); + viewDidMount: () => { + setTimeout(() => this.addTotalFooter(), 100); + }, + eventDidMount: () => { + setTimeout(() => this.addTotalFooter(), 100); + }, + eventChange: () => { + setTimeout(() => this.addTotalFooter(), 100); }, }); @@ -190,6 +193,119 @@ export default class MyTimeTrackingController extends Controller { disconnect():void { document.removeEventListener('dialog:close', this.dialogCloseListener); + + // Clean up calendar when controller disconnects + if (this.calendar) { + this.calendar.destroy(); + } + } + + addTotalFooter() { + if (!this.calendar) return; + const calendarScrollGridWrapper = document.querySelector('.fc-timegrid .fc-scrollgrid tbody'); + + if (!calendarScrollGridWrapper) return; + + // Add footer dividier if it doesn't exist + if (calendarScrollGridWrapper.querySelector('.fc-timegrid-footer-divider') === null) { + calendarScrollGridWrapper.insertAdjacentHTML('beforeend', ''); + } + + // Remove existing footer if it exists + const existingFooter = document.querySelector('.fc-timegrid-footer-totals'); + if (existingFooter) { + existingFooter.remove(); + } + const days:string[] = []; + document.querySelectorAll('.fc-timegrid-cols .fc-day').forEach((dayElement) => { + days.push(dayElement.getAttribute('data-date') as string); + }); + + calendarScrollGridWrapper.appendChild(this.buildHtmlFooter(days)); + } + + calculateTotalHours(dayStr:string):number { + // Calculate total hours for this day + let totalHours = 0; + + this.calendar.getEvents().forEach((event) => { + const eventStart = event.start; + if (!eventStart) return; + + // Format event date for comparison + const eventDateStr = eventStart.toISOString().slice(0, 10); + + if (eventDateStr === dayStr && event.extendedProps && event.extendedProps.hours) { + totalHours += event.extendedProps.hours as number; + } + }); + + return totalHours; + } + + buildHtmlFooter(days:string[]):HTMLTableRowElement { + const tr = document.createElement('tr'); + tr.setAttribute('role', 'presentation'); + tr.className = 'fc-scrollgrid-section fc-scrollgrid-section-footer fc-timegrid-footer-totals'; + + const td = document.createElement('td'); + td.setAttribute('role', 'presentation'); + + const scrollerHarness = document.createElement('div'); + scrollerHarness.className = 'fc-scroller-harness'; + + const scroller = document.createElement('div'); + scroller.className = 'fc-scroller'; + scroller.style.overflow = 'hidden scroll'; + + const table = document.createElement('table'); + table.setAttribute('role', 'presentation'); + table.className = 'fc-col-footer'; + + const colgroup = document.createElement('colgroup'); + const col = document.createElement('col'); + col.style.width = '61px'; + + const tbody = document.createElement('tbody'); + tbody.setAttribute('role', 'presentation'); + + const tbodyTr = document.createElement('tr'); + tbodyTr.setAttribute('role', 'row'); + + const th1 = document.createElement('th'); + th1.setAttribute('aria-hidden', 'true'); + th1.className = 'fc-timegrid-axis'; + + const axisFrame = document.createElement('div'); + axisFrame.className = 'fc-timegrid-axis-frame'; + + th1.appendChild(axisFrame); + tbodyTr.appendChild(th1); + + // Add columns for each day + days.forEach((day) => { + const footerCell = document.createElement('th'); + footerCell.setAttribute('role', 'columnfooter'); + footerCell.className = 'fc-col-footer-cell fc-day'; + + // Inner div in der zweiten Zelle erstellen + const syncInner = document.createElement('div'); + syncInner.className = 'fc-scrollgrid-sync-inner'; + syncInner.textContent = this.displayDuration(this.calculateTotalHours(day)); + footerCell.appendChild(syncInner); + tbodyTr.appendChild(footerCell); + }); + + tbody.appendChild(tbodyTr); + colgroup.appendChild(col); + table.appendChild(colgroup); + table.appendChild(tbody); + scroller.appendChild(table); + scrollerHarness.appendChild(scroller); + td.appendChild(scrollerHarness); + tr.appendChild(td); + + return tr; } updateTimeEntry(timeEntryId:string, spentOn:string, startTime:string | null, hours:number, revertFunction:() => void) { @@ -232,6 +348,9 @@ export default class MyTimeTrackingController extends Controller { if (minutes === 0) { return `${hours}h`; } + if (hours === 0) { + return `${minutes}m`; + } return `${hours}h ${minutes}m`; } @@ -249,7 +368,7 @@ export default class MyTimeTrackingController extends Controller { } dialogCloseListener(this:void, event:CustomEvent):void { - const { detail: { dialog, submitted } } = event as { detail:{ dialog:HTMLDialogElement; submitted:boolean }; }; + const { detail: { dialog, submitted } } = event as { detail:{ dialog:HTMLDialogElement; submitted:boolean } }; if (dialog.id === 'time-entry-dialog' && submitted) { window.location.reload(); } diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index fd8861bdbae..d9b301699e3 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -23,5 +23,7 @@
-
<%= total_hours %>
+ <%- if mode != :day %> +
<%= total_hours %>
+ <%- end %> <%- end %> diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb index ec126f2ae92..d24e5643612 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.rb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -61,7 +61,7 @@ module My def total_hours total_hours = time_entries.sum(&:hours).round(2) - total_str = DurationConverter.output(total_hours, format: :hours_and_minutes) + total_str = DurationConverter.output(total_hours, format: :hours_and_minutes).presence || t("label_no_time") I18n.t(mode, scope: "total_times", hours: total_str) end diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index d9908438e7b..d86942f18b2 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -165,6 +165,13 @@ en: label_costs: "Costs" label_mandatory_fields: "Mandatory fields" label_my_time_tracking: "My time tracking" + label_no_time: "No time tracked" + label_next_day: "Next day" + label_next_week: "Next week" + label_next_month: "Next month" + label_previous_day: "Previous day" + label_previous_week: "Previous week" + label_previous_month: "Previous month" placeholder_activity_select_work_package_first: Work package selection is required first From 80fdfaccdd66b6be0aa8d1a2526d9ab562ee7894 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 7 Apr 2025 16:05:52 +0200 Subject: [PATCH 47/88] set locale for calendar from the outside --- .../controllers/dynamic/my/time-tracking.controller.ts | 9 ++++++--- .../components/my/time_tracking/calendar_component.rb | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index e35bb3cda22..6c5632c0c94 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -6,6 +6,7 @@ import interactionPlugin from '@fullcalendar/interaction'; import { TurboRequestsService } from 'core-app/core/turbo/turbo-requests.service'; import { PathHelperService } from 'core-app/core/path-helper/path-helper.service'; import moment from 'moment'; +import allLocales from '@fullcalendar/core/locales-all'; import { renderStreamMessage } from '@hotwired/turbo'; export default class MyTimeTrackingController extends Controller { @@ -19,6 +20,7 @@ export default class MyTimeTrackingController extends Controller { timeEntries: Array, initialDate: String, canCreate: Boolean, + locale: String, canEdit: Boolean, forceTimes: Boolean, }; @@ -30,6 +32,7 @@ export default class MyTimeTrackingController extends Controller { declare readonly canCreateValue:boolean; declare readonly canEditValue:boolean; declare readonly forceTimesValue:boolean; + declare readonly localeValue:string; private calendar:Calendar; @@ -46,8 +49,8 @@ export default class MyTimeTrackingController extends Controller { this.calendar = new Calendar(this.calendarTarget, { plugins: [timeGridPlugin, dayGridPlugin, interactionPlugin], initialView: this.calendarView(), - firstDay: 1, // get from settings - locale: 'de', // also get from settings + locales: allLocales, + locale: this.localeValue, events: this.timeEntriesValue, headerToolbar: false, initialDate: this.initialDateValue, @@ -71,7 +74,7 @@ export default class MyTimeTrackingController extends Controller { let timeDetails = ''; if (!arg.event.allDay) { - timeDetails = `
${moment(arg.event.start).format('HH:mm')} - ${moment(arg.event.end).format('HH:mm')}
`; + timeDetails = `
${moment(arg.event.start).format('LT')} - ${moment(arg.event.end).format('LT')}
`; } return { diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb index d24e5643612..c6c39eccc7a 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.rb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -49,7 +49,8 @@ module My "my--time-tracking-initial-date-value" => date.iso8601, "my--time-tracking-can-create-value" => User.current.allowed_in_any_project?(:log_own_time), "my--time-tracking-can-edit-value" => User.current.allowed_in_any_project?(:edit_own_time_entries), - "my--time-tracking-force-times-value" => TimeEntry.must_track_start_and_end_time? + "my--time-tracking-force-times-value" => TimeEntry.must_track_start_and_end_time?, + "my--time-tracking-locale-value" => I18n.locale } end From 3bfadb51f14af4f09249e53137f3516b596af944 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 7 Apr 2025 16:20:11 +0200 Subject: [PATCH 48/88] height and other stuff --- .../dynamic/my/time-tracking.controller.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 6c5632c0c94..0dc18fa49f8 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -53,10 +53,8 @@ export default class MyTimeTrackingController extends Controller { locale: this.localeValue, events: this.timeEntriesValue, headerToolbar: false, + aspectRatio: 2, initialDate: this.initialDateValue, - height: 800, - contentHeight: 780, - aspectRatio: 3, selectable: this.canCreateValue, editable: this.canEditValue, eventResizableFromStart: true, @@ -68,24 +66,26 @@ export default class MyTimeTrackingController extends Controller { 'calendar-time-entry-event', `__hl_status_${arg.event.extendedProps.statusId}`, '__hl_border_top', + 'ellipsis', ]; }, eventContent: (arg) => { let timeDetails = ''; if (!arg.event.allDay) { - timeDetails = `
${moment(arg.event.start).format('LT')} - ${moment(arg.event.end).format('LT')}
`; + const time = `${moment(arg.event.start).format('LT')} - ${moment(arg.event.end).format('LT')}`; + timeDetails = `
${time}
`; } return { html: `
-
${this.displayDuration(arg.event.extendedProps.hours as number)}
+
${this.displayDuration(arg.event.extendedProps.hours as number)}
-
+ -
${arg.event.extendedProps.projectName}
+
${arg.event.extendedProps.projectName}
${timeDetails}
`, From d5aa27770b6511de9add62ed7c58269894212f84 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 7 Apr 2025 16:37:34 +0200 Subject: [PATCH 49/88] improve viewing experience --- .../controllers/dynamic/my/time-tracking.controller.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 0dc18fa49f8..502b96f8938 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -61,6 +61,10 @@ export default class MyTimeTrackingController extends Controller { defaultTimedEventDuration: DEFAULT_TIMED_EVENT_DURATION, allDayContent: I18n.t('js.myTimeTracking.noSpecificTime'), dayMaxEventRows: 4, // 3 + more link + eventMinHeight: 30, + eventMaxStack: 2, + eventShortHeight: 31, + nowIndicator: true, eventClassNames(arg) { return [ 'calendar-time-entry-event', From b5b94399073ed67635835b6741538cebf160f129 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 8 Apr 2025 11:19:02 +0200 Subject: [PATCH 50/88] Extract the page header into its own component --- .../time_tracking/header_component.html.erb | 41 +++++++++++ .../my/time_tracking/header_component.rb | 69 +++++++++++++++++++ .../app/views/my/time_tracking/day.html.erb | 13 +--- .../app/views/my/time_tracking/month.html.erb | 13 +--- .../app/views/my/time_tracking/week.html.erb | 36 +--------- modules/costs/config/locales/en.yml | 4 ++ 6 files changed, 117 insertions(+), 59 deletions(-) create mode 100644 modules/costs/app/components/my/time_tracking/header_component.html.erb create mode 100644 modules/costs/app/components/my/time_tracking/header_component.rb diff --git a/modules/costs/app/components/my/time_tracking/header_component.html.erb b/modules/costs/app/components/my/time_tracking/header_component.html.erb new file mode 100644 index 00000000000..3fa0caefe7e --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/header_component.html.erb @@ -0,0 +1,41 @@ +<%= render(Primer::OpenProject::PageHeader.new) do |header| + header.with_title(variant: :large) { title } + header.with_breadcrumbs( + [ + { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path }, + title + ] + ) + + cal_button = lambda do |button| + button.with_leading_visual_icon(icon: :calendar) + button.with_trailing_action_icon(icon: :"triangle-down") + t(:label_calendar) + end + + list_button = lambda do |button| + button.with_leading_visual_icon(icon: "list-unordered") + button.with_trailing_action_icon(icon: :"triangle-down") + t(:label_list) + end + + header.with_action_menu( + menu_arguments: { anchor_align: :end }, + button_arguments: { button_block: params[:view_mode] == "list" ? list_button : cal_button } + ) do |menu| + menu.with_item( + label: "Calendar", tag: :a, + href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "calendar")) + ) do |item| + item.with_leading_visual_icon(icon: :calendar) + end + menu.with_item( + label: "List", tag: :a, + href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "list")) + ) do |item| + item.with_leading_visual_icon(icon: "list-unordered") + end + end + + header.with_action_zen_mode_button + end %> diff --git a/modules/costs/app/components/my/time_tracking/header_component.rb b/modules/costs/app/components/my/time_tracking/header_component.rb new file mode 100644 index 00000000000..34a79fdc6eb --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/header_component.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module My + module TimeTracking + class HeaderComponent < ApplicationComponent + options :date, :mode + + def title + case mode + when :week then week_title + when :month then month_title + when :day then day_title + end + end + + def day_title + if Date.current == date + I18n.t(:label_today) + else + I18n.t(:label_specific_day, day: I18n.l(date, format: :short)) + end + end + + def week_title + if Date.current.all_week.include?(date) + I18n.t(:label_this_week) + else + I18n.t(:label_specific_week, week: I18n.l(date, format: "%W %Y")) + end + end + + def month_title + if Date.current == date + I18n.t(:label_today) + else + I18n.t(:label_specific_day, day: I18n.l(date, format: :short)) + end + end + end + end +end diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb index 141f7b39ece..c11e1bd347b 100644 --- a/modules/costs/app/views/my/time_tracking/day.html.erb +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -1,13 +1,2 @@ -<%= - render Primer::OpenProject::PageHeader.new do |header| - header.with_title { I18n.t(:label_today) } - header.with_breadcrumbs( - [ - { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path }, - I18n.t(:label_today) - ] - ) - end -%> - +<%= render(My::TimeTracking::HeaderComponent.new(mode: params[:action].to_sym, date: current_day)) %> <%= render(list_view_component) %> diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb index 6f55ca5bce5..c11e1bd347b 100644 --- a/modules/costs/app/views/my/time_tracking/month.html.erb +++ b/modules/costs/app/views/my/time_tracking/month.html.erb @@ -1,13 +1,2 @@ -<%= - render Primer::OpenProject::PageHeader.new do |header| - header.with_title { I18n.t(:label_this_month) } - header.with_breadcrumbs( - [ - { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path }, - I18n.t(:label_this_month) - ] - ) - end -%> - +<%= render(My::TimeTracking::HeaderComponent.new(mode: params[:action].to_sym, date: current_day)) %> <%= render(list_view_component) %> diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb index 4aea1d1b5fa..c11e1bd347b 100644 --- a/modules/costs/app/views/my/time_tracking/week.html.erb +++ b/modules/costs/app/views/my/time_tracking/week.html.erb @@ -1,36 +1,2 @@ -<%= - render Primer::OpenProject::PageHeader.new do |header| - header.with_title(variant: :large) { I18n.t(:label_this_week) } - header.with_breadcrumbs( - [ - { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path }, - I18n.t(:label_this_week) - ] - ) - - cal_button = lambda do |button| - button.with_leading_visual_icon(icon: :calendar) - button.with_trailing_action_icon(icon: :"triangle-down") - "Calendar" - end - - list_button = lambda do |button| - button.with_leading_visual_icon(icon: "list-unordered") - button.with_trailing_action_icon(icon: :"triangle-down") - "List" - end - - header.with_action_menu(menu_arguments: { anchor_align: :end }, button_arguments: { button_block: params[:view_mode] == "list" ? list_button : cal_button }) do |menu| - menu.with_item(label: "Calendar", tag: :a, href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "calendar"))) do |item| - item.with_leading_visual_icon(icon: :calendar) - end - menu.with_item(label: "List", tag: :a, href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "list"))) do |item| - item.with_leading_visual_icon(icon: "list-unordered") - end - end - - header.with_action_zen_mode_button - end -%> - +<%= render(My::TimeTracking::HeaderComponent.new(mode: params[:action].to_sym, date: current_day)) %> <%= render(list_view_component) %> diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index d86942f18b2..7b0ee4e9718 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -172,6 +172,10 @@ en: label_previous_day: "Previous day" label_previous_week: "Previous week" label_previous_month: "Previous month" + label_specific_day: "Day %{day}" + label_specific_week: "Week #%{week}" + label_specific_month: "Month %{month}" + label_list: "List" placeholder_activity_select_work_package_first: Work package selection is required first From 4f39ee95c313eeed1e14376a96bf56c9517f08c5 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 8 Apr 2025 11:20:39 +0200 Subject: [PATCH 51/88] refactor a bit more stuff --- .../my/time_tracking/calendar_component.html.erb | 10 ++++++---- .../components/my/time_tracking/calendar_component.rb | 3 ++- .../my/time_tracking/list_component.html.erb | 2 +- .../app/components/my/time_tracking/list_component.rb | 2 +- .../{component_navigation.rb => shared_component.rb} | 10 +++++----- .../app/components/my/time_tracking/time_entry_row.rb | 4 +++- .../costs/app/controllers/time_entries_controller.rb | 2 -- 7 files changed, 18 insertions(+), 15 deletions(-) rename modules/costs/app/components/my/time_tracking/{component_navigation.rb => shared_component.rb} (90%) diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index d9b301699e3..edd6f7c9dd8 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -23,7 +23,9 @@
- <%- if mode != :day %> -
<%= total_hours %>
- <%- end %> -<%- end %> + <% if mode != :day %> + <%= flex_layout(align_items: :flex_end) do |flex| %> + <% flex.with_row(mt: 2, font_weight: :bold) { total_hours } %> + <% end %> + <% end %> +<% end %> diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb index c6c39eccc7a..4a470fe99a5 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.rb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -32,7 +32,8 @@ module My module TimeTracking class CalendarComponent < ApplicationComponent include OpTurbo::Streamable - include ComponentNavigation + include OpPrimer::ComponentHelpers + include SharedComponent options time_entries: [], mode: :week, diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index 13dd2c0c42c..50b66aa528f 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -9,7 +9,7 @@ end end - component.with_action_button(tag: :a, href: today_href) { "Today" } + component.with_action_button(tag: :a, href: today_href) { t("label_today") } component.with_action_button( scheme: :primary, diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 885763cbba4..4e1fea3c00c 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -32,7 +32,7 @@ module My module TimeTracking class ListComponent < ApplicationComponent include OpTurbo::Streamable - include ComponentNavigation + include SharedComponent options time_entries: [], mode: :week, diff --git a/modules/costs/app/components/my/time_tracking/component_navigation.rb b/modules/costs/app/components/my/time_tracking/shared_component.rb similarity index 90% rename from modules/costs/app/components/my/time_tracking/component_navigation.rb rename to modules/costs/app/components/my/time_tracking/shared_component.rb index 175d1fdcb8c..618fd5b40a4 100644 --- a/modules/costs/app/components/my/time_tracking/component_navigation.rb +++ b/modules/costs/app/components/my/time_tracking/shared_component.rb @@ -30,7 +30,7 @@ # module My module TimeTracking - module ComponentNavigation + module SharedComponent extend ActiveSupport::Concern def title # rubocop:disable Metrics/AbcSize case mode @@ -41,14 +41,14 @@ module My eow = date.end_of_week if bow.year == eow.year && bow.month == eow.month - "#{bow.strftime('%d.')} - #{eow.strftime('%d. %B %Y')}" + [I18n.l(bow, format: "%d."), I18n.l(eow, format: "%d. %B %Y")].join(" - ") elsif bow.year == eow.year - "#{bow.strftime('%d. %B')} - #{eow.strftime('%d. %B %Y')}" + [I18n.l(bow, format: "%d. %B"), I18n.l(eow, format: "%d. %B %Y")].join(" - ") else - "#{bow.strftime('%d. %B %Y')} - #{eow.strftime('%d. %B %Y')}" + [I18n.l(bow, format: "%d. %B %Y"), I18n.l(eow, format: "%d. %B %Y")].join(" - ") end when :month - date.strftime("%B %Y") + I18n.l(date, format: "%B %Y") end end diff --git a/modules/costs/app/components/my/time_tracking/time_entry_row.rb b/modules/costs/app/components/my/time_tracking/time_entry_row.rb index 4c4215b446d..d85fa6568ef 100644 --- a/modules/costs/app/components/my/time_tracking/time_entry_row.rb +++ b/modules/costs/app/components/my/time_tracking/time_entry_row.rb @@ -34,10 +34,12 @@ module My def button_links [ action_menu - ] + ].compact end def action_menu + return nil unless User.current.allowed_in_work_package?(:edit_own_time_entries, time_entry.work_package) + render(Primer::Alpha::ActionMenu.new) do |menu| menu.with_show_button(icon: "kebab-horizontal", "aria-label": t("label_more"), scheme: :invisible) menu.with_item( diff --git a/modules/costs/app/controllers/time_entries_controller.rb b/modules/costs/app/controllers/time_entries_controller.rb index 7e5f15ab3d1..40fb48ef8d3 100644 --- a/modules/costs/app/controllers/time_entries_controller.rb +++ b/modules/costs/app/controllers/time_entries_controller.rb @@ -125,8 +125,6 @@ class TimeEntriesController < ApplicationController form_component = TimeEntries::TimeEntryFormComponent.new(time_entry: @time_entry, **form_config_options) update_via_turbo_stream(component: form_component, status: :bad_request) end - - respond_with_turbo_streams end private From d6c56cd06c59fe3be11aa11c55328355d8695dcf Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 8 Apr 2025 11:22:07 +0200 Subject: [PATCH 52/88] remove one last hard coded string --- modules/costs/app/controllers/time_entries_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/costs/app/controllers/time_entries_controller.rb b/modules/costs/app/controllers/time_entries_controller.rb index 40fb48ef8d3..dd83bf9c9ec 100644 --- a/modules/costs/app/controllers/time_entries_controller.rb +++ b/modules/costs/app/controllers/time_entries_controller.rb @@ -105,7 +105,7 @@ class TimeEntriesController < ApplicationController @time_entry = call.result if call.success? - render_success_flash_message_via_turbo_stream(message: "Event successfully changed") + render_success_flash_message_via_turbo_stream(message: t("notice_updated_successfully")) else form_component = TimeEntries::TimeEntryFormComponent.new(time_entry: @time_entry, **form_config_options) update_via_turbo_stream(component: form_component, status: :bad_request) From cbf2f581dc4826983289eb5a01dc5c60755348f9 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 8 Apr 2025 11:54:38 +0200 Subject: [PATCH 53/88] do not allow resizing all day events --- .../dynamic/my/time-tracking.controller.ts | 68 +++++++++---------- .../controllers/time_entries_controller.rb | 7 +- modules/costs/config/locales/en.yml | 2 + 3 files changed, 40 insertions(+), 37 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 502b96f8938..6b893e8ebe6 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -110,12 +110,18 @@ export default class MyTimeTrackingController extends Controller { ); }, eventResize: (info) => { + // it does not make sense to resize the events without start & end times + // we cannot only disable resize, because we want to be able to drag the events + // so we need to revert the event to its original size + if (info.event.allDay) { + info.revert(); + return; + } + const startMoment = moment(info.event.startStr); const endMoment = moment(info.event.endStr); - const newEventHours = info.event.allDay - ? (info.event.extendedProps.hours as number) - : moment.duration(endMoment.diff(startMoment)).asHours(); + const newEventHours = moment.duration(endMoment.diff(startMoment)).asHours(); info.event.setExtendedProp('hours', newEventHours); @@ -132,12 +138,7 @@ export default class MyTimeTrackingController extends Controller { // When dragging from all day into the calendar set the defaultTimedEventDuration to the hours of the event so // that we display it correctly in the calendar. Will be reset in the drop event if (info.event.allDay) { - this.calendar.setOption( - 'defaultTimedEventDuration', - moment - .duration(info.event.extendedProps.hours as number, 'hours') - .asMilliseconds(), - ); + this.calendar.setOption('defaultTimedEventDuration', moment.duration(info.event.extendedProps.hours as number, 'hours').asMilliseconds()); } }, @@ -168,10 +169,7 @@ export default class MyTimeTrackingController extends Controller { ); } - this.calendar.setOption( - 'defaultTimedEventDuration', - DEFAULT_TIMED_EVENT_DURATION, - ); + this.calendar.setOption('defaultTimedEventDuration', DEFAULT_TIMED_EVENT_DURATION); }, eventClick: (info) => { // check if we clicked on a link tag, if so exit early as we don't want to show the modal @@ -184,15 +182,9 @@ export default class MyTimeTrackingController extends Controller { { method: 'GET' }, ); }, - viewDidMount: () => { - setTimeout(() => this.addTotalFooter(), 100); - }, - eventDidMount: () => { - setTimeout(() => this.addTotalFooter(), 100); - }, - eventChange: () => { - setTimeout(() => this.addTotalFooter(), 100); - }, + viewDidMount: () => { setTimeout(() => this.addTotalFooter(), 100); }, + eventDidMount: () => { setTimeout(() => this.addTotalFooter(), 100); }, + eventChange: () => { setTimeout(() => this.addTotalFooter(), 100); }, }); this.calendar.render(); @@ -215,18 +207,22 @@ export default class MyTimeTrackingController extends Controller { // Add footer dividier if it doesn't exist if (calendarScrollGridWrapper.querySelector('.fc-timegrid-footer-divider') === null) { - calendarScrollGridWrapper.insertAdjacentHTML('beforeend', ''); + calendarScrollGridWrapper.insertAdjacentHTML( + 'beforeend', + '', + ); } // Remove existing footer if it exists const existingFooter = document.querySelector('.fc-timegrid-footer-totals'); - if (existingFooter) { - existingFooter.remove(); - } + if (existingFooter) { existingFooter.remove(); } + const days:string[] = []; - document.querySelectorAll('.fc-timegrid-cols .fc-day').forEach((dayElement) => { - days.push(dayElement.getAttribute('data-date') as string); - }); + document + .querySelectorAll('.fc-timegrid-cols .fc-day') + .forEach((dayElement) => { + days.push(dayElement.getAttribute('data-date') as string); + }); calendarScrollGridWrapper.appendChild(this.buildHtmlFooter(days)); } @@ -298,7 +294,9 @@ export default class MyTimeTrackingController extends Controller { // Inner div in der zweiten Zelle erstellen const syncInner = document.createElement('div'); syncInner.className = 'fc-scrollgrid-sync-inner'; - syncInner.textContent = this.displayDuration(this.calculateTotalHours(day)); + syncInner.textContent = this.displayDuration( + this.calculateTotalHours(day), + ); footerCell.appendChild(syncInner); tbodyTr.appendChild(footerCell); }); @@ -330,14 +328,14 @@ export default class MyTimeTrackingController extends Controller { start_time: startTime, hours, }, + no_dialog: true, }), }) .then((response) => { - if (response.ok) { - void response.text().then((html) => { - renderStreamMessage(html); - }); - } else if (revertFunction) { + void response.text().then((html) => { + renderStreamMessage(html); + }); + if (!response.ok && revertFunction) { revertFunction(); } }) diff --git a/modules/costs/app/controllers/time_entries_controller.rb b/modules/costs/app/controllers/time_entries_controller.rb index dd83bf9c9ec..5462ad8c385 100644 --- a/modules/costs/app/controllers/time_entries_controller.rb +++ b/modules/costs/app/controllers/time_entries_controller.rb @@ -97,7 +97,7 @@ class TimeEntriesController < ApplicationController end end - def update + def update # rubocop:disable Metrics/AbcSize call = TimeEntries::UpdateService .new(user: current_user, model: @time_entry) .call(permitted_params.time_entries) @@ -106,12 +106,15 @@ class TimeEntriesController < ApplicationController if call.success? render_success_flash_message_via_turbo_stream(message: t("notice_updated_successfully")) + elsif params[:no_dialog] + render_error_flash_message_via_turbo_stream(message: t("notice_update_failed", + errors: call.errors.full_messages.join(", "))) else form_component = TimeEntries::TimeEntryFormComponent.new(time_entry: @time_entry, **form_config_options) update_via_turbo_stream(component: form_component, status: :bad_request) end - respond_with_turbo_streams + respond_with_turbo_streams(status: call.success? ? :ok : :bad_request) end def destroy diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index 7b0ee4e9718..7be9e5fe9f5 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -184,6 +184,8 @@ en: notice_successful_lock: "Locked successfully." notice_cost_logged_successfully: "Unit cost logged successfully." notice_different_time_zones: "This user has a different time zone (%{tz}). Time will be logged using their time zone." + notice_updated_successfully: "Time entry updated successfully." + notice_update_failed: "Failed to update time entry. Errors: %{errors}" permission_edit_cost_entries: "Edit booked unit costs" permission_edit_own_cost_entries: "Edit own booked unit costs" From b67c1e514db0370fc0e4a009069195568515ced2 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 8 Apr 2025 12:20:36 +0200 Subject: [PATCH 54/88] correct link styling --- .../controllers/dynamic/my/time-tracking.controller.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 6b893e8ebe6..bade0239f41 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -86,8 +86,10 @@ export default class MyTimeTrackingController extends Controller {
${this.displayDuration(arg.event.extendedProps.hours as number)}
-
- ${arg.event.extendedProps.workPackageSubject} +
${arg.event.extendedProps.projectName}
${timeDetails} From fb8d3237350e97d68ffcbd22e5ae6434bb74c5d2 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 9 Apr 2025 10:58:21 +0200 Subject: [PATCH 55/88] final styling --- .../dynamic/my/time-tracking.controller.ts | 2 +- .../components/my/time_tracking/calendar.sass | 39 +++++++++++++++---- .../time_tracking/calendar_component.html.erb | 9 +++-- 3 files changed, 38 insertions(+), 12 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index bade0239f41..1363089c097 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -53,7 +53,7 @@ export default class MyTimeTrackingController extends Controller { locale: this.localeValue, events: this.timeEntriesValue, headerToolbar: false, - aspectRatio: 2, + height: '100%', initialDate: this.initialDateValue, selectable: this.canCreateValue, editable: this.canEditValue, diff --git a/modules/costs/app/components/my/time_tracking/calendar.sass b/modules/costs/app/components/my/time_tracking/calendar.sass index aa0392402be..21366a456a6 100644 --- a/modules/costs/app/components/my/time_tracking/calendar.sass +++ b/modules/costs/app/components/my/time_tracking/calendar.sass @@ -1,8 +1,31 @@ -.calendar-time-entry-event - display: flex - user-select: none - border: 1px solid var(--borderColor-default) - border-radius: 5px - background: var(--body-background) - color: var(--body-font-color) - font-size: var(--body-font-size) +.time-entry-calendar + height: calc(100% - 110px) + + .op-fc-wrapper + flex-grow: 1 + + .fc-timegrid-event-short + .fc-event-time::after + content: '' + display: none + + .calendar-time-entry-event + display: flex + user-select: none + border: 1px solid var(--borderColor-default) + border-radius: 5px + background: var(--body-background) + color: var(--body-font-color) + font-size: var(--body-font-size) + + .fc-popover + background: var(--fc-page-bg-color) !important + + .fc-daygrid-event + background: var(--body-background) !important + + .fc-event-title-container + margin: 0 !important + + .fc-event-time + font-weight: normal diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index edd6f7c9dd8..0cebef6a5f5 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -1,4 +1,5 @@ -<%= component_wrapper(tag: "div", data: wrapper_data) do %> +<%= flex_layout(data: wrapper_data, classes: "time-entry-calendar") do |calendar_page| %> +<% calendar_page.with_row do %> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_text { title } @@ -20,12 +21,14 @@ href: dialog_time_entries_path(onlyMe: true) ) { "Log Time" } end %> - -
+ <% end %> + <% calendar_page.with_row(classes: "op-fc-wrapper", data: { "my--time-tracking-target" => "calendar" }) %> <% if mode != :day %> +<% calendar_page.with_row do %> <%= flex_layout(align_items: :flex_end) do |flex| %> <% flex.with_row(mt: 2, font_weight: :bold) { total_hours } %> <% end %> + <% end %> <% end %> <% end %> From d1de6a39f3b33352e89e1996bf761a6d27b16359 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 9 Apr 2025 11:24:38 +0200 Subject: [PATCH 56/88] correctly handle the page header --- modules/costs/app/components/my/time_tracking/calendar.sass | 2 +- .../components/my/time_tracking/calendar_component.html.erb | 3 ++- .../app/components/my/time_tracking/list_component.html.erb | 5 +++-- modules/costs/app/views/my/time_tracking/day.html.erb | 1 - modules/costs/app/views/my/time_tracking/month.html.erb | 1 - modules/costs/app/views/my/time_tracking/week.html.erb | 1 - 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/calendar.sass b/modules/costs/app/components/my/time_tracking/calendar.sass index 21366a456a6..076c5ad7a26 100644 --- a/modules/costs/app/components/my/time_tracking/calendar.sass +++ b/modules/costs/app/components/my/time_tracking/calendar.sass @@ -1,5 +1,5 @@ .time-entry-calendar - height: calc(100% - 110px) + height: 100% .op-fc-wrapper flex-grow: 1 diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index 0cebef6a5f5..7b7564bc4ff 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -1,4 +1,5 @@ <%= flex_layout(data: wrapper_data, classes: "time-entry-calendar") do |calendar_page| %> +<% calendar_page.with_row { render(My::TimeTracking::HeaderComponent.new(mode: mode, date: date)) } %> <% calendar_page.with_row do %> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_text { title } @@ -10,7 +11,7 @@ end end - component.with_action_button(tag: :a, href: today_href) { "Today" } + component.with_action_button(tag: :a, href: today_href) { I18n.t(:label_today) } component.with_action_button( scheme: :primary, diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index 50b66aa528f..e30bd310c6c 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -1,4 +1,5 @@ <%= component_wrapper(tag: "div", data: wrapper_data) do %> + <%= render(My::TimeTracking::HeaderComponent.new(mode: mode, date: date)) %> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_text { title } @@ -9,7 +10,7 @@ end end - component.with_action_button(tag: :a, href: today_href) { t("label_today") } + component.with_action_button(tag: :a, href: today_href) { t(:label_today) } component.with_action_button( scheme: :primary, @@ -18,7 +19,7 @@ }, tag: :a, href: dialog_time_entries_path(onlyMe: true) - ) { "Log Time" } + ) { t(:button_log_time) } end %> <%- range.each_with_index do |date, i| %> diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/day.html.erb index c11e1bd347b..b3f3712cf7d 100644 --- a/modules/costs/app/views/my/time_tracking/day.html.erb +++ b/modules/costs/app/views/my/time_tracking/day.html.erb @@ -1,2 +1 @@ -<%= render(My::TimeTracking::HeaderComponent.new(mode: params[:action].to_sym, date: current_day)) %> <%= render(list_view_component) %> diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb index c11e1bd347b..b3f3712cf7d 100644 --- a/modules/costs/app/views/my/time_tracking/month.html.erb +++ b/modules/costs/app/views/my/time_tracking/month.html.erb @@ -1,2 +1 @@ -<%= render(My::TimeTracking::HeaderComponent.new(mode: params[:action].to_sym, date: current_day)) %> <%= render(list_view_component) %> diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb index c11e1bd347b..b3f3712cf7d 100644 --- a/modules/costs/app/views/my/time_tracking/week.html.erb +++ b/modules/costs/app/views/my/time_tracking/week.html.erb @@ -1,2 +1 @@ -<%= render(My::TimeTracking::HeaderComponent.new(mode: params[:action].to_sym, date: current_day)) %> <%= render(list_view_component) %> From 3ac027a4a12727e28ace9045842d2694142ff345 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 9 Apr 2025 11:34:14 +0200 Subject: [PATCH 57/88] fix some labels and redirect to correct view mode --- .../time_tracking/header_component.html.erb | 6 ++-- .../my/time_tracking_controller.rb | 30 +++++++++---------- modules/costs/config/routes.rb | 9 ++---- 3 files changed, 22 insertions(+), 23 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/header_component.html.erb b/modules/costs/app/components/my/time_tracking/header_component.html.erb index 3fa0caefe7e..22696b5ca1b 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/header_component.html.erb @@ -24,13 +24,15 @@ button_arguments: { button_block: params[:view_mode] == "list" ? list_button : cal_button } ) do |menu| menu.with_item( - label: "Calendar", tag: :a, + label: t(:label_calendar), + tag: :a, href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "calendar")) ) do |item| item.with_leading_visual_icon(icon: :calendar) end menu.with_item( - label: "List", tag: :a, + label: t(:label_list), + tag: :a, href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "list")) ) do |item| item.with_leading_visual_icon(icon: "list-unordered") diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index cf596ae1623..ae18f2fe518 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -52,34 +52,22 @@ module My def calendar if browser.device.mobile? - redirect_to action: :day + redirect_to action: :day, view_mode: default_view_mode else - redirect_to action: :week + redirect_to action: :week, view_mode: default_view_mode end end def day load_time_entries(current_day) - - # TODO: At some point the filters will reduce the list, so we need to load them seperately - @project_filters = @time_entries.map(&:project).uniq - @activity_filters = @time_entries.map(&:activity).uniq end def week load_time_entries(current_day.all_week) - - # TODO: At some point the filters will reduce the list, so we need to load them seperately - @project_filters = @time_entries.map(&:project).uniq - @activity_filters = @time_entries.map(&:activity).uniq end def month load_time_entries(current_day.all_month) - - # TODO: At some point the filters will reduce the list, so we need to load them seperately - @project_filters = @time_entries.map(&:project).uniq - @activity_filters = @time_entries.map(&:activity).uniq end def today? @@ -110,6 +98,18 @@ module My @current_day = parsed_date || current_date end + def default_view_mode + if TimeEntry.can_track_start_and_end_time? + "calendar" + else + "list" + end + end + + def view_mode + ActiveSupport::StringInquirer.new(params[:view_mode] || default_view_mode) + end + def current_date case params[:action].to_sym when :day then Time.zone.today @@ -126,7 +126,7 @@ module My end def list_view_component - if params[:view_mode] == "list" + if view_mode.list? My::TimeTracking::ListComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) else My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index 08529386bbe..29a8ce80b38 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -51,14 +51,11 @@ Rails.application.routes.draw do get "/timer" => "timer#show", as: "timers" get "/time-tracking" => "time_tracking#calendar" - get "/time-tracking/day(-:view_mode)" => "time_tracking#day", - defaults: { view_mode: "calendar" }, + get "/time-tracking/day(-:view_mode)(/:date)" => "time_tracking#day", as: :time_tracking_day - get "/time-tracking/week(-:view_mode)" => "time_tracking#week", - defaults: { view_mode: "calendar" }, + get "/time-tracking/week(-:view_mode)(/:date)" => "time_tracking#week", as: :time_tracking_week - get "/time-tracking/month(-:view_mode)" => "time_tracking#month", - defaults: { view_mode: "calendar" }, + get "/time-tracking/month(-:view_mode)(/:date)" => "time_tracking#month", as: :time_tracking_month end From dd514eeec2e32a5e109cee6e0274a2bf7097fa91 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 9 Apr 2025 11:38:14 +0200 Subject: [PATCH 58/88] some more details --- .../dynamic/my/time-tracking.controller.ts | 2 +- .../my/time_tracking/calendar_component.html.erb | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 1363089c097..061c9723990 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -78,7 +78,7 @@ export default class MyTimeTrackingController extends Controller { if (!arg.event.allDay) { const time = `${moment(arg.event.start).format('LT')} - ${moment(arg.event.end).format('LT')}`; - timeDetails = `
${time}
`; + timeDetails = `
${time}
`; } return { diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index 7b7564bc4ff..55b8708b9f6 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -20,16 +20,17 @@ }, tag: :a, href: dialog_time_entries_path(onlyMe: true) - ) { "Log Time" } + ) { t(:button_log_time) } end %> <% end %> + <% calendar_page.with_row(classes: "op-fc-wrapper", data: { "my--time-tracking-target" => "calendar" }) %> <% if mode != :day %> -<% calendar_page.with_row do %> - <%= flex_layout(align_items: :flex_end) do |flex| %> - <% flex.with_row(mt: 2, font_weight: :bold) { total_hours } %> - <% end %> + <% calendar_page.with_row do %> + <%= flex_layout(align_items: :flex_end) do |flex| %> + <% flex.with_row(mt: 2, font_weight: :bold) { total_hours } %> + <% end %> <% end %> <% end %> <% end %> From c18f3fe5442d65ceb1fb0482573a535b70941b18 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 9 Apr 2025 14:19:32 +0200 Subject: [PATCH 59/88] remove success banner --- .../app/controllers/time_entries_controller.rb | 16 ++++++++-------- modules/costs/config/locales/en.yml | 3 +-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/modules/costs/app/controllers/time_entries_controller.rb b/modules/costs/app/controllers/time_entries_controller.rb index 5462ad8c385..9dfce83a6c8 100644 --- a/modules/costs/app/controllers/time_entries_controller.rb +++ b/modules/costs/app/controllers/time_entries_controller.rb @@ -104,14 +104,14 @@ class TimeEntriesController < ApplicationController @time_entry = call.result - if call.success? - render_success_flash_message_via_turbo_stream(message: t("notice_updated_successfully")) - elsif params[:no_dialog] - render_error_flash_message_via_turbo_stream(message: t("notice_update_failed", - errors: call.errors.full_messages.join(", "))) - else - form_component = TimeEntries::TimeEntryFormComponent.new(time_entry: @time_entry, **form_config_options) - update_via_turbo_stream(component: form_component, status: :bad_request) + if call.failure? + if params[:no_dialog] + render_error_flash_message_via_turbo_stream(message: t("notice_time_entry_update_failed", + errors: call.errors.full_messages.join(", "))) + else + form_component = TimeEntries::TimeEntryFormComponent.new(time_entry: @time_entry, **form_config_options) + update_via_turbo_stream(component: form_component, status: :bad_request) + end end respond_with_turbo_streams(status: call.success? ? :ok : :bad_request) diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index 7be9e5fe9f5..6e0d3bf9f4c 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -184,8 +184,7 @@ en: notice_successful_lock: "Locked successfully." notice_cost_logged_successfully: "Unit cost logged successfully." notice_different_time_zones: "This user has a different time zone (%{tz}). Time will be logged using their time zone." - notice_updated_successfully: "Time entry updated successfully." - notice_update_failed: "Failed to update time entry. Errors: %{errors}" + notice_time_entry_update_failed: "Failed to update time entry. Errors: %{errors}" permission_edit_cost_entries: "Edit booked unit costs" permission_edit_own_cost_entries: "Edit own booked unit costs" From 5c60d725f5996101e5b97388cb48686d57106f78 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 9 Apr 2025 14:44:36 +0200 Subject: [PATCH 60/88] tests for the My::TimeTrackingController#calendar method --- .../my/time_tracking_controller.rb | 6 +- .../my/time_tracking_controller_spec.rb | 97 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 modules/costs/spec/controllers/my/time_tracking_controller_spec.rb diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index ae18f2fe518..2ed409eb2d7 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -51,7 +51,7 @@ module My helper_method :current_day, :today?, :this_week?, :this_month?, :list_view_component def calendar - if browser.device.mobile? + if mobile? redirect_to action: :day, view_mode: default_view_mode else redirect_to action: :week, view_mode: default_view_mode @@ -132,5 +132,9 @@ module My My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) end end + + def mobile? + browser.device.mobile? + end end end diff --git a/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb b/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb new file mode 100644 index 00000000000..81a144d372d --- /dev/null +++ b/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require_relative "../../spec_helper" + +RSpec.describe My::TimeTrackingController do + let(:user) { create(:user) } + + before do + login_as user + end + + describe "GET /my/time-tracking" do + context "when requesting on a non mobile device" do + before do + allow(controller).to receive(:mobile?).and_return(false) + end + + context "and tracking start and end times is enabled" do + before do + allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(true) + end + + it "redirects to the week calendar view" do + get :calendar + expect(response).to redirect_to(action: :week, view_mode: "calendar") + end + end + + context "and tracking start and end times is disabled" do + before do + allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(false) + end + + it "redirects to the week list view" do + get :calendar + expect(response).to redirect_to(action: :week, view_mode: "list") + end + end + end + + context "when requesting on a mobile device" do + before do + allow(controller).to receive(:mobile?).and_return(true) + end + + context "and tracking start and end times is enabled" do + before do + allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(true) + end + + it "redirects to the day calendar view" do + get :calendar + expect(response).to redirect_to(action: :day, view_mode: "calendar") + end + end + + context "and tracking start and end times is disabled" do + before do + allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(false) + end + + it "redirects to the day list view" do + get :calendar + expect(response).to redirect_to(action: :day, view_mode: "list") + end + end + end + end +end From 76e1062e1e0cf597b7f7b24585b1d0be1732e8b0 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Wed, 9 Apr 2025 15:55:51 +0200 Subject: [PATCH 61/88] start with feature tests --- .../my/time_tracking/daily_entries_table.rb | 16 ++--- .../my/time_tracking/time_entry_row.rb | 2 +- .../my/time_tracking_controller.rb | 6 ++ modules/costs/config/locales/en.yml | 2 + .../spec/features/my_time_tracking_spec.rb | 70 +++++++++++++++++++ 5 files changed, 87 insertions(+), 9 deletions(-) create mode 100644 modules/costs/spec/features/my_time_tracking_spec.rb diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb index cdcdb70162c..8c4f9d4b1b0 100644 --- a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb +++ b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb @@ -53,19 +53,19 @@ module My tooltip_direction: :e, href: dialog_time_entries_path(onlyMe: true, date: options[:date]), data: { "turbo-stream" => true }, - label: t("label_log_time"), - aria: { label: t("label_log_time") } + label: t("button_log_time"), + aria: { label: t("button_log_time") } )) end def headers [ - TimeEntry.can_track_start_and_end_time? ? [:time, { caption: "Time" }] : nil, - [:hours, { caption: "Hours" }], - [:subject, { caption: "Subject" }], - [:project, { caption: "Project" }], - [:activity, { caption: "Activity" }], - [:comments, { caption: "Comments" }] + TimeEntry.can_track_start_and_end_time? ? [:time, { caption: TimeEntry.human_attribute_name(:time) }] : nil, + [:hours, { caption: TimeEntry.human_attribute_name(:hours) }], + [:subject, { caption: TimeEntry.human_attribute_name(:subject) }], + [:project, { caption: TimeEntry.human_attribute_name(:project) }], + [:activity, { caption: TimeEntry.human_attribute_name(:activity) }], + [:comments, { caption: TimeEntry.human_attribute_name(:comments) }] ].compact end diff --git a/modules/costs/app/components/my/time_tracking/time_entry_row.rb b/modules/costs/app/components/my/time_tracking/time_entry_row.rb index d85fa6568ef..0ddcfdfe3a1 100644 --- a/modules/costs/app/components/my/time_tracking/time_entry_row.rb +++ b/modules/costs/app/components/my/time_tracking/time_entry_row.rb @@ -34,7 +34,7 @@ module My def button_links [ action_menu - ].compact + ] end def action_menu diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 2ed409eb2d7..dfc83cc71f7 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -136,5 +136,11 @@ module My def mobile? browser.device.mobile? end + + def default_breadcrumb; end + + def show_local_breadcrumb + false + end end end diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index 6e0d3bf9f4c..8fa28fb6c10 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -59,12 +59,14 @@ en: project: Project user: User work_package: Work package + subject: Subject hours: Hours comments: Comment activity: Activity spent_on: Date start_time: Start time end_time: Finish time + time: Time models: time_entry: one: "Time entry" diff --git a/modules/costs/spec/features/my_time_tracking_spec.rb b/modules/costs/spec/features/my_time_tracking_spec.rb new file mode 100644 index 00000000000..3bad31bc4d4 --- /dev/null +++ b/modules/costs/spec/features/my_time_tracking_spec.rb @@ -0,0 +1,70 @@ +# frozen_string_literal: true + +#-- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +#++ + +require_relative "../spec_helper" + +RSpec.describe "my time tracking", :js do + let(:time_zone) { "Etc/UTC" } + let(:user) do + create(:user, + preferences: { time_zone: }, + member_with_permissions: { + project1 => %i[view_project view_time_entries log_own_time edit_own_time_entries], + project2 => %i[view_project view_time_entries log_own_time] + }) + end + + let(:project1) { create(:project_with_types) } + let(:project2) { create(:project_with_types) } + let(:work_package1) { create(:work_package, project: project1) } + let(:work_package2) { create(:work_package, project: project2) } + let!(:time_entry1) { create(:time_entry, user:, work_package: work_package1, spent_on: "2025-04-07", hours: 6.5) } + let!(:time_entry2) do + create(:time_entry, work_package: work_package1, user:, spent_on: "2025-04-09", hours: 1.0, start_time: (9 * 60) + 30, + time_zone:) + end + let!(:time_entry3) { create(:time_entry, user:, work_package: work_package1, spent_on: "2025-04-14", hours: 3.0) } + let!(:time_entry4) { create(:time_entry, user:, work_package: work_package2, spent_on: "2025-04-09", hours: 2.0) } + let!(:time_entry5) do + create(:time_entry, user:, work_package: work_package2, spent_on: "2025-04-09", hours: 1.0, start_time: (13 * 60) + 30, + time_zone:) + end + let!(:time_entry6) { create(:time_entry, user:, work_package: work_package2, spent_on: "2025-04-14", hours: 2.0) } + + before do + login_as user + end + + it "does something" do + allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(true) + visit my_time_tracking_week_path(date: "2025-04-09", view_mode: "list") + # save_and_open_screenshot + end +end From a10a1754169499db4d46c953d6f333f6212d8f0f Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 10 Apr 2025 14:19:07 +0200 Subject: [PATCH 62/88] remove extra info from the heading for now --- .../app/components/my/time_tracking/list_component.rb | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 4e1fea3c00c..02036020861 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -66,7 +66,7 @@ module My DurationConverter.output(total_hours, format: :hours_and_minutes) end - def date_title(date) # rubocop:disable Metrics/AbcSize + def date_title(date) result = [ render(Primer::Beta::Text.new) { date.strftime("%A %d") } ] @@ -77,13 +77,6 @@ module My result << render(Primer::Beta::Text.new(color: :muted)) { t("label_yesterday") } end - result << render(Primer::Beta::Text.new(color: :muted)) do - count = time_entries_by_day[date].count - "#{count} #{TimeEntry.model_name.human(count: count)}" - end - - result << render(Primer::Beta::Text.new) { total_hours_per_day(date) } - safe_join(result, " ") end end From 7a94fe97f891366e0e24d791734d290a04dc7bc4 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 10 Apr 2025 14:29:55 +0200 Subject: [PATCH 63/88] switch calendar view mode --- .../time_tracking/calendar_component.html.erb | 2 +- .../time_tracking/header_component.html.erb | 25 +++++----- .../my/time_tracking/header_component.rb | 47 +++++++++++++++++-- .../my/time_tracking/list_component.html.erb | 2 +- .../my/time_tracking_controller.rb | 34 +++++++------- modules/costs/config/locales/en.yml | 2 + modules/costs/lib/costs/engine.rb | 34 +++++++------- 7 files changed, 96 insertions(+), 50 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index 55b8708b9f6..f5db518556a 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -1,5 +1,5 @@ <%= flex_layout(data: wrapper_data, classes: "time-entry-calendar") do |calendar_page| %> -<% calendar_page.with_row { render(My::TimeTracking::HeaderComponent.new(mode: mode, date: date)) } %> +<% calendar_page.with_row { render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :calendar)) } %> <% calendar_page.with_row do %> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_text { title } diff --git a/modules/costs/app/components/my/time_tracking/header_component.html.erb b/modules/costs/app/components/my/time_tracking/header_component.html.erb index 22696b5ca1b..0201cb40011 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/header_component.html.erb @@ -7,21 +7,24 @@ ] ) - cal_button = lambda do |button| - button.with_leading_visual_icon(icon: :calendar) - button.with_trailing_action_icon(icon: :"triangle-down") - t(:label_calendar) - end - - list_button = lambda do |button| - button.with_leading_visual_icon(icon: "list-unordered") - button.with_trailing_action_icon(icon: :"triangle-down") - t(:label_list) + header.with_action_menu( + menu_arguments: { anchor_align: :end }, + button_arguments: { button_block: calendar_mode_button_block } + ) do |menu| + %i[day week month].each do |action| + menu.with_item( + label: t("label_#{action}"), + tag: :a, + href: url_for(params.permit(:controller, :date, :view_mode).merge(action:)) + ) do |item| + item.with_leading_visual_icon(icon: :calendar) + end + end end header.with_action_menu( menu_arguments: { anchor_align: :end }, - button_arguments: { button_block: params[:view_mode] == "list" ? list_button : cal_button } + button_arguments: { button_block: view_mode_block } ) do |menu| menu.with_item( label: t(:label_calendar), diff --git a/modules/costs/app/components/my/time_tracking/header_component.rb b/modules/costs/app/components/my/time_tracking/header_component.rb index 34a79fdc6eb..4bd7f0b302d 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.rb +++ b/modules/costs/app/components/my/time_tracking/header_component.rb @@ -31,7 +31,7 @@ module My module TimeTracking class HeaderComponent < ApplicationComponent - options :date, :mode + options :date, :mode, :view_mode def title case mode @@ -58,10 +58,49 @@ module My end def month_title - if Date.current == date - I18n.t(:label_today) + if Date.current.all_month.include?(date) + I18n.t(:label_this_month) else - I18n.t(:label_specific_day, day: I18n.l(date, format: :short)) + I18n.t(:label_specific_month, month: I18n.l(date, format: "%B %Y")) + end + end + + def view_mode_block + if view_mode == :list + lambda do |button| + button.with_leading_visual_icon(icon: "list-unordered") + button.with_trailing_action_icon(icon: "triangle-down") + t(:label_list) + end + else + lambda do |button| + button.with_leading_visual_icon(icon: :calendar) + button.with_trailing_action_icon(icon: "triangle-down") + t(:label_calendar) + end + end + end + + def calendar_mode_button_block + case mode + when :day + lambda do |button| + button.with_leading_visual_icon(icon: :calendar) + button.with_trailing_action_icon(icon: "triangle-down") + t(:label_day) + end + when :week + lambda do |button| + button.with_leading_visual_icon(icon: :calendar) + button.with_trailing_action_icon(icon: "triangle-down") + t(:label_week) + end + when :month + lambda do |button| + button.with_leading_visual_icon(icon: :calendar) + button.with_trailing_action_icon(icon: "triangle-down") + t(:label_month) + end end end end diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index e30bd310c6c..cdc2790dd98 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -1,5 +1,5 @@ <%= component_wrapper(tag: "div", data: wrapper_data) do %> - <%= render(My::TimeTracking::HeaderComponent.new(mode: mode, date: date)) %> + <%= render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :list)) %> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_text { title } diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index dfc83cc71f7..1f799f29d77 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -34,21 +34,11 @@ module My no_authorization_required!(:calendar, :day, :week, :month) - current_menu_item do |ctrl| - if ctrl.params[:action] == "day" && ctrl.today? - :my_time_tracking_today - elsif ctrl.params[:action] == "week" && ctrl.this_week? - :my_time_tracking_this_week - elsif ctrl.params[:action] == "month" && ctrl.this_month? - :my_time_tracking_this_month - else - :my_time_tracking - end - end + menu_item :my_time_tracking layout "global" - helper_method :current_day, :today?, :this_week?, :this_month?, :list_view_component + helper_method :current_day, :today?, :this_week?, :this_month?, :list_view_component, :view_mode def calendar if mobile? @@ -107,7 +97,11 @@ module My end def view_mode - ActiveSupport::StringInquirer.new(params[:view_mode] || default_view_mode) + if ["calendar", "list"].include?(params[:view_mode]) + params[:view_mode] + else + default_view_mode + end.to_sym end def current_date @@ -126,10 +120,18 @@ module My end def list_view_component - if view_mode.list? - My::TimeTracking::ListComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) + if view_mode == :list + My::TimeTracking::ListComponent.new( + time_entries: @time_entries, + mode: params[:action].to_sym, + date: current_day + ) else - My::TimeTracking::CalendarComponent.new(time_entries: @time_entries, mode: params[:action].to_sym, date: current_day) + My::TimeTracking::CalendarComponent.new( + time_entries: @time_entries, + mode: params[:action].to_sym, + date: current_day + ) end end diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index 8fa28fb6c10..7b688e0302a 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -178,6 +178,8 @@ en: label_specific_week: "Week #%{week}" label_specific_month: "Month %{month}" label_list: "List" + label_month: "Month" + label_day: "Day" placeholder_activity_select_work_package_first: Work package selection is required first diff --git a/modules/costs/lib/costs/engine.rb b/modules/costs/lib/costs/engine.rb index 5366b3c45bd..111ab738a03 100644 --- a/modules/costs/lib/costs/engine.rb +++ b/modules/costs/lib/costs/engine.rb @@ -149,23 +149,23 @@ module Costs caption: :label_my_time_tracking, icon: :stopwatch - menu :global_menu, - :my_time_tracking_today, - { controller: "/my/time_tracking", action: "day" }, - parent: :my_time_tracking, - caption: :label_today - - menu :global_menu, - :my_time_tracking_this_week, - { controller: "/my/time_tracking", action: "week" }, - parent: :my_time_tracking, - caption: :label_this_week - - menu :global_menu, - :my_time_tracking_this_month, - { controller: "/my/time_tracking", action: "month" }, - parent: :my_time_tracking, - caption: :label_this_month + # menu :global_menu, + # :my_time_tracking_today, + # { controller: "/my/time_tracking", action: "day" }, + # parent: :my_time_tracking, + # caption: :label_today + # + # menu :global_menu, + # :my_time_tracking_this_week, + # { controller: "/my/time_tracking", action: "week" }, + # parent: :my_time_tracking, + # caption: :label_this_week + # + # menu :global_menu, + # :my_time_tracking_this_month, + # { controller: "/my/time_tracking", action: "month" }, + # parent: :my_time_tracking, + # caption: :label_this_month end initializer "costs.settings" do From 05981940a8c471caf38e5b047db7d599182b4b5f Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Thu, 10 Apr 2025 15:46:26 +0200 Subject: [PATCH 64/88] Use a SegmentedControl to switch views --- .../time_tracking/calendar_component.html.erb | 15 ++++++++++++ .../time_tracking/header_component.html.erb | 17 +------------- .../my/time_tracking/header_component.rb | 23 ------------------- 3 files changed, 16 insertions(+), 39 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index f5db518556a..73843a75584 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -2,6 +2,21 @@ <% calendar_page.with_row { render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :calendar)) } %> <% calendar_page.with_row do %> <%= render(Primer::OpenProject::SubHeader.new) do |component| + component.with_filter_component do + render(Primer::Alpha::SegmentedControl.new("aria-label": I18n.t(:label_meeting_date_time))) do |control| + %i[day week month].each do |action| + control.with_item( + tag: :a, + href: url_for(params.permit(:controller, :date).merge(action:, view_mode: "calendar")), + icon: "calendar", # TODO: Replace with proper icon + label: t("label_#{action}"), + title: t("label_#{action}"), + selected: (mode == action) + ) + end + end + end + component.with_text { title } component.with_action_component do diff --git a/modules/costs/app/components/my/time_tracking/header_component.html.erb b/modules/costs/app/components/my/time_tracking/header_component.html.erb index 0201cb40011..0c620795ded 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/header_component.html.erb @@ -7,21 +7,6 @@ ] ) - header.with_action_menu( - menu_arguments: { anchor_align: :end }, - button_arguments: { button_block: calendar_mode_button_block } - ) do |menu| - %i[day week month].each do |action| - menu.with_item( - label: t("label_#{action}"), - tag: :a, - href: url_for(params.permit(:controller, :date, :view_mode).merge(action:)) - ) do |item| - item.with_leading_visual_icon(icon: :calendar) - end - end - end - header.with_action_menu( menu_arguments: { anchor_align: :end }, button_arguments: { button_block: view_mode_block } @@ -36,7 +21,7 @@ menu.with_item( label: t(:label_list), tag: :a, - href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "list")) + href: url_for(params.permit(:controller, :date).merge(view_mode: "list", action: "week")) ) do |item| item.with_leading_visual_icon(icon: "list-unordered") end diff --git a/modules/costs/app/components/my/time_tracking/header_component.rb b/modules/costs/app/components/my/time_tracking/header_component.rb index 4bd7f0b302d..3b3adb37f79 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.rb +++ b/modules/costs/app/components/my/time_tracking/header_component.rb @@ -80,29 +80,6 @@ module My end end end - - def calendar_mode_button_block - case mode - when :day - lambda do |button| - button.with_leading_visual_icon(icon: :calendar) - button.with_trailing_action_icon(icon: "triangle-down") - t(:label_day) - end - when :week - lambda do |button| - button.with_leading_visual_icon(icon: :calendar) - button.with_trailing_action_icon(icon: "triangle-down") - t(:label_week) - end - when :month - lambda do |button| - button.with_leading_visual_icon(icon: :calendar) - button.with_trailing_action_icon(icon: "triangle-down") - t(:label_month) - end - end - end end end end From 806a2bd0ae73b51ddc304eb03f1a7faed9c19790 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 11 Apr 2025 08:56:54 +0200 Subject: [PATCH 65/88] fix footer layout of calendar --- .../controllers/dynamic/my/time-tracking.controller.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 061c9723990..5621b8cb6d9 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -207,14 +207,6 @@ export default class MyTimeTrackingController extends Controller { if (!calendarScrollGridWrapper) return; - // Add footer dividier if it doesn't exist - if (calendarScrollGridWrapper.querySelector('.fc-timegrid-footer-divider') === null) { - calendarScrollGridWrapper.insertAdjacentHTML( - 'beforeend', - '', - ); - } - // Remove existing footer if it exists const existingFooter = document.querySelector('.fc-timegrid-footer-totals'); if (existingFooter) { existingFooter.remove(); } @@ -251,7 +243,7 @@ export default class MyTimeTrackingController extends Controller { buildHtmlFooter(days:string[]):HTMLTableRowElement { const tr = document.createElement('tr'); tr.setAttribute('role', 'presentation'); - tr.className = 'fc-scrollgrid-section fc-scrollgrid-section-footer fc-timegrid-footer-totals'; + tr.className = 'fc-scrollgrid-section fc-timegrid-footer-totals'; const td = document.createElement('td'); td.setAttribute('role', 'presentation'); From 9d3348a6fa48f807e03515487e0b84ad36f77587 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 11 Apr 2025 11:44:31 +0200 Subject: [PATCH 66/88] Introduce a ModeSwitcherComponent and add it to the list component --- .../time_tracking/calendar_component.html.erb | 13 +---- .../time_tracking/header_component.html.erb | 2 +- .../my/time_tracking/list_component.html.erb | 4 ++ .../time_tracking/mode_switcher_component.rb | 53 +++++++++++++++++++ modules/costs/config/locales/en.yml | 2 +- 5 files changed, 60 insertions(+), 14 deletions(-) create mode 100644 modules/costs/app/components/my/time_tracking/mode_switcher_component.rb diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index 73843a75584..ddbeadf9292 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -3,18 +3,7 @@ <% calendar_page.with_row do %> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_filter_component do - render(Primer::Alpha::SegmentedControl.new("aria-label": I18n.t(:label_meeting_date_time))) do |control| - %i[day week month].each do |action| - control.with_item( - tag: :a, - href: url_for(params.permit(:controller, :date).merge(action:, view_mode: "calendar")), - icon: "calendar", # TODO: Replace with proper icon - label: t("label_#{action}"), - title: t("label_#{action}"), - selected: (mode == action) - ) - end - end + render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: :calendar)) end component.with_text { title } diff --git a/modules/costs/app/components/my/time_tracking/header_component.html.erb b/modules/costs/app/components/my/time_tracking/header_component.html.erb index 0c620795ded..27bcf1636f2 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/header_component.html.erb @@ -21,7 +21,7 @@ menu.with_item( label: t(:label_list), tag: :a, - href: url_for(params.permit(:controller, :date).merge(view_mode: "list", action: "week")) + href: url_for(params.permit(:controller, :date, :action).merge(view_mode: "list")) ) do |item| item.with_leading_visual_icon(icon: "list-unordered") end diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index cdc2790dd98..42edbec2327 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -1,6 +1,10 @@ <%= component_wrapper(tag: "div", data: wrapper_data) do %> <%= render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :list)) %> <%= render(Primer::OpenProject::SubHeader.new) do |component| + component.with_filter_component do + render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: :calendar)) + end + component.with_text { title } component.with_action_component do diff --git a/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb b/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb new file mode 100644 index 00000000000..81e1170cd4f --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module My + module TimeTracking + class ModeSwitcherComponent < ApplicationComponent + options :current_mode, + :view_mode + + def call + render(Primer::Alpha::SegmentedControl.new("aria-label": I18n.t(:label_meeting_date_time))) do |control| + %i[day week month].each do |action| + control.with_item( + tag: :a, + href: url_for(params.permit(:controller, :date).merge(action:, view_mode: view_mode)), + icon: "calendar", # TODO: Replace with proper icon + label: t("label_#{action}"), + title: t("label_#{action}"), + selected: (current_mode == action) + ) + end + end + end + end + end +end diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index 7b688e0302a..8d68c796eea 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -175,7 +175,7 @@ en: label_previous_week: "Previous week" label_previous_month: "Previous month" label_specific_day: "Day %{day}" - label_specific_week: "Week #%{week}" + label_specific_week: "Calendar week %{week}" label_specific_month: "Month %{month}" label_list: "List" label_month: "Month" From 1a9e8ad81b71a3fb88acaa5c5ceed39074591d6b Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 11 Apr 2025 11:46:25 +0200 Subject: [PATCH 67/88] For the month-list-view, group by week and not date --- .../my/time_tracking/daily_entries_table.rb | 5 ++- .../my/time_tracking/list_component.html.erb | 2 +- .../my/time_tracking/list_component.rb | 40 ++++++++++++------- .../my/time_tracking/time_entry_row.rb | 4 ++ 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb index 8c4f9d4b1b0..8c6f9d80c56 100644 --- a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb +++ b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb @@ -31,7 +31,7 @@ module My module TimeTracking class DailyEntriesTable < OpPrimer::BorderBoxTableComponent - columns :time, :hours, :subject, :project, :activity, :comments + columns :spent_on, :time, :hours, :subject, :project, :activity, :comments main_column :time, :subject, :project def row_class @@ -60,6 +60,7 @@ module My def headers [ + options[:mode] == :month ? [:spent_on, { caption: TimeEntry.human_attribute_name(:spent_on) }] : nil, TimeEntry.can_track_start_and_end_time? ? [:time, { caption: TimeEntry.human_attribute_name(:time) }] : nil, [:hours, { caption: TimeEntry.human_attribute_name(:hours) }], [:subject, { caption: TimeEntry.human_attribute_name(:subject) }], @@ -72,6 +73,8 @@ module My def skip_column?(column) if column == :time !TimeEntry.can_track_start_and_end_time? + elsif column == :spent_on + options[:mode] != :month else false end diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index 42edbec2327..34ac3c07df1 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -30,6 +30,6 @@ <%= render(Primer::OpenProject::Heading.new(tag: :h2, classes: class_names("mt-3" => !i.zero?))) do date_title(date) end %> - <%= render(My::TimeTracking::DailyEntriesTable.new(rows: time_entries_by_day[date], date: date)) %> + <%= render(My::TimeTracking::DailyEntriesTable.new(rows: grouped_time_entries[date], date: date, mode: mode)) %> <%- end %> <%- end %> diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 02036020861..2ff0450c4d3 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -51,33 +51,43 @@ module My case mode when :day then [date] when :week then date.all_week - when :month then date.all_month + when :month then date.all_month.map(&:beginning_of_week).uniq end end - def time_entries_by_day - @time_entries_by_day ||= time_entries.group_by(&:spent_on).tap do |hash| - hash.default_proc = ->(h, k) { h[k] = [] } - end + def grouped_time_entries + @grouped_time_entries ||= time_entries + .group_by { |entry| mode == :month ? entry.spent_on.beginning_of_week : entry.spent_on } + .tap do |hash| + hash.default_proc = ->(h, k) { h[k] = [] } + end end - def total_hours_per_day(date) + def total_hours_for(date) total_hours = time_entries_by_day[date].sum(&:hours).round(2) DurationConverter.output(total_hours, format: :hours_and_minutes) end def date_title(date) - result = [ - render(Primer::Beta::Text.new) { date.strftime("%A %d") } - ] - - if date.today? - result << render(Primer::Beta::Text.new(color: :muted)) { t("label_today") } - elsif date.yesterday? - result << render(Primer::Beta::Text.new(color: :muted)) { t("label_yesterday") } + if mode == :month + I18n.t(:label_specific_week, week: date.strftime("%W %Y")) + else + date.strftime("%A %d") end + end - safe_join(result, " ") + def date_additional_info(date) + if mode == :month + if Date.current.beginning_of_week == date + t(:label_this_week) + elsif 1.week.ago.beginning_of_week == date + t(:label_last_week) + end + elsif date.today? + t(:label_today) + elsif date.yesterday? + t(:label_yesterday) + end end end end diff --git a/modules/costs/app/components/my/time_tracking/time_entry_row.rb b/modules/costs/app/components/my/time_tracking/time_entry_row.rb index 0ddcfdfe3a1..a5e604343a5 100644 --- a/modules/costs/app/components/my/time_tracking/time_entry_row.rb +++ b/modules/costs/app/components/my/time_tracking/time_entry_row.rb @@ -57,6 +57,10 @@ module My end end + def spent_on + I18n.l(time_entry.spent_on) + end + def time # rubocop:disable Metrics/AbcSize return if time_entry.start_time.blank? From fb94c77b98757d1c5adeaf95b9417bee0a4db217 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 11 Apr 2025 11:59:28 +0200 Subject: [PATCH 68/88] specify a min-height for the no specific time column --- modules/costs/app/components/my/time_tracking/calendar.sass | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/costs/app/components/my/time_tracking/calendar.sass b/modules/costs/app/components/my/time_tracking/calendar.sass index 076c5ad7a26..d617c69559c 100644 --- a/modules/costs/app/components/my/time_tracking/calendar.sass +++ b/modules/costs/app/components/my/time_tracking/calendar.sass @@ -18,6 +18,9 @@ color: var(--body-font-color) font-size: var(--body-font-size) + .fc-scrollgrid-sync-table + min-height: 80px + .fc-popover background: var(--fc-page-bg-color) !important From cf7fd88e2aefa9df5d675a693f6ec44be38a35bc Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Fri, 11 Apr 2025 14:46:38 +0200 Subject: [PATCH 69/88] do not show the plus button for the month list view --- .../app/components/my/time_tracking/daily_entries_table.rb | 2 ++ .../app/components/my/time_tracking/list_component.html.erb | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb index 8c6f9d80c56..903e285751f 100644 --- a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb +++ b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb @@ -45,6 +45,8 @@ module My def has_actions? = true def action_row_header_content + return if options[:mode] == :month + render(Primer::Beta::IconButton.new( icon: "plus", scheme: :invisible, diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index 34ac3c07df1..49f20518569 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -2,7 +2,7 @@ <%= render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :list)) %> <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_filter_component do - render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: :calendar)) + render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: :list)) end component.with_text { title } From 58e90be85e4ee54fdc61a9ced7b1d905431dfe82 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 08:57:54 +0200 Subject: [PATCH 70/88] Use proper icons --- .../my/time_tracking/mode_switcher_component.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb b/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb index 81e1170cd4f..429a6b85ac6 100644 --- a/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb +++ b/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb @@ -40,7 +40,7 @@ module My control.with_item( tag: :a, href: url_for(params.permit(:controller, :date).merge(action:, view_mode: view_mode)), - icon: "calendar", # TODO: Replace with proper icon + icon: icon_for_mode(action), label: t("label_#{action}"), title: t("label_#{action}"), selected: (current_mode == action) @@ -48,6 +48,17 @@ module My end end end + + def icon_for_mode(mode) + case mode + when :day + "op-calendar-day" + when :week + "op-calendar-week" + else + "calendar" + end + end end end end From d134e518c095971a944d7be16a414836670c8b57 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 09:25:38 +0200 Subject: [PATCH 71/88] fix translation for my page in main menu --- config/initializers/menus.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/initializers/menus.rb b/config/initializers/menus.rb index c0c41966505..18519a9a37f 100644 --- a/config/initializers/menus.rb +++ b/config/initializers/menus.rb @@ -135,7 +135,8 @@ Redmine::MenuManager.map :global_menu do |menu| menu.push :my_page, { controller: "/my/page", action: "show" }, after: :home, - icon: "person" + icon: "person", + caption: I18n.t("my_page.label") # Projects menu.push :projects, From a02b99bf171f8f5dbfc737b67d91a7963f0fb99e Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 09:53:22 +0200 Subject: [PATCH 72/88] show weekend in darker gray --- .../stimulus/controllers/dynamic/my/time-tracking.controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 5621b8cb6d9..7a1e1397796 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -65,6 +65,7 @@ export default class MyTimeTrackingController extends Controller { eventMaxStack: 2, eventShortHeight: 31, nowIndicator: true, + businessHours: { daysOfWeek: [1, 2, 3, 4, 5], startTime: '00:00', endTime: '24:00' }, eventClassNames(arg) { return [ 'calendar-time-entry-event', From 108a21ad4631fa6f1fb8e70e9a65a2f4a86ab8da Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 12:20:35 +0200 Subject: [PATCH 73/88] remove text for allDay column --- .../controllers/dynamic/my/time-tracking.controller.ts | 5 +++-- modules/costs/app/components/my/time_tracking/calendar.sass | 3 --- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 7a1e1397796..7a012a6a71f 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -59,7 +59,7 @@ export default class MyTimeTrackingController extends Controller { editable: this.canEditValue, eventResizableFromStart: true, defaultTimedEventDuration: DEFAULT_TIMED_EVENT_DURATION, - allDayContent: I18n.t('js.myTimeTracking.noSpecificTime'), + allDayContent: '', dayMaxEventRows: 4, // 3 + more link eventMinHeight: 30, eventMaxStack: 2, @@ -262,7 +262,8 @@ export default class MyTimeTrackingController extends Controller { const colgroup = document.createElement('colgroup'); const col = document.createElement('col'); - col.style.width = '61px'; + const otherCol = document.querySelector('.fc-scrollgrid-section-header .fc-col-header col') as HTMLElement; + col.style.width = otherCol?.style?.width; const tbody = document.createElement('tbody'); tbody.setAttribute('role', 'presentation'); diff --git a/modules/costs/app/components/my/time_tracking/calendar.sass b/modules/costs/app/components/my/time_tracking/calendar.sass index d617c69559c..076c5ad7a26 100644 --- a/modules/costs/app/components/my/time_tracking/calendar.sass +++ b/modules/costs/app/components/my/time_tracking/calendar.sass @@ -18,9 +18,6 @@ color: var(--body-font-color) font-size: var(--body-font-size) - .fc-scrollgrid-sync-table - min-height: 80px - .fc-popover background: var(--fc-page-bg-color) !important From 6f0a331bdd6f41c5287dc1f4516a859810f7ba1a Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 12:21:35 +0200 Subject: [PATCH 74/88] Better labels --- modules/costs/app/components/my/time_tracking/list_component.rb | 2 +- modules/costs/config/locales/en.yml | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 2ff0450c4d3..9f74645253b 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -70,7 +70,7 @@ module My def date_title(date) if mode == :month - I18n.t(:label_specific_week, week: date.strftime("%W %Y")) + I18n.t(:label_specific_week, week: date.strftime("%W")) else date.strftime("%A %d") end diff --git a/modules/costs/config/locales/en.yml b/modules/costs/config/locales/en.yml index 8d68c796eea..f27e8a5b660 100644 --- a/modules/costs/config/locales/en.yml +++ b/modules/costs/config/locales/en.yml @@ -180,6 +180,7 @@ en: label_list: "List" label_month: "Month" label_day: "Day" + label_today_capitalized: "Today" placeholder_activity_select_work_package_first: Work package selection is required first From 1bc09d6aa5e4f0eb6386d47f0d8fade2bca5d085 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 12:25:57 +0200 Subject: [PATCH 75/88] Correct page header and extract redundant sub header --- .../time_tracking/calendar_component.html.erb | 31 ++---------------- .../my/time_tracking/calendar_component.rb | 1 - .../time_tracking/header_component.html.erb | 6 ++-- .../my/time_tracking/header_component.rb | 32 ------------------- .../my/time_tracking/list_component.html.erb | 26 +-------------- .../my/time_tracking/list_component.rb | 1 - .../sub_header_component.html.erb | 30 +++++++++++++++++ ...d_component.rb => sub_header_component.rb} | 7 ++-- 8 files changed, 40 insertions(+), 94 deletions(-) create mode 100644 modules/costs/app/components/my/time_tracking/sub_header_component.html.erb rename modules/costs/app/components/my/time_tracking/{shared_component.rb => sub_header_component.rb} (97%) diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index ddbeadf9292..618580688ef 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -1,33 +1,6 @@ <%= flex_layout(data: wrapper_data, classes: "time-entry-calendar") do |calendar_page| %> -<% calendar_page.with_row { render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :calendar)) } %> -<% calendar_page.with_row do %> - <%= render(Primer::OpenProject::SubHeader.new) do |component| - component.with_filter_component do - render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: :calendar)) - end - - component.with_text { title } - - component.with_action_component do - render(Primer::Beta::ButtonGroup.new) do |group| - group.with_button(icon: "arrow-left", tag: :a, **previous_attrs) - group.with_button(icon: "arrow-right", tag: :a, **next_attrs) - end - end - - component.with_action_button(tag: :a, href: today_href) { I18n.t(:label_today) } - - component.with_action_button( - scheme: :primary, - data: { - "turbo-stream" => true - }, - tag: :a, - href: dialog_time_entries_path(onlyMe: true) - ) { t(:button_log_time) } - end %> - <% end %> - + <% calendar_page.with_row { render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :calendar)) } %> + <% calendar_page.with_row { render(My::TimeTracking::SubHeaderComponent.new(mode:, date:, view_mode: :calendar)) } %> <% calendar_page.with_row(classes: "op-fc-wrapper", data: { "my--time-tracking-target" => "calendar" }) %> <% if mode != :day %> diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb index 4a470fe99a5..f502d4d1ef7 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.rb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -33,7 +33,6 @@ module My class CalendarComponent < ApplicationComponent include OpTurbo::Streamable include OpPrimer::ComponentHelpers - include SharedComponent options time_entries: [], mode: :week, diff --git a/modules/costs/app/components/my/time_tracking/header_component.html.erb b/modules/costs/app/components/my/time_tracking/header_component.html.erb index 27bcf1636f2..dc850a30bad 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/header_component.html.erb @@ -1,9 +1,9 @@ <%= render(Primer::OpenProject::PageHeader.new) do |header| - header.with_title(variant: :large) { title } + header.with_title { I18n.t(:label_my_time_tracking) } header.with_breadcrumbs( [ - { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path }, - title + { href: home_path, text: helpers.organization_name }, + { text: I18n.t(:label_my_time_tracking), href: my_time_tracking_path } ] ) diff --git a/modules/costs/app/components/my/time_tracking/header_component.rb b/modules/costs/app/components/my/time_tracking/header_component.rb index 3b3adb37f79..91f70a3a9d9 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.rb +++ b/modules/costs/app/components/my/time_tracking/header_component.rb @@ -33,38 +33,6 @@ module My class HeaderComponent < ApplicationComponent options :date, :mode, :view_mode - def title - case mode - when :week then week_title - when :month then month_title - when :day then day_title - end - end - - def day_title - if Date.current == date - I18n.t(:label_today) - else - I18n.t(:label_specific_day, day: I18n.l(date, format: :short)) - end - end - - def week_title - if Date.current.all_week.include?(date) - I18n.t(:label_this_week) - else - I18n.t(:label_specific_week, week: I18n.l(date, format: "%W %Y")) - end - end - - def month_title - if Date.current.all_month.include?(date) - I18n.t(:label_this_month) - else - I18n.t(:label_specific_month, month: I18n.l(date, format: "%B %Y")) - end - end - def view_mode_block if view_mode == :list lambda do |button| diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index 49f20518569..b9988e90c71 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -1,30 +1,6 @@ <%= component_wrapper(tag: "div", data: wrapper_data) do %> <%= render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :list)) %> - <%= render(Primer::OpenProject::SubHeader.new) do |component| - component.with_filter_component do - render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: :list)) - end - - component.with_text { title } - - component.with_action_component do - render(Primer::Beta::ButtonGroup.new) do |group| - group.with_button(icon: "arrow-left", tag: :a, **previous_attrs) - group.with_button(icon: "arrow-right", tag: :a, **next_attrs) - end - end - - component.with_action_button(tag: :a, href: today_href) { t(:label_today) } - - component.with_action_button( - scheme: :primary, - data: { - "turbo-stream" => true - }, - tag: :a, - href: dialog_time_entries_path(onlyMe: true) - ) { t(:button_log_time) } - end %> + <%= render(My::TimeTracking::SubHeaderComponent.new(mode:, date:, view_mode: :list)) %> <%- range.each_with_index do |date, i| %> <%= render(Primer::OpenProject::Heading.new(tag: :h2, classes: class_names("mt-3" => !i.zero?))) do diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 9f74645253b..fb94d9ba26f 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -32,7 +32,6 @@ module My module TimeTracking class ListComponent < ApplicationComponent include OpTurbo::Streamable - include SharedComponent options time_entries: [], mode: :week, diff --git a/modules/costs/app/components/my/time_tracking/sub_header_component.html.erb b/modules/costs/app/components/my/time_tracking/sub_header_component.html.erb new file mode 100644 index 00000000000..f3cbceefe02 --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/sub_header_component.html.erb @@ -0,0 +1,30 @@ +<%= render(Primer::OpenProject::SubHeader.new) do |component| + component.with_filter_component do + render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: view_mode)) + end + + component.with_text { title } + + component.with_action_component do + render(Primer::Beta::ButtonGroup.new) do |group| + group.with_button(icon: "arrow-left", tag: :a, **previous_attrs) + group.with_button(icon: "arrow-right", tag: :a, **next_attrs) + end + end + + component.with_action_button(tag: :a, href: today_href) do |button| + button.with_leading_visual_icon(icon: "op-calendar-day") + I18n.t(:label_today_capitalized) + end + + component.with_action_button( + scheme: :primary, + data: { "turbo-stream" => true }, + aria: { "label" => t(:button_log_time) }, + tag: :a, + href: dialog_time_entries_path(onlyMe: true) + ) do |button| + button.with_leading_visual_icon(icon: "plus") + t(:button_log_time) + end + end %> diff --git a/modules/costs/app/components/my/time_tracking/shared_component.rb b/modules/costs/app/components/my/time_tracking/sub_header_component.rb similarity index 97% rename from modules/costs/app/components/my/time_tracking/shared_component.rb rename to modules/costs/app/components/my/time_tracking/sub_header_component.rb index 618fd5b40a4..0428a2516a2 100644 --- a/modules/costs/app/components/my/time_tracking/shared_component.rb +++ b/modules/costs/app/components/my/time_tracking/sub_header_component.rb @@ -27,11 +27,12 @@ # # See COPYRIGHT and LICENSE files for more details. # ++ -# + module My module TimeTracking - module SharedComponent - extend ActiveSupport::Concern + class SubHeaderComponent < ApplicationComponent + options :date, :mode, :view_mode + def title # rubocop:disable Metrics/AbcSize case mode when :day From 568710c43a3fc6b552a1c18cfa96b9ceb32fb244 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 14:30:26 +0200 Subject: [PATCH 76/88] use the collapsible section component in the list view --- .../my/time_tracking/list_component.html.erb | 23 +++++++++++++------ .../my/time_tracking/list_component.rb | 15 +++++++++--- 2 files changed, 28 insertions(+), 10 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index b9988e90c71..2ce032fa8ad 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -2,10 +2,19 @@ <%= render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :list)) %> <%= render(My::TimeTracking::SubHeaderComponent.new(mode:, date:, view_mode: :list)) %> - <%- range.each_with_index do |date, i| %> - <%= render(Primer::OpenProject::Heading.new(tag: :h2, classes: class_names("mt-3" => !i.zero?))) do - date_title(date) - end %> - <%= render(My::TimeTracking::DailyEntriesTable.new(rows: grouped_time_entries[date], date: date, mode: mode)) %> - <%- end %> -<%- end %> + <% range.each do |date| %> + <%= render(Primer::OpenProject::CollapsibleSection.new, collapsed: collapsed?(date)) do |section| %> + <% section.with_title { date_title(date) } %> + <% section.with_caption { date_caption(date) } %> + <% section.with_additional_information do %> + <%= + render(Primer::Beta::Text.new(color: :muted)) { "#{date_additional(date)} - " } + + render(Primer::Beta::Text.new) { total_hours_for(date) } + %> + <% end %> + <% section.with_collapsible_content do %> + <%= render(My::TimeTracking::DailyEntriesTable.new(rows: grouped_time_entries[date], date: date, mode: mode)) %> + <% end %> + <% end %> + <% end %> +<% end %> diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index fb94d9ba26f..b7b13d9f206 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -63,8 +63,8 @@ module My end def total_hours_for(date) - total_hours = time_entries_by_day[date].sum(&:hours).round(2) - DurationConverter.output(total_hours, format: :hours_and_minutes) + total_hours = grouped_time_entries[date].sum(&:hours).round(2) + DurationConverter.output(total_hours, format: :hours_and_minutes).presence || "0h" end def date_title(date) @@ -75,7 +75,16 @@ module My end end - def date_additional_info(date) + def date_additional(date) + entries_count = grouped_time_entries[date].size + "#{entries_count} #{TimeEntry.model_name.human(count: entries_count)}" + end + + def collapsed?(_date) + false + end + + def date_caption(date) if mode == :month if Date.current.beginning_of_week == date t(:label_this_week) From 0f9688971354d7bde5af485806946750c77fa02a Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 14:31:03 +0200 Subject: [PATCH 77/88] Rename #calendar method to #index --- .../my/time_tracking_controller.rb | 4 ++-- modules/costs/config/routes.rb | 2 +- modules/costs/lib/costs/engine.rb | 20 +------------------ .../my/time_tracking_controller_spec.rb | 8 ++++---- 4 files changed, 8 insertions(+), 26 deletions(-) diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 1f799f29d77..ad9260c3684 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -32,7 +32,7 @@ module My class TimeTrackingController < ApplicationController before_action :require_login - no_authorization_required!(:calendar, :day, :week, :month) + no_authorization_required!(:index, :day, :week, :month) menu_item :my_time_tracking @@ -40,7 +40,7 @@ module My helper_method :current_day, :today?, :this_week?, :this_month?, :list_view_component, :view_mode - def calendar + def index if mobile? redirect_to action: :day, view_mode: default_view_mode else diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index 29a8ce80b38..642a837e29f 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -50,7 +50,7 @@ Rails.application.routes.draw do namespace "my" do get "/timer" => "timer#show", as: "timers" - get "/time-tracking" => "time_tracking#calendar" + get "/time-tracking" => "time_tracking#index" get "/time-tracking/day(-:view_mode)(/:date)" => "time_tracking#day", as: :time_tracking_day get "/time-tracking/week(-:view_mode)(/:date)" => "time_tracking#week", diff --git a/modules/costs/lib/costs/engine.rb b/modules/costs/lib/costs/engine.rb index 111ab738a03..f62be67afc3 100644 --- a/modules/costs/lib/costs/engine.rb +++ b/modules/costs/lib/costs/engine.rb @@ -144,28 +144,10 @@ module Costs menu :global_menu, :my_time_tracking, - { controller: "/my/time_tracking", action: "calendar" }, + { controller: "/my/time_tracking", action: "index" }, after: :my_page, caption: :label_my_time_tracking, icon: :stopwatch - - # menu :global_menu, - # :my_time_tracking_today, - # { controller: "/my/time_tracking", action: "day" }, - # parent: :my_time_tracking, - # caption: :label_today - # - # menu :global_menu, - # :my_time_tracking_this_week, - # { controller: "/my/time_tracking", action: "week" }, - # parent: :my_time_tracking, - # caption: :label_this_week - # - # menu :global_menu, - # :my_time_tracking_this_month, - # { controller: "/my/time_tracking", action: "month" }, - # parent: :my_time_tracking, - # caption: :label_this_month end initializer "costs.settings" do diff --git a/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb b/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb index 81a144d372d..6fb62ff2e40 100644 --- a/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb +++ b/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb @@ -49,7 +49,7 @@ RSpec.describe My::TimeTrackingController do end it "redirects to the week calendar view" do - get :calendar + get :index expect(response).to redirect_to(action: :week, view_mode: "calendar") end end @@ -60,7 +60,7 @@ RSpec.describe My::TimeTrackingController do end it "redirects to the week list view" do - get :calendar + get :index expect(response).to redirect_to(action: :week, view_mode: "list") end end @@ -77,7 +77,7 @@ RSpec.describe My::TimeTrackingController do end it "redirects to the day calendar view" do - get :calendar + get :index expect(response).to redirect_to(action: :day, view_mode: "calendar") end end @@ -88,7 +88,7 @@ RSpec.describe My::TimeTrackingController do end it "redirects to the day list view" do - get :calendar + get :index expect(response).to redirect_to(action: :day, view_mode: "list") end end From 9302a83eef9d633fdcd5aaeba0a48bbb322ac992 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 14:31:23 +0200 Subject: [PATCH 78/88] Restrict time entries to visible projects --- modules/costs/app/controllers/my/time_tracking_controller.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index ad9260c3684..6111bd14d60 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -115,6 +115,7 @@ module My def load_time_entries(time_scope) @time_entries = TimeEntry .includes(:project, :activity, { work_package: :status }) + .where(project_id: Project.visible.select(:id)) .where(user: User.current, spent_on: time_scope) .order(:spent_on, :start_time, :hours) end From f0ece697898d03293d25cfb03efe38a5b340f5ec Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 14:54:43 +0200 Subject: [PATCH 79/88] Use collapsed for past entries --- .../app/components/my/time_tracking/list_component.html.erb | 2 +- .../costs/app/components/my/time_tracking/list_component.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index 2ce032fa8ad..1d4e6fd5aed 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -3,7 +3,7 @@ <%= render(My::TimeTracking::SubHeaderComponent.new(mode:, date:, view_mode: :list)) %> <% range.each do |date| %> - <%= render(Primer::OpenProject::CollapsibleSection.new, collapsed: collapsed?(date)) do |section| %> + <%= render(Primer::OpenProject::CollapsibleSection.new(collapsed: collapsed?(date))) do |section| %> <% section.with_title { date_title(date) } %> <% section.with_caption { date_caption(date) } %> <% section.with_additional_information do %> diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index b7b13d9f206..319c8eed7c1 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -80,8 +80,8 @@ module My "#{entries_count} #{TimeEntry.model_name.human(count: entries_count)}" end - def collapsed?(_date) - false + def collapsed?(date) + date.past? end def date_caption(date) From ce199f5041793bacb420eb25d4496a1e57824154 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Mon, 14 Apr 2025 15:19:08 +0200 Subject: [PATCH 80/88] add tests for determining date behavior --- .../my/time_tracking_controller_spec.rb | 51 +++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb b/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb index 6fb62ff2e40..1ec0f15ef15 100644 --- a/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb +++ b/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb @@ -94,4 +94,55 @@ RSpec.describe My::TimeTrackingController do end end end + + describe "GET /my/time-tracking/day" do + it "without a date param it uses the current date" do + get :day + expect(assigns(:current_day)).to eq(Date.current) + end + + it "with a date param it uses the given date" do + get :day, params: { date: "2023-12-31" } + expect(assigns(:current_day)).to eq(Date.parse("2023-12-31")) + end + + it "with an invalid date param it uses the current date" do + get :day, params: { date: "invalid-date" } + expect(assigns(:current_day)).to eq(Date.current) + end + end + + describe "GET /my/time-tracking/week" do + it "without a date param it uses the beginning of current week" do + get :week + expect(assigns(:current_day)).to eq(Date.current.beginning_of_week) + end + + it "with a date param it uses the given date" do + get :week, params: { date: "2023-12-31" } + expect(assigns(:current_day)).to eq(Date.parse("2023-12-31")) + end + + it "with an invalid date param it uses the beginning of current week" do + get :week, params: { date: "invalid-date" } + expect(assigns(:current_day)).to eq(Date.current.beginning_of_week) + end + end + + describe "GET /my/time-tracking/month" do + it "without a date param it uses the beginning of current month" do + get :month + expect(assigns(:current_day)).to eq(Date.current.beginning_of_month) + end + + it "with a date param it uses the given date" do + get :month, params: { date: "2023-12-31" } + expect(assigns(:current_day)).to eq(Date.parse("2023-12-31")) + end + + it "with an invalid date param it uses the beginning of current month" do + get :month, params: { date: "invalid-date" } + expect(assigns(:current_day)).to eq(Date.current.beginning_of_month) + end + end end From 11810c217d65f1394efd822d962a349d9612b091 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 15 Apr 2025 10:47:16 +0200 Subject: [PATCH 81/88] allow setting an id on the container for TableComponent --- app/components/op_primer/border_box_table_component.html.erb | 1 + app/components/table_component.html.erb | 2 +- app/components/table_component.rb | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/components/op_primer/border_box_table_component.html.erb b/app/components/op_primer/border_box_table_component.html.erb index 927e3f9d997..2bba751fdde 100644 --- a/app/components/op_primer/border_box_table_component.html.erb +++ b/app/components/op_primer/border_box_table_component.html.erb @@ -30,6 +30,7 @@ See COPYRIGHT and LICENSE files for more details. <%= render( border_box_container( + id: container_id, classes: container_class, test_selector: ) diff --git a/app/components/table_component.html.erb b/app/components/table_component.html.erb index 9449d5dc489..f3125367435 100644 --- a/app/components/table_component.html.erb +++ b/app/components/table_component.html.erb @@ -27,7 +27,7 @@ See COPYRIGHT and LICENSE files for more details. ++#%> -
+
diff --git a/app/components/table_component.rb b/app/components/table_component.rb index e88c9db9593..369f8a9c69c 100644 --- a/app/components/table_component.rb +++ b/app/components/table_component.rb @@ -159,6 +159,10 @@ class TableComponent < ApplicationComponent nil end + def container_id + nil + end + def columns self.class.columns.reject { skip_column?(it) } end From 7a717699874ea18cc9d13622080f11804245c022 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 15 Apr 2025 10:47:38 +0200 Subject: [PATCH 82/88] add an id for the DailyEntriesTable to allow turbo replacing it --- .../app/components/my/time_tracking/daily_entries_table.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb index 903e285751f..7839842cd05 100644 --- a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb +++ b/modules/costs/app/components/my/time_tracking/daily_entries_table.rb @@ -38,6 +38,10 @@ module My TimeEntryRow end + def container_id + "time-entries-table-#{options[:date].iso8601}" + end + def mobile_title TimeEntry.model_name.human(count: 2) end From 25c8e889f681432d18a336af910b05501dc106e1 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 15 Apr 2025 11:49:01 +0200 Subject: [PATCH 83/88] add IDs to things we want to replace via turbo --- .../app/components/my/time_tracking/list_component.html.erb | 4 ++-- .../costs/app/components/my/time_tracking/list_component.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index 1d4e6fd5aed..64d1123ab1e 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -8,8 +8,8 @@ <% section.with_caption { date_caption(date) } %> <% section.with_additional_information do %> <%= - render(Primer::Beta::Text.new(color: :muted)) { "#{date_additional(date)} - " } + - render(Primer::Beta::Text.new) { total_hours_for(date) } + render(Primer::Beta::Text.new(color: :muted, id: "time-entries-count-#{date.iso8601}")) { "#{entry_count_for(date)} - " } + + render(Primer::Beta::Text.new(id: "time-entries-total-#{date.iso8601}")) { total_hours_for(date) } %> <% end %> <% section.with_collapsible_content do %> diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 319c8eed7c1..048481c8006 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -75,7 +75,7 @@ module My end end - def date_additional(date) + def entry_count_for(date) entries_count = grouped_time_entries[date].size "#{entries_count} #{TimeEntry.model_name.human(count: entries_count)}" end From c733466243de343452f340f15bca1be6140b77b8 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 15 Apr 2025 11:50:12 +0200 Subject: [PATCH 84/88] simplify rendering logic --- .../time_tracking/mode_switcher_component.rb | 15 +-- .../sub_header_component.html.erb | 2 +- .../my/time_tracking/sub_header_component.rb | 21 ++--- .../my/time_tracking_controller.rb | 92 +++++++++---------- .../{day.html.erb => index.html.erb} | 0 .../app/views/my/time_tracking/month.html.erb | 1 - .../app/views/my/time_tracking/week.html.erb | 1 - modules/costs/config/routes.rb | 16 ++-- .../my/time_tracking_controller_spec.rb | 56 +++++------ 9 files changed, 98 insertions(+), 106 deletions(-) rename modules/costs/app/views/my/time_tracking/{day.html.erb => index.html.erb} (100%) delete mode 100644 modules/costs/app/views/my/time_tracking/month.html.erb delete mode 100644 modules/costs/app/views/my/time_tracking/week.html.erb diff --git a/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb b/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb index 429a6b85ac6..7a9460c0a9c 100644 --- a/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb +++ b/modules/costs/app/components/my/time_tracking/mode_switcher_component.rb @@ -32,18 +32,19 @@ module My module TimeTracking class ModeSwitcherComponent < ApplicationComponent options :current_mode, - :view_mode + :view_mode, + :date def call render(Primer::Alpha::SegmentedControl.new("aria-label": I18n.t(:label_meeting_date_time))) do |control| - %i[day week month].each do |action| + %i[day week month].each do |mode| control.with_item( tag: :a, - href: url_for(params.permit(:controller, :date).merge(action:, view_mode: view_mode)), - icon: icon_for_mode(action), - label: t("label_#{action}"), - title: t("label_#{action}"), - selected: (current_mode == action) + href: my_time_tracking_path(date:, view_mode:, mode:), + icon: icon_for_mode(mode), + label: t("label_#{mode}"), + title: t("label_#{mode}"), + selected: (current_mode == mode) ) end end diff --git a/modules/costs/app/components/my/time_tracking/sub_header_component.html.erb b/modules/costs/app/components/my/time_tracking/sub_header_component.html.erb index f3cbceefe02..a823a822bb0 100644 --- a/modules/costs/app/components/my/time_tracking/sub_header_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/sub_header_component.html.erb @@ -1,6 +1,6 @@ <%= render(Primer::OpenProject::SubHeader.new) do |component| component.with_filter_component do - render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: view_mode)) + render(My::TimeTracking::ModeSwitcherComponent.new(current_mode: mode, view_mode: view_mode, date: date)) end component.with_text { title } diff --git a/modules/costs/app/components/my/time_tracking/sub_header_component.rb b/modules/costs/app/components/my/time_tracking/sub_header_component.rb index 0428a2516a2..daa5dc72d69 100644 --- a/modules/costs/app/components/my/time_tracking/sub_header_component.rb +++ b/modules/costs/app/components/my/time_tracking/sub_header_component.rb @@ -54,26 +54,19 @@ module My end def today_href - case mode - when :day - my_time_tracking_day_path(date: Date.current, view_mode: params[:view_mode]) - when :week - my_time_tracking_week_path(date: Date.current, view_mode: params[:view_mode]) - when :month - my_time_tracking_month_path(date: Date.current, view_mode: params[:view_mode]) - end + my_time_tracking_path(date: Date.current, view_mode:, mode:) end def previous_attrs # rubocop:disable Metrics/AbcSize case mode when :day - { href: my_time_tracking_day_path(date: date - 1.day, view_mode: params[:view_mode]), + { href: my_time_tracking_path(date: date - 1.day, view_mode:, mode:), aria: { label: I18n.t(:label_previous_day) } } when :week - { href: my_time_tracking_week_path(date: date - 1.week, view_mode: params[:view_mode]), + { href: my_time_tracking_path(date: date - 1.week, view_mode:, mode:), aria: { label: I18n.t(:label_previous_week) } } when :month - { href: my_time_tracking_month_path(date: date - 1.month, view_mode: params[:view_mode]), + { href: my_time_tracking_path(date: date - 1.month, view_mode:, mode:), aria: { label: I18n.t(:label_previous_month) } } end end @@ -81,13 +74,13 @@ module My def next_attrs # rubocop:disable Metrics/AbcSize case mode when :day - { href: my_time_tracking_day_path(date: date + 1.day, view_mode: params[:view_mode]), + { href: my_time_tracking_path(date: date + 1.day, view_mode:, mode:), aria: { label: I18n.t(:label_next_day) } } when :week - { href: my_time_tracking_week_path(date: date + 1.week, view_mode: params[:view_mode]), + { href: my_time_tracking_path(date: date + 1.week, view_mode:, mode:), aria: { label: I18n.t(:label_next_week) } } when :month - { href: my_time_tracking_month_path(date: date + 1.month, view_mode: params[:view_mode]), + { href: my_time_tracking_path(date: date + 1.month, view_mode:, mode:), aria: { label: I18n.t(:label_next_month) } } end end diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 6111bd14d60..8b559a5390c 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -30,62 +30,60 @@ module My class TimeTrackingController < ApplicationController - before_action :require_login + include OpTurbo::ComponentStream - no_authorization_required!(:index, :day, :week, :month) + before_action :require_login, :view_mode, :mode, :date + + no_authorization_required!(:index, :refresh) menu_item :my_time_tracking layout "global" - helper_method :current_day, :today?, :this_week?, :this_month?, :list_view_component, :view_mode + helper_method :list_view_component def index - if mobile? - redirect_to action: :day, view_mode: default_view_mode - else - redirect_to action: :week, view_mode: default_view_mode + case mode + when :day then load_time_entries(date) + when :week then load_time_entries(date.all_week) + when :month then load_time_entries(date.all_month) end end - def day - load_time_entries(current_day) - end - - def week - load_time_entries(current_day.all_week) - end - - def month - load_time_entries(current_day.all_month) - end - - def today? - current_day == Time.zone.today - end - - def this_week? - current_day == Time.zone.today.beginning_of_week - end - - def this_month? - current_day == Time.zone.today.beginning_of_month + def refresh + if mode == :month + load_time_entries(date.all_week) + else + load_time_entries(date) + end end private - def current_day - return @current_day if defined?(@current_day) + def date + @date ||= parsed_date || current_date + end - parsed_date = if params[:date].present? - begin - Date.iso8601(params[:date]) - rescue StandardError - nil - end - end + def parsed_date + if params[:date].present? + begin + Date.iso8601(params[:date]) + rescue StandardError + nil + end + end + end - @current_day = parsed_date || current_date + def default_mode + if mobile? + "day" + else + "week" + end + end + + def mode + @mode ||= (params[:mode].presence || default_mode).to_sym end def default_view_mode @@ -97,15 +95,11 @@ module My end def view_mode - if ["calendar", "list"].include?(params[:view_mode]) - params[:view_mode] - else - default_view_mode - end.to_sym + @view_mode ||= (params[:view_mode].presence || default_view_mode).to_sym end def current_date - case params[:action].to_sym + case mode when :day then Time.zone.today when :week then Time.zone.today.beginning_of_week when :month then Time.zone.today.beginning_of_month @@ -124,14 +118,14 @@ module My if view_mode == :list My::TimeTracking::ListComponent.new( time_entries: @time_entries, - mode: params[:action].to_sym, - date: current_day + mode: mode, + date: date ) else My::TimeTracking::CalendarComponent.new( time_entries: @time_entries, - mode: params[:action].to_sym, - date: current_day + mode: mode, + date: date ) end end diff --git a/modules/costs/app/views/my/time_tracking/day.html.erb b/modules/costs/app/views/my/time_tracking/index.html.erb similarity index 100% rename from modules/costs/app/views/my/time_tracking/day.html.erb rename to modules/costs/app/views/my/time_tracking/index.html.erb diff --git a/modules/costs/app/views/my/time_tracking/month.html.erb b/modules/costs/app/views/my/time_tracking/month.html.erb deleted file mode 100644 index b3f3712cf7d..00000000000 --- a/modules/costs/app/views/my/time_tracking/month.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render(list_view_component) %> diff --git a/modules/costs/app/views/my/time_tracking/week.html.erb b/modules/costs/app/views/my/time_tracking/week.html.erb deleted file mode 100644 index b3f3712cf7d..00000000000 --- a/modules/costs/app/views/my/time_tracking/week.html.erb +++ /dev/null @@ -1 +0,0 @@ -<%= render(list_view_component) %> diff --git a/modules/costs/config/routes.rb b/modules/costs/config/routes.rb index 642a837e29f..856b91dc7e4 100644 --- a/modules/costs/config/routes.rb +++ b/modules/costs/config/routes.rb @@ -50,13 +50,15 @@ Rails.application.routes.draw do namespace "my" do get "/timer" => "timer#show", as: "timers" - get "/time-tracking" => "time_tracking#index" - get "/time-tracking/day(-:view_mode)(/:date)" => "time_tracking#day", - as: :time_tracking_day - get "/time-tracking/week(-:view_mode)(/:date)" => "time_tracking#week", - as: :time_tracking_week - get "/time-tracking/month(-:view_mode)(/:date)" => "time_tracking#month", - as: :time_tracking_month + get "/time-tracking/(:mode-:view_mode)(/:date)" => "time_tracking#index", + as: :time_tracking, + constraints: { + mode: /day|week|month/, + view_mode: /list|calendar/, + date: /\d{4}-\d{2}-\d{2}/ + } + get "/time-tracking/refresh" => "time_tracking#refresh", + as: :time_tracking_refresh end scope "projects/:project_id", as: "project", module: "projects" do diff --git a/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb b/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb index 1ec0f15ef15..eefcebe57c0 100644 --- a/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb +++ b/modules/costs/spec/controllers/my/time_tracking_controller_spec.rb @@ -48,9 +48,10 @@ RSpec.describe My::TimeTrackingController do allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(true) end - it "redirects to the week calendar view" do + it "renders the calendar week view" do get :index - expect(response).to redirect_to(action: :week, view_mode: "calendar") + expect(assigns(:mode)).to eq(:week) + expect(assigns(:view_mode)).to eq(:calendar) end end @@ -59,9 +60,10 @@ RSpec.describe My::TimeTrackingController do allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(false) end - it "redirects to the week list view" do + it "renders the week list view" do get :index - expect(response).to redirect_to(action: :week, view_mode: "list") + expect(assigns(:mode)).to eq(:week) + expect(assigns(:view_mode)).to eq(:list) end end end @@ -76,9 +78,10 @@ RSpec.describe My::TimeTrackingController do allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(true) end - it "redirects to the day calendar view" do + it "renders the day calendar view" do get :index - expect(response).to redirect_to(action: :day, view_mode: "calendar") + expect(assigns(:mode)).to eq(:day) + expect(assigns(:view_mode)).to eq(:calendar) end end @@ -87,9 +90,10 @@ RSpec.describe My::TimeTrackingController do allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(false) end - it "redirects to the day list view" do + it "renders the day list view" do get :index - expect(response).to redirect_to(action: :day, view_mode: "list") + expect(assigns(:mode)).to eq(:day) + expect(assigns(:view_mode)).to eq(:list) end end end @@ -97,52 +101,52 @@ RSpec.describe My::TimeTrackingController do describe "GET /my/time-tracking/day" do it "without a date param it uses the current date" do - get :day - expect(assigns(:current_day)).to eq(Date.current) + get :index, params: { mode: :day } + expect(assigns(:date)).to eq(Date.current) end it "with a date param it uses the given date" do - get :day, params: { date: "2023-12-31" } - expect(assigns(:current_day)).to eq(Date.parse("2023-12-31")) + get :index, params: { mode: :day, date: "2023-12-31" } + expect(assigns(:date)).to eq(Date.parse("2023-12-31")) end it "with an invalid date param it uses the current date" do - get :day, params: { date: "invalid-date" } - expect(assigns(:current_day)).to eq(Date.current) + get :index, params: { mode: :day, date: "invalid-date" } + expect(assigns(:date)).to eq(Date.current) end end describe "GET /my/time-tracking/week" do it "without a date param it uses the beginning of current week" do - get :week - expect(assigns(:current_day)).to eq(Date.current.beginning_of_week) + get :index, params: { mode: :week } + expect(assigns(:date)).to eq(Date.current.beginning_of_week) end it "with a date param it uses the given date" do - get :week, params: { date: "2023-12-31" } - expect(assigns(:current_day)).to eq(Date.parse("2023-12-31")) + get :index, params: { mode: :week, date: "2023-12-31" } + expect(assigns(:date)).to eq(Date.parse("2023-12-31")) end it "with an invalid date param it uses the beginning of current week" do - get :week, params: { date: "invalid-date" } - expect(assigns(:current_day)).to eq(Date.current.beginning_of_week) + get :index, params: { mode: :week, date: "invalid-date" } + expect(assigns(:date)).to eq(Date.current.beginning_of_week) end end describe "GET /my/time-tracking/month" do it "without a date param it uses the beginning of current month" do - get :month - expect(assigns(:current_day)).to eq(Date.current.beginning_of_month) + get :index, params: { mode: :month } + expect(assigns(:date)).to eq(Date.current.beginning_of_month) end it "with a date param it uses the given date" do - get :month, params: { date: "2023-12-31" } - expect(assigns(:current_day)).to eq(Date.parse("2023-12-31")) + get :index, params: { mode: :month, date: "2023-12-31" } + expect(assigns(:date)).to eq(Date.parse("2023-12-31")) end it "with an invalid date param it uses the beginning of current month" do - get :month, params: { date: "invalid-date" } - expect(assigns(:current_day)).to eq(Date.current.beginning_of_month) + get :index, params: { mode: :month, date: "invalid-date" } + expect(assigns(:date)).to eq(Date.current.beginning_of_month) end end end From 5360342ec28e1f6ca4e1f04fb0f2a53904ccd069 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 15 Apr 2025 13:44:41 +0200 Subject: [PATCH 85/88] refresh only a single element of the list view --- .../core/path-helper/path-helper.service.ts | 4 ++ .../dynamic/my/time-tracking.controller.ts | 67 ++++++++++++++----- .../my/time_tracking/calendar_component.rb | 1 + .../my/time_tracking/list_component.html.erb | 7 +- .../my/time_tracking/list_component.rb | 16 ++--- .../my/time_tracking/list_stats_component.rb | 60 +++++++++++++++++ .../time_tracking/list_wrapper_component.rb | 49 ++++++++++++++ ...able.rb => time_entries_list_component.rb} | 14 ++-- .../my/time_tracking_controller.rb | 11 ++- 9 files changed, 188 insertions(+), 41 deletions(-) create mode 100644 modules/costs/app/components/my/time_tracking/list_stats_component.rb create mode 100644 modules/costs/app/components/my/time_tracking/list_wrapper_component.rb rename modules/costs/app/components/my/time_tracking/{daily_entries_table.rb => time_entries_list_component.rb} (90%) diff --git a/frontend/src/app/core/path-helper/path-helper.service.ts b/frontend/src/app/core/path-helper/path-helper.service.ts index 05fc48fd619..f09e48fac92 100644 --- a/frontend/src/app/core/path-helper/path-helper.service.ts +++ b/frontend/src/app/core/path-helper/path-helper.service.ts @@ -373,4 +373,8 @@ export class PathHelperService { public timeEntryUpdate(timeEntryId:string) { return `${this.staticBase}/time_entries/${timeEntryId}`; } + + public myTimeTrackingRefresh(date:string, viewMode:string, mode:string) { + return `${this.staticBase}/my/time-tracking/refresh?date=${date}&view_mode=${viewMode}&mode=${mode}`; + } } diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 7a012a6a71f..54af2962088 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -1,4 +1,4 @@ -import { Controller } from '@hotwired/stimulus'; +import { ActionEvent, Controller } from '@hotwired/stimulus'; import { Calendar } from '@fullcalendar/core'; import timeGridPlugin from '@fullcalendar/timegrid'; import dayGridPlugin from '@fullcalendar/daygrid'; @@ -17,6 +17,7 @@ export default class MyTimeTrackingController extends Controller { static values = { mode: String, + viewMode: String, timeEntries: Array, initialDate: String, canCreate: Boolean, @@ -26,6 +27,7 @@ export default class MyTimeTrackingController extends Controller { }; declare readonly calendarTarget:HTMLElement; + declare readonly hasCalendarTarget:boolean; declare readonly modeValue:string; declare readonly timeEntriesValue:object[]; declare readonly initialDateValue:string; @@ -33,19 +35,36 @@ export default class MyTimeTrackingController extends Controller { declare readonly canEditValue:boolean; declare readonly forceTimesValue:boolean; declare readonly localeValue:string; + declare readonly viewModeValue:string; private calendar:Calendar; + private DEFAULT_TIMED_EVENT_DURATION = '01:00'; + private updatingDate:string|null = null; + private boundListener = this.dialogCloseListener.bind(this); async connect() { const context = await window.OpenProject.getPluginContext(); this.turboRequests = context.services.turboRequests; this.pathHelper = context.services.pathHelperService; + if (this.hasCalendarTarget && this.viewModeValue === 'calendar') { + this.initializeCalendar(); + } + // handle dialog close event - document.addEventListener('dialog:close', this.dialogCloseListener); + document.addEventListener('dialog:close', this.boundListener); + } - const DEFAULT_TIMED_EVENT_DURATION = '01:00'; + disconnect():void { + document.removeEventListener('dialog:close', this.boundListener); + // Clean up calendar when controller disconnects + if (this.calendar) { + this.calendar.destroy(); + } + } + + initializeCalendar() { this.calendar = new Calendar(this.calendarTarget, { plugins: [timeGridPlugin, dayGridPlugin, interactionPlugin], initialView: this.calendarView(), @@ -58,7 +77,7 @@ export default class MyTimeTrackingController extends Controller { selectable: this.canCreateValue, editable: this.canEditValue, eventResizableFromStart: true, - defaultTimedEventDuration: DEFAULT_TIMED_EVENT_DURATION, + defaultTimedEventDuration: this.DEFAULT_TIMED_EVENT_DURATION, allDayContent: '', dayMaxEventRows: 4, // 3 + more link eventMinHeight: 30, @@ -172,7 +191,7 @@ export default class MyTimeTrackingController extends Controller { ); } - this.calendar.setOption('defaultTimedEventDuration', DEFAULT_TIMED_EVENT_DURATION); + this.calendar.setOption('defaultTimedEventDuration', this.DEFAULT_TIMED_EVENT_DURATION); }, eventClick: (info) => { // check if we clicked on a link tag, if so exit early as we don't want to show the modal @@ -193,15 +212,6 @@ export default class MyTimeTrackingController extends Controller { this.calendar.render(); } - disconnect():void { - document.removeEventListener('dialog:close', this.dialogCloseListener); - - // Clean up calendar when controller disconnects - if (this.calendar) { - this.calendar.destroy(); - } - } - addTotalFooter() { if (!this.calendar) return; const calendarScrollGridWrapper = document.querySelector('.fc-timegrid .fc-scrollgrid tbody'); @@ -368,10 +378,35 @@ export default class MyTimeTrackingController extends Controller { } } - dialogCloseListener(this:void, event:CustomEvent):void { + newTimeEntry(event:ActionEvent) { + const dialogParams = `onlyMe=true&date=${event.params.date}`; + this.updatingDate = event.params.date as string; + + void this.turboRequests.request( + `${this.pathHelper.timeEntryDialog()}?${dialogParams}`, + { method: 'GET' }, + ); + } + + dialogCloseListener(event:CustomEvent):void { const { detail: { dialog, submitted } } = event as { detail:{ dialog:HTMLDialogElement; submitted:boolean } }; - if (dialog.id === 'time-entry-dialog' && submitted) { + if (dialog.id !== 'time-entry-dialog' || !submitted) { return; } + + // we simply refresh the calendar page + if (this.viewModeValue === 'calendar') { window.location.reload(); + return; + } + + // list view replaces only the updated date + if (this.viewModeValue === 'list') { + // we don't know what date we clicked, so we need to reload the whole page + if (this.updatingDate) { + void this.turboRequests.request(this.pathHelper.myTimeTrackingRefresh(this.updatingDate, this.viewModeValue, this.modeValue), { method: 'GET' }); + this.updatingDate = null; + } else { + window.location.reload(); + } } } } diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb index f502d4d1ef7..c5276e6f2cf 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.rb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -45,6 +45,7 @@ module My "controller" => "my--time-tracking", "application-target" => "dynamic", "my--time-tracking-mode-value" => mode, + "my--time-tracking-view-mode-value" => "calendar", "my--time-tracking-time-entries-value" => time_entries_json, "my--time-tracking-initial-date-value" => date.iso8601, "my--time-tracking-can-create-value" => User.current.allowed_in_any_project?(:log_own_time), diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index 64d1123ab1e..bf0a724566c 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -7,13 +7,10 @@ <% section.with_title { date_title(date) } %> <% section.with_caption { date_caption(date) } %> <% section.with_additional_information do %> - <%= - render(Primer::Beta::Text.new(color: :muted, id: "time-entries-count-#{date.iso8601}")) { "#{entry_count_for(date)} - " } + - render(Primer::Beta::Text.new(id: "time-entries-total-#{date.iso8601}")) { total_hours_for(date) } - %> + <%= render(My::TimeTracking::ListStatsComponent.new(time_entries: grouped_time_entries[date], date: date)) %> <% end %> <% section.with_collapsible_content do %> - <%= render(My::TimeTracking::DailyEntriesTable.new(rows: grouped_time_entries[date], date: date, mode: mode)) %> + <%= render(My::TimeTracking::ListWrapperComponent.new(time_entries: grouped_time_entries[date], date: date, mode: mode)) %> <% end %> <% end %> <% end %> diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 048481c8006..848dc4f7f64 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -41,8 +41,10 @@ module My def wrapper_data { - "controller" => "generic-dialog-close", - "application-target" => "dynamic" + "controller" => "my--time-tracking", + "application-target" => "dynamic", + "my--time-tracking-mode-value" => mode, + "my--time-tracking-view-mode-value" => "list" } end @@ -62,11 +64,6 @@ module My end end - def total_hours_for(date) - total_hours = grouped_time_entries[date].sum(&:hours).round(2) - DurationConverter.output(total_hours, format: :hours_and_minutes).presence || "0h" - end - def date_title(date) if mode == :month I18n.t(:label_specific_week, week: date.strftime("%W")) @@ -75,11 +72,6 @@ module My end end - def entry_count_for(date) - entries_count = grouped_time_entries[date].size - "#{entries_count} #{TimeEntry.model_name.human(count: entries_count)}" - end - def collapsed?(date) date.past? end diff --git a/modules/costs/app/components/my/time_tracking/list_stats_component.rb b/modules/costs/app/components/my/time_tracking/list_stats_component.rb new file mode 100644 index 00000000000..9082a81643d --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/list_stats_component.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module My + module TimeTracking + class ListStatsComponent < OpPrimer::BorderBoxTableComponent + include OpTurbo::Streamable + + options :time_entries, :date + + def wrapper_key + "time-entries-list-stats-#{date.iso8601}" + end + + def call + component_wrapper do + render(Primer::Beta::Text.new(color: :muted)) { "#{entry_count} - " } + + render(Primer::Beta::Text.new) { total_hours } + end + end + + def total_hours + total_hours = time_entries.sum(&:hours).round(2) + DurationConverter.output(total_hours, format: :hours_and_minutes).presence || "0h" + end + + def entry_count + entries_count = time_entries.size + "#{entries_count} #{TimeEntry.model_name.human(count: entries_count)}" + end + end + end +end diff --git a/modules/costs/app/components/my/time_tracking/list_wrapper_component.rb b/modules/costs/app/components/my/time_tracking/list_wrapper_component.rb new file mode 100644 index 00000000000..e56700131b9 --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/list_wrapper_component.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# -- copyright +# OpenProject is an open source project management software. +# Copyright (C) the OpenProject GmbH +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License version 3. +# +# OpenProject is a fork of ChiliProject, which is a fork of Redmine. The copyright follows: +# Copyright (C) 2006-2013 Jean-Philippe Lang +# Copyright (C) 2010-2013 the ChiliProject Team +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# See COPYRIGHT and LICENSE files for more details. +# ++ + +module My + module TimeTracking + class ListWrapperComponent < OpPrimer::BorderBoxTableComponent + include OpTurbo::Streamable + + options :time_entries, :date, :mode + + def wrapper_key + "time-entries-list-#{options[:date].iso8601}" + end + + def call + component_wrapper do + render(My::TimeTracking::TimeEntriesListComponent.new(rows: time_entries, date: date, mode: mode)) + end + end + end + end +end diff --git a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb b/modules/costs/app/components/my/time_tracking/time_entries_list_component.rb similarity index 90% rename from modules/costs/app/components/my/time_tracking/daily_entries_table.rb rename to modules/costs/app/components/my/time_tracking/time_entries_list_component.rb index 7839842cd05..62375e62a41 100644 --- a/modules/costs/app/components/my/time_tracking/daily_entries_table.rb +++ b/modules/costs/app/components/my/time_tracking/time_entries_list_component.rb @@ -30,7 +30,8 @@ module My module TimeTracking - class DailyEntriesTable < OpPrimer::BorderBoxTableComponent + class TimeEntriesListComponent < OpPrimer::BorderBoxTableComponent + include OpTurbo::Streamable columns :spent_on, :time, :hours, :subject, :project, :activity, :comments main_column :time, :subject, :project @@ -38,10 +39,6 @@ module My TimeEntryRow end - def container_id - "time-entries-table-#{options[:date].iso8601}" - end - def mobile_title TimeEntry.model_name.human(count: 2) end @@ -57,8 +54,11 @@ module My size: :small, tag: :a, tooltip_direction: :e, - href: dialog_time_entries_path(onlyMe: true, date: options[:date]), - data: { "turbo-stream" => true }, + href: "#", + data: { + action: "my--time-tracking#newTimeEntry", + "my--time-tracking-date-param" => options[:date] + }, label: t("button_log_time"), aria: { label: t("button_log_time") } )) diff --git a/modules/costs/app/controllers/my/time_tracking_controller.rb b/modules/costs/app/controllers/my/time_tracking_controller.rb index 8b559a5390c..acd1a945dfb 100644 --- a/modules/costs/app/controllers/my/time_tracking_controller.rb +++ b/modules/costs/app/controllers/my/time_tracking_controller.rb @@ -51,11 +51,20 @@ module My end def refresh - if mode == :month + if mode == :month # for the month we have the whole week in the table, for the rest it's the day load_time_entries(date.all_week) else load_time_entries(date) end + + update_via_turbo_stream( + component: My::TimeTracking::ListWrapperComponent.new(time_entries: @time_entries, date: date, mode: mode) + ) + update_via_turbo_stream( + component: My::TimeTracking::ListStatsComponent.new(time_entries: @time_entries, date: date) + ) + + respond_with_turbo_streams end private From e5dd4b5ee7f43f3077054cd63582a1afd9169e4a Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 15 Apr 2025 14:09:15 +0200 Subject: [PATCH 86/88] do not allow moving an event from allDay to the normal calendar event when we are not allowing it --- .../controllers/dynamic/my/time-tracking.controller.ts | 8 +++++++- .../app/components/my/time_tracking/calendar_component.rb | 1 + .../components/my/time_tracking/header_component.html.erb | 4 ++-- modules/costs/lib/costs/patches/permitted_params_patch.rb | 2 +- 4 files changed, 11 insertions(+), 4 deletions(-) diff --git a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts index 54af2962088..eee431cf0aa 100644 --- a/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts +++ b/frontend/src/stimulus/controllers/dynamic/my/time-tracking.controller.ts @@ -23,6 +23,7 @@ export default class MyTimeTrackingController extends Controller { canCreate: Boolean, locale: String, canEdit: Boolean, + allowTimes: Boolean, forceTimes: Boolean, }; @@ -33,6 +34,7 @@ export default class MyTimeTrackingController extends Controller { declare readonly initialDateValue:string; declare readonly canCreateValue:boolean; declare readonly canEditValue:boolean; + declare readonly allowTimesValue:boolean; declare readonly forceTimesValue:boolean; declare readonly localeValue:string; declare readonly viewModeValue:string; @@ -164,11 +166,15 @@ export default class MyTimeTrackingController extends Controller { } }, - eventAllow: (dropInfo) => { + eventAllow: (dropInfo, draggedEvent) => { if (dropInfo.allDay && this.forceTimesValue) { return false; } + if (!dropInfo.allDay && draggedEvent?.allDay && !this.allowTimesValue) { + return false; + } + return true; }, diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.rb b/modules/costs/app/components/my/time_tracking/calendar_component.rb index c5276e6f2cf..8d229afcb62 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.rb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.rb @@ -50,6 +50,7 @@ module My "my--time-tracking-initial-date-value" => date.iso8601, "my--time-tracking-can-create-value" => User.current.allowed_in_any_project?(:log_own_time), "my--time-tracking-can-edit-value" => User.current.allowed_in_any_project?(:edit_own_time_entries), + "my--time-tracking-allow-times-value" => TimeEntry.can_track_start_and_end_time?, "my--time-tracking-force-times-value" => TimeEntry.must_track_start_and_end_time?, "my--time-tracking-locale-value" => I18n.locale } diff --git a/modules/costs/app/components/my/time_tracking/header_component.html.erb b/modules/costs/app/components/my/time_tracking/header_component.html.erb index dc850a30bad..bd4f8b7fd8b 100644 --- a/modules/costs/app/components/my/time_tracking/header_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/header_component.html.erb @@ -14,14 +14,14 @@ menu.with_item( label: t(:label_calendar), tag: :a, - href: url_for(params.permit(:controller, :action, :date).merge(view_mode: "calendar")) + href: my_time_tracking_path(date: date, mode: mode, view_mode: "calendar") ) do |item| item.with_leading_visual_icon(icon: :calendar) end menu.with_item( label: t(:label_list), tag: :a, - href: url_for(params.permit(:controller, :date, :action).merge(view_mode: "list")) + href: my_time_tracking_path(date: date, mode: mode, view_mode: "list") ) do |item| item.with_leading_visual_icon(icon: "list-unordered") end diff --git a/modules/costs/lib/costs/patches/permitted_params_patch.rb b/modules/costs/lib/costs/patches/permitted_params_patch.rb index c3d4d269c68..6a770885345 100644 --- a/modules/costs/lib/costs/patches/permitted_params_patch.rb +++ b/modules/costs/lib/costs/patches/permitted_params_patch.rb @@ -68,7 +68,7 @@ module Costs::Patches::PermittedParamsPatch def time_entries additional_fields = [] - additional_fields << :start_time if TimeEntry.can_track_start_and_end_time? + additional_fields << :start_time if TimeEntry.can_track_start_and_end_time? || params.dig(:time_entry, :start_time).nil? params .require(:time_entry) From 44e74fefbb6eb2f88c30944928240beb76b1f72b Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 15 Apr 2025 14:27:45 +0200 Subject: [PATCH 87/88] fix spec --- modules/costs/spec/features/my_time_tracking_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/costs/spec/features/my_time_tracking_spec.rb b/modules/costs/spec/features/my_time_tracking_spec.rb index 3bad31bc4d4..c8b19270680 100644 --- a/modules/costs/spec/features/my_time_tracking_spec.rb +++ b/modules/costs/spec/features/my_time_tracking_spec.rb @@ -64,7 +64,7 @@ RSpec.describe "my time tracking", :js do it "does something" do allow(TimeEntry).to receive(:can_track_start_and_end_time?).and_return(true) - visit my_time_tracking_week_path(date: "2025-04-09", view_mode: "list") + visit my_time_tracking_path(date: "2025-04-09", view_mode: "list") # save_and_open_screenshot end end From 2d0f518b5511bb1864bcd5f95073460e885b25f2 Mon Sep 17 00:00:00 2001 From: Klaus Zanders Date: Tue, 15 Apr 2025 16:04:22 +0200 Subject: [PATCH 88/88] apply styling to updated CollapsibleSection --- modules/costs/app/components/_index.sass | 3 ++- .../time_tracking/calendar_component.html.erb | 2 +- ...{calendar.sass => calendar_component.sass} | 2 +- .../my/time_tracking/list_component.html.erb | 20 +++++++++++-------- .../my/time_tracking/list_component.rb | 1 + .../my/time_tracking/list_component.sass | 2 ++ 6 files changed, 19 insertions(+), 11 deletions(-) rename modules/costs/app/components/my/time_tracking/{calendar.sass => calendar_component.sass} (95%) create mode 100644 modules/costs/app/components/my/time_tracking/list_component.sass diff --git a/modules/costs/app/components/_index.sass b/modules/costs/app/components/_index.sass index 9e62662f576..2ceb251abf5 100644 --- a/modules/costs/app/components/_index.sass +++ b/modules/costs/app/components/_index.sass @@ -1,2 +1,3 @@ @import "time_entries/entry_dialog_component" -@import "my/time_tracking/calendar" +@import "my/time_tracking/calendar_component" +@import "my/time_tracking/list_component" diff --git a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb index 618580688ef..1fafb63fa90 100644 --- a/modules/costs/app/components/my/time_tracking/calendar_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/calendar_component.html.erb @@ -1,4 +1,4 @@ -<%= flex_layout(data: wrapper_data, classes: "time-entry-calendar") do |calendar_page| %> +<%= flex_layout(data: wrapper_data, classes: "my-time-tracking-calendar-view") do |calendar_page| %> <% calendar_page.with_row { render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :calendar)) } %> <% calendar_page.with_row { render(My::TimeTracking::SubHeaderComponent.new(mode:, date:, view_mode: :calendar)) } %> <% calendar_page.with_row(classes: "op-fc-wrapper", data: { "my--time-tracking-target" => "calendar" }) %> diff --git a/modules/costs/app/components/my/time_tracking/calendar.sass b/modules/costs/app/components/my/time_tracking/calendar_component.sass similarity index 95% rename from modules/costs/app/components/my/time_tracking/calendar.sass rename to modules/costs/app/components/my/time_tracking/calendar_component.sass index 076c5ad7a26..323dd3e210c 100644 --- a/modules/costs/app/components/my/time_tracking/calendar.sass +++ b/modules/costs/app/components/my/time_tracking/calendar_component.sass @@ -1,4 +1,4 @@ -.time-entry-calendar +.my-time-tracking-calendar-view height: 100% .op-fc-wrapper diff --git a/modules/costs/app/components/my/time_tracking/list_component.html.erb b/modules/costs/app/components/my/time_tracking/list_component.html.erb index bf0a724566c..d08b7916ba2 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.html.erb +++ b/modules/costs/app/components/my/time_tracking/list_component.html.erb @@ -2,16 +2,20 @@ <%= render(My::TimeTracking::HeaderComponent.new(mode:, date:, view_mode: :list)) %> <%= render(My::TimeTracking::SubHeaderComponent.new(mode:, date:, view_mode: :list)) %> + <%= flex_layout(classes: ["my-time-tracking-list-view"]) do |flex| %> <% range.each do |date| %> - <%= render(Primer::OpenProject::CollapsibleSection.new(collapsed: collapsed?(date))) do |section| %> - <% section.with_title { date_title(date) } %> - <% section.with_caption { date_caption(date) } %> - <% section.with_additional_information do %> - <%= render(My::TimeTracking::ListStatsComponent.new(time_entries: grouped_time_entries[date], date: date)) %> - <% end %> - <% section.with_collapsible_content do %> - <%= render(My::TimeTracking::ListWrapperComponent.new(time_entries: grouped_time_entries[date], date: date, mode: mode)) %> + <% flex.with_row do %> + <%= render(Primer::OpenProject::CollapsibleSection.new(collapsed: collapsed?(date))) do |section| %> + <% section.with_title { date_title(date) } %> + <% section.with_caption { date_caption(date) } %> + <% section.with_additional_information do %> + <%= render(My::TimeTracking::ListStatsComponent.new(time_entries: grouped_time_entries[date], date: date)) %> + <% end %> + <% section.with_collapsible_content do %> + <%= render(My::TimeTracking::ListWrapperComponent.new(time_entries: grouped_time_entries[date], date: date, mode: mode)) %> + <% end %> <% end %> <% end %> <% end %> + <% end %> <% end %> diff --git a/modules/costs/app/components/my/time_tracking/list_component.rb b/modules/costs/app/components/my/time_tracking/list_component.rb index 848dc4f7f64..6062de42cc8 100644 --- a/modules/costs/app/components/my/time_tracking/list_component.rb +++ b/modules/costs/app/components/my/time_tracking/list_component.rb @@ -32,6 +32,7 @@ module My module TimeTracking class ListComponent < ApplicationComponent include OpTurbo::Streamable + include OpPrimer::ComponentHelpers options time_entries: [], mode: :week, diff --git a/modules/costs/app/components/my/time_tracking/list_component.sass b/modules/costs/app/components/my/time_tracking/list_component.sass new file mode 100644 index 00000000000..5b6fed818b9 --- /dev/null +++ b/modules/costs/app/components/my/time_tracking/list_component.sass @@ -0,0 +1,2 @@ +.my-time-tracking-list-view + row-gap: var(--Layout-row-gap)