require "csv" class Entry < ApplicationRecord belongs_to :user # ------------------------------------------------------------ # Konstanten # ------------------------------------------------------------ PRAKTIKUMSTYPEN = %w[ propädeutikum fachspezifikum mediation ].freeze ENTRY_ARTEN = [ # Allgemein "Praktikum", "Selbsterfahrung", "Supervision", "Fortbildung", "Semesterkosten", # Mediation "Präsenzmodul", "Peergruppenarbeit", "Fallarbeit", "Praxisseminare", "Literatur- und Selbststudium", # Fachspezifikum "Gruppenselbsterfahrung", "Theorie/Methodikseminare", "Peergruppensupervision", "Einzel-/Kleingruppensupervision", "Eigenständige Tätigkeit" ].freeze LEGACY_ENTRY_ART_ALIASES = { "Peergroup" => "Peergruppenarbeit", "Peer-Gruppe" => "Peergruppenarbeit" }.freeze # ------------------------------------------------------------ # Validierungen # ------------------------------------------------------------ validates :date, presence: true validates :hours, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, unless: -> { start_time.present? && end_time.nil? } validates :minutes, numericality: { only_integer: true, greater_than_or_equal_to: 0, less_than: 60 }, unless: -> { start_time.present? && end_time.nil? } validates :praktikums_typ, inclusion: { in: PRAKTIKUMSTYPEN } validates :entry_art, inclusion: { in: ENTRY_ARTEN } validates :distance_km, numericality: { only_integer: true, greater_than_or_equal_to: 0 }, allow_nil: true # ------------------------------------------------------------ # Callbacks # ------------------------------------------------------------ before_validation :normalize_legacy_entry_art before_save :normalize_time after_initialize :set_default_date, if: :new_record? # ------------------------------------------------------------ # Scopes # ------------------------------------------------------------ scope :propädeutikum, -> { where(praktikums_typ: "propädeutikum") } scope :fachspezifikum, -> { where(praktikums_typ: "fachspezifikum") } scope :mediation, -> { where(praktikums_typ: "mediation") } scope :peergruppenarbeit, -> { where(entry_art: "Peergruppenarbeit") } scope :fallarbeit, -> { where(entry_art: "Fallarbeit") } scope :praxisseminare, -> { where(entry_art: "Praxisseminare") } scope :praesenzmodule, -> { where(entry_art: "Präsenzmodul") } scope :literatur_und_selbststudium, -> { where(entry_art: "Literatur- und Selbststudium") } # ------------------------------------------------------------ # Defaults # ------------------------------------------------------------ def set_default_date self.date ||= Date.current end # ------------------------------------------------------------ # Zeit # ------------------------------------------------------------ def total_minutes hours.to_i * 60 + minutes.to_i end def total_minutes_including_break return nil unless start_time.present? && end_time.present? worked_minutes = ((end_time - start_time) / 60).to_i worked_minutes -= lunch_break_minutes.to_i [worked_minutes, 0].max end def hours_and_minutes mins = total_minutes_including_break return [hours.to_i, minutes.to_i] if mins.nil? [ mins / 60, mins % 60 ] end # ------------------------------------------------------------ # Datum # ------------------------------------------------------------ def jahr date&.year end # ------------------------------------------------------------ # Kilometer # ------------------------------------------------------------ def kilometer_pauschale return 0 unless distance_km.present? year = date&.year rate = MileageRate.find_by(year: year)&.rate_per_km || 0.42 (distance_km.to_d * rate.to_d).round(2) end def self.total_kilometer_cost_by_year(user) yearly_distances = where(user: user) .where.not(distance_km: nil) .group(Arel.sql("EXTRACT(YEAR FROM date)")) .order(Arel.sql("EXTRACT(YEAR FROM date) DESC")) .sum(:distance_km) yearly_distances.each_with_object({}) do |(year, km_sum), result| rate = MileageRate.find_by(year: year.to_i)&.rate_per_km || 0.42 result[year.to_i] = (km_sum.to_d * rate.to_d).round(2) end end # ------------------------------------------------------------ # Kosten # ------------------------------------------------------------ def self.total_fortbildungskosten_by_year(user) where(user: user) .where.not(kosten: nil) .where( Entry.arel_table[:entry_art] .eq("Fortbildung") .or( Entry.arel_table[:zaehlt_als_fortbildung] .eq(true) ) ) .group(Arel.sql("DATE_PART('year', date)")) .order(Arel.sql("DATE_PART('year', date) DESC")) .pluck( Arel.sql("DATE_PART('year', date)::int"), Arel.sql("SUM(kosten)") ) .to_h end def self.total_selbsterfahrungskosten_by_year(user) where( user: user, entry_art: [ "Selbsterfahrung", "Gruppenselbsterfahrung" ] ) .where.not(kosten: nil) .group(Arel.sql("DATE_PART('year', date)")) .order(Arel.sql("DATE_PART('year', date) DESC")) .pluck( Arel.sql("DATE_PART('year', date)::int"), Arel.sql("SUM(kosten)") ) .to_h end def self.total_supervision_by_year(user) where( user: user, entry_art: [ "Supervision", "Peergruppensupervision", "Einzel-/Kleingruppensupervision" ] ) .where.not(kosten: nil) .group(Arel.sql("DATE_PART('year', date)")) .order(Arel.sql("DATE_PART('year', date) DESC")) .pluck( Arel.sql("DATE_PART('year', date)::int"), Arel.sql("SUM(kosten)") ) .to_h end # Der Name bleibt aus Kompatibilitätsgründen erhalten. # # Die Methode summiert weiterhin alle eingetragenen Kosten # eines Jahres, so wie dein bisheriger Code. def self.total_semesterkosten_by_year(user) where(user: user) .where.not(kosten: nil) .group(Arel.sql("DATE_PART('year', date)")) .order(Arel.sql("DATE_PART('year', date) DESC")) .pluck( Arel.sql("DATE_PART('year', date)::int"), Arel.sql("SUM(kosten)") ) .to_h end def self.total_gesamtkosten_by_year(user) semester_by_year = total_semesterkosten_by_year(user) km_by_year = total_kilometer_cost_by_year(user) years = ( semester_by_year.keys + km_by_year.keys ) .uniq .sort .reverse years.each_with_object({}) do |year, out| semester = (semester_by_year[year] || 0).to_d km = (km_by_year[year] || 0).to_d out[year] = (semester + km).round(2) end end # ------------------------------------------------------------ # Mediation # ------------------------------------------------------------ def mediation? praktikums_typ == "mediation" end def praesenzmodul? mediation? && entry_art == "Präsenzmodul" end def mediation_supervision? mediation? && entry_art == "Supervision" end def peergruppenarbeit? mediation? && entry_art == "Peergruppenarbeit" end def fallarbeit? mediation? && entry_art == "Fallarbeit" end def praxisseminar? mediation? && entry_art == "Praxisseminare" end def literatur_und_selbststudium? mediation? && entry_art == "Literatur- und Selbststudium" end # ------------------------------------------------------------ # CSV # ------------------------------------------------------------ def self.to_csv CSV.generate( headers: true, col_sep: ";" ) do |csv| csv << [ "Datum", "Stunden", "Minuten", "Typ", "Art", "Kilometer", "Beschreibung", "Kosten", "User" ] all.each do |entry| csv << [ entry.date, entry.hours, entry.minutes, entry.praktikums_typ, entry.entry_art, entry.distance_km, entry.beschreibung, entry.kosten, entry.user.email ] end end end private # ------------------------------------------------------------ # Alte Bezeichnungen automatisch migrieren # ------------------------------------------------------------ def normalize_legacy_entry_art return if entry_art.blank? self.entry_art = LEGACY_ENTRY_ART_ALIASES.fetch( entry_art, entry_art ) end # ------------------------------------------------------------ # Zeit normalisieren # ------------------------------------------------------------ def normalize_time if hours.to_i.zero? && minutes.to_i.zero? && start_time.present? && end_time.present? total_worked_minutes = total_minutes_including_break if total_worked_minutes self.hours = total_worked_minutes / 60 self.minutes = total_worked_minutes % 60 end else total = hours.to_i * 60 + minutes.to_i self.hours = total / 60 self.minutes = total % 60 end end end