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.
 
 
 
 
 
 

431 lines
9.4 KiB

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