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
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
|