Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions app/assets/javascripts/worktimes.js.coffee
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ app.worktimes = new class
$('#multi').hide()
e.preventDefault() if e

parseDate = (dateStr) ->
[d, m, y] = dateStr.split('.')
new Date "#{y}-#{m}-#{d}"

init: ->
@bind()
@initWaypoint()
Expand Down Expand Up @@ -75,8 +79,12 @@ app.worktimes = new class
else if $('#new_absencetime').length
showRegularAbsence(null)

if $('#ordertime_repetitions').val()
@recalcMaxRepetitions()

$('#multi_absence_link').click(showMultiAbsence)
$('#regular_absence_link').click(showRegularAbsence)
$('#ordertime_work_date').change(@recalcMaxRepetitions)

initWaypoint: ->
if worktimesWaypoint
Expand Down Expand Up @@ -161,5 +169,22 @@ app.worktimes = new class
setTimeout((-> entries.removeClass('highlight')), 400)
)

# Calculates max amount of ordertime repetitions based on weekday
# Monday -> 5, Tuesday -> 4, ...
recalcMaxRepetitions: () ->
repField = $('#ordertime_repetitions')
dateStr = $('#ordertime_work_date').val()

weekDay = parseDate(dateStr).getDay()
max = switch weekDay
when 0, 6 then 1 # Sunday, Saturday
else 6 - weekDay # Weekdays

currentVal = Number(repField.val())
repField.attr('max', max)
# Repetitions must not be higher than new max value
repField.val(Math.min(currentVal, max))


$(document).on 'turbolinks:load', ->
app.worktimes.init()
25 changes: 25 additions & 0 deletions app/controllers/ordertimes_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,31 @@ class OrdertimesController < WorktimesController

after_destroy :send_email_notification

def create
repetitions = params[:ordertime][:repetitions].to_i.nonzero? || 1
if repetitions > 1
@multi_form = Forms::MultiOrdertime.new(params.require(:ordertime).permit(permitted_attrs + [:repetitions]))
@multi_form.employee = Employee.find_by(id: employee_id)

@multi_form.prepare_each.with_index do |ot_params, idx|
@_params = ot_params
# Otherwise only one entity will be created and afterward updated with each cycle
model_ivar_set(Ordertime.new)

is_last = idx == repetitions - 1
super do |format|
if is_last
flash[:notice] = "#{repetitions} Arbeitszeiten wurden erfasst"
else
format.html
end
end
end
else
super
end
end

def update
if entry.employee_id == @user.id && !entry.worktimes_committed?
super
Expand Down
75 changes: 75 additions & 0 deletions app/domain/forms/multi_ordertime.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# frozen_string_literal: true

# Copyright (c) 2006-2025, Puzzle ITC GmbH. This file is part of
# PuzzleTime and licensed under the Affero General Public License version 3
# or later. See the COPYING file at the top-level directory or at
# https://github.com/puzzle/puzzletime.

module Forms
class MultiOrdertime
include ActiveModel::Model
include ActiveModel::Attributes

attribute :account_id, :integer
attribute :ticket, :string
attribute :description, :string
attribute :internal_description, :string
attribute :work_date, :date
attribute :repetitions, :integer, default: 1
attribute :hours, :decimal
attribute :billable, :boolean
attribute :from_start_time, :string
attribute :to_end_time, :string

attr_accessor :employee

validates :work_date, presence: true
validates :repetitions,
presence: true,
numericality: {
only_integer: true,
greater_than_or_equal_to: 1,
less_than_or_equal_to: ->(form) { form.max_allowed_repetitions }
},
if: :work_date

def prepare_each
return enum_for(:prepare_each) unless block_given?

period.step do |date|
employment = employee.employment_at(date)
next unless employment

yield build_params(date)
end
end

def max_allowed_repetitions
return 1 unless work_date

case work_date.wday
when 0, 6 # Sunday, Saturday
1
else # Weekdays
6 - work_date.wday
end
end

private

def end_date
work_date + repetitions - 1
end

def period
Period.new(work_date, end_date)
end

def build_params(date)
p = attributes.except('repetitions')
p['work_date'] = date

ActionController::Parameters.new(ordertime: p)
end
end
end
8 changes: 7 additions & 1 deletion app/views/ordertimes/_form.html.haml
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,16 @@
= f.labeled_input_field(:ticket, span: 2)
= f.labeled_text_area(:description)
= f.labeled_text_area(:internal_description, html_options = {title: "Wird innerhalb von PuzzleTime und im CSV-Export angezeigt, ist im Zeitrapport jedoch nicht enthalten", data: {toggle: :tooltip}, rows: 2},)
= f.labeled_date_field(:work_date,
.form-group
= f.label(:work_date, class: 'col-md-2 control-label')
.col-md-2
= f.date_field(:work_date,
data: { remote: true,
url: url_for(action: 'existing'),
dynamic_params: 'ordertime[employee_id]' })
= f.label(:repetitions, 'Wiederholungen an Folgetagen', class: 'col-md-2 control-label')
.col-md-1
= f.number_field(:repetitions, html_options = {title: "Erstellt Kopien für aufeinanderfolgende Tage ab dem gewähltem Datum", data: {toggle: :tooltip}, value: 1, min: 1, max: 5, step: 1})
.form-group
= f.label(:hours, class: 'col-md-2 control-label')
.col-md-1
Expand Down
28 changes: 28 additions & 0 deletions test/controllers/ordertimes_controller_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,34 @@ def test_create_with_zero_percent_employment
assert_match(/unbezahlter Urlaub/, flash[:warning])
end

def test_create_repeated_ordertimes
reps = 3
work_date = Date.parse('2026-01-05')
login_as :long_time_john

assert_difference 'Ordertime.count', reps, "Es sollten genau #{reps} Einträge erstellt werden" do
post :create, params: {
ordertime: {
account_id: work_items(:allgemein),
work_date: work_date,
hours: '5:30',
employee: employees(:long_time_john),
repetitions: reps,
description: 'Repeated worktime'
}
}
end

assert_response :redirect
assert_redirected_to ordertimes_path(week_date: work_date + reps - 1)
assert_equal "#{reps} Arbeitszeiten wurden erfasst", flash[:notice]

created_dates = Ordertime.last(reps).map(&:work_date)
expected_dates = (0..reps - 1).map { |i| work_date + i }

assert_equal expected_dates, created_dates
end

def test_create_other
work_items(:allgemein).update(closed: false)
post :create, params: {
Expand Down
45 changes: 45 additions & 0 deletions test/domain/forms/multi_ordertime_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

require 'test_helper'

module Forms
class MultiOrdertimeTest < ActiveSupport::TestCase
test 'max_allowed_repetitions logic' do
# 05.01.2025 (Monday) -> 5
form_mo = Forms::MultiOrdertime.new(work_date: Date.parse('2026-01-05'))

assert_equal 5, form_mo.send(:max_allowed_repetitions)

# 08.01.2025 (Thursday) -> 2
form_th = Forms::MultiOrdertime.new(work_date: Date.parse('2026-01-08'))

assert_equal 2, form_th.send(:max_allowed_repetitions)

# 09.01.2025 (Friday) -> 1
form_fr = Forms::MultiOrdertime.new(work_date: Date.parse('2026-01-09'))

assert_equal 1, form_fr.send(:max_allowed_repetitions)

# 10.01.2025 (Saturday) -> 1
form_sa = Forms::MultiOrdertime.new(work_date: Date.parse('2026-01-10'))

assert_equal 1, form_sa.send(:max_allowed_repetitions)

# 11.01.2025 (Sunday) -> 1
form_su = Forms::MultiOrdertime.new(work_date: Date.parse('2026-01-11'))

assert_equal 1, form_su.send(:max_allowed_repetitions)
end

test 'validation of repetitions' do
form = Forms::MultiOrdertime.new(work_date: Date.parse('2026-01-07'), repetitions: 4)

assert_not_predicate form, :valid?
assert_includes form.errors[:repetitions], 'muss kleiner oder gleich 3 sein'

form = Forms::MultiOrdertime.new(work_date: Date.parse('2026-01-07'), repetitions: 3)

assert_predicate form, :valid?
end
end
end
9 changes: 9 additions & 0 deletions test/integration/create_ordertime_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,15 @@ class CreateOrdertimeTest < ActionDispatch::IntegrationTest
assert_equal progressbar_color, expected_color(percentage)
end

test 'creating repeated worktimes from a Wednesday on only allows 3 repetitions' do
timeout_safe do
fill_in('ordertime_work_date', with: '07.01.2026') # A Wednesday
input = find('input[name*="repetitions"]')

assert_equal '3', input[:max]
end
end

def login
login_as(:pascal)
visit(new_ordertime_path)
Expand Down