You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
362 lines
8.7 KiB
362 lines
8.7 KiB
class EntriesController < ApplicationController
|
|
before_action :authenticate_user!
|
|
before_action :set_entry, only: %i[edit update destroy stop_timer]
|
|
|
|
def index
|
|
@entries = current_user.entries.order(date: :desc)
|
|
|
|
# Für Berechnungen nur Einträge bis einschließlich heute verwenden.
|
|
# Keine Sortierung setzen, da diese Relation später unterschiedlich
|
|
# ausgewertet wird.
|
|
@calculation_entries = current_user.entries
|
|
.where("date <= ?", Date.current)
|
|
|
|
@running_entry = current_user.entries.find_by(
|
|
end_time: nil,
|
|
beschreibung: "Timer"
|
|
)
|
|
|
|
|
|
required_hours_matrix = current_user.required_hours_matrix.to_h
|
|
weekly_target_matrix = current_user.weekly_target_matrix.to_h
|
|
|
|
@total_minutes = sum_minutes(@calculation_entries)
|
|
|
|
@total_minutes_praktikum_prop = sum_minutes(
|
|
@calculation_entries.where(
|
|
praktikums_typ: Entry::PRAKTIKUMSTYPEN[0],
|
|
entry_art: Entry::ENTRY_ARTEN[0]
|
|
)
|
|
)
|
|
|
|
@total_minutes_praktikum_fach = sum_minutes(
|
|
@calculation_entries.where(
|
|
praktikums_typ: Entry::PRAKTIKUMSTYPEN[1],
|
|
entry_art: Entry::ENTRY_ARTEN[0]
|
|
)
|
|
)
|
|
|
|
@total_kilometer_pauschale = @entries.sum(&:kilometer_pauschale)
|
|
|
|
# Bereits geleistete Zeit je Kombination aus Typ und Art
|
|
@time_by_typ_art = @calculation_entries
|
|
.group_by(&:praktikums_typ)
|
|
.transform_values do |type_entries|
|
|
type_entries
|
|
.group_by(&:entry_art)
|
|
.transform_values { |entries| sum_minutes(entries) }
|
|
end
|
|
|
|
@remaining_minutes_matrix = {}
|
|
@spent_minutes_matrix = {}
|
|
|
|
@time_by_typ_art.each do |typ, arts|
|
|
@remaining_minutes_matrix[typ] ||= {}
|
|
@spent_minutes_matrix[typ] ||= {}
|
|
|
|
arts.each do |art, spent_minutes|
|
|
required_hours = required_hours_matrix.dig(typ, art).to_f
|
|
|
|
# Erst nach der Multiplikation runden, damit auch Dezimalstunden
|
|
# wie 0.5 korrekt als 30 Minuten berechnet werden.
|
|
required_minutes = (required_hours * 60).round
|
|
|
|
remaining_minutes = [
|
|
required_minutes - spent_minutes,
|
|
0
|
|
].max
|
|
|
|
@remaining_minutes_matrix[typ][art] = remaining_minutes
|
|
@spent_minutes_matrix[typ][art] = spent_minutes
|
|
end
|
|
end
|
|
|
|
@completion_percent_by_typ_art = {}
|
|
@progress_chart_data = {}
|
|
@progress_colors = {}
|
|
|
|
@spent_minutes_matrix.each do |typ, arts|
|
|
arts.each do |art, spent_minutes|
|
|
required_hours = required_hours_matrix.dig(typ, art).to_f
|
|
required_minutes = (required_hours * 60).round
|
|
|
|
next unless required_minutes.positive?
|
|
next unless spent_minutes.positive?
|
|
|
|
completed_minutes = [spent_minutes, required_minutes].min
|
|
|
|
percent = (
|
|
completed_minutes / required_minutes.to_f * 100
|
|
).round
|
|
|
|
percent = [percent, 100].min
|
|
|
|
key = "#{typ.capitalize} – #{art}"
|
|
|
|
@completion_percent_by_typ_art[[typ, art]] = percent
|
|
@progress_chart_data[key] = percent
|
|
|
|
@progress_colors[key] =
|
|
if percent < 50
|
|
"#007bff"
|
|
elsif percent < 90
|
|
"#28a745"
|
|
else
|
|
"#dc3545"
|
|
end
|
|
end
|
|
end
|
|
|
|
@total_kilometer_costs_by_year =
|
|
Entry.total_kilometer_cost_by_year(current_user)
|
|
|
|
@fortbildungskosten_by_year =
|
|
Entry.total_fortbildungskosten_by_year(current_user)
|
|
|
|
@selbstsupervision_by_year =
|
|
Entry.total_supervision_by_year(current_user)
|
|
|
|
@selbsterfahrungskosten_by_year =
|
|
Entry.total_selbsterfahrungskosten_by_year(current_user)
|
|
|
|
@allekosten_by_year =
|
|
Entry.total_semesterkosten_by_year(current_user)
|
|
|
|
@gesamtkosten_by_year =
|
|
Entry.total_gesamtkosten_by_year(current_user)
|
|
|
|
calculate_estimated_end_dates(weekly_target_matrix)
|
|
calculate_actual_hours_per_week(required_hours_matrix)
|
|
end
|
|
|
|
def new
|
|
@entry = current_user.entries.build
|
|
end
|
|
|
|
def start_timer
|
|
typ = params[:typ]
|
|
art = params[:art]
|
|
|
|
current_user.entries.create!(
|
|
start_time: Time.current,
|
|
beschreibung: "Timer",
|
|
praktikums_typ: typ,
|
|
entry_art: art
|
|
)
|
|
|
|
redirect_to entries_path, notice: "Timer gestartet"
|
|
end
|
|
|
|
def stop_timer
|
|
@entry.end_time = Time.current
|
|
|
|
if ActiveModel::Type::Boolean.new.cast(params[:lunch_break])
|
|
@entry.lunch_break_minutes = 30
|
|
end
|
|
|
|
total_minutes = @entry.total_minutes_including_break
|
|
|
|
@entry.hours = total_minutes / 60
|
|
@entry.minutes = total_minutes % 60
|
|
|
|
@entry.save!
|
|
|
|
redirect_to entries_path,
|
|
notice: "Eintrag gestoppt – Gesamtzeit: #{@entry.hours} h #{@entry.minutes} min"
|
|
end
|
|
|
|
def create
|
|
@entry = current_user.entries.new(entry_params)
|
|
|
|
if current_user.praepedeutikum_abgeschlossen? &&
|
|
@entry.praktikums_typ == Entry::PRAKTIKUMSTYPEN[0]
|
|
|
|
redirect_to entries_path,
|
|
alert: "Propädeutikum ist bereits abgeschlossen – Neuer Eintrag dieses Typs ist nicht erlaubt."
|
|
|
|
return
|
|
end
|
|
|
|
if @entry.save
|
|
redirect_to entries_path, notice: "Eintrag gespeichert"
|
|
else
|
|
render :new, status: :unprocessable_entity
|
|
end
|
|
end
|
|
|
|
def edit
|
|
end
|
|
|
|
def update
|
|
if @entry.update(entry_params)
|
|
redirect_to entries_path, notice: "Eintrag aktualisiert"
|
|
else
|
|
render :edit, status: :unprocessable_entity
|
|
end
|
|
end
|
|
|
|
def destroy
|
|
@entry.destroy!
|
|
|
|
redirect_to entries_path, notice: "Eintrag gelöscht"
|
|
end
|
|
|
|
def export_csv
|
|
@entries = current_user.entries.order(date: :desc)
|
|
|
|
respond_to do |format|
|
|
format.csv do
|
|
send_data(
|
|
@entries.to_csv,
|
|
filename: "eintraege-#{Date.current}.csv"
|
|
)
|
|
end
|
|
end
|
|
end
|
|
|
|
def monthly_report
|
|
rows = current_user.entries
|
|
.select(
|
|
Arel.sql("DATE_TRUNC('month', date) AS month"),
|
|
:praktikums_typ,
|
|
:entry_art,
|
|
Arel.sql("SUM(hours * 60 + minutes) AS total_minutes")
|
|
)
|
|
.group(
|
|
Arel.sql("DATE_TRUNC('month', date)"),
|
|
:praktikums_typ,
|
|
:entry_art
|
|
)
|
|
.order(
|
|
Arel.sql("DATE_TRUNC('month', date) DESC")
|
|
)
|
|
|
|
@report = {}
|
|
|
|
rows.each do |row|
|
|
month = row.attributes["month"].to_date
|
|
total_minutes = row.attributes["total_minutes"].to_i
|
|
|
|
next if total_minutes.zero?
|
|
|
|
typ_art = [
|
|
row.praktikums_typ,
|
|
row.entry_art
|
|
]
|
|
|
|
@report[month] ||= {}
|
|
|
|
@report[month][typ_art] = {
|
|
total_minutes: total_minutes,
|
|
hours: total_minutes / 60,
|
|
minutes: total_minutes % 60
|
|
}
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def set_entry
|
|
@entry = current_user.entries.find(params[:id])
|
|
end
|
|
|
|
def sum_minutes(entries)
|
|
entries.sum do |entry|
|
|
entry.hours.to_i * 60 + entry.minutes.to_i
|
|
end
|
|
end
|
|
|
|
def calculate_estimated_end_dates(weekly_target_matrix)
|
|
@estimated_end_by_typ_art = {}
|
|
|
|
@remaining_minutes_matrix.each do |typ, arts|
|
|
@estimated_end_by_typ_art[typ] ||= {}
|
|
|
|
arts.each do |art, remaining_minutes|
|
|
hours_remaining = remaining_minutes / 60.0
|
|
weekly_hours = weekly_target_matrix.dig(typ, art).to_f
|
|
|
|
if weekly_hours.positive? && hours_remaining.positive?
|
|
weeks_remaining = (
|
|
hours_remaining / weekly_hours
|
|
).ceil
|
|
|
|
@estimated_end_by_typ_art[typ][art] =
|
|
Date.current + weeks_remaining.weeks
|
|
else
|
|
@estimated_end_by_typ_art[typ][art] = nil
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def calculate_actual_hours_per_week(required_hours_matrix)
|
|
@actual_hours_per_week = {}
|
|
|
|
Entry::PRAKTIKUMSTYPEN
|
|
.product(Entry::ENTRY_ARTEN)
|
|
.each do |typ, art|
|
|
|
|
required_hours = required_hours_matrix.dig(typ, art).to_f
|
|
|
|
next unless required_hours.positive?
|
|
|
|
entries = @calculation_entries
|
|
.where(
|
|
praktikums_typ: typ,
|
|
entry_art: art
|
|
)
|
|
.where(
|
|
"COALESCE(hours, 0) > 0 OR COALESCE(minutes, 0) > 0"
|
|
)
|
|
|
|
next unless entries.exists?
|
|
|
|
spent_minutes = entries.sum do |entry|
|
|
entry.hours.to_i * 60 + entry.minutes.to_i
|
|
end
|
|
|
|
first_entry_date = entries.minimum(:date)
|
|
last_entry_date = entries.maximum(:date)
|
|
|
|
next unless first_entry_date
|
|
|
|
required_minutes = (required_hours * 60).round
|
|
completed = spent_minutes >= required_minutes
|
|
|
|
period_end =
|
|
if completed
|
|
last_entry_date
|
|
else
|
|
Date.current
|
|
end
|
|
|
|
total_days = (period_end - first_entry_date).to_i + 1
|
|
total_days = [total_days, 7].max
|
|
|
|
weeks = total_days / 7.0
|
|
|
|
actual_hours_per_week =
|
|
(spent_minutes / 60.0) / weeks
|
|
|
|
@actual_hours_per_week[[typ, art]] =
|
|
actual_hours_per_week.round(2)
|
|
end
|
|
end
|
|
|
|
def entry_params
|
|
params.require(:entry).permit(
|
|
:date,
|
|
:hours,
|
|
:minutes,
|
|
:praktikums_typ,
|
|
:entry_art,
|
|
:distance_km,
|
|
:beschreibung,
|
|
:kosten,
|
|
:zaehlt_als_fortbildung,
|
|
:start_time,
|
|
:end_time
|
|
)
|
|
end
|
|
end
|