Browse Source

fix controller

main
Christoph Marzell 5 days ago
parent
commit
20836f4349
  1. 608
      app/controllers/entries_controller.rb

608
app/controllers/entries_controller.rb

@ -1,163 +1,203 @@
class EntriesController < ApplicationController class EntriesController < ApplicationController
before_action :authenticate_user! before_action :authenticate_user!
before_action :set_entry, only: %i[edit update destroy stop_timer]
before_action :set_entry, only: %i[
edit
update
destroy
stop_timer
]
# ============================================================
# INDEX
# ============================================================
def index def index
@entries = current_user.entries.order(date: :desc) @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)
# Für sämtliche Fortschrittsberechnungen nur Einträge
# bis einschließlich heute berücksichtigen.
#
# Zukünftige geplante Einträge erscheinen weiterhin in
# @entries, verändern aber noch nicht den Fortschritt.
@calculation_entries =
current_user.entries.where("date <= ?", Date.current)
@running_entry =
current_user.entries.find_by(
end_time: nil,
beschreibung: "Timer"
)
@running_entry = current_user.entries.find_by(
end_time: nil,
beschreibung: "Timer"
)
# ----------------------------------------------------------
# Gesamtzeiten
# ----------------------------------------------------------
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 = sum_minutes(@calculation_entries)
@total_minutes_praktikum_prop =
sum_minutes(
@calculation_entries.where(
praktikums_typ: "propädeutikum",
entry_art: "Praktikum"
)
)
@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: "fachspezifikum",
entry_art: "Praktikum"
)
) )
)
@total_minutes_praktikum_fach = sum_minutes(
@calculation_entries.where(
praktikums_typ: Entry::PRAKTIKUMSTYPEN[1],
entry_art: Entry::ENTRY_ARTEN[0]
@total_minutes_praktikum_mediation =
sum_minutes(
@calculation_entries.where(
praktikums_typ: "mediation"
)
) )
)
@total_kilometer_pauschale = @entries.sum(&:kilometer_pauschale)
# ----------------------------------------------------------
# Kilometer
# ----------------------------------------------------------
# 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
@total_kilometer_pauschale =
@entries.sum(&:kilometer_pauschale)
@remaining_minutes_matrix = {}
@spent_minutes_matrix = {}
# ----------------------------------------------------------
# Tatsächlich geleistete Zeit nach Typ / Art
# ----------------------------------------------------------
@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
build_time_by_typ_art
@completion_percent_by_typ_art = {}
@progress_chart_data = {}
@progress_colors = {}
# ----------------------------------------------------------
# Soll / Erledigt / Verbleibend
# ----------------------------------------------------------
@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
build_progress_matrices
# ----------------------------------------------------------
# Prozentwerte + Diagramm
# ----------------------------------------------------------
@total_kilometer_costs_by_year =
build_completion_data
# ----------------------------------------------------------
# Kosten
# ----------------------------------------------------------
@total_kilometer_costs_by_year =
Entry.total_kilometer_cost_by_year(current_user) Entry.total_kilometer_cost_by_year(current_user)
@fortbildungskosten_by_year =
@fortbildungskosten_by_year =
Entry.total_fortbildungskosten_by_year(current_user) Entry.total_fortbildungskosten_by_year(current_user)
@selbstsupervision_by_year =
@selbstsupervision_by_year =
Entry.total_supervision_by_year(current_user) Entry.total_supervision_by_year(current_user)
@selbsterfahrungskosten_by_year = @selbsterfahrungskosten_by_year =
Entry.total_selbsterfahrungskosten_by_year(current_user) Entry.total_selbsterfahrungskosten_by_year(current_user)
@allekosten_by_year =
@allekosten_by_year =
Entry.total_semesterkosten_by_year(current_user) Entry.total_semesterkosten_by_year(current_user)
@gesamtkosten_by_year =
@gesamtkosten_by_year =
Entry.total_gesamtkosten_by_year(current_user) Entry.total_gesamtkosten_by_year(current_user)
calculate_estimated_end_dates(weekly_target_matrix)
calculate_actual_hours_per_week(required_hours_matrix)
# ----------------------------------------------------------
# Hochrechnungen
# ----------------------------------------------------------
calculate_estimated_end_dates
calculate_actual_hours_per_week
end end
# ============================================================
# NEW
# ============================================================
def new def new
@entry = current_user.entries.build @entry = current_user.entries.build
end end
# ============================================================
# TIMER START
# ============================================================
def start_timer def start_timer
typ = params[:typ] typ = params[:typ]
art = params[:art] art = params[:art]
unless Entry::PRAKTIKUMSTYPEN.include?(typ)
redirect_to entries_path,
alert: "Ungültiger Ausbildungstyp."
return
end
unless Entry::ENTRY_ARTEN.include?(art)
redirect_to entries_path,
alert: "Ungültige Eintragsart."
return
end
if current_user.praepedeutikum_abgeschlossen? &&
typ == "propädeutikum"
redirect_to entries_path,
alert: "Propädeutikum ist bereits abgeschlossen – Neuer Eintrag dieses Typs ist nicht erlaubt."
return
end
if current_user.entries.exists?(
end_time: nil,
beschreibung: "Timer"
)
redirect_to entries_path,
alert: "Es läuft bereits ein Timer."
return
end
current_user.entries.create!( current_user.entries.create!(
date: Date.current,
start_time: Time.current, start_time: Time.current,
beschreibung: "Timer", beschreibung: "Timer",
praktikums_typ: typ, praktikums_typ: typ,
entry_art: art entry_art: art
) )
redirect_to entries_path, notice: "Timer gestartet"
redirect_to entries_path,
notice: "Timer gestartet"
end end
# ============================================================
# TIMER STOP
# ============================================================
def stop_timer def stop_timer
unless @entry.start_time.present?
redirect_to entries_path,
alert: "Dieser Eintrag besitzt keine Startzeit."
return
end
@entry.end_time = Time.current @entry.end_time = Time.current
if ActiveModel::Type::Boolean.new.cast(params[:lunch_break]) if ActiveModel::Type::Boolean.new.cast(params[:lunch_break])
@entry.lunch_break_minutes = 30 @entry.lunch_break_minutes = 30
else
@entry.lunch_break_minutes ||= 0
end end
total_minutes = @entry.total_minutes_including_break
total_minutes =
@entry.total_minutes_including_break.to_i
@entry.hours =
total_minutes / 60
@entry.hours = total_minutes / 60
@entry.minutes = total_minutes % 60
@entry.minutes =
total_minutes % 60
@entry.save! @entry.save!
@ -165,11 +205,16 @@ class EntriesController < ApplicationController
notice: "Eintrag gestoppt – Gesamtzeit: #{@entry.hours} h #{@entry.minutes} min" notice: "Eintrag gestoppt – Gesamtzeit: #{@entry.hours} h #{@entry.minutes} min"
end end
# ============================================================
# CREATE
# ============================================================
def create def create
@entry = current_user.entries.new(entry_params)
@entry =
current_user.entries.new(entry_params)
if current_user.praepedeutikum_abgeschlossen? && if current_user.praepedeutikum_abgeschlossen? &&
@entry.praktikums_typ == Entry::PRAKTIKUMSTYPEN[0]
@entry.praktikums_typ == "propädeutikum"
redirect_to entries_path, redirect_to entries_path,
alert: "Propädeutikum ist bereits abgeschlossen – Neuer Eintrag dieses Typs ist nicht erlaubt." alert: "Propädeutikum ist bereits abgeschlossen – Neuer Eintrag dieses Typs ist nicht erlaubt."
@ -178,31 +223,53 @@ class EntriesController < ApplicationController
end end
if @entry.save if @entry.save
redirect_to entries_path, notice: "Eintrag gespeichert"
redirect_to entries_path,
notice: "Eintrag gespeichert"
else else
render :new, status: :unprocessable_entity
render :new,
status: :unprocessable_entity
end end
end end
# ============================================================
# EDIT
# ============================================================
def edit def edit
end end
# ============================================================
# UPDATE
# ============================================================
def update def update
if @entry.update(entry_params) if @entry.update(entry_params)
redirect_to entries_path, notice: "Eintrag aktualisiert"
redirect_to entries_path,
notice: "Eintrag aktualisiert"
else else
render :edit, status: :unprocessable_entity
render :edit,
status: :unprocessable_entity
end end
end end
# ============================================================
# DESTROY
# ============================================================
def destroy def destroy
@entry.destroy! @entry.destroy!
redirect_to entries_path, notice: "Eintrag gelöscht"
redirect_to entries_path,
notice: "Eintrag gelöscht"
end end
# ============================================================
# CSV EXPORT
# ============================================================
def export_csv def export_csv
@entries = current_user.entries.order(date: :desc)
@entries =
current_user.entries.order(date: :desc)
respond_to do |format| respond_to do |format|
format.csv do format.csv do
@ -214,28 +281,44 @@ class EntriesController < ApplicationController
end end
end end
# ============================================================
# MONATSBERICHT
# ============================================================
def monthly_report 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")
)
rows =
current_user.entries
.select(
Arel.sql(
"DATE_TRUNC('month', date) AS month"
),
:praktikums_typ,
:entry_art,
Arel.sql(
"SUM(COALESCE(hours, 0) * 60 + COALESCE(minutes, 0)) AS total_minutes"
)
)
.group(
Arel.sql(
"DATE_TRUNC('month', date)"
),
:praktikums_typ,
:entry_art
)
.order(
Arel.sql(
"DATE_TRUNC('month', date) DESC"
)
)
@report = {} @report = {}
rows.each do |row| rows.each do |row|
month = row.attributes["month"].to_date
total_minutes = row.attributes["total_minutes"].to_i
month =
row.attributes["month"].to_date
total_minutes =
row.attributes["total_minutes"].to_i
next if total_minutes.zero? next if total_minutes.zero?
@ -256,73 +339,263 @@ class EntriesController < ApplicationController
private private
# ============================================================
# ENTRY LADEN
# ============================================================
def set_entry def set_entry
@entry = current_user.entries.find(params[:id])
@entry =
current_user.entries.find(params[:id])
end end
# ============================================================
# MINUTEN SUMMIEREN
# ============================================================
def sum_minutes(entries) def sum_minutes(entries)
entries.sum do |entry| entries.sum do |entry|
entry.hours.to_i * 60 + entry.minutes.to_i
entry.hours.to_i * 60 +
entry.minutes.to_i
end
end
# ============================================================
# GELEISTETE ZEIT NACH TYP / ART
# ============================================================
def build_time_by_typ_art
@time_by_typ_art =
@calculation_entries
.group_by(&:praktikums_typ)
.transform_values do |type_entries|
type_entries
.group_by(&:entry_art)
.transform_values do |entries|
sum_minutes(entries)
end
end
end
# ============================================================
# FORTSCHRITTSMATRIZEN
#
# Wichtig:
#
# Nicht nur Kombinationen mit bestehenden Entries erzeugen,
# sondern ALLE bekannten Kombinationen.
#
# Dadurch existiert z. B.
#
# Mediation / Peergruppenarbeit
# 0 h erledigt / 24 h offen
#
# auch bevor der erste Entry angelegt wurde.
# ============================================================
def build_progress_matrices
@remaining_minutes_matrix = {}
@spent_minutes_matrix = {}
Entry::PRAKTIKUMSTYPEN.each do |typ|
@remaining_minutes_matrix[typ] = {}
@spent_minutes_matrix[typ] = {}
Entry::ENTRY_ARTEN.each do |art|
spent_minutes =
@time_by_typ_art
.dig(typ, art)
.to_i
required_hours =
current_user
.required_hours_for(typ, art)
.to_f
required_minutes =
(required_hours * 60).round
remaining_minutes = [
required_minutes - spent_minutes,
0
].max
@spent_minutes_matrix[typ][art] =
spent_minutes
@remaining_minutes_matrix[typ][art] =
remaining_minutes
end
end end
end end
def calculate_estimated_end_dates(weekly_target_matrix)
# ============================================================
# FORTSCHRITT IN PROZENT
# ============================================================
def build_completion_data
@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 =
current_user
.required_hours_for(typ, art)
.to_f
required_minutes =
(required_hours * 60).round
# Kategorien ohne Sollwert wie Fortbildung oder
# Semesterkosten gehören nicht in die Fortschrittsrechnung.
next unless required_minutes.positive?
completed_minutes = [
spent_minutes,
required_minutes
].min
percent =
if required_minutes.positive?
(
completed_minutes /
required_minutes.to_f *
100
).round
else
0
end
percent = [
percent,
100
].min
@completion_percent_by_typ_art[
[typ, art]
] = percent
# Im Chart nur Kategorien anzeigen, bei denen tatsächlich
# bereits etwas erledigt wurde.
#
# Die Tabelle kennt trotzdem auch 0-%-Anforderungen.
next unless spent_minutes.positive?
key =
"#{typ.capitalize}#{art}"
@progress_chart_data[key] =
percent
@progress_colors[key] =
progress_color_for(percent)
end
end
end
# ============================================================
# FARBE FÜR FORTSCHRITT
# ============================================================
def progress_color_for(percent)
if percent < 50
"#007bff"
elsif percent < 90
"#28a745"
else
"#dc3545"
end
end
# ============================================================
# VORAUSSICHTLICHES ENDDATUM
# ============================================================
def calculate_estimated_end_dates
@estimated_end_by_typ_art = {} @estimated_end_by_typ_art = {}
@remaining_minutes_matrix.each do |typ, arts| @remaining_minutes_matrix.each do |typ, arts|
@estimated_end_by_typ_art[typ] ||= {}
@estimated_end_by_typ_art[typ] = {}
arts.each do |art, remaining_minutes| arts.each do |art, remaining_minutes|
hours_remaining = remaining_minutes / 60.0
weekly_hours = weekly_target_matrix.dig(typ, art).to_f
hours_remaining =
remaining_minutes / 60.0
weekly_hours =
current_user
.weekly_target_for(typ, art)
.to_f
if weekly_hours.positive? && hours_remaining.positive?
weeks_remaining = (
hours_remaining / weekly_hours
).ceil
if weekly_hours.positive? &&
hours_remaining.positive?
weeks_remaining =
(
hours_remaining /
weekly_hours
).ceil
@estimated_end_by_typ_art[typ][art] = @estimated_end_by_typ_art[typ][art] =
Date.current + weeks_remaining.weeks
Date.current +
weeks_remaining.weeks
else else
@estimated_end_by_typ_art[typ][art] = nil
@estimated_end_by_typ_art[typ][art] =
nil
end end
end end
end end
end end
def calculate_actual_hours_per_week(required_hours_matrix)
# ============================================================
# TATSÄCHLICHE STUNDEN PRO WOCHE
# ============================================================
def calculate_actual_hours_per_week
@actual_hours_per_week = {} @actual_hours_per_week = {}
Entry::PRAKTIKUMSTYPEN Entry::PRAKTIKUMSTYPEN
.product(Entry::ENTRY_ARTEN) .product(Entry::ENTRY_ARTEN)
.each do |typ, art| .each do |typ, art|
required_hours = required_hours_matrix.dig(typ, art).to_f
required_hours =
current_user
.required_hours_for(typ, art)
.to_f
# Ohne Sollwert ergibt eine Fortschrittsgeschwindigkeit
# keinen sinnvollen Wert.
next unless required_hours.positive? 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"
)
entries =
@calculation_entries
.where(
praktikums_typ: typ,
entry_art: art
)
.where(
"COALESCE(hours, 0) > 0 OR COALESCE(minutes, 0) > 0"
)
next unless entries.exists? next unless entries.exists?
spent_minutes = entries.sum do |entry|
entry.hours.to_i * 60 + entry.minutes.to_i
end
spent_minutes =
sum_minutes(entries)
first_entry_date =
entries.minimum(:date)
first_entry_date = entries.minimum(:date)
last_entry_date = entries.maximum(:date)
last_entry_date =
entries.maximum(:date)
next unless first_entry_date next unless first_entry_date
required_minutes = (required_hours * 60).round
completed = spent_minutes >= required_minutes
required_minutes =
(required_hours * 60).round
completed =
spent_minutes >= required_minutes
period_end = period_end =
if completed if completed
@ -331,19 +604,34 @@ class EntriesController < ApplicationController
Date.current Date.current
end end
total_days = (period_end - first_entry_date).to_i + 1
total_days = [total_days, 7].max
total_days =
(period_end - first_entry_date).to_i + 1
weeks = total_days / 7.0
# Mindestens eine Woche als Berechnungsbasis,
# damit einzelne Tagesblöcke keine künstlich extrem
# hohen Wochenwerte erzeugen.
total_days = [
total_days,
7
].max
weeks =
total_days / 7.0
actual_hours_per_week = actual_hours_per_week =
(spent_minutes / 60.0) / weeks
(spent_minutes / 60.0) /
weeks
@actual_hours_per_week[[typ, art]] =
actual_hours_per_week.round(2)
@actual_hours_per_week[
[typ, art]
] = actual_hours_per_week.round(2)
end end
end end
# ============================================================
# STRONG PARAMS
# ============================================================
def entry_params def entry_params
params.require(:entry).permit( params.require(:entry).permit(
:date, :date,

Loading…
Cancel
Save