Skip to content

Commit

Permalink
add time calculation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
klaustopher committed Dec 10, 2024
1 parent bbbd688 commit 978564d
Show file tree
Hide file tree
Showing 3 changed files with 56 additions and 19 deletions.
68 changes: 51 additions & 17 deletions frontend/src/stimulus/controllers/dynamic/time-entry.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,19 @@
*/

import { Controller } from '@hotwired/stimulus';
import { parseChronicDuration, outputChronicDuration } from 'core-app/shared/helpers/chronic_duration';
import flatpickr from 'flatpickr';

interface HTMLInputElementWithFlatpickr extends HTMLInputElement {
_flatpickr?:flatpickr.Instance;
}

export default class TimeEntryController extends Controller {
static targets = ['startTimeInput', 'endTimeInput', 'hoursInput'];

declare readonly startTimeInputTarget: HTMLInputElement;
declare readonly endTimeInputTarget: HTMLInputElement;
declare readonly hoursInputTarget: HTMLInputElement;
declare readonly startTimeInputTarget:HTMLInputElementWithFlatpickr;
declare readonly endTimeInputTarget:HTMLInputElementWithFlatpickr;
declare readonly hoursInputTarget:HTMLInputElement;

startTimeInputTargetConnected() {
this.initTimePicker(this.startTimeInputTarget);
Expand All @@ -46,33 +51,62 @@ export default class TimeEntryController extends Controller {
this.initTimePicker(this.endTimeInputTarget);
}

dateChanged() {
datesChanged(initiatedBy:HTMLInputElement) {
const startTimeParts = this.startTimeInputTarget.value.split(':');
const endTimeParts = this.endTimeInputTarget.value.split(':');

const startTime = parseInt(startTimeParts[0], 10) * 60 + parseInt(startTimeParts[1], 10);
const endTime = parseInt(endTimeParts[0], 10) * 60 + parseInt(endTimeParts[1], 10);
const startTimeInMinutes = parseInt(startTimeParts[0], 10) * 60 + parseInt(startTimeParts[1], 10);
const endTimeInMinutes = parseInt(endTimeParts[0], 10) * 60 + parseInt(endTimeParts[1], 10);
const hoursInMinutes = Math.round((parseChronicDuration(this.hoursInputTarget.value) || 0) / 60);

// We calculate the hours field if:
// - We have start & end time and no hours
// - We have start & end time and we have triggered the change from the end time field
if (startTimeInMinutes && endTimeInMinutes && (hoursInMinutes === 0 || initiatedBy === this.endTimeInputTarget)) {
const duration = endTimeInMinutes - startTimeInMinutes;
this.hoursInputTarget.value = outputChronicDuration(duration * 60, { format: 'hours_only' }) || '';
} else if (startTimeInMinutes && hoursInMinutes) {
const newEndTime = startTimeInMinutes + hoursInMinutes;

this.toggleEndPlusOneDayCapion(endTime < startTime);
const targetDate = new Date();
targetDate.setHours(Math.floor(newEndTime / 60));
targetDate.setMinutes(Math.round(newEndTime % 60));
targetDate.setSeconds(0);
this.endTimeInputTarget._flatpickr!.setDate(targetDate); // eslint-disable-line no-underscore-dangle
}

this.toggleEndTimePlusCaption(startTimeInMinutes + hoursInMinutes);
}

hoursChanged() {
// Parse input through our chronic duration parser and then reformat as hours that can be nicely parsed on the
// backend
const hours = parseChronicDuration(this.hoursInputTarget.value, { defaultUnit: 'hours', ignoreSecondsWhenColonSeperated: true });
this.hoursInputTarget.value = outputChronicDuration(hours, { format: 'hours_only' }) || '';

this.datesChanged(this.hoursInputTarget);
}

hoursKeyEnterPress(event:KeyboardEvent) {
if (event.currentTarget instanceof HTMLInputElement) {
event.currentTarget.blur();
}
}

toggleEndPlusOneDayCapion(show: boolean) {
toggleEndTimePlusCaption(endTimeInMinutes:number) {
const formControl = this.endTimeInputTarget.closest('.FormControl') as HTMLElement;
formControl.querySelectorAll('.FormControl-caption').forEach((caption) => caption.remove());

if (show) {
if (endTimeInMinutes > (24 * 60)) {
const diffInDays = Math.floor(endTimeInMinutes / (60 * 24));
const span = document.createElement('span');
span.className = 'FormControl-caption';
span.innerText = '+1 day';
span.innerText = `+ ${diffInDays} ${diffInDays === 1 ? 'day' : 'days'}`;
formControl.append(span);
} else {
const caption = formControl.querySelector('.FormControl-caption');
if (caption) {
caption.remove();
}
}
}

initTimePicker(field: HTMLInputElement) {
initTimePicker(field:HTMLInputElement) {
flatpickr(field, {
enableTime: true,
noCalendar: true,
Expand All @@ -81,7 +115,7 @@ export default class TimeEntryController extends Controller {
static: true,
appendTo: document.querySelector('#time-entry-dialog') as HTMLElement,
onChange: () => {
this.dateChanged();
this.datesChanged(field);
},
});
}
Expand Down
5 changes: 3 additions & 2 deletions modules/costs/app/components/time_entries/time_entry_form.rb
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ class TimeEntryForm < ApplicationForm
type: "date",
required: true,
datepicker_options: { inDialog: true },
value: model.spent_on&.iso8601,
label: TimeEntry.human_attribute_name(:spent_on)

f.group(layout: :horizontal) do |g|
# TODO: Add a time picker based on the date picker linked above
g.text_field name: :start_time,
required: true,
label: TimeEntry.human_attribute_name(:start_time),
Expand All @@ -47,7 +47,8 @@ class TimeEntryForm < ApplicationForm
f.text_field name: :hours,
required: true,
label: TimeEntry.human_attribute_name(:hours),
data: { "time-entry-target" => "hoursInput" }
data: { "time-entry-target" => "hoursInput",
"action" => "blur->time-entry#hoursChanged keypress.enter->time-entry#hoursKeyEnterPress" }

f.work_package_autocompleter name: :work_package_id,
label: TimeEntry.human_attribute_name(:work_package),
Expand Down
2 changes: 2 additions & 0 deletions modules/costs/app/models/time_entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ def costs_visible_by?(usr)
def start_timestamp
return nil if start_time.blank?
return nil if time_zone.blank?
return nil if spent_on.blank?

time_zone_object.local(spent_on.year, spent_on.month, spent_on.day, start_time / 60, start_time % 60)
end
Expand All @@ -136,6 +137,7 @@ def end_timestamp
return nil if start_time.blank?
return nil if time_zone.blank?
return nil if hours.blank?
return nil if spent_on.blank?

start_timestamp + hours.hours
end
Expand Down

0 comments on commit 978564d

Please sign in to comment.