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.
650 lines
15 KiB
650 lines
15 KiB
class EntriesController < ApplicationController
|
|
before_action :authenticate_user!
|
|
before_action :set_entry, only: %i[
|
|
edit
|
|
update
|
|
destroy
|
|
stop_timer
|
|
]
|
|
|
|
# ============================================================
|
|
# INDEX
|
|
# ============================================================
|
|
|
|
def index
|
|
@entries = current_user.entries.order(date: :desc)
|
|
|
|
# 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"
|
|
)
|
|
|
|
# ----------------------------------------------------------
|
|
# Gesamtzeiten
|
|
# ----------------------------------------------------------
|
|
|
|
@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_fach =
|
|
sum_minutes(
|
|
@calculation_entries.where(
|
|
praktikums_typ: "fachspezifikum",
|
|
entry_art: "Praktikum"
|
|
)
|
|
)
|
|
|
|
@total_minutes_praktikum_mediation =
|
|
sum_minutes(
|
|
@calculation_entries.where(
|
|
praktikums_typ: "mediation"
|
|
)
|
|
)
|
|
|
|
# ----------------------------------------------------------
|
|
# Kilometer
|
|
# ----------------------------------------------------------
|
|
|
|
@total_kilometer_pauschale =
|
|
@entries.sum(&:kilometer_pauschale)
|
|
|
|
# ----------------------------------------------------------
|
|
# Tatsächlich geleistete Zeit nach Typ / Art
|
|
# ----------------------------------------------------------
|
|
|
|
build_time_by_typ_art
|
|
|
|
# ----------------------------------------------------------
|
|
# Soll / Erledigt / Verbleibend
|
|
# ----------------------------------------------------------
|
|
|
|
build_progress_matrices
|
|
|
|
# ----------------------------------------------------------
|
|
# Prozentwerte + Diagramm
|
|
# ----------------------------------------------------------
|
|
|
|
build_completion_data
|
|
|
|
# ----------------------------------------------------------
|
|
# Kosten
|
|
# ----------------------------------------------------------
|
|
|
|
@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)
|
|
|
|
# ----------------------------------------------------------
|
|
# Hochrechnungen
|
|
# ----------------------------------------------------------
|
|
|
|
calculate_estimated_end_dates
|
|
calculate_actual_hours_per_week
|
|
end
|
|
|
|
# ============================================================
|
|
# NEW
|
|
# ============================================================
|
|
|
|
def new
|
|
@entry = current_user.entries.build
|
|
end
|
|
|
|
# ============================================================
|
|
# TIMER START
|
|
# ============================================================
|
|
|
|
def start_timer
|
|
typ = params[:typ]
|
|
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!(
|
|
date: Date.current,
|
|
start_time: Time.current,
|
|
beschreibung: "Timer",
|
|
praktikums_typ: typ,
|
|
entry_art: art
|
|
)
|
|
|
|
redirect_to entries_path,
|
|
notice: "Timer gestartet"
|
|
end
|
|
|
|
# ============================================================
|
|
# TIMER STOP
|
|
# ============================================================
|
|
|
|
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
|
|
|
|
if ActiveModel::Type::Boolean.new.cast(params[:lunch_break])
|
|
@entry.lunch_break_minutes = 30
|
|
else
|
|
@entry.lunch_break_minutes ||= 0
|
|
end
|
|
|
|
total_minutes =
|
|
@entry.total_minutes_including_break.to_i
|
|
|
|
@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
|
|
|
|
# ============================================================
|
|
# CREATE
|
|
# ============================================================
|
|
|
|
def create
|
|
@entry =
|
|
current_user.entries.new(entry_params)
|
|
|
|
if current_user.praepedeutikum_abgeschlossen? &&
|
|
@entry.praktikums_typ == "propädeutikum"
|
|
|
|
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
|
|
|
|
# ============================================================
|
|
# EDIT
|
|
# ============================================================
|
|
|
|
def edit
|
|
end
|
|
|
|
# ============================================================
|
|
# UPDATE
|
|
# ============================================================
|
|
|
|
def update
|
|
if @entry.update(entry_params)
|
|
redirect_to entries_path,
|
|
notice: "Eintrag aktualisiert"
|
|
else
|
|
render :edit,
|
|
status: :unprocessable_entity
|
|
end
|
|
end
|
|
|
|
# ============================================================
|
|
# DESTROY
|
|
# ============================================================
|
|
|
|
def destroy
|
|
@entry.destroy!
|
|
|
|
redirect_to entries_path,
|
|
notice: "Eintrag gelöscht"
|
|
end
|
|
|
|
# ============================================================
|
|
# CSV EXPORT
|
|
# ============================================================
|
|
|
|
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
|
|
|
|
# ============================================================
|
|
# MONATSBERICHT
|
|
# ============================================================
|
|
|
|
def monthly_report
|
|
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 = {}
|
|
|
|
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
|
|
|
|
# ============================================================
|
|
# ENTRY LADEN
|
|
# ============================================================
|
|
|
|
def set_entry
|
|
@entry =
|
|
current_user.entries.find(params[:id])
|
|
end
|
|
|
|
# ============================================================
|
|
# MINUTEN SUMMIEREN
|
|
# ============================================================
|
|
|
|
def sum_minutes(entries)
|
|
entries.sum do |entry|
|
|
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
|
|
|
|
# ============================================================
|
|
# 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 = {}
|
|
|
|
@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 =
|
|
current_user
|
|
.weekly_target_for(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
|
|
|
|
# ============================================================
|
|
# TATSÄCHLICHE STUNDEN PRO WOCHE
|
|
# ============================================================
|
|
|
|
def calculate_actual_hours_per_week
|
|
@actual_hours_per_week = {}
|
|
|
|
Entry::PRAKTIKUMSTYPEN
|
|
.product(Entry::ENTRY_ARTEN)
|
|
.each do |typ, art|
|
|
|
|
required_hours =
|
|
current_user
|
|
.required_hours_for(typ, art)
|
|
.to_f
|
|
|
|
# Ohne Sollwert ergibt eine Fortschrittsgeschwindigkeit
|
|
# keinen sinnvollen Wert.
|
|
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 =
|
|
sum_minutes(entries)
|
|
|
|
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
|
|
|
|
# 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 =
|
|
(spent_minutes / 60.0) /
|
|
weeks
|
|
|
|
@actual_hours_per_week[
|
|
[typ, art]
|
|
] = actual_hours_per_week.round(2)
|
|
end
|
|
end
|
|
|
|
# ============================================================
|
|
# STRONG PARAMS
|
|
# ============================================================
|
|
|
|
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
|